|
|
|
|
<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>
|