forked from mengyxu/noob-components
10 changed files with 2011 additions and 1 deletions
@ -0,0 +1,361 @@ |
|||||||
|
<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>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> |
||||||
|
</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" |
||||||
|
: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" |
||||||
|
: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> |
||||||
|
</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, |
||||||
|
showLine: true, |
||||||
|
showLineNumber: false, |
||||||
|
showIcon: true, |
||||||
|
showLength: true, |
||||||
|
theme: "light" as "light" | "dark", |
||||||
|
indent: 2, |
||||||
|
height: 360, |
||||||
|
itemHeight: 20, |
||||||
|
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 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> |
||||||
@ -0,0 +1,452 @@ |
|||||||
|
import type { BuildVisibleJsonRowsOptions, JsonViewNode, JsonViewNodeType } from "./types"; |
||||||
|
|
||||||
|
type PlainObject = Record<string, unknown>; |
||||||
|
type JsonContainerKind = "object" | "array" | "map" | "set"; |
||||||
|
type JsonContainerValue = PlainObject | unknown[] | Map<unknown, unknown> | Set<unknown>; |
||||||
|
|
||||||
|
interface RowContext { |
||||||
|
path: string; |
||||||
|
level: number; |
||||||
|
key?: string; |
||||||
|
displayKey?: string; |
||||||
|
keyValue?: unknown; |
||||||
|
index?: number; |
||||||
|
showComma: boolean; |
||||||
|
posInSet: number; |
||||||
|
setSize: number; |
||||||
|
} |
||||||
|
|
||||||
|
interface LineCounter { |
||||||
|
nextLineNumber: number; |
||||||
|
} |
||||||
|
|
||||||
|
function isRecordLike(value: unknown): value is Record<string, unknown> { |
||||||
|
return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Map) && !(value instanceof Set); |
||||||
|
} |
||||||
|
|
||||||
|
function buildNodeId(path: string, type: JsonViewNodeType) { |
||||||
|
return `${path}:${type}`; |
||||||
|
} |
||||||
|
|
||||||
|
function buildObjectPath(parentPath: string, key: string) { |
||||||
|
if (/^[A-Za-z_$][\w$]*$/.test(key)) { |
||||||
|
return `${parentPath}.${key}`; |
||||||
|
} |
||||||
|
return `${parentPath}[${JSON.stringify(key)}]`; |
||||||
|
} |
||||||
|
|
||||||
|
function buildArrayPath(parentPath: string, index: number) { |
||||||
|
return `${parentPath}[${index}]`; |
||||||
|
} |
||||||
|
|
||||||
|
function buildMapPath(parentPath: string, key: unknown, index: number) { |
||||||
|
if (typeof key === "string") { |
||||||
|
return `${parentPath}.get(${JSON.stringify(key)})`; |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof key === "number" || typeof key === "boolean" || typeof key === "bigint") { |
||||||
|
return `${parentPath}.get(${String(key)})`; |
||||||
|
} |
||||||
|
|
||||||
|
if (key === null) { |
||||||
|
return `${parentPath}.get(null)`; |
||||||
|
} |
||||||
|
|
||||||
|
if (key === undefined) { |
||||||
|
return `${parentPath}.get(undefined)`; |
||||||
|
} |
||||||
|
|
||||||
|
if (typeof key === "symbol") { |
||||||
|
return `${parentPath}.get(${String(key)})`; |
||||||
|
} |
||||||
|
|
||||||
|
return `${parentPath}.entries[${index}]`; |
||||||
|
} |
||||||
|
|
||||||
|
function isCollapsedByDefault(level: number, length: number, options: BuildVisibleJsonRowsOptions) { |
||||||
|
const deepCollapsed = typeof options.deep === "number" ? level + 1 > options.deep : false; |
||||||
|
const lengthCollapsed = |
||||||
|
typeof options.collapsedNodeLength === "number" ? length > options.collapsedNodeLength : false; |
||||||
|
return deepCollapsed || lengthCollapsed; |
||||||
|
} |
||||||
|
|
||||||
|
function isExpanded(path: string, level: number, length: number, options: BuildVisibleJsonRowsOptions) { |
||||||
|
const explicit = options.expandedState.get(path); |
||||||
|
if (explicit !== undefined) { |
||||||
|
return explicit; |
||||||
|
} |
||||||
|
return !isCollapsedByDefault(level, length, options); |
||||||
|
} |
||||||
|
|
||||||
|
function formatKeyDisplay(value: unknown) { |
||||||
|
if (typeof value === "string") { |
||||||
|
return JSON.stringify(value); |
||||||
|
} |
||||||
|
if (typeof value === "number" || typeof value === "boolean") { |
||||||
|
return String(value); |
||||||
|
} |
||||||
|
if (typeof value === "bigint") { |
||||||
|
return `${String(value)}n`; |
||||||
|
} |
||||||
|
if (value === null) { |
||||||
|
return "null"; |
||||||
|
} |
||||||
|
if (value === undefined) { |
||||||
|
return "undefined"; |
||||||
|
} |
||||||
|
if (typeof value === "symbol") { |
||||||
|
return value.toString(); |
||||||
|
} |
||||||
|
if (typeof value === "function") { |
||||||
|
return value.name ? `[Function ${value.name}]` : "[Function]"; |
||||||
|
} |
||||||
|
if (Array.isArray(value)) { |
||||||
|
return `Array(${value.length})`; |
||||||
|
} |
||||||
|
if (value instanceof Map) { |
||||||
|
return `Map(${value.size})`; |
||||||
|
} |
||||||
|
if (value instanceof Set) { |
||||||
|
return `Set(${value.size})`; |
||||||
|
} |
||||||
|
return "Object"; |
||||||
|
} |
||||||
|
|
||||||
|
function formatPrimitiveDisplay(value: unknown) { |
||||||
|
if (typeof value === "string") { |
||||||
|
return JSON.stringify(value); |
||||||
|
} |
||||||
|
if (typeof value === "number" || typeof value === "boolean") { |
||||||
|
return String(value); |
||||||
|
} |
||||||
|
if (typeof value === "bigint") { |
||||||
|
return `${String(value)}n`; |
||||||
|
} |
||||||
|
if (value === null) { |
||||||
|
return "null"; |
||||||
|
} |
||||||
|
if (value === undefined) { |
||||||
|
return "undefined"; |
||||||
|
} |
||||||
|
if (typeof value === "symbol") { |
||||||
|
return value.toString(); |
||||||
|
} |
||||||
|
if (typeof value === "function") { |
||||||
|
return value.name ? `[Function ${value.name}]` : "[Function]"; |
||||||
|
} |
||||||
|
return String(value); |
||||||
|
} |
||||||
|
|
||||||
|
function getContainerKind(value: unknown): JsonContainerKind | null { |
||||||
|
if (Array.isArray(value)) { |
||||||
|
return "array"; |
||||||
|
} |
||||||
|
if (value instanceof Map) { |
||||||
|
return "map"; |
||||||
|
} |
||||||
|
if (value instanceof Set) { |
||||||
|
return "set"; |
||||||
|
} |
||||||
|
if (isRecordLike(value)) { |
||||||
|
return "object"; |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function getContainerLength(value: JsonContainerValue, kind: JsonContainerKind) { |
||||||
|
switch (kind) { |
||||||
|
case "array": |
||||||
|
return (value as unknown[]).length; |
||||||
|
case "map": |
||||||
|
return (value as Map<unknown, unknown>).size; |
||||||
|
case "set": |
||||||
|
return (value as Set<unknown>).size; |
||||||
|
case "object": |
||||||
|
default: |
||||||
|
return Object.keys(value as PlainObject).length; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getContainerContent(kind: JsonContainerKind, state: "start" | "end" | "collapsed") { |
||||||
|
switch (kind) { |
||||||
|
case "map": |
||||||
|
if (state === "start") return "Map {"; |
||||||
|
if (state === "end") return "}"; |
||||||
|
return "Map {...}"; |
||||||
|
case "set": |
||||||
|
if (state === "start") return "Set ["; |
||||||
|
if (state === "end") return "]"; |
||||||
|
return "Set [...]"; |
||||||
|
case "object": |
||||||
|
if (state === "start") return "{"; |
||||||
|
if (state === "end") return "}"; |
||||||
|
return "{...}"; |
||||||
|
case "array": |
||||||
|
default: |
||||||
|
if (state === "start") return "["; |
||||||
|
if (state === "end") return "]"; |
||||||
|
return "[...]"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getContainerNodeType(kind: JsonContainerKind, state: "start" | "end" | "collapsed"): JsonViewNodeType { |
||||||
|
if (kind === "object" || kind === "map") { |
||||||
|
if (state === "start") return "objectStart"; |
||||||
|
if (state === "end") return "objectEnd"; |
||||||
|
return "objectCollapsed"; |
||||||
|
} |
||||||
|
|
||||||
|
if (state === "start") return "arrayStart"; |
||||||
|
if (state === "end") return "arrayEnd"; |
||||||
|
return "arrayCollapsed"; |
||||||
|
} |
||||||
|
|
||||||
|
function countValueLines(value: unknown, ancestors = new WeakSet<object>()): number { |
||||||
|
const kind = getContainerKind(value); |
||||||
|
if (!kind) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
const reference = value as object; |
||||||
|
if (ancestors.has(reference)) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
ancestors.add(reference); |
||||||
|
|
||||||
|
const total = |
||||||
|
kind === "array" |
||||||
|
? 2 + (value as unknown[]).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0) |
||||||
|
: kind === "set" |
||||||
|
? 2 + Array.from(value as Set<unknown>).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0) |
||||||
|
: kind === "map" |
||||||
|
? 2 + |
||||||
|
Array.from((value as Map<unknown, unknown>).values()).reduce<number>( |
||||||
|
(sum, entry) => sum + countValueLines(entry, ancestors), |
||||||
|
0 |
||||||
|
) |
||||||
|
: 2 + |
||||||
|
Object.values(value as PlainObject).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0); |
||||||
|
|
||||||
|
ancestors.delete(reference); |
||||||
|
return total; |
||||||
|
} |
||||||
|
|
||||||
|
function countContainerChildLines(kind: JsonContainerKind, value: JsonContainerValue, ancestors: WeakSet<object>) { |
||||||
|
if (kind === "array") { |
||||||
|
return (value as unknown[]).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0); |
||||||
|
} |
||||||
|
|
||||||
|
if (kind === "set") { |
||||||
|
return Array.from(value as Set<unknown>).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0); |
||||||
|
} |
||||||
|
|
||||||
|
if (kind === "map") { |
||||||
|
return Array.from((value as Map<unknown, unknown>).values()).reduce<number>( |
||||||
|
(sum, entry) => sum + countValueLines(entry, ancestors), |
||||||
|
0 |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return Object.values(value as PlainObject).reduce<number>((sum, entry) => sum + countValueLines(entry, ancestors), 0); |
||||||
|
} |
||||||
|
|
||||||
|
function createNode( |
||||||
|
counter: LineCounter, |
||||||
|
context: RowContext, |
||||||
|
type: JsonViewNodeType, |
||||||
|
content: string, |
||||||
|
value: unknown, |
||||||
|
extras?: Partial<JsonViewNode> |
||||||
|
): JsonViewNode { |
||||||
|
return { |
||||||
|
id: buildNodeId(context.path, type), |
||||||
|
path: context.path, |
||||||
|
lineNumber: counter.nextLineNumber++, |
||||||
|
level: context.level, |
||||||
|
key: context.key, |
||||||
|
displayKey: context.displayKey, |
||||||
|
keyValue: context.keyValue, |
||||||
|
index: context.index, |
||||||
|
content, |
||||||
|
showComma: context.showComma, |
||||||
|
type, |
||||||
|
value, |
||||||
|
isLeaf: type === "content", |
||||||
|
hasToggle: false, |
||||||
|
isExpanded: false, |
||||||
|
posInSet: context.posInSet, |
||||||
|
setSize: context.setSize, |
||||||
|
...extras, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function appendValueRows( |
||||||
|
value: unknown, |
||||||
|
context: RowContext, |
||||||
|
rows: JsonViewNode[], |
||||||
|
options: BuildVisibleJsonRowsOptions, |
||||||
|
counter: LineCounter, |
||||||
|
ancestors: WeakSet<object> |
||||||
|
) { |
||||||
|
const kind = getContainerKind(value); |
||||||
|
if (!kind) { |
||||||
|
rows.push(createNode(counter, context, "content", formatPrimitiveDisplay(value), value, { isLeaf: true })); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const reference = value as object; |
||||||
|
if (ancestors.has(reference)) { |
||||||
|
rows.push(createNode(counter, context, "content", "[Circular]", value, { isLeaf: true })); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
ancestors.add(reference); |
||||||
|
appendContainerRows(kind, value as JsonContainerValue, context, rows, options, counter, ancestors); |
||||||
|
ancestors.delete(reference); |
||||||
|
} |
||||||
|
|
||||||
|
function appendContainerRows( |
||||||
|
kind: JsonContainerKind, |
||||||
|
value: JsonContainerValue, |
||||||
|
context: RowContext, |
||||||
|
rows: JsonViewNode[], |
||||||
|
options: BuildVisibleJsonRowsOptions, |
||||||
|
counter: LineCounter, |
||||||
|
ancestors: WeakSet<object> |
||||||
|
) { |
||||||
|
const length = getContainerLength(value, kind); |
||||||
|
const expanded = isExpanded(context.path, context.level, length, options); |
||||||
|
const totalLineCount = 2 + countContainerChildLines(kind, value, ancestors); |
||||||
|
|
||||||
|
if (!expanded) { |
||||||
|
rows.push( |
||||||
|
createNode(counter, context, getContainerNodeType(kind, "collapsed"), getContainerContent(kind, "collapsed"), value, { |
||||||
|
length, |
||||||
|
hasToggle: true, |
||||||
|
isExpanded: false, |
||||||
|
isLeaf: false, |
||||||
|
}) |
||||||
|
); |
||||||
|
counter.nextLineNumber += totalLineCount - 1; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
rows.push( |
||||||
|
createNode(counter, context, getContainerNodeType(kind, "start"), getContainerContent(kind, "start"), value, { |
||||||
|
length, |
||||||
|
hasToggle: true, |
||||||
|
isExpanded: true, |
||||||
|
isLeaf: false, |
||||||
|
showComma: false, |
||||||
|
}) |
||||||
|
); |
||||||
|
|
||||||
|
if (kind === "object") { |
||||||
|
Object.entries(value as PlainObject).forEach(([key, childValue], index) => { |
||||||
|
appendValueRows( |
||||||
|
childValue, |
||||||
|
{ |
||||||
|
path: buildObjectPath(context.path, key), |
||||||
|
level: context.level + 1, |
||||||
|
key, |
||||||
|
showComma: index < length - 1, |
||||||
|
posInSet: index + 1, |
||||||
|
setSize: length, |
||||||
|
}, |
||||||
|
rows, |
||||||
|
options, |
||||||
|
counter, |
||||||
|
ancestors |
||||||
|
); |
||||||
|
}); |
||||||
|
} else if (kind === "array") { |
||||||
|
(value as unknown[]).forEach((childValue, index) => { |
||||||
|
appendValueRows( |
||||||
|
childValue, |
||||||
|
{ |
||||||
|
path: buildArrayPath(context.path, index), |
||||||
|
level: context.level + 1, |
||||||
|
index, |
||||||
|
showComma: index < length - 1, |
||||||
|
posInSet: index + 1, |
||||||
|
setSize: length, |
||||||
|
}, |
||||||
|
rows, |
||||||
|
options, |
||||||
|
counter, |
||||||
|
ancestors |
||||||
|
); |
||||||
|
}); |
||||||
|
} else if (kind === "set") { |
||||||
|
Array.from(value as Set<unknown>).forEach((childValue, index) => { |
||||||
|
appendValueRows( |
||||||
|
childValue, |
||||||
|
{ |
||||||
|
path: buildArrayPath(context.path, index), |
||||||
|
level: context.level + 1, |
||||||
|
index, |
||||||
|
showComma: index < length - 1, |
||||||
|
posInSet: index + 1, |
||||||
|
setSize: length, |
||||||
|
}, |
||||||
|
rows, |
||||||
|
options, |
||||||
|
counter, |
||||||
|
ancestors |
||||||
|
); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
Array.from(value as Map<unknown, unknown>).forEach(([keyValue, childValue], index) => { |
||||||
|
appendValueRows( |
||||||
|
childValue, |
||||||
|
{ |
||||||
|
path: buildMapPath(context.path, keyValue, index), |
||||||
|
level: context.level + 1, |
||||||
|
displayKey: formatKeyDisplay(keyValue), |
||||||
|
keyValue, |
||||||
|
showComma: index < length - 1, |
||||||
|
posInSet: index + 1, |
||||||
|
setSize: length, |
||||||
|
}, |
||||||
|
rows, |
||||||
|
options, |
||||||
|
counter, |
||||||
|
ancestors |
||||||
|
); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
rows.push( |
||||||
|
createNode(counter, context, getContainerNodeType(kind, "end"), getContainerContent(kind, "end"), value, { |
||||||
|
length, |
||||||
|
hasToggle: false, |
||||||
|
isExpanded: expanded, |
||||||
|
isLeaf: false, |
||||||
|
key: undefined, |
||||||
|
displayKey: undefined, |
||||||
|
keyValue: undefined, |
||||||
|
index: undefined, |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export function buildVisibleJsonRows(value: unknown, options: BuildVisibleJsonRowsOptions) { |
||||||
|
const rows: JsonViewNode[] = []; |
||||||
|
const counter: LineCounter = { nextLineNumber: 1 }; |
||||||
|
appendValueRows( |
||||||
|
value, |
||||||
|
{ |
||||||
|
path: options.rootPath, |
||||||
|
level: 0, |
||||||
|
showComma: false, |
||||||
|
posInSet: 1, |
||||||
|
setSize: 1, |
||||||
|
}, |
||||||
|
rows, |
||||||
|
options, |
||||||
|
counter, |
||||||
|
new WeakSet<object>() |
||||||
|
); |
||||||
|
return rows; |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
import JsonView from "./json-view.vue"; |
||||||
|
|
||||||
|
export { JsonView }; |
||||||
|
export default JsonView; |
||||||
|
|
||||||
|
export { buildVisibleJsonRows } from "./flattenJson"; |
||||||
|
export type { |
||||||
|
BuildVisibleJsonRowsOptions, |
||||||
|
JsonViewNode, |
||||||
|
JsonViewNodeActionsRenderer, |
||||||
|
JsonViewNodeKeyRenderer, |
||||||
|
JsonViewNodeRendererArgs, |
||||||
|
JsonViewNodeType, |
||||||
|
JsonViewNodeValueRenderer, |
||||||
|
JsonViewProps, |
||||||
|
JsonViewTheme, |
||||||
|
} from "./types"; |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,95 @@ |
|||||||
|
import type { VNodeChild } from "vue"; |
||||||
|
|
||||||
|
export type JsonViewTheme = "light" | "dark"; |
||||||
|
|
||||||
|
export type JsonViewNodeType = |
||||||
|
| "content" |
||||||
|
| "objectStart" |
||||||
|
| "objectEnd" |
||||||
|
| "objectCollapsed" |
||||||
|
| "arrayStart" |
||||||
|
| "arrayEnd" |
||||||
|
| "arrayCollapsed"; |
||||||
|
|
||||||
|
export interface JsonViewNode { |
||||||
|
id: string; |
||||||
|
path: string; |
||||||
|
lineNumber: number; |
||||||
|
level: number; |
||||||
|
key?: string; |
||||||
|
displayKey?: string; |
||||||
|
keyValue?: unknown; |
||||||
|
index?: number; |
||||||
|
content: string; |
||||||
|
length?: number; |
||||||
|
showComma: boolean; |
||||||
|
type: JsonViewNodeType; |
||||||
|
value?: unknown; |
||||||
|
isLeaf: boolean; |
||||||
|
hasToggle: boolean; |
||||||
|
isExpanded: boolean; |
||||||
|
posInSet: number; |
||||||
|
setSize: number; |
||||||
|
} |
||||||
|
|
||||||
|
export interface JsonViewNodeRendererArgs { |
||||||
|
node: JsonViewNode; |
||||||
|
defaultKey?: string; |
||||||
|
defaultValue?: string; |
||||||
|
defaultActions?: null; |
||||||
|
} |
||||||
|
|
||||||
|
export interface JsonViewMenuActionContext { |
||||||
|
node: JsonViewNode; |
||||||
|
path: string; |
||||||
|
value: unknown; |
||||||
|
copy: (text: string) => Promise<boolean>; |
||||||
|
closeMenu: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
export interface JsonViewMenuItem { |
||||||
|
key: string; |
||||||
|
label: string; |
||||||
|
disabled?: boolean; |
||||||
|
onSelect?: (context: JsonViewMenuActionContext) => void | Promise<void>; |
||||||
|
} |
||||||
|
|
||||||
|
export type JsonViewMenuItemsResolver = |
||||||
|
| JsonViewMenuItem[] |
||||||
|
| ((context: JsonViewMenuActionContext) => JsonViewMenuItem[]); |
||||||
|
|
||||||
|
export type JsonViewNodeKeyRenderer = (args: JsonViewNodeRendererArgs) => VNodeChild; |
||||||
|
export type JsonViewNodeValueRenderer = (args: JsonViewNodeRendererArgs) => VNodeChild; |
||||||
|
export type JsonViewNodeActionsRenderer = (args: JsonViewNodeRendererArgs) => VNodeChild; |
||||||
|
|
||||||
|
export interface JsonViewProps { |
||||||
|
data?: unknown; |
||||||
|
rootPath?: string; |
||||||
|
indent?: number; |
||||||
|
collapsedNodeLength?: number; |
||||||
|
deep?: number; |
||||||
|
showLength?: boolean; |
||||||
|
showLine?: boolean; |
||||||
|
showLineNumber?: boolean; |
||||||
|
showIcon?: boolean; |
||||||
|
showDoubleQuotes?: boolean; |
||||||
|
showKeyValueSpace?: boolean; |
||||||
|
virtual?: boolean; |
||||||
|
height?: number; |
||||||
|
itemHeight?: number; |
||||||
|
dynamicHeight?: boolean; |
||||||
|
collapsedOnClickBrackets?: boolean; |
||||||
|
theme?: JsonViewTheme; |
||||||
|
renderNodeKey?: JsonViewNodeKeyRenderer; |
||||||
|
renderNodeValue?: JsonViewNodeValueRenderer; |
||||||
|
renderNodeActions?: JsonViewNodeActionsRenderer; |
||||||
|
showMenu?: boolean; |
||||||
|
menuItems?: JsonViewMenuItemsResolver; |
||||||
|
} |
||||||
|
|
||||||
|
export interface BuildVisibleJsonRowsOptions { |
||||||
|
rootPath: string; |
||||||
|
deep?: number; |
||||||
|
collapsedNodeLength?: number; |
||||||
|
expandedState: ReadonlyMap<string, boolean>; |
||||||
|
} |
||||||
Loading…
Reference in new issue