json-diff-viewer-component 0.1.1 → 0.3.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 +8 -10
- package/package.json +1 -1
- package/src/lib/diff.js +45 -15
- package/src/lib/styles.js +15 -15
- package/src/lib/viewer.js +172 -74
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<img src="public/logo.svg" alt="logo" height="128" />
|
|
2
|
+
<img src="https://raw.githubusercontent.com/metaory/json-diff-viewer-component/refs/heads/master/public/logo.svg" alt="logo" height="128" />
|
|
3
3
|
<h2>json-diff-viewer</h2>
|
|
4
4
|
<h5>
|
|
5
5
|
Compare JSON side-by-side, visually
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<br>
|
|
12
12
|
Perfect for debugging, API comparisons, and configuration diffs
|
|
13
13
|
</p>
|
|
14
|
-
<img src="public/screenshot.png" alt="demo" width="80%" />
|
|
14
|
+
<img src="https://raw.githubusercontent.com/metaory/json-diff-viewer-component/refs/heads/master/public/screenshot.png" alt="demo" width="80%" />
|
|
15
15
|
<h5>
|
|
16
16
|
<a href="https://metaory.github.io/json-diff-viewer-component/" target="_blank">metaory.github.io/json-diff-viewer-component</a>
|
|
17
17
|
</h5>
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- Side-by-side synchronized scrolling
|
|
24
24
|
- Collapsible nodes (synced between panels)
|
|
25
25
|
- Diff indicators bubble up to parent nodes
|
|
26
|
-
- Stats summary (added/removed/modified
|
|
26
|
+
- Stats summary (added/removed/modified)
|
|
27
27
|
- Syntax highlighting
|
|
28
28
|
- Zero dependencies
|
|
29
29
|
- Shadow DOM encapsulation
|
|
@@ -123,12 +123,11 @@ watch(
|
|
|
123
123
|
|
|
124
124
|
## Diff Types
|
|
125
125
|
|
|
126
|
-
| Type
|
|
127
|
-
|
|
|
128
|
-
| Added
|
|
129
|
-
| Removed
|
|
130
|
-
| Modified
|
|
131
|
-
| Type Changed | Orange | Type mismatch (e.g. number → string) |
|
|
126
|
+
| Type | Color | Description |
|
|
127
|
+
| -------- | ------ | ------------------------ |
|
|
128
|
+
| Added | Green | Key exists only in right |
|
|
129
|
+
| Removed | Red | Key exists only in left |
|
|
130
|
+
| Modified | Yellow | Value changed |
|
|
132
131
|
|
|
133
132
|
## Styling
|
|
134
133
|
|
|
@@ -142,7 +141,6 @@ json-diff-viewer {
|
|
|
142
141
|
--add: #22c55e; /* Added items */
|
|
143
142
|
--rem: #ef4444; /* Removed items */
|
|
144
143
|
--mod: #eab308; /* Modified items */
|
|
145
|
-
--typ: #f97316; /* Type changed items */
|
|
146
144
|
|
|
147
145
|
/* Backgrounds */
|
|
148
146
|
--bg: #18181b; /* Main background */
|
package/package.json
CHANGED
package/src/lib/diff.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
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
|
-
const typeOf =
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
9
|
+
const isObj = (v) => v !== null && typeof v === 'object';
|
|
8
10
|
|
|
9
|
-
const
|
|
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)
|
|
20
|
-
isObj(val)
|
|
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,22 @@ const diffContainer = (left, right, key, isArr) => {
|
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
const diff = (left, right, key = 'root') => {
|
|
31
|
-
if (left === undefined)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
return node(key, TYPE.MODIFIED, left, right);
|
|
61
|
+
}
|
|
62
|
+
if (typeOf(left) !== typeOf(right)) {
|
|
63
|
+
return node(key, TYPE.MODIFIED, left, right, { children: [], ...container(left, Array.isArray(left)) });
|
|
64
|
+
}
|
|
65
|
+
return diffContainer(left, right, key, Array.isArray(left));
|
|
66
|
+
};
|
|
37
67
|
|
|
38
68
|
export { diff, TYPE }
|
package/src/lib/styles.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default `
|
|
2
2
|
:host {
|
|
3
|
-
--add: #22c55e; --rem: #ef4444; --mod: #eab308;
|
|
3
|
+
--add: #22c55e; --rem: #ef4444; --mod: #eab308;
|
|
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;
|
|
@@ -15,7 +15,7 @@ export default `
|
|
|
15
15
|
.node { padding-left: 1.25rem; }
|
|
16
16
|
.node.root { padding-left: 0; }
|
|
17
17
|
.line { display: flex; align-items: flex-start; gap: 0.5rem; padding: 2px 4px; border-radius: 4px; cursor: pointer; transition: background .15s; }
|
|
18
|
-
.line:hover { background: rgba(
|
|
18
|
+
.line:hover { background: rgba(0,0,0,.03); }
|
|
19
19
|
.tog { width: 1rem; flex-shrink: 0; color: var(--br); user-select: none; }
|
|
20
20
|
.tog:hover { color: var(--txt); }
|
|
21
21
|
.key { color: var(--key); }
|
|
@@ -26,31 +26,31 @@ export default `
|
|
|
26
26
|
.val-boolean { color: var(--bool); }
|
|
27
27
|
.val-null { color: var(--nul); font-style: italic; }
|
|
28
28
|
.br { color: var(--br); }
|
|
29
|
-
.diff-added { background: rgba(34,197,94,.15); }
|
|
30
|
-
.diff-removed { background: rgba(239,68,68,.15); }
|
|
31
|
-
.diff-modified { background: rgba(234,179,8,.15); }
|
|
32
|
-
.diff-
|
|
33
|
-
.diff-
|
|
34
|
-
.diff-
|
|
35
|
-
.diff-modified .key { color: var(--mod); }
|
|
36
|
-
.diff-type_changed .key { color: var(--typ); }
|
|
29
|
+
.node.diff-added { background: rgba(34,197,94,.15); }
|
|
30
|
+
.node.diff-removed { background: rgba(239,68,68,.15); }
|
|
31
|
+
.node.diff-modified { background: rgba(234,179,8,.15); }
|
|
32
|
+
.node.diff-added .key { color: var(--add); }
|
|
33
|
+
.node.diff-removed .key { color: var(--rem); }
|
|
34
|
+
.node.diff-modified .key { color: var(--mod); }
|
|
37
35
|
.dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; margin-top: 6px; }
|
|
38
36
|
.dot-added { background: var(--add); }
|
|
39
37
|
.dot-removed { background: var(--rem); }
|
|
40
38
|
.dot-modified { background: var(--mod); }
|
|
41
|
-
.dot-type_changed { background: var(--typ); }
|
|
42
39
|
.preview { color: var(--dim); font-style: italic; }
|
|
43
40
|
.preview::before { content: ' '; }
|
|
44
41
|
.preview::after { content: ' items'; }
|
|
45
|
-
.stats { display:
|
|
42
|
+
.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
|
+
.stats-items { display: grid; grid-auto-flow: column; gap: 2rem; justify-content: start; }
|
|
46
44
|
.stats-buttons { display: flex; gap: 0.5rem; }
|
|
47
|
-
.btn-collapse, .btn-expand { padding: 0.
|
|
48
|
-
.btn-collapse
|
|
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; }
|
|
49
50
|
.stat { display: grid; grid-template-columns: auto 1fr; align-items: baseline; gap: .35rem; }
|
|
50
51
|
.stat .dot { width: 8px; height: 8px; }
|
|
51
52
|
.stat-added .dot { background: var(--add); }
|
|
52
53
|
.stat-removed .dot { background: var(--rem); }
|
|
53
54
|
.stat-modified .dot { background: var(--mod); }
|
|
54
|
-
.stat-type_changed .dot { background: var(--typ); }
|
|
55
55
|
.empty { padding: 2rem; color: var(--dim); }
|
|
56
56
|
`
|
package/src/lib/viewer.js
CHANGED
|
@@ -1,109 +1,207 @@
|
|
|
1
|
-
import { diff, TYPE } from
|
|
2
|
-
import styles from
|
|
1
|
+
import { diff, TYPE } from "./diff.js";
|
|
2
|
+
import styles from "./styles.js";
|
|
3
3
|
|
|
4
|
-
const STAT_TYPES = [
|
|
4
|
+
const STAT_TYPES = ["added", "removed", "modified"];
|
|
5
5
|
|
|
6
|
-
const format = val =>
|
|
7
|
-
null
|
|
8
|
-
undefined
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
boolean
|
|
12
|
-
|
|
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
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this.#
|
|
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
|
|
44
|
-
fn(node,
|
|
45
|
-
for (const
|
|
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.#
|
|
51
|
-
this.#
|
|
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
|
-
|
|
56
|
-
this.#
|
|
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)
|
|
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
|
-
|
|
107
|
+
<div class="stats-items">
|
|
108
|
+
${STAT_TYPES.map((t) => `<div class="stat stat-${t}"><span class="dot"></span>${this.#stats[t]} ${t.replace("_", " ")}</div>`).join("")}
|
|
109
|
+
</div>
|
|
65
110
|
<div class="stats-buttons">
|
|
66
|
-
<button class="btn-
|
|
67
|
-
|
|
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>
|
|
117
|
+
<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
|
+
<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>
|
|
68
119
|
</div>
|
|
69
120
|
</div>
|
|
70
121
|
<div class="container">
|
|
71
|
-
${[
|
|
72
|
-
|
|
73
|
-
|
|
122
|
+
${["left", "right"]
|
|
123
|
+
.map((side) => {
|
|
124
|
+
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>`;
|
|
126
|
+
})
|
|
127
|
+
.join("")}
|
|
128
|
+
</div>`;
|
|
129
|
+
this.#bind();
|
|
74
130
|
}
|
|
75
131
|
|
|
76
|
-
#node(
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
132
|
+
#node(node, side, path, root = true) {
|
|
133
|
+
const currentPath = path ? `${path}.${node.key}` : String(node.key);
|
|
134
|
+
const value = node[side];
|
|
135
|
+
const hasDiff = node.hasDiff && node.type !== TYPE.UNCHANGED;
|
|
136
|
+
const diffClass = hasDiff ? `diff-${node.type}` : "";
|
|
137
|
+
const hasChildDiff = node.hasDiff && node.children?.some((c) => c.hasDiff);
|
|
138
|
+
const dotType = node.type === TYPE.UNCHANGED ? "modified" : node.type;
|
|
139
|
+
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
|
+
const nodeDiffClass = hasDiff && !hasChildDiff ? ` ${diffClass}` : "";
|
|
145
|
+
|
|
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>`;
|
|
86
149
|
}
|
|
87
150
|
|
|
88
|
-
const [open, close] =
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
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>`;
|
|
164
|
+
}
|
|
92
165
|
|
|
93
|
-
return `<div class="node${
|
|
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>`;
|
|
94
167
|
}
|
|
95
168
|
|
|
96
169
|
#bind() {
|
|
97
|
-
const [
|
|
98
|
-
let
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
170
|
+
const [leftPanel, rightPanel] = this.shadowRoot.querySelectorAll(".panel");
|
|
171
|
+
let syncing = false;
|
|
172
|
+
|
|
173
|
+
const syncScroll = (source) => () => {
|
|
174
|
+
if (syncing) return;
|
|
175
|
+
syncing = true;
|
|
176
|
+
const target = source === leftPanel ? rightPanel : leftPanel;
|
|
177
|
+
target.scrollTop = source.scrollTop;
|
|
178
|
+
target.scrollLeft = source.scrollLeft;
|
|
179
|
+
syncing = false;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
leftPanel?.addEventListener("scroll", syncScroll(leftPanel));
|
|
183
|
+
rightPanel?.addEventListener("scroll", syncScroll(rightPanel));
|
|
184
|
+
|
|
185
|
+
for (const el of this.shadowRoot.querySelectorAll("[data-p]")) {
|
|
186
|
+
el.onclick = () => {
|
|
187
|
+
this.#proxy[el.dataset.p] = this.#proxy[el.dataset.p] === false;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.shadowRoot
|
|
192
|
+
.querySelector('[data-action="filter"]')
|
|
193
|
+
?.addEventListener("click", () => {
|
|
194
|
+
this.#showOnlyChanged = !this.#showOnlyChanged;
|
|
195
|
+
this.#render();
|
|
196
|
+
});
|
|
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());
|
|
105
203
|
}
|
|
106
204
|
}
|
|
107
205
|
|
|
108
|
-
customElements.define(
|
|
109
|
-
export { JsonDiffViewer }
|
|
206
|
+
customElements.define("json-diff-viewer", JsonDiffViewer);
|
|
207
|
+
export { JsonDiffViewer };
|