json-diff-viewer-component 0.3.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.3.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
  }
@@ -16,6 +17,8 @@ export default `
16
17
  .node.root { padding-left: 0; }
17
18
  .line { display: flex; align-items: flex-start; gap: 0.5rem; padding: 2px 4px; border-radius: 4px; cursor: pointer; transition: background .15s; }
18
19
  .line:hover { background: rgba(0,0,0,.03); }
20
+ .line.placeholder { cursor: default; pointer-events: none; }
21
+ .line.placeholder:hover { background: transparent; }
19
22
  .tog { width: 1rem; flex-shrink: 0; color: var(--br); user-select: none; }
20
23
  .tog:hover { color: var(--txt); }
21
24
  .key { color: var(--key); }
@@ -41,12 +44,17 @@ export default `
41
44
  .preview::after { content: ' items'; }
42
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; }
43
46
  .stats-items { display: grid; grid-auto-flow: column; gap: 2rem; justify-content: start; }
44
- .stats-buttons { display: flex; gap: 0.5rem; }
45
- .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; }
46
- .btn-filter svg, .btn-collapse svg, .btn-expand svg { width: 18px; height: 18px; }
47
- .btn-filter:hover, .btn-collapse:hover, .btn-expand:hover { background: rgba(0,0,0,.05); box-shadow: 0 0 4px var(--bdr); }
48
- .btn-filter .checkbox-icon { opacity: 0.3; transition: opacity .15s; }
49
- .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); }
50
58
  .stat { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: .35rem; }
51
59
  .stat .dot { width: 8px; height: 8px; }
52
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,84 +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
- #node(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];
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);
181
+
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) {
190
+ const [val, type] = format(value);
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>`;
198
+ }
199
+
200
+ const [open, close] = getBrackets(node.isArray);
201
+ const expanded = isExpanded(this.#proxy, currentPath);
202
+ const children = node.children || [];
203
+ const filtered = filterChildren(children, this.#showOnlyChanged);
204
+ const childrenHtml = expanded
205
+ ? filtered.map((c) => this.#renderNode(c, side, currentPath, false, placeholder)).join("")
206
+ : "";
207
+ const preview = `${filtered.length}`;
208
+
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>`;
214
+ }
215
+
135
216
  const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
136
217
  const diffClass = hasDiff ? `diff-${node.type}` : "";
137
- const hasChildDiff = node.hasDiff && node.children?.some((c) => c.hasDiff);
218
+ const hasChildDiff = node.hasDiff && children.some((c) => c.hasDiff);
138
219
  const dotType = node.type === TYPE.UNCHANGED ? "modified" : node.type;
139
220
  const dot = hasChildDiff ? `<span class="dot dot-${dotType}"></span>` : "";
140
- const keyHtml = root
141
- ? ""
142
- : `<span class="key">${node.key}</span><span class="colon">:</span>`;
143
- const rootClass = root ? " root" : "";
144
221
  const nodeDiffClass = hasDiff && !hasChildDiff ? ` ${diffClass}` : "";
222
+ const toggle = expanded ? "▼" : "▶";
223
+ const dataPath = ` data-p="${currentPath}"`;
145
224
 
146
- if (!node.isArray && !node.isObject) {
147
- const [val, type] = format(value);
148
- return `<div class="node${rootClass}${nodeDiffClass}"><div class="line"><span class="tog"></span>${dot}${keyHtml}<span class="val-${type}">${val}</span></div></div>`;
149
- }
150
-
151
- const [open, close] = node.isArray ? ["[", "]"] : ["{", "}"];
152
- const isExpanded = this.#proxy[currentPath] !== false;
153
- const filteredChildren = this.#showOnlyChanged
154
- ? node.children?.filter((c) => c.hasDiff) || []
155
- : node.children || [];
156
- const childrenHtml =
157
- filteredChildren
158
- .map((c) => this.#node(c, side, currentPath, false))
159
- .join("") || "";
160
- const preview = `${filteredChildren.length}`;
161
-
162
- if (!isExpanded) {
163
- 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>`;
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>`;
164
227
  }
165
228
 
166
- 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>`;
167
230
  }
168
231
 
169
232
  #bind() {
170
233
  const [leftPanel, rightPanel] = this.shadowRoot.querySelectorAll(".panel");
171
- let syncing = false;
234
+ const syncing = { value: false };
172
235
 
173
236
  const syncScroll = (source) => () => {
174
- if (syncing) return;
175
- syncing = true;
237
+ if (syncing.value) return;
238
+ syncing.value = true;
176
239
  const target = source === leftPanel ? rightPanel : leftPanel;
177
240
  target.scrollTop = source.scrollTop;
178
241
  target.scrollLeft = source.scrollLeft;
179
- syncing = false;
242
+ syncing.value = false;
180
243
  };
181
244
 
182
245
  leftPanel?.addEventListener("scroll", syncScroll(leftPanel));
183
246
  rightPanel?.addEventListener("scroll", syncScroll(rightPanel));
184
247
 
185
- for (const el of this.shadowRoot.querySelectorAll("[data-p]")) {
248
+ Array.from(this.shadowRoot.querySelectorAll("[data-p]")).forEach((el) => {
186
249
  el.onclick = () => {
187
250
  this.#proxy[el.dataset.p] = this.#proxy[el.dataset.p] === false;
188
251
  };
189
- }
252
+ });
190
253
 
191
- this.shadowRoot
192
- .querySelector('[data-action="filter"]')
193
- ?.addEventListener("click", () => {
194
- 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;
195
258
  this.#render();
196
259
  });
197
- this.shadowRoot
198
- .querySelector('[data-action="collapse"]')
199
- ?.addEventListener("click", () => this.#collapseAll());
200
- this.shadowRoot
201
- .querySelector('[data-action="expand"]')
202
- ?.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
+ });
203
270
  }
204
271
  }
205
272