json-diff-viewer-component 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,8 +1,21 @@
1
- # json-diff-viewer
2
-
3
- **Compare JSON side-by-side, visually**
4
-
5
- A zero-dependency web component for visualizing JSON differences with synchronized scrolling, collapsible nodes, and syntax highlighting. Perfect for debugging, API comparisons, and configuration diffs
1
+ <div align="center">
2
+ <img src="public/logo.svg" alt="logo" height="128" />
3
+ <h2>json-diff-viewer</h2>
4
+ <h5>
5
+ Compare JSON side-by-side, visually
6
+ </h5>
7
+ <p>
8
+ A zero-dependency web component for visualizing JSON differences
9
+ <br>
10
+ with synchronized scrolling, collapsible nodes, and syntax highlighting
11
+ <br>
12
+ Perfect for debugging, API comparisons, and configuration diffs
13
+ </p>
14
+ <img src="public/screenshot.png" alt="demo" width="80%" />
15
+ <h5>
16
+ <a href="https://metaory.github.io/json-diff-viewer-component/" target="_blank">metaory.github.io/json-diff-viewer-component</a>
17
+ </h5>
18
+ </div>
6
19
 
7
20
  ## Features
8
21
 
@@ -101,7 +114,7 @@ watch(
101
114
  viewerRef.value.setData(props.left, props.right);
102
115
  }
103
116
  },
104
- { immediate: true }
117
+ { immediate: true },
105
118
  );
106
119
  </script>
107
120
  ```
@@ -119,52 +132,52 @@ watch(
119
132
 
120
133
  ## Styling
121
134
 
122
- Override CSS custom properties:
135
+ Customize the component by overriding CSS custom properties (design tokens) on the `json-diff-viewer` element. All tokens are defined on `:host` and can be overridden from outside the shadow DOM.
136
+
137
+ ### Design Tokens
123
138
 
124
139
  ```css
125
140
  json-diff-viewer {
126
141
  /* Diff colors */
127
- --added: #22c55e;
128
- --removed: #ef4444;
129
- --modified: #eab308;
130
- --type-changed: #f97316;
131
- --unchanged: #71717a;
142
+ --add: #22c55e; /* Added items */
143
+ --rem: #ef4444; /* Removed items */
144
+ --mod: #eab308; /* Modified items */
145
+ --typ: #f97316; /* Type changed items */
146
+
147
+ /* Backgrounds */
148
+ --bg: #18181b; /* Main background */
149
+ --bg2: #27272a; /* Panel background */
132
150
 
133
- /* Background */
134
- --bg: #18181b;
135
- --bg-panel: #27272a;
136
- --border: #3f3f46;
151
+ /* Borders */
152
+ --bdr: #3f3f46; /* Border color */
137
153
 
138
154
  /* Text */
139
- --text: #fafafa;
140
- --text-dim: #a1a1aa;
141
-
142
- /* Syntax */
143
- --key: #38bdf8;
144
- --string: #a78bfa;
145
- --number: #34d399;
146
- --boolean: #fb923c;
147
- --null: #f472b6;
148
- --bracket: #71717a;
149
-
150
- /* Layout */
151
- --radius: 12px;
152
- --font: "JetBrains Mono", monospace;
155
+ --txt: #fafafa; /* Primary text */
156
+ --dim: #a1a1aa; /* Dimmed/secondary text */
157
+
158
+ /* Syntax highlighting */
159
+ --key: #38bdf8; /* Object keys */
160
+ --str: #a78bfa; /* String values */
161
+ --num: #34d399; /* Number values */
162
+ --bool: #fb923c; /* Boolean values */
163
+ --nul: #f472b6; /* Null values */
164
+ --br: #71717a; /* Brackets and braces */
153
165
  }
154
166
  ```
155
167
 
156
- ### Light Theme
168
+ Create your own theme by overriding these tokens. For example, a light theme:
157
169
 
158
170
  ```css
