json-diff-viewer-component 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +1 -1
- package/src/lib/diff.js +45 -26
- package/src/lib/styles.js +12 -6
- package/src/lib/viewer.js +127 -96
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- Collapsible nodes (synced between panels)
|
|
25
25
|
- Diff indicators bubble up to parent nodes
|
|
26
26
|
- Stats summary (added/removed/modified)
|
|
27
|
+
- Show only changed filter toggle
|
|
27
28
|
- Syntax highlighting
|
|
28
29
|
- Zero dependencies
|
|
29
30
|
- Shadow DOM encapsulation
|
|
@@ -153,6 +154,9 @@ json-diff-viewer {
|
|
|
153
154
|
--txt: #fafafa; /* Primary text */
|
|
154
155
|
--dim: #a1a1aa; /* Dimmed/secondary text */
|
|
155
156
|
|
|
157
|
+
/* Controls */
|
|
158
|
+
--slider: var(--bdr); /* Slider toggle active color */
|
|
159
|
+
|
|
156
160
|
/* Syntax highlighting */
|
|
157
161
|
--key: #38bdf8; /* Object keys */
|
|
158
162
|
--str: #a78bfa; /* String values */
|
package/package.json
CHANGED
package/src/lib/diff.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const TYPE = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified' }
|
|
1
|
+
const TYPE = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified' };
|
|
2
2
|
|
|
3
3
|
const typeOf = (v) => {
|
|
4
4
|
if (v === null) return 'null';
|
|
@@ -8,9 +8,9 @@ const typeOf = (v) => {
|
|
|
8
8
|
|
|
9
9
|
const isObj = (v) => v !== null && typeof v === 'object';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const allKeys = (a, b) => [...new Set([...Object.keys(a || {}), ...Object.keys(b || {})])];
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const createNode = (key, type, left, right, extra = {}) => ({
|
|
14
14
|
key,
|
|
15
15
|
type,
|
|
16
16
|
left,
|
|
@@ -19,50 +19,69 @@ const node = (key, type, left, right, extra = {}) => ({
|
|
|
19
19
|
...extra
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
22
|
+
const getContainerProps = (val) => {
|
|
23
|
+
if (Array.isArray(val)) return { isArray: true };
|
|
24
24
|
if (isObj(val)) return { isObject: true };
|
|
25
25
|
return {};
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
)
|
|
28
|
+
const mapChildrenForSide = (val, side) => {
|
|
29
|
+
const type = side === 'added' ? TYPE.ADDED : TYPE.REMOVED;
|
|
30
|
+
const createChildNode = (value, key) => {
|
|
31
|
+
const leftValue = side === 'added' ? undefined : value;
|
|
32
|
+
const rightValue = side === 'added' ? value : undefined;
|
|
33
|
+
const childExtra = isObj(value) ? {
|
|
34
|
+
children: mapChildrenForSide(value, side),
|
|
35
|
+
...getContainerProps(value)
|
|
36
|
+
} : {};
|
|
37
|
+
return createNode(key, type, leftValue, rightValue, childExtra);
|
|
38
|
+
};
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
if (Array.isArray(val)) {
|
|
41
|
+
return val.map((item, index) => createChildNode(item, index));
|
|
42
|
+
}
|
|
43
|
+
if (isObj(val)) {
|
|
44
|
+
return Object.entries(val).map(([k, v]) => createChildNode(v, k));
|
|
45
|
+
}
|
|
38
46
|
return [];
|
|
39
47
|
};
|
|
40
48
|
|
|
41
49
|
const diffContainer = (left, right, key, isArr) => {
|
|
42
50
|
const items = isArr
|
|
43
51
|
? Array.from({ length: Math.max(left?.length || 0, right?.length || 0) }, (_, i) => diff(left?.[i], right?.[i], i))
|
|
44
|
-
:
|
|
45
|
-
const hasDiff = items.some(c => c.hasDiff)
|
|
46
|
-
return
|
|
47
|
-
|
|
52
|
+
: allKeys(left, right).map(k => diff(left?.[k], right?.[k], k));
|
|
53
|
+
const hasDiff = items.some(c => c.hasDiff);
|
|
54
|
+
return createNode(key, hasDiff ? TYPE.MODIFIED : TYPE.UNCHANGED, left, right, {
|
|
55
|
+
children: items,
|
|
56
|
+
...getContainerProps(left)
|
|
57
|
+
});
|
|
58
|
+
};
|
|
48
59
|
|
|
49
60
|
const diff = (left, right, key = 'root') => {
|
|
50
61
|
if (left === undefined) {
|
|
51
|
-
const extra = isObj(right) ? {
|
|
52
|
-
|
|
62
|
+
const extra = isObj(right) ? {
|
|
63
|
+
children: mapChildrenForSide(right, 'added'),
|
|
64
|
+
...getContainerProps(right)
|
|
65
|
+
} : {};
|
|
66
|
+
return createNode(key, TYPE.ADDED, left, right, extra);
|
|
53
67
|
}
|
|
54
68
|
if (right === undefined) {
|
|
55
|
-
const extra = isObj(left) ? {
|
|
56
|
-
|
|
69
|
+
const extra = isObj(left) ? {
|
|
70
|
+
children: mapChildrenForSide(left, 'removed'),
|
|
71
|
+
...getContainerProps(left)
|
|
72
|
+
} : {};
|
|
73
|
+
return createNode(key, TYPE.REMOVED, left, right, extra);
|
|
57
74
|
}
|
|
58
75
|
if (!isObj(left) && !isObj(right)) {
|
|
59
|
-
|
|
60
|
-
return node(key, TYPE.MODIFIED, left, right);
|
|
76
|
+
return createNode(key, left === right ? TYPE.UNCHANGED : TYPE.MODIFIED, left, right);
|
|
61
77
|
}
|
|
62
78
|
if (typeOf(left) !== typeOf(right)) {
|
|
63
|
-
return
|
|
79
|
+
return createNode(key, TYPE.MODIFIED, left, right, {
|
|
80
|
+
children: [],
|
|
81
|
+
...getContainerProps(left)
|
|
82
|
+
});
|
|
64
83
|
}
|
|
65
84
|
return diffContainer(left, right, key, Array.isArray(left));
|
|
66
85
|
};
|
|
67
86
|
|
|
68
|
-
export { diff, TYPE }
|
|
87
|
+
export { diff, TYPE };
|
package/src/lib/styles.js
CHANGED
|
@@ -4,6 +4,7 @@ export default `
|
|
|
4
4
|
--bg: #18181b; --bg2: #27272a; --bdr: #3f3f46;
|
|
5
5
|
--txt: #fafafa; --dim: #a1a1aa;
|
|
6
6
|
--key: #38bdf8; --str: #a78bfa; --num: #34d399; --bool: #fb923c; --nul: #f472b6; --br: #71717a;
|
|
7
|
+
--slider: var(--bdr);
|
|
7
8
|
display: flex; flex-direction: column; font: 13px 'JetBrains Mono', 'Fira Code', monospace;
|
|
8
9
|
background: var(--bg); color: var(--txt); border-radius: 12px; overflow: hidden;
|
|
9
10
|
}
|
|
@@ -43,12 +44,17 @@ export default `
|
|
|
43
44
|
.preview::after { content: ' items'; }
|
|
44
45
|
.stats { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 1rem; padding: .75rem 1rem; background: var(--bg); border-bottom: 2px solid var(--bdr); font-size: 12px; }
|
|
45
46
|
.stats-items { display: grid; grid-auto-flow: column; gap: 2rem; justify-content: start; }
|
|
46
|
-
.stats-buttons { display: flex; gap: 0.5rem; }
|
|
47
|
-
.
|
|
48
|
-
.
|
|
49
|
-
.
|
|
50
|
-
.
|
|
51
|
-
.
|
|
47
|
+
.stats-buttons { display: flex; gap: 0.5rem; align-items: center; }
|
|
48
|
+
.switch { display: inline-block; cursor: pointer; }
|
|
49
|
+
.checkbox { display: none; }
|
|
50
|
+
.slider { width: 48px; height: 24px; background-color: var(--bdr); border-radius: 16px; overflow: hidden; display: flex; align-items: center; border: 3px solid transparent; transition: .3s; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.25) inset; cursor: pointer; }
|
|
51
|
+
.slider::before { content: ''; display: block; width: 100%; height: 100%; background-color: var(--txt); transform: translateX(-24px); border-radius: 16px; transition: .3s; box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.25); }
|
|
52
|
+
.checkbox:checked ~ .slider::before { transform: translateX(24px); box-shadow: 0 0 10px 3px rgba(0, 0, 0, 0.25); }
|
|
53
|
+
.checkbox:checked ~ .slider { background-color: var(--slider); }
|
|
54
|
+
.checkbox:active ~ .slider::before { transform: translate(0); }
|
|
55
|
+
.btn-collapse, .btn-expand { padding: 0.5rem; background: var(--bg2); border: 1px solid var(--bdr); border-radius: 10px; color: var(--txt); cursor: pointer; transition: background .15s, border-color .15s; display: flex; align-items: center; justify-content: center; }
|
|
56
|
+
.btn-collapse svg, .btn-expand svg { width: 18px; height: 18px; }
|
|
57
|
+
.btn-collapse:hover, .btn-expand:hover { background: rgba(0,0,0,.05); box-shadow: 0 0 4px var(--bdr); }
|
|
52
58
|
.stat { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: .35rem; }
|
|
53
59
|
.stat .dot { width: 8px; height: 8px; }
|
|
54
60
|
.stat-added .dot { background: var(--add); }
|
package/src/lib/viewer.js
CHANGED
|
@@ -12,6 +12,44 @@ const format = (val) => {
|
|
|
12
12
|
return [JSON.stringify(val), "string"];
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
const buildPath = (path, key) => path ? `${path}.${key}` : String(key);
|
|
16
|
+
|
|
17
|
+
const filterChildren = (children, showOnlyChanged) =>
|
|
18
|
+
showOnlyChanged ? children.filter((c) => c.hasDiff) : children;
|
|
19
|
+
|
|
20
|
+
const isExpanded = (proxy, path) => proxy[path] !== false;
|
|
21
|
+
|
|
22
|
+
const buildKeyHtml = (key, root, hidden = false) =>
|
|
23
|
+
root ? "" : `<span class="key"${hidden ? ' style="visibility: hidden;"' : ""}>${key}</span><span class="colon"${hidden ? ' style="visibility: hidden;"' : ""}>:</span>`;
|
|
24
|
+
|
|
25
|
+
const buildRootClass = (root) => root ? " root" : "";
|
|
26
|
+
|
|
27
|
+
const getBrackets = (isArray) => isArray ? ["[", "]"] : ["{", "}"];
|
|
28
|
+
|
|
29
|
+
const collectStats = (tree) => {
|
|
30
|
+
const stats = Object.fromEntries(STAT_TYPES.map((t) => [t, 0]));
|
|
31
|
+
const walk = (node, path = "") => {
|
|
32
|
+
const currentPath = buildPath(path, node.key);
|
|
33
|
+
if (node.type !== TYPE.UNCHANGED) stats[node.type]++;
|
|
34
|
+
(node.children || []).forEach((child) => {
|
|
35
|
+
walk(child, currentPath);
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
walk(tree);
|
|
39
|
+
return stats;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const initializeExpanded = (tree, proxy) => {
|
|
43
|
+
const walk = (node, path = "") => {
|
|
44
|
+
const currentPath = buildPath(path, node.key);
|
|
45
|
+
if ((node.isArray || node.isObject) && !node.hasDiff) proxy[currentPath] = false;
|
|
46
|
+
(node.children || []).forEach((child) => {
|
|
47
|
+
walk(child, currentPath);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
walk(tree);
|
|
51
|
+
};
|
|
52
|
+
|
|
15
53
|
class JsonDiffViewer extends HTMLElement {
|
|
16
54
|
#left = null;
|
|
17
55
|
#right = null;
|
|
@@ -26,7 +64,7 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
26
64
|
},
|
|
27
65
|
});
|
|
28
66
|
#stats = {};
|
|
29
|
-
#showOnlyChanged =
|
|
67
|
+
#showOnlyChanged = true;
|
|
30
68
|
|
|
31
69
|
static observedAttributes = ["left", "right"];
|
|
32
70
|
constructor() {
|
|
@@ -64,43 +102,45 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
64
102
|
#compute() {
|
|
65
103
|
if (!this.#left || !this.#right) return;
|
|
66
104
|
this.#tree = diff(this.#left, this.#right);
|
|
67
|
-
this.#stats =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
for (const k of Object.keys(this.#exp)) delete this.#exp[k];
|
|
72
|
-
this.#walk(this.#tree, (n, p) => {
|
|
73
|
-
if ((n.isArray || n.isObject) && !n.hasDiff) this.#exp[p] = false;
|
|
105
|
+
this.#stats = collectStats(this.#tree);
|
|
106
|
+
Object.keys(this.#exp).forEach((k) => {
|
|
107
|
+
delete this.#exp[k];
|
|
74
108
|
});
|
|
109
|
+
initializeExpanded(this.#tree, this.#exp);
|
|
75
110
|
this.#render();
|
|
76
111
|
}
|
|
77
112
|
|
|
78
|
-
#walk(node, fn, path = "") {
|
|
79
|
-
const currentPath = path ? `${path}.${node.key}` : String(node.key);
|
|
80
|
-
fn(node, currentPath);
|
|
81
|
-
for (const child of node.children || []) this.#walk(child, fn, currentPath);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
113
|
#collapseAll() {
|
|
85
114
|
if (!this.#tree) return;
|
|
86
115
|
this.#rendering = true;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
116
|
+
const walk = (node, path = "") => {
|
|
117
|
+
const currentPath = buildPath(path, node.key);
|
|
118
|
+
if (node.isArray || node.isObject) this.#proxy[currentPath] = false;
|
|
119
|
+
(node.children || []).forEach((child) => {
|
|
120
|
+
walk(child, currentPath);
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
walk(this.#tree);
|
|
90
124
|
this.#rendering = false;
|
|
91
125
|
this.#render();
|
|
92
126
|
}
|
|
93
127
|
|
|
94
128
|
#expandAll() {
|
|
95
129
|
this.#rendering = true;
|
|
96
|
-
|
|
130
|
+
Object.keys(this.#exp).forEach((k) => {
|
|
131
|
+
delete this.#exp[k];
|
|
132
|
+
});
|
|
97
133
|
this.#rendering = false;
|
|
98
134
|
this.#render();
|
|
99
135
|
}
|
|
100
136
|
|
|
101
137
|
#render() {
|
|
102
|
-
if (!this.#tree)
|
|
103
|
-
|
|
138
|
+
if (!this.#tree) {
|
|
139
|
+
this.shadowRoot.innerHTML = `<style>${styles}</style><div class="empty">Provide left and right JSON</div>`;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const panel = this.shadowRoot.querySelector('.panel');
|
|
143
|
+
const scroll = { top: panel?.scrollTop || 0, left: panel?.scrollLeft || 0 };
|
|
104
144
|
this.shadowRoot.innerHTML = `
|
|
105
145
|
<style>${styles}</style>
|
|
106
146
|
<div class="stats">
|
|
@@ -108,12 +148,10 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
108
148
|
${STAT_TYPES.map((t) => `<div class="stat stat-${t}"><span class="dot"></span>${this.#stats[t]} ${t.replace("_", " ")}</div>`).join("")}
|
|
109
149
|
</div>
|
|
110
150
|
<div class="stats-buttons">
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</svg>
|
|
116
|
-
</button>
|
|
151
|
+
<label class="switch" aria-label="Show only changed" title="Show only changed">
|
|
152
|
+
<input type="checkbox" class="checkbox" data-action="filter" ${this.#showOnlyChanged ? "checked" : ""}>
|
|
153
|
+
<div class="slider"></div>
|
|
154
|
+
</label>
|
|
117
155
|
<button class="btn-collapse" data-action="collapse" title="Collapse all"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9 15H6q-.425 0-.712-.288T5 14t.288-.712T6 13h4q.425 0 .713.288T11 14v4q0 .425-.288.713T10 19t-.712-.288T9 18zm6-6h3q.425 0 .713.288T19 10t-.288.713T18 11h-4q-.425 0-.712-.288T13 10V6q0-.425.288-.712T14 5t.713.288T15 6z"/></svg></button>
|
|
118
156
|
<button class="btn-expand" data-action="expand" title="Expand all"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M7 17h3q.425 0 .713.288T11 18t-.288.713T10 19H6q-.425 0-.712-.288T5 18v-4q0-.425.288-.712T6 13t.713.288T7 14zM17 7h-3q-.425 0-.712-.288T13 6t.288-.712T14 5h4q.425 0 .713.288T19 6v4q0 .425-.288.713T18 11t-.712-.288T17 10z"/></svg></button>
|
|
119
157
|
</div>
|
|
@@ -122,120 +160,113 @@ class JsonDiffViewer extends HTMLElement {
|
|
|
122
160
|
${["left", "right"]
|
|
123
161
|
.map((side) => {
|
|
124
162
|
const label = side === "left" ? "Original" : "Modified";
|
|
125
|
-
return `<div class="panel" data-side="${side}"><div class="header">${label}</div>${this.#
|
|
163
|
+
return `<div class="panel" data-side="${side}"><div class="header">${label}</div>${this.#renderNode(this.#tree, side, "")}</div>`;
|
|
126
164
|
})
|
|
127
165
|
.join("")}
|
|
128
166
|
</div>`;
|
|
129
167
|
this.#bind();
|
|
168
|
+
this.shadowRoot.querySelectorAll('.panel').forEach(p => {
|
|
169
|
+
p.scrollTop = scroll.top;
|
|
170
|
+
p.scrollLeft = scroll.left;
|
|
171
|
+
});
|
|
130
172
|
}
|
|
131
173
|
|
|
132
|
-
#
|
|
133
|
-
const currentPath = path
|
|
174
|
+
#renderNode(node, side, path, root = true, placeholderParam = false) {
|
|
175
|
+
const currentPath = buildPath(path, node.key);
|
|
134
176
|
const value = node[side];
|
|
135
|
-
const
|
|
136
|
-
const
|
|
177
|
+
const rootClass = buildRootClass(root);
|
|
178
|
+
const placeholder = value === undefined && node.children?.length ? true : placeholderParam;
|
|
179
|
+
const hidden = placeholder ? ' style="visibility: hidden;"' : "";
|
|
180
|
+
const keyHtml = buildKeyHtml(node.key, root, placeholder);
|
|
137
181
|
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
182
|
+
if (value === undefined && !node.children?.length) {
|
|
183
|
+
const otherValue = side === 'left' ? node.right : node.left;
|
|
184
|
+
const [val, type] = format(otherValue);
|
|
185
|
+
const hiddenKey = buildKeyHtml(node.key, root, true);
|
|
186
|
+
return `<div class="node${rootClass}"><div class="line placeholder">${hiddenKey}<span class="val-${type}" style="visibility: hidden;">${val}</span></div></div>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value !== undefined && !node.isArray && !node.isObject) {
|
|
142
190
|
const [val, type] = format(value);
|
|
143
|
-
|
|
191
|
+
if (placeholder) {
|
|
192
|
+
return `<div class="node${rootClass}"><div class="line placeholder">${keyHtml}<span class="val-${type}"${hidden}>${val}</span></div></div>`;
|
|
193
|
+
}
|
|
194
|
+
const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
|
|
195
|
+
const diffClass = hasDiff ? `diff-${node.type}` : "";
|
|
196
|
+
const nodeDiffClass = hasDiff ? ` ${diffClass}` : "";
|
|
197
|
+
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"><span class="tog"></span>${keyHtml}<span class="val-${type}">${val}</span></div></div>`;
|
|
144
198
|
}
|
|
145
199
|
|
|
146
|
-
const [open, close] = node.isArray
|
|
200
|
+
const [open, close] = getBrackets(node.isArray);
|
|
201
|
+
const expanded = isExpanded(this.#proxy, currentPath);
|
|
147
202
|
const children = node.children || [];
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
?
|
|
151
|
-
: children;
|
|
152
|
-
const childrenHtml = isExpanded
|
|
153
|
-
? filteredChildren.map((c) => this.#placeholder(c, side, currentPath, false)).join("")
|
|
203
|
+
const filtered = filterChildren(children, this.#showOnlyChanged);
|
|
204
|
+
const childrenHtml = expanded
|
|
205
|
+
? filtered.map((c) => this.#renderNode(c, side, currentPath, false, placeholder)).join("")
|
|
154
206
|
: "";
|
|
155
|
-
const preview = `${
|
|
207
|
+
const preview = `${filtered.length}`;
|
|
156
208
|
|
|
157
|
-
if (
|
|
158
|
-
|
|
209
|
+
if (placeholder) {
|
|
210
|
+
if (!expanded) {
|
|
211
|
+
return `<div class="node${rootClass}"><div class="line placeholder">${keyHtml}<span class="br"${hidden}>${open}</span><span class="preview"${hidden}>${preview}</span><span class="br"${hidden}>${close}</span></div></div>`;
|
|
212
|
+
}
|
|
213
|
+
return `<div class="node${rootClass}"><div class="line placeholder">${keyHtml}<span class="br"${hidden}>${open}</span></div>${childrenHtml}<div class="line placeholder"><span class="br"${hidden}>${close}</span></div></div>`;
|
|
159
214
|
}
|
|
160
215
|
|
|
161
|
-
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog">▼</span>${keyHtml}<span class="br" style="visibility: hidden;">${open}</span></div>${childrenHtml}<div class="line placeholder"><span class="tog"></span><span class="br" style="visibility: hidden;">${close}</span></div></div>`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
#node(node, side, path, root = true) {
|
|
165
|
-
const currentPath = path ? `${path}.${node.key}` : String(node.key);
|
|
166
|
-
const value = node[side];
|
|
167
216
|
const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
|
|
168
217
|
const diffClass = hasDiff ? `diff-${node.type}` : "";
|
|
169
|
-
const hasChildDiff = node.hasDiff &&
|
|
218
|
+
const hasChildDiff = node.hasDiff && children.some((c) => c.hasDiff);
|
|
170
219
|
const dotType = node.type === TYPE.UNCHANGED ? "modified" : node.type;
|
|
171
220
|
const dot = hasChildDiff ? `<span class="dot dot-${dotType}"></span>` : "";
|
|
172
|
-
const keyHtml = root
|
|
173
|
-
? ""
|
|
174
|
-
: `<span class="key">${node.key}</span><span class="colon">:</span>`;
|
|
175
|
-
const rootClass = root ? " root" : "";
|
|
176
221
|
const nodeDiffClass = hasDiff && !hasChildDiff ? ` ${diffClass}` : "";
|
|
222
|
+
const toggle = expanded ? "▼" : "▶";
|
|
223
|
+
const dataPath = ` data-p="${currentPath}"`;
|
|
177
224
|
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
return this.#placeholder(node, side, path, root);
|
|
181
|
-
}
|
|
182
|
-
return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog"></span>${root ? "" : `<span class="key" style="visibility: hidden;">${node.key}</span><span class="colon" style="visibility: hidden;">:</span>`}</div></div>`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (!node.isArray && !node.isObject) {
|
|
186
|
-
const [val, type] = format(value);
|
|
187
|
-
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"><span class="tog"></span>${dot}${keyHtml}<span class="val-${type}">${val}</span></div></div>`;
|
|
225
|
+
if (!expanded) {
|
|
226
|
+
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"${dataPath}><span class="tog">${toggle}</span>${dot}${keyHtml}<span class="br">${open}</span><span class="preview">${preview}</span><span class="br">${close}</span></div></div>`;
|
|
188
227
|
}
|
|
189
228
|
|
|
190
|
-
|
|
191
|
-
const isExpanded = this.#proxy[currentPath] !== false;
|
|
192
|
-
const filteredChildren = this.#showOnlyChanged
|
|
193
|
-
? node.children?.filter((c) => c.hasDiff) || []
|
|
194
|
-
: node.children || [];
|
|
195
|
-
const childrenHtml = filteredChildren.map((c) => this.#node(c, side, currentPath, false)).join("");
|
|
196
|
-
const preview = `${filteredChildren.length}`;
|
|
197
|
-
|
|
198
|
-
if (!isExpanded) {
|
|
199
|
-
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line" data-p="${currentPath}"><span class="tog">▶</span>${dot}${keyHtml}<span class="br">${open}</span><span class="preview">${preview}</span><span class="br">${close}</span></div></div>`;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line" data-p="${currentPath}"><span class="tog">▼</span>${dot}${keyHtml}<span class="br">${open}</span></div>${childrenHtml}<div class="line"><span class="tog"></span><span class="br">${close}</span></div></div>`;
|
|
229
|
+
return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"${dataPath}><span class="tog">${toggle}</span>${dot}${keyHtml}<span class="br">${open}</span></div>${childrenHtml}<div class="line"><span class="tog"></span><span class="br">${close}</span></div></div>`;
|
|
203
230
|
}
|
|
204
231
|
|
|
205
232
|
#bind() {
|
|
206
233
|
const [leftPanel, rightPanel] = this.shadowRoot.querySelectorAll(".panel");
|
|
207
|
-
|
|
234
|
+
const syncing = { value: false };
|
|
208
235
|
|
|
209
236
|
const syncScroll = (source) => () => {
|
|
210
|
-
if (syncing) return;
|
|
211
|
-
syncing = true;
|
|
237
|
+
if (syncing.value) return;
|
|
238
|
+
syncing.value = true;
|
|
212
239
|
const target = source === leftPanel ? rightPanel : leftPanel;
|
|
213
240
|
target.scrollTop = source.scrollTop;
|
|
214
241
|
target.scrollLeft = source.scrollLeft;
|
|
215
|
-
syncing = false;
|
|
242
|
+
syncing.value = false;
|
|
216
243
|
};
|
|
217
244
|
|
|
218
245
|
leftPanel?.addEventListener("scroll", syncScroll(leftPanel));
|
|
219
246
|
rightPanel?.addEventListener("scroll", syncScroll(rightPanel));
|
|
220
247
|
|
|
221
|
-
|
|
248
|
+
Array.from(this.shadowRoot.querySelectorAll("[data-p]")).forEach((el) => {
|
|
222
249
|
el.onclick = () => {
|
|
223
250
|
this.#proxy[el.dataset.p] = this.#proxy[el.dataset.p] === false;
|
|
224
251
|
};
|
|
225
|
-
}
|
|
252
|
+
});
|
|
226
253
|
|
|
227
|
-
this.shadowRoot
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.#showOnlyChanged =
|
|
254
|
+
const filterCheckbox = this.shadowRoot.querySelector(`[data-action="filter"]`);
|
|
255
|
+
if (filterCheckbox) {
|
|
256
|
+
filterCheckbox.addEventListener("change", (e) => {
|
|
257
|
+
this.#showOnlyChanged = e.target.checked;
|
|
231
258
|
this.#render();
|
|
232
259
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const actions = {
|
|
263
|
+
collapse: () => this.#collapseAll(),
|
|
264
|
+
expand: () => this.#expandAll(),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
Object.entries(actions).forEach(([action, handler]) => {
|
|
268
|
+
this.shadowRoot.querySelector(`[data-action="${action}"]`)?.addEventListener("click", handler);
|
|
269
|
+
});
|
|
239
270
|
}
|
|
240
271
|
}
|
|
241
272
|
|