forked from mengyxu/noob-components
10 changed files with 2011 additions and 1 deletions
@ -0,0 +1,361 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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