forked from mengyxu/noob-components
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
446 lines
11 KiB
446 lines
11 KiB
<template> |
|
<div class="json-view-demo"> |
|
<section class="demo-panel"> |
|
<div class="demo-panel__header"> |
|
<div> |
|
<h2>JSON View</h2> |
|
<p>Read-only JSON tree with collapse controls, virtualization, and low-node CSS-based indentation guides.</p> |
|
</div> |
|
</div> |
|
|
|
<div class="demo-grid"> |
|
<div class="demo-card textarea"> |
|
<h3>Source</h3> |
|
<textarea v-model="source" class="demo-source"></textarea> |
|
<p v-if="parseError" class="demo-error">{{ parseError }}</p> |
|
</div> |
|
|
|
<div class="demo-card options"> |
|
<h3>Options</h3> |
|
<label class="demo-option"> |
|
<span>Virtual</span> |
|
<input v-model="options.virtual" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Dynamic height</span> |
|
<input v-model="options.dynamicHeight" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Inline collapsed preview</span> |
|
<input v-model="options.inline" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Collapse fully inline node</span> |
|
<input v-model="options.collapseFullyInlineNode" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Show line guides</span> |
|
<input v-model="options.showLine" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Show line numbers</span> |
|
<input v-model="options.showLineNumber" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Show toggles</span> |
|
<input v-model="options.showIcon" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Show collapsed length</span> |
|
<input v-model="options.showLength" type="checkbox" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Theme</span> |
|
<select v-model="options.theme"> |
|
<option value="light">light</option> |
|
<option value="dark">dark</option> |
|
</select> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Indent</span> |
|
<input v-model.number="options.indent" min="1" max="6" type="number" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Height</span> |
|
<input v-model.number="options.height" min="180" max="720" step="20" type="number" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Item height</span> |
|
<input v-model.number="options.itemHeight" min="18" max="48" step="2" type="number" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Collapse deeper than</span> |
|
<input v-model.number="options.deep" min="1" max="8" type="number" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Collapse length over</span> |
|
<input v-model.number="options.collapsedNodeLength" min="1" max="40" type="number" /> |
|
</label> |
|
<label class="demo-option"> |
|
<span>Max inline width</span> |
|
<input v-model.number="options.maxInlineDiplayWidth" min="120" max="1200" step="20" type="number" /> |
|
</label> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
<section class="demo-panel"> |
|
<div class="demo-panel__header"> |
|
<div> |
|
<h3>Parsed Input</h3> |
|
<p>Primary viewer using the current source text, including the built-in copy menu and one custom item.</p> |
|
</div> |
|
</div> |
|
|
|
<JsonView |
|
:data="parsedData" |
|
:virtual="options.virtual" |
|
:dynamic-height="options.dynamicHeight" |
|
:inline="options.inline" |
|
:collapse-fully-inline-node="options.collapseFullyInlineNode" |
|
:show-line="options.showLine" |
|
:show-line-number="options.showLineNumber" |
|
:show-icon="options.showIcon" |
|
:show-length="options.showLength" |
|
:theme="options.theme" |
|
:indent="options.indent" |
|
:height="options.height" |
|
:item-height="options.itemHeight" |
|
:max-inline-diplay-width="options.maxInlineDiplayWidth" |
|
:deep="normalizeNumber(options.deep)" |
|
:collapsed-node-length="normalizeNumber(options.collapsedNodeLength)" |
|
:menu-items="menuItems" |
|
/> |
|
</section> |
|
|
|
<section class="demo-panel"> |
|
<div class="demo-panel__header"> |
|
<div> |
|
<h3>Large Virtualized Payload</h3> |
|
<p>Stress case with a large array to exercise flattening and virtualization.</p> |
|
</div> |
|
</div> |
|
|
|
<JsonView |
|
:data="largeData" |
|
:virtual="true" |
|
:dynamic-height="false" |
|
:show-line="true" |
|
:show-line-number="true" |
|
:show-icon="true" |
|
:show-length="true" |
|
theme="dark" |
|
:indent="2" |
|
:height="420" |
|
:item-height="22" |
|
:collapsed-node-length="18" |
|
/> |
|
</section> |
|
|
|
<section class="demo-panel"> |
|
<div class="demo-panel__header"> |
|
<div> |
|
<h3>JS Collections And Cycles</h3> |
|
<p>Verifies support for `Map`, `Set`, and cyclic references that are not representable in JSON text.</p> |
|
</div> |
|
</div> |
|
|
|
<JsonView |
|
:data="jsData" |
|
:virtual="true" |
|
:dynamic-height="true" |
|
:show-line="true" |
|
:show-line-number="true" |
|
:show-icon="true" |
|
:show-length="true" |
|
theme="light" |
|
:indent="2" |
|
:height="320" |
|
:item-height="20" |
|
/> |
|
</section> |
|
|
|
<section class="demo-panel"> |
|
<div class="demo-panel__header"> |
|
<div> |
|
<h3>Vue Reactivity Wrappers</h3> |
|
<p>Exercises nested `ref`, `computed`, `reactive`, and reactive `Map`/`Set` values.</p> |
|
</div> |
|
</div> |
|
|
|
<JsonView |
|
:data="vueWrappedData" |
|
:virtual="true" |
|
:dynamic-height="true" |
|
:show-line="true" |
|
:show-line-number="true" |
|
:show-icon="true" |
|
:show-length="true" |
|
theme="dark" |
|
:indent="2" |
|
:height="320" |
|
:item-height="20" |
|
/> |
|
</section> |
|
</div> |
|
</template> |
|
|
|
<script setup lang="ts"> |
|
import { computed, reactive, ref } from "vue"; |
|
import { JsonView } from "noob-mengyxu"; |
|
|
|
const initialData = { |
|
id: "case-1024", |
|
owner: { |
|
id: 7, |
|
name: "Avery Stone", |
|
roles: ["admin", "ops", "audit"], |
|
}, |
|
metrics: { |
|
total: 1532, |
|
successRate: 0.9821, |
|
active: true, |
|
notes: null, |
|
}, |
|
timeline: [ |
|
{ at: 1710000000, event: "created" }, |
|
{ at: 1710003600, event: "validated" }, |
|
{ at: 1710007200, event: "published" }, |
|
], |
|
payload: { |
|
summary: "This is a longer string intended to demonstrate optional dynamic row height handling in the viewer.", |
|
tags: ["alpha", "beta", "gamma"], |
|
nested: { |
|
one: { two: { three: { four: "deep value" } } }, |
|
mixed: [1, "two", false, null, { end: true }], |
|
}, |
|
}, |
|
}; |
|
|
|
const source = ref(JSON.stringify(initialData, null, 2)); |
|
const parseError = ref(""); |
|
|
|
const options = reactive({ |
|
virtual: true, |
|
dynamicHeight: true, |
|
inline: true, |
|
collapseFullyInlineNode: false, |
|
showLine: true, |
|
showLineNumber: false, |
|
showIcon: true, |
|
showLength: true, |
|
theme: "light" as "light" | "dark", |
|
indent: 2, |
|
height: 360, |
|
itemHeight: 20, |
|
maxInlineDiplayWidth: 600, |
|
deep: 4, |
|
collapsedNodeLength: 10, |
|
}); |
|
|
|
const parsedData = computed(() => { |
|
try { |
|
parseError.value = ""; |
|
return JSON.parse(source.value); |
|
} catch (error) { |
|
parseError.value = error instanceof Error ? error.message : String(error); |
|
return initialData; |
|
} |
|
}); |
|
|
|
const largeData = computed(() => ({ |
|
meta: { |
|
generatedAt: "2026-04-09T11:30:00Z", |
|
totalRows: 1500, |
|
}, |
|
items: Array.from({ length: 1500 }, (_, index) => ({ |
|
id: index + 1, |
|
group: `batch-${Math.floor(index / 50)}`, |
|
active: index % 3 === 0, |
|
score: Number((Math.sin(index / 10) * 100).toFixed(3)), |
|
tags: [`tag-${index % 5}`, `tag-${index % 7}`, `tag-${index % 11}`], |
|
details: { |
|
owner: `user-${index % 23}`, |
|
region: ["us", "eu", "apac"][index % 3], |
|
comment: `Item ${ |
|
index + 1 |
|
} contains enough text to keep the viewer honest about large arrays and scrolling behavior.`, |
|
}, |
|
})), |
|
})); |
|
|
|
const jsData = computed(() => { |
|
const owner = { |
|
id: 7, |
|
name: "Avery Stone", |
|
}; |
|
|
|
const cycleRoot: Record<string, unknown> = { |
|
id: "cyclic-root", |
|
owner, |
|
}; |
|
|
|
const linked = { |
|
parent: cycleRoot, |
|
owner, |
|
}; |
|
|
|
cycleRoot.self = cycleRoot; |
|
cycleRoot.linked = linked; |
|
|
|
return { |
|
owner, |
|
pairMap: new Map<unknown, unknown>([ |
|
["owner", owner], |
|
[42, { status: "ok" }], |
|
[{ kind: "object-key" }, new Set(["alpha", "beta"])], |
|
]), |
|
valueSet: new Set<unknown>(["alpha", 42, owner, cycleRoot]), |
|
cycleRoot, |
|
}; |
|
}); |
|
|
|
const vueWrappedData = computed(() => { |
|
const status = ref("ready"); |
|
const score = ref(42); |
|
const doubledScore = computed(() => score.value * 2); |
|
const owner = reactive({ |
|
id: ref(7), |
|
name: "Avery Stone", |
|
role: computed(() => "ops"), |
|
}); |
|
|
|
const wrappedMap = reactive( |
|
new Map<unknown, unknown>([ |
|
["status", status], |
|
["doubled", doubledScore], |
|
["owner", owner], |
|
]) |
|
); |
|
|
|
const wrappedSet = reactive(new Set<unknown>([status, doubledScore, owner])); |
|
|
|
const state = reactive({ |
|
status, |
|
doubledScore, |
|
owner, |
|
wrappedMap, |
|
wrappedSet, |
|
nested: { |
|
currentScore: score, |
|
summary: computed(() => `${status.value}:${doubledScore.value}`), |
|
}, |
|
}); |
|
|
|
const rawState = state as unknown as Record<string, unknown>; |
|
rawState.self = ref(state); |
|
|
|
return ref({ |
|
root: state, |
|
derived: computed(() => ({ |
|
active: status.value === "ready", |
|
ownerName: owner.name, |
|
})), |
|
}); |
|
}); |
|
|
|
const menuItems = ({ value, copy }: { value: unknown; copy: (text: string) => Promise<boolean> }) => [ |
|
{ |
|
key: "copy-type", |
|
label: "Copy Type", |
|
onSelect: async () => { |
|
const type = |
|
value === null ? "null" : Array.isArray(value) ? "array" : typeof value === "object" ? "object" : typeof value; |
|
await copy(type); |
|
}, |
|
}, |
|
]; |
|
|
|
function normalizeNumber(value: number) { |
|
return Number.isFinite(value) && value > 0 ? value : undefined; |
|
} |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.json-view-demo { |
|
display: grid; |
|
gap: 20px; |
|
padding: 20px; |
|
} |
|
|
|
.demo-panel { |
|
background: #fff; |
|
border: 1px solid #dfe4ea; |
|
border-radius: 14px; |
|
display: grid; |
|
gap: 16px; |
|
padding: 18px; |
|
} |
|
|
|
.demo-panel__header h2, |
|
.demo-panel__header h3 { |
|
margin: 0 0 6px; |
|
} |
|
|
|
.demo-panel__header p { |
|
color: #5b6472; |
|
margin: 0; |
|
} |
|
|
|
.demo-grid { |
|
display: flex; |
|
flex-direction: row; |
|
gap: 16px; |
|
} |
|
|
|
.demo-card { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 12px; |
|
} |
|
|
|
.demo-card.textarea { |
|
flex: 1; |
|
min-width: 0; |
|
} |
|
|
|
.demo-card.options { |
|
/* margin: 20px; */ |
|
} |
|
|
|
.demo-card h3 { |
|
margin: 0; |
|
} |
|
|
|
.demo-source { |
|
border: 1px solid #cbd5e1; |
|
border-radius: 10px; |
|
font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace; |
|
min-height: 260px; |
|
padding: 12px; |
|
resize: vertical; |
|
} |
|
|
|
.demo-option { |
|
align-items: center; |
|
display: grid; |
|
gap: 10px; |
|
grid-template-columns: 1fr auto; |
|
} |
|
|
|
.demo-option input, |
|
.demo-option select { |
|
min-width: 96px; |
|
} |
|
|
|
.demo-error { |
|
color: #b42318; |
|
margin: 0; |
|
} |
|
|
|
@media (max-width: 960px) { |
|
.demo-grid { |
|
grid-template-columns: 1fr; |
|
} |
|
} |
|
</style>
|
|
|