159
171
  json-diff-viewer {
160
172
  --bg: #fafafa;
161
- --bg-panel: #ffffff;
162
- --border: #e4e4e7;
163
- --text: #18181b;
164
- --text-dim: #71717a;
173
+ --bg2: #ffffff;
174
+ --bdr: #e4e4e7;
175
+ --txt: #18181b;
176
+ --dim: #71717a;
165
177
  --key: #0284c7;
166
- --string: #7c3aed;
167
- --number: #059669;
178
+ --str: #7c3aed;
179
+ --num: #059669;
180
+ /* ... override other tokens as needed */
168
181
  }
169
182
  ```
170
183
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-diff-viewer-component",
3
- "version": "0.1.0",
3
+ "version": "0.2.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,12 +1,29 @@
1
1
  const TYPE = { UNCHANGED: 'unchanged', ADDED: 'added', REMOVED: 'removed', MODIFIED: 'modified', TYPE_CHANGED: 'type_changed' }
2
2
 
3
- const typeOf = v => v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v
4
- const isObj = v => v !== null && typeof v === 'object'
5
- const keys = (a, b) => [...new Set([...Object.keys(a || {}), ...Object.keys(b || {})])]
3
+ const typeOf = (v) => {
4
+ if (v === null) return 'null';
5
+ if (Array.isArray(v)) return 'array';
6
+ return typeof v;
7
+ };
6
8
 
7
- const node = (key, type, left, right, extra = {}) => ({ key, type, left, right, hasDiff: type !== TYPE.UNCHANGED, ...extra })
9
+ const isObj = (v) => v !== null && typeof v === 'object';
8
10
 
9
- const container = (val, isArr) => isArr ? { isArray: true } : isObj(val) ? { isObject: true } : {}
11
+ const keys = (a, b) => [...new Set([...Object.keys(a || {}), ...Object.keys(b || {})])];
12
+
13
+ const node = (key, type, left, right, extra = {}) => ({
14
+ key,
15
+ type,
16
+ left,
17
+ right,
18
+ hasDiff: type !== TYPE.UNCHANGED,
19
+ ...extra
20
+ });
21
+
22
+ const container = (val, isArr) => {
23
+ if (isArr) return { isArray: true };
24
+ if (isObj(val)) return { isObject: true };
25
+ return {};
26
+ };
10
27
 
11
28
  const childMap = (val, side) => (v, k) => node(
12
29
  k, TYPE.UNCHANGED,
@@ -15,9 +32,11 @@ const childMap = (val, side) => (v, k) => node(
15
32
  isObj(v) && { children: mapChildren(v, side), ...container(v, Array.isArray(v)) }
16
33
  )
17
34
 
18
- const mapChildren = (val, side) =>
19
- Array.isArray(val) ? val.map(childMap(val, side)) :
20
- isObj(val) ? Object.entries(val).map(([k, v]) => childMap(val, side)(v, k)) : []
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));
38
+ return [];
39
+ };
21
40
 
22
41
  const diffContainer = (left, right, key, isArr) => {
23
42
  const items = isArr
@@ -28,11 +47,23 @@ const diffContainer = (left, right, key, isArr) => {
28
47
  }
29
48
 
30
49
  const diff = (left, right, key = 'root') => {
31
- if (left === undefined) return node(key, TYPE.ADDED, left, right, isObj(right) && { children: mapChildren(right, 'added'), ...container(right, Array.isArray(right)) })
32
- if (right === undefined) return node(key, TYPE.REMOVED, left, right, isObj(left) && { children: mapChildren(left, 'removed'), ...container(left, Array.isArray(left)) })
33
- if (!isObj(left) && !isObj(right)) return node(key, left === right ? TYPE.UNCHANGED : typeOf(left) !== typeOf(right) ? TYPE.TYPE_CHANGED : TYPE.MODIFIED, left, right)
34
- if (typeOf(left) !== typeOf(right)) return node(key, TYPE.TYPE_CHANGED, left, right, { children: [], ...container(left, Array.isArray(left)) })
35
- return diffContainer(left, right, key, Array.isArray(left))
36
- }
50
+ 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);
53
+ }
54
+ 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);
57
+ }
58
+ if (!isObj(left) && !isObj(right)) {
59
+ if (left === right) return node(key, TYPE.UNCHANGED, left, right);
60
+ if (typeOf(left) !== typeOf(right)) return node(key, TYPE.TYPE_CHANGED, left, right);
61
+ return node(key, TYPE.MODIFIED, left, right);
62
+ }
63
+ if (typeOf(left) !== typeOf(right)) {
64
+ return node(key, TYPE.TYPE_CHANGED, left, right, { children: [], ...container(left, Array.isArray(left)) });
65
+ }
66
+ return diffContainer(left, right, key, Array.isArray(left));
67
+ };
37
68
 
38
69
  export { diff, TYPE }
package/src/lib/styles.js CHANGED
@@ -44,8 +44,11 @@ export default `
44
44
  .preview::after { content: ' items'; }
