Skip to content

Commit 23fe31b

Browse files
committed
Refactor JSON viewer component to preserve expanded state during tree rebuild, improve error handling in JsonNode component, and clean up unused styles in App.vue.
1 parent 10440d2 commit 23fe31b

File tree

3 files changed

+80
-84
lines changed

3 files changed

+80
-84
lines changed

src/App.vue

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -269,12 +269,9 @@ const loadSample = (type: keyof typeof sampleData) => {
269269
270270
const toggleTheme = () => {
271271
// This only affects the JsonViewer component theme
272-
// No global body class modifications
273-
console.log("Toggle theme clicked - theme will only affect JsonViewer component");
274272
};
275273
276274
const handleDataUpdate = (newData: any) => {
277-
console.log("Data updated:", newData);
278275
jsonData.value = newData;
279276
};
280277
@@ -292,7 +289,6 @@ const handleEditCancel = () => {
292289
293290
const handleThemeChange = (theme: "light" | "dark") => {
294291
isDarkTheme.value = theme === "dark";
295-
console.log("Theme changed to:", theme);
296292
};
297293
298294
// Lifecycle
@@ -377,8 +373,6 @@ body {
377373
font-weight: 600;
378374
}
379375
380-
381-
382376
.sample-buttons {
383377
display: flex;
384378
gap: 0.5rem;
@@ -403,8 +397,6 @@ body {
403397
border-color: #3b82f6;
404398
}
405399
406-
407-
408400
.theme-controls {
409401
padding-top: 1rem;
410402
border-top: 1px solid #e5e7eb;
@@ -413,8 +405,6 @@ body {
413405
flex-wrap: wrap;
414406
}
415407
416-
417-
418408
.theme-controls label {
419409
display: flex;
420410
align-items: center;
@@ -423,32 +413,19 @@ body {
423413
font-size: 0.875rem;
424414
}
425415
426-
.theme-note {
427-
font-size: 0.75rem;
428-
color: #6b7280;
429-
margin-bottom: 0.5rem;
430-
font-style: italic;
431-
}
432-
433416
.props-controls {
434417
padding-top: 1rem;
435418
border-top: 1px solid #e5e7eb;
436419
margin-top: 1rem;
437420
}
438421
439-
body.dark-theme .props-controls {
440-
border-color: #4b5563;
441-
}
442-
443422
.props-controls h4 {
444423
margin-bottom: 1rem;
445424
color: #374151;
446425
font-weight: 600;
447426
font-size: 1rem;
448427
}
449428
450-
451-
452429
.props-grid {
453430
display: grid;
454431
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
@@ -470,8 +447,6 @@ body.dark-theme .props-controls {
470447
background-color: #f3f4f6;
471448
}
472449
473-
474-
475450
.json-viewer-container {
476451
flex: 1;
477452
min-height: 600px;
@@ -487,17 +462,13 @@ body.dark-theme .props-controls {
487462
height: fit-content;
488463
}
489464
490-
491-
492465
.info-panel h3,
493466
.info-panel h4 {
494467
color: #374151;
495468
font-weight: 600;
496469
margin-bottom: 1rem;
497470
}
498471
499-
500-
501472
.feature-list {
502473
list-style: none;
503474
margin-bottom: 2rem;
@@ -509,15 +480,11 @@ body.dark-theme .props-controls {
509480
font-size: 0.875rem;
510481
}
511482
512-
513-
514483
.usage-example {
515484
padding-top: 1rem;
516485
border-top: 1px solid #e5e7eb;
517486
}
518487
519-
520-
521488
.usage-example pre {
522489
background: #f8fafc;
523490
padding: 1rem;
@@ -528,8 +495,6 @@ body.dark-theme .props-controls {
528495
font-family: 'Fira Code', 'Monaco', monospace;
529496
}
530497
531-
532-
533498
/* Footer */
534499
.app-footer {
535500
background: #f1f5f9;
@@ -540,8 +505,6 @@ body.dark-theme .props-controls {
540505
font-size: 0.875rem;
541506
}
542507
543-
544-
545508
.footer-content {
546509
display: flex;
547510
flex-direction: column;
@@ -592,8 +555,6 @@ body.dark-theme .props-controls {
592555
transform: translateY(-1px);
593556
}
594557
595-
596-
597558
.github-icon,
598559
.npm-icon {
599560
width: 20px;

src/components/JsonNode.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ const saveValue = () => {
353353
354354
emit("value-change", { node: props.node, value: newValue });
355355
} catch (error) {
356-
alert("Invalid value format");
356+
console.error(error);
357357
return;
358358
}
359359
@@ -424,7 +424,7 @@ const addNewItem = () => {
424424
emit("node-add", { parent: props.node, key, value });
425425
cancelAdd();
426426
} catch (error) {
427-
alert("Invalid value format");
427+
console.error(error);
428428
}
429429
};
430430

src/components/JsonViewer.vue

Lines changed: 78 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -371,9 +371,7 @@ const validateJson = () => {
371371
}
372372
};
373373
374-
const buildTreeNodes = (data: any, path: string[] = [], level: number = 0): JsonNodeType[] => {
375-
const nodes: JsonNodeType[] = [];
376-
374+
const buildTreeNodes = (data: any, path: string[] = [], level: number = 0, preserveState?: JsonNodeType[]): JsonNodeType[] => {
377375
if (data === null) {
378376
return [{
379377
key: "",
@@ -384,40 +382,85 @@ const buildTreeNodes = (data: any, path: string[] = [], level: number = 0): Json
384382
}];
385383
}
386384
385+
// For root level objects and arrays, create a single container node
386+
if (level === 0 && (Array.isArray(data) || (typeof data === "object" && data !== null))) {
387+
const rootType = Array.isArray(data) ? "array" : "object";
388+
389+
// Try to preserve expanded state from existing root node
390+
const existingRoot = preserveState && preserveState[0];
391+
const shouldExpand = existingRoot ? existingRoot.expanded : true;
392+
393+
const rootNode: JsonNodeType = {
394+
key: rootType === "array" ? "Array" : "Object",
395+
value: data,
396+
type: rootType,
397+
path: [],
398+
level: 0,
399+
expanded: shouldExpand,
400+
children: buildTreeNodesInternal(data, [], 1, existingRoot?.children)
401+
};
402+
return [rootNode];
403+
}
404+
405+
return buildTreeNodesInternal(data, path, level, preserveState);
406+
};
407+
408+
const buildTreeNodesInternal = (data: any, path: string[] = [], level: number = 0, preserveState?: JsonNodeType[]): JsonNodeType[] => {
409+
const nodes: JsonNodeType[] = [];
410+
411+
// Helper function to find existing node by path
412+
const findExistingNode = (existingNodes: JsonNodeType[] | undefined, targetPath: string[]): JsonNodeType | undefined => {
413+
if (!existingNodes) return undefined;
414+
return existingNodes.find(node =>
415+
node.path.length === targetPath.length &&
416+
node.path.every((segment, index) => segment === targetPath[index])
417+
);
418+
};
419+
387420
if (Array.isArray(data)) {
388421
data.forEach((item, index) => {
389422
const itemPath = [...path, index.toString()];
390423
const itemType = getValueType(item);
424+
425+
// Try to find existing node to preserve expanded state
426+
const existingNode = findExistingNode(preserveState, itemPath);
427+
const shouldExpand = existingNode ? existingNode.expanded : level < props.maxDepth;
428+
391429
const node: JsonNodeType = {
392430
key: index.toString(),
393431
value: item,
394432
type: itemType,
395433
path: itemPath,
396434
level,
397-
expanded: level < props.maxDepth,
435+
expanded: shouldExpand,
398436
};
399437
400438
if (itemType === "object" || itemType === "array") {
401-
node.children = buildTreeNodes(item, itemPath, level + 1);
439+
node.children = buildTreeNodesInternal(item, itemPath, level + 1, existingNode?.children);
402440
}
403441
404442
nodes.push(node);
405443
});
406-
} else if (typeof data === "object") {
444+
} else if (typeof data === "object" && data !== null) {
407445
Object.entries(data).forEach(([key, value]) => {
408446
const itemPath = [...path, key];
409447
const itemType = getValueType(value);
448+
449+
// Try to find existing node to preserve expanded state
450+
const existingNode = findExistingNode(preserveState, itemPath);
451+
const shouldExpand = existingNode ? existingNode.expanded : level < props.maxDepth;
452+
410453
const node: JsonNodeType = {
411454
key,
412455
value,
413456
type: itemType,
414457
path: itemPath,
415458
level,
416-
expanded: level < props.maxDepth,
459+
expanded: shouldExpand,
417460
};
418461
419462
if (itemType === "object" || itemType === "array") {
420-
node.children = buildTreeNodes(value, itemPath, level + 1);
463+
node.children = buildTreeNodesInternal(value, itemPath, level + 1, existingNode?.children);
421464
}
422465
423466
nodes.push(node);
@@ -478,15 +521,31 @@ const handleKeyChange = (event: { node: JsonNodeType; oldKey: string; newKey: st
478521
const parentPath = event.node.path.slice(0, -1);
479522
const parent = getNestedValue(newData, parentPath);
480523
481-
if (parent && typeof parent === 'object') {
482-
// Store the value
483-
const value = parent[event.oldKey];
484-
// Delete the old key
485-
delete parent[event.oldKey];
486-
// Add the new key with the same value
487-
parent[event.newKey] = value;
488-
// Update the path in the node
489-
event.node.path[event.node.path.length - 1] = event.newKey;
524+
if (parent && typeof parent === 'object' && !Array.isArray(parent)) {
525+
// Get all keys in their current order
526+
const keys = Object.keys(parent);
527+
const values = keys.map(key => parent[key]);
528+
529+
// Find the index of the old key
530+
const keyIndex = keys.indexOf(event.oldKey);
531+
532+
if (keyIndex !== -1) {
533+
// Replace the old key with the new key at the same position
534+
keys[keyIndex] = event.newKey;
535+
536+
// Clear the parent object
537+
for (const key of Object.keys(parent)) {
538+
delete parent[key];
539+
}
540+
541+
// Rebuild the object with keys in the same order
542+
keys.forEach((key, index) => {
543+
parent[key] = values[index];
544+
});
545+
546+
// Update the path in the node
547+
event.node.path[event.node.path.length - 1] = event.newKey;
548+
}
490549
}
491550
emit("update:data", newData);
492551
};
@@ -533,7 +592,8 @@ const getNestedValue = (obj: any, path: string[]) => {
533592
};
534593
535594
watch(() => props.data, (newData) => {
536-
rootNodes.value = buildTreeNodes(newData);
595+
// Preserve current expanded state when rebuilding tree
596+
rootNodes.value = buildTreeNodes(newData, [], 0, rootNodes.value);
537597
jsonText.value = JSON.stringify(newData, null, 2);
538598
}, { deep: true, immediate: true });
539599
@@ -544,18 +604,6 @@ onMounted(() => {
544604
</script>
545605

546606
<style scoped>
547-
.json-viewer {
548-
display: flex;
549-
flex-direction: column;
550-
height: 100%;
551-
font-family: 'Inter', 'Roboto', system-ui, sans-serif;
552-
font-size: 14px;
553-
line-height: 1.5;
554-
border-radius: 8px;
555-
overflow: hidden;
556-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
557-
}
558-
559607
.json-viewer {
560608
/* Base styles */
561609
display: flex;
@@ -720,14 +768,6 @@ onMounted(() => {
720768
transition: all 0.3s ease;
721769
}
722770
723-
.theme-btn:hover .theme-icon {
724-
transform: scale(1.1);
725-
}
726-
727-
.theme-btn {
728-
transition: all 0.2s ease;
729-
}
730-
731771
/* Ensure SVG icons inherit the theme icon styling */
732772
.theme-icon svg {
733773
transition: all 0.3s ease;
@@ -1027,11 +1067,6 @@ onMounted(() => {
10271067
}
10281068
}
10291069
1030-
/* Hide hidden sections completely */
1031-
.menu-bar.hidden {
1032-
display: none !important;
1033-
}
1034-
10351070
@media (max-width: 768px) {
10361071
.menu-bar {
10371072
flex-direction: row;

0 commit comments

Comments
 (0)