json-diff-viewer-component 0.4.0 → 0.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-diff-viewer-component",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Vanilla JS web component for side-by-side JSON diff visualization",
6
6
  "keywords": [
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 keys = (a, b) => [...new Set([...Object.keys(a || {}), ...Object.keys(b || {})])];
11
+ const allKeys = (a, b) => [...new Set([...Object.keys(a || {}), ...Object.keys(b || {})])];
12
12
 
13
- const node = (key, type, left, right, extra = {}) => ({
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 container = (val, isArr) => {
23
- if (isArr) return { isArray: true };
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 childMap = (val, side) => (v, k) => node(
29
- k, TYPE.UNCHANGED,
30
- side === 'added' ? undefined : v,
31
- side === 'added' ? v : undefined,
32
- isObj(v) && { children: mapChildren(v, side), ...container(v, Array.isArray(v)) }
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
- const mapChildren = (val, side) => {
36
- if (Array.isArray(val)) return val.map(childMap(val, side));
37
- if (isObj(val)) return Object.entries(val).map(([k, v]) => childMap(val, side)(v, k));
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
- : keys(left, right).map(k => diff(left?.[k], right?.[k], k))
45
- const hasDiff = items.some(c => c.hasDiff)
46
- return node(key, hasDiff ? TYPE.MODIFIED : TYPE.UNCHANGED, left, right, { children: items, ...container(left, isArr) })
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) ? { children: mapChildren(right, 'added'), ...container(right, Array.isArray(right)) } : {};
52
- return node(key, TYPE.ADDED, left, right, extra);
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) ? { children: mapChildren(left, 'removed'), ...container(left, Array.isArray(left)) } : {};
56
- return node(key, TYPE.REMOVED, left, right, extra);
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
- if (left === right) return node(key, TYPE.UNCHANGED, left, right);
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 node(key, TYPE.MODIFIED, left, right, { children: [], ...container(left, Array.isArray(left)) });
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(--mod);
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
- .btn-filter, .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; }
48
- .btn-filter svg, .btn-collapse svg, .btn-expand svg { width: 18px; height: 18px; }
49
- .btn-filter:hover, .btn-collapse:hover, .btn-expand:hover { background: rgba(0,0,0,.05); box-shadow: 0 0 4px var(--bdr); }
50
- .btn-filter .checkbox-icon { opacity: 0.3; transition: opacity .15s; }
51
- .btn-filter .checkbox-icon.checked { opacity: 1; }
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 = false;
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 = Object.fromEntries(STAT_TYPES.map((t) => [t, 0]));
68
- this.#walk(this.#tree, (n) => {
69
- if (n.type !== TYPE.UNCHANGED) this.#stats[n.type]++;
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
- this.#walk(this.#tree, (n, p) => {
88
- if (n.isArray || n.isObject) this.#proxy[p] = false;
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
- for (const k of Object.keys(this.#exp)) delete this.#exp[k];
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
- return (this.shadowRoot.innerHTML = `<style>${styles}</style><div class="empty">Provide left and right JSON</div>`);
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
- <button class="btn-filter" data-action="filter" aria-label="Show only changed" title="Show only changed">
112
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" class="checkbox-icon ${this.#showOnlyChanged ? "checked" : ""}">
113
- <path fill="currentColor" d="M4.25 12a7.75 7.75 0 1 1 15.5 0a7.75 7.75 0 0 1-15.5 0" opacity="0.5" />
114
- <path fill="currentColor" d="M8.25 12a3.75 3.75 0 1 0 7.5 0a3.75 3.75 0 0 0-7.5 0" />
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.#node(this.#tree, side, "")}</div>`;
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
- #placeholder(node, side, path, root = true) {
133
- const currentPath = path ? `${path}.${node.key}` : String(node.key);
174
+ #renderNode(node, side, path, root = true, placeholderParam = false) {
175
+ const currentPath = buildPath(path, node.key);
134
176
  const value = node[side];
135
- const keyHtml = root ? "" : `<span class="key" style="visibility: hidden;">${node.key}</span><span class="colon" style="visibility: hidden;">:</span>`;
136
- const rootClass = root ? " root" : "";
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 (!node.isArray && !node.isObject) {
139
- if (value === undefined) {
140
- return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog"></span>${keyHtml}</div></div>`;
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
- return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog"></span>${keyHtml}<span class="val-${type}" style="visibility: hidden;">${val}</span></div></div>`;
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 isExpanded = this.#proxy[currentPath] !== false;
149
- const filteredChildren = this.#showOnlyChanged
150
- ? children.filter((c) => c.hasDiff)
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 = `${filteredChildren.length}`;
207
+ const preview = `${filtered.length}`;
156
208
 
157
- if (!isExpanded) {
158
- return `<div class="node${rootClass}"><div class="line placeholder"><span class="tog">▶</span>${keyHtml}<span class="br" style="visibility: hidden;">${open}</span><span class="preview" style="visibility: hidden;">${preview}</span><span class="br" style="visibility: hidden;">${close}</span></div></div>`;
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 && node.children?.some((c) => c.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 (value === undefined) {
179
- if (node.children?.length) {
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
- const [open, close] = node.isArray ? ["[", "]"] : ["{", "}"];
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
- let syncing = false;
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
- for (const el of this.shadowRoot.querySelectorAll("[data-p]")) {
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
- .querySelector('[data-action="filter"]')
229
- ?.addEventListener("click", () => {
230
- this.#showOnlyChanged = !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
- this.shadowRoot
234
- .querySelector('[data-action="collapse"]')
235
- ?.addEventListener("click", () => this.#collapseAll());
236
- this.shadowRoot
237
- .querySelector('[data-action="expand"]')
238
- ?.addEventListener("click", () => this.#expandAll());
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