45
45
  .stats { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: .75rem 1rem; background: var(--bg); border-bottom: 2px solid var(--bdr); font-size: 12px; }
46
46
  .stats-buttons { display: flex; gap: 0.5rem; }
47
- .btn-collapse, .btn-expand { padding: 0.35rem 0.75rem; background: var(--bg2); border: 1px solid var(--bdr); border-radius: 6px; color: var(--txt); font-size: 11px; font-family: inherit; cursor: pointer; transition: background .15s, border-color .15s; }
48
- .btn-collapse:hover, .btn-expand:hover { background: rgba(255,255,255,.05); border-color: var(--dim); }
47
+ .btn-filter, .btn-collapse, .btn-expand { padding: 0.5rem; background: var(--bg2); border: 1px solid var(--bdr); border-radius: 6px; 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(255,255,255,.05); border-color: var(--dim); }
50
+ .btn-filter .checkbox-icon { opacity: 0.3; transition: opacity .15s; }
51
+ .btn-filter .checkbox-icon.checked { opacity: 1; }
49
52
  .stat { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: .35rem; }
50
53
  .stat .dot { width: 8px; height: 8px; }
51
54
  .stat-added .dot { background: var(--add); }
package/src/lib/viewer.js CHANGED
@@ -1,109 +1,203 @@
1
- import { diff, TYPE } from './diff.js'
2
- import styles from './styles.js'
1
+ import { diff, TYPE } from "./diff.js";
2
+ import styles from "./styles.js";
3
3
 
4
- const STAT_TYPES = ['added', 'removed', 'modified', 'type_changed']
4
+ const STAT_TYPES = ["added", "removed", "modified", "type_changed"];
5
5
 
6
- const format = val => ({
7
- null: ['null', 'null'],
8
- undefined: ['undefined', 'null'],
9
- string: [val, 'string'],
10
- number: [String(val), 'number'],
11
- boolean: [String(val), 'boolean']
12
- })[val === null ? 'null' : typeof val] || [JSON.stringify(val), 'string']
6
+ const format = (val) => {
7
+ if (val === null) return ["null", "null"];
8
+ if (val === undefined) return ["undefined", "null"];
9
+ const type = typeof val;
10
+ if (type === "string") return [val, "string"];
11
+ if (type === "number" || type === "boolean") return [String(val), type];
12
+ return [JSON.stringify(val), "string"];
13
+ };
13
14
 
14
15
  class JsonDiffViewer extends HTMLElement {
15
- #left = null
16
- #right = null
17
- #tree = null
18
- #exp = {}
19
- #proxy = new Proxy(this.#exp, { set: (t, k, v) => (t[k] = v, this.#render(), true) })
20
- #stats = {}
21
-
22
- static observedAttributes = ['left', 'right']
23
- constructor() { super(); this.attachShadow({ mode: 'open' }) }
24
- connectedCallback() { this.#render() }
25
- attributeChangedCallback(n, _, v) { n === 'left' ? this.#left = JSON.parse(v) : this.#right = JSON.parse(v); this.#compute() }
26
- set left(v) { this.#left = v; this.#compute() }
27
- set right(v) { this.#right = v; this.#compute() }
28
- get left() { return this.#left }
29
- get right() { return this.#right }
30
- setData(l, r) { this.#left = l; this.#right = r; this.#compute() }
16
+ #left = null;
17
+ #right = null;
18
+ #tree = null;
19
+ #exp = {};
20
+ #rendering = false;
21
+ #proxy = new Proxy(this.#exp, {
22
+ set: (t, k, v) => {
23
+ t[k] = v;
24
+ if (!this.#rendering) this.#render();
25
+ return true;
26
+ },
27
+ });
28
+ #stats = {};
29
+ #showOnlyChanged = false;
30
+
31
+ static observedAttributes = ["left", "right"];
32
+ constructor() {
33
+ super();
34
+ this.attachShadow({ mode: "open" });
35
+ }
36
+ connectedCallback() {
37
+ this.#render();
38
+ }
39
+ attributeChangedCallback(name, _, value) {
40
+ if (name === "left") this.#left = JSON.parse(value);
41
+ if (name === "right") this.#right = JSON.parse(value);
42
+ this.#compute();
43
+ }
44
+ set left(v) {
45
+ this.#left = v;
46
+ this.#compute();
47
+ }
48
+ set right(v) {
49
+ this.#right = v;
50
+ this.#compute();
51
+ }
52
+ get left() {
53
+ return this.#left;
54
+ }
55
+ get right() {
56
+ return this.#right;
57
+ }
58
+ setData(left, right) {
59
+ this.#left = left;
60
+ this.#right = right;
61
+ this.#compute();
62
+ }
31
63
 
32
64
  #compute() {
33
- if (!this.#left || !this.#right) return
34
- this.#tree = diff(this.#left, this.#right)
35
- this.#stats = Object.fromEntries(STAT_TYPES.map(t => [t, 0]))
36
- this.#walk(this.#tree, n => n.type !== TYPE.UNCHANGED && this.#stats[n.type]++)
37
- for (const k in this.#exp) delete this.#exp[k]
38
- this.#walk(this.#tree, (n, p) => (n.isArray || n.isObject) && !n.hasDiff && (this.#exp[p] = false))
39
- this.#render()
65
+ if (!this.#left || !this.#right) return;
66
+ 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;
74
+ });
75
+ this.#render();
40
76
  }
41
77
 
42
- #walk(node, fn, path = '') {
43
- const p = path ? `${path}.${node.key}` : String(node.key)
44
- fn(node, p)
45
- for (const c of node.children || []) this.#walk(c, fn, p)
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);
46
82
  }
47
83
 
48
84
  #collapseAll() {
49
- if (!this.#tree) return
50
- this.#walk(this.#tree, (n, p) => (n.isArray || n.isObject) && (this.#exp[p] = false))
51
- this.#render()
85
+ if (!this.#tree) return;
86
+ this.#rendering = true;
87
+ this.#walk(this.#tree, (n, p) => {
88
+ if (n.isArray || n.isObject) this.#proxy[p] = false;
89
+ });
90
+ this.#rendering = false;
91
+ this.#render();
52
92
  }
53
93
 
54
94
  #expandAll() {
55
- for (const k in this.#exp) delete this.#exp[k]
56
- this.#render()
95
+ this.#rendering = true;
96
+ for (const k of Object.keys(this.#exp)) delete this.#exp[k];
97
+ this.#rendering = false;
98
+ this.#render();
57
99
  }
58
100
 
59
101
  #render() {
60
- if (!this.#tree) return (this.shadowRoot.innerHTML = `<style>${styles}</style><div class="empty">Provide left and right JSON</div>`)
102
+ if (!this.#tree)
103
+ return (this.shadowRoot.innerHTML = `<style>${styles}</style><div class="empty">Provide left and right JSON</div>`);
61
104
  this.shadowRoot.innerHTML = `
62
105
  <style>${styles}</style>
63
106
  <div class="stats">
64
- ${STAT_TYPES.map(t => `<div class="stat stat-${t}"><span class="dot"></span>${this.#stats[t]} ${t.replace('_', ' ')}</div>`).join('')}
107
+ ${STAT_TYPES.map((t) => `<div class="stat stat-${t}"><span class="dot"></span>${this.#stats[t]} ${t.replace("_", " ")}</div>`).join("")}
65
108
  <div class="stats-buttons">
66
- <button class="btn-collapse" data-action="collapse">Collapse All</button>
67
- <button class="btn-expand" data-action="expand">Expand All</button>
109
+ <button class="btn-filter" data-action="filter" aria-label="Show only changed">
110
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" class="checkbox-icon ${this.#showOnlyChanged ? 'checked' : ''}">
111
+ <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
112
+ </svg>
113
+ </button>
114
+ <button class="btn-collapse" data-action="collapse"><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>
115
+ <button class="btn-expand" data-action="expand"><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>
68
116
  </div>
69
117
  </div>
70
118
  <div class="container">
71
- ${['left', 'right'].map(s => `<div class="panel" data-side="${s}"><div class="header">${s === 'left' ? 'Original' : 'Modified'}</div>${this.#node(this.#tree, s, '')}</div>`).join('')}
72
- </div>`
73
- this.#bind()
119
+ ${["left", "right"]
120
+ .map((side) => {
121
+ const label = side === "left" ? "Original" : "Modified";
122
+ return `<div class="panel" data-side="${side}"><div class="header">${label}</div>${this.#node(this.#tree, side, "")}</div>`;
123
+ })
124
+ .join("")}
125
+ </div>`;
126
+ this.#bind();
74
127
  }
75
128
 
76
- #node(n, side, path, root = true) {
77
- const p = path ? `${path}.${n.key}` : String(n.key)
78
- const val = n[side]
79
- const cls = n.hasDiff && n.type !== TYPE.UNCHANGED ? `diff-${n.type}` : ''
80
- const dot = n.hasDiff && n.children?.some(c => c.hasDiff) ? `<span class="dot dot-${n.type === TYPE.UNCHANGED ? 'modified' : n.type}"></span>` : ''
81
- const key = root ? '' : `<span class="key">${n.key}</span><span class="colon">:</span>`
82
-
83
- if (!n.isArray && !n.isObject) {
84
- const [v, t] = format(val)
85
- return `<div class="node${root ? ' root' : ''}"><div class="line ${cls}"><span class="tog"></span>${dot}${key}<span class="val-${t}">${v}</span></div></div>`
129
+ #node(node, side, path, root = true) {
130
+ const currentPath = path ? `${path}.${node.key}` : String(node.key);
131
+ const value = node[side];
132
+ const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
133
+ const diffClass = hasDiff ? `diff-${node.type}` : "";
134
+ const hasChildDiff = node.hasDiff && node.children?.some((c) => c.hasDiff);
135
+ const dotType = node.type === TYPE.UNCHANGED ? "modified" : node.type;
136
+ const dot = hasChildDiff ? `<span class="dot dot-${dotType}"></span>` : "";
137
+ const keyHtml = root
138
+ ? ""
139
+ : `<span class="key">${node.key}</span><span class="colon">:</span>`;
140
+ const rootClass = root ? " root" : "";
141
+
142
+ if (!node.isArray && !node.isObject) {
143
+ const [val, type] = format(value);
144
+ return `<div class="node${rootClass}"><div class="line ${diffClass}"><span class="tog"></span>${dot}${keyHtml}<span class="val-${type}">${val}</span></div></div>`;
86
145
  }
87
146
 
88
- const [open, close] = n.isArray ? ['[', ']'] : ['{', '}']
89
- const exp = this.#proxy[p] !== false
90
-
91
- if (!exp) return `<div class="node${root ? ' root' : ''}"><div class="line ${cls}" data-p="${p}"><span class="tog">▶</span>${dot}${key}<span class="br">${open}</span><span class="preview">${n.children?.length || 0}</span><span class="br">${close}</span></div></div>`
147
+ const [open, close] = node.isArray ? ["[", "]"] : ["{", "}"];
148
+ const isExpanded = this.#proxy[currentPath] !== false;
149
+ const filteredChildren = this.#showOnlyChanged
150
+ ? node.children?.filter((c) => c.hasDiff) || []
151
+ : node.children || [];
152
+ const childrenHtml =
153
+ filteredChildren
154
+ .map((c) => this.#node(c, side, currentPath, false))
155
+ .join("") || "";
156
+ const preview = `${filteredChildren.length}`;
157
+
158
+ if (!isExpanded) {
159
+ return `<div class="node${rootClass}"><div class="line ${diffClass}" 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>`;
160
+ }
92
161
 
93
- return `<div class="node${root ? ' root' : ''}"><div class="line ${cls}" data-p="${p}"><span class="tog">▼</span>${dot}${key}<span class="br">${open}</span></div>${n.children?.map(c => this.#node(c, side, p, false)).join('') || ''}<div class="line"><span class="tog"></span><span class="br">${close}</span></div></div>`
162
+ return `<div class="node${rootClass}"><div class="line ${diffClass}" 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>`;
94
163
  }
95
164
 
96
165
  #bind() {
97
- const [l, r] = this.shadowRoot.querySelectorAll('.panel')
98
- let sync = false
99
- const scroll = src => e => { if (sync) return; sync = true; const t = src === l ? r : l; t.scrollTop = src.scrollTop; t.scrollLeft = src.scrollLeft; sync = false }
100
- l?.addEventListener('scroll', scroll(l))
101
- r?.addEventListener('scroll', scroll(r))
102
- for (const el of this.shadowRoot.querySelectorAll('[data-p]')) el.onclick = () => (this.#proxy[el.dataset.p] = this.#proxy[el.dataset.p] === false)
103
- this.shadowRoot.querySelector('[data-action="collapse"]')?.addEventListener('click', () => this.#collapseAll())
104
- this.shadowRoot.querySelector('[data-action="expand"]')?.addEventListener('click', () => this.#expandAll())
166
+ const [leftPanel, rightPanel] = this.shadowRoot.querySelectorAll(".panel");
167
+ let syncing = false;
168
+
169
+ const syncScroll = (source) => () => {
170
+ if (syncing) return;
171
+ syncing = true;
172
+ const target = source === leftPanel ? rightPanel : leftPanel;
173
+ target.scrollTop = source.scrollTop;
174
+ target.scrollLeft = source.scrollLeft;
175
+ syncing = false;
176
+ };
177
+
178
+ leftPanel?.addEventListener("scroll", syncScroll(leftPanel));
179
+ rightPanel?.addEventListener("scroll", syncScroll(rightPanel));
180
+
181
+ for (const el of this.shadowRoot.querySelectorAll("[data-p]")) {
182
+ el.onclick = () => {
183
+ this.#proxy[el.dataset.p] = this.#proxy[el.dataset.p] === false;
184
+ };
185
+ }
186
+
187
+ this.shadowRoot
188
+ .querySelector('[data-action="filter"]')
189
+ ?.addEventListener("click", () => {
190
+ this.#showOnlyChanged = !this.#showOnlyChanged;
191
+ this.#render();
192
+ });
193
+ this.shadowRoot
194
+ .querySelector('[data-action="collapse"]')
195
+ ?.addEventListener("click", () => this.#collapseAll());
196
+ this.shadowRoot
197
+ .querySelector('[data-action="expand"]')
198
+ ?.addEventListener("click", () => this.#expandAll());
105
199
  }
106
200
  }
107
201
 
108
- customElements.define('json-diff-viewer', JsonDiffViewer)
109
- export { JsonDiffViewer }
202
+ customElements.define("json-diff-viewer", JsonDiffViewer);
203
+ export { JsonDiffViewer };