json-viewer-element 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Lruihao (https://lruihao.cn)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,120 @@
1
+ # <json-viewer> Element
2
+
3
+ > 🌈 A lightweight, modern Web Component for JSON visualization and interaction.
4
+
5
+ ## Features
6
+
7
+ - 🌟 **Web Component**: Native, framework-agnostic
8
+ - 🎨 **Theme**: Light & dark mode
9
+ - 📦 **Boxed**: Optional border and padding
10
+ - 📋 **Copyable**: One-click copy JSON
11
+ - 🔑 **Sort**: Key sorting support
12
+ - 🔍 **Expand Depth**: Control initial expand level
13
+ - 🧩 **Custom Copy Button**: Slot for custom copy button
14
+ - 🧬 **Type Highlight**: Colorful type highlighting
15
+ - 🛠️ **Custom Events**: Listen for copy/toggle events
16
+
17
+ ## Usage
18
+
19
+ ### Install
20
+
21
+ ```bash
22
+ npm install json-viewer-element
23
+ ```
24
+
25
+ ### Import
26
+
27
+ #### As a module
28
+
29
+ ```js
30
+ import 'json-viewer-element'
31
+ ```
32
+
33
+ #### UMD (CDN)
34
+
35
+ ```html
36
+ <script src="https://unpkg.com/json-viewer-element/dist/json-viewer-element.umd.js"></script>
37
+ ```
38
+
39
+ ### Basic Example
40
+
41
+ Set value by script:
42
+
43
+ ```html
44
+ <json-viewer id="viewer" boxed copyable sort expand-depth="2" theme="dark"></json-viewer>
45
+ <script>
46
+ document.getElementById('viewer').value = { hello: "world", arr: [1,2,3] };
47
+ </script>
48
+ ```
49
+
50
+ Set value by attribute:
51
+
52
+ ```html
53
+ <json-viewer value='{"hello":"world","arr":[1,2,3]}' boxed copyable sort expand-depth="2" theme="dark"></json-viewer>
54
+ ```
55
+
56
+ Use in Vue framework:
57
+
58
+ ```vue
59
+ <template>
60
+ <json-viewer :value="JSON.stringify(json)" boxed copyable sort expand-depth="2" theme="dark"></json-viewer>
61
+ </template>
62
+
63
+ <script>
64
+ export default {
65
+ data() {
66
+ return {
67
+ json: { hello: "world", arr: [1,2,3] },
68
+ }
69
+ },
70
+ }
71
+ </script>
72
+ ```
73
+
74
+ ## Props
75
+
76
+ > [!TIP]
77
+ > When using with frameworks like Vue, you should pass value and copyable props as strings.
78
+
79
+ | Prop | Type | Default | Description |
80
+ | :----------- | :----------------------------------------- | :------ | :---------------------------------------------------------- |
81
+ | value | object / array / string / number / boolean | null | JSON data |
82
+ | expand-depth | number | 1 | Initial expand depth |
83
+ | copyable | boolean / CopyableOptions | false | Enable copy button or custom copy button config (see below) |
84
+ | sort | boolean | false | Whether to sort object keys |
85
+ | boxed | boolean | false | Whether to show border and padding |
86
+ | theme | 'light' / 'dark' | 'light' | Theme |
87
+ | parse | boolean | true | Whether to parse string value as JSON |
88
+
89
+ ### CopyableOptions
90
+
91
+ | Prop | Type | Default | Description |
92
+ | :---------- | :---------------- | :-------- | :---------------------------------- |
93
+ | copyText | string | Copy | Text shown on the copy button |
94
+ | copiedText | string | Copied | Text shown after successful copy |
95
+ | timeout | number | 2000 | How long to show copiedText (ms) |
96
+ | align | 'left' / 'right' | right | Copy button alignment |
97
+
98
+ ## Events
99
+
100
+ | Event | Description |
101
+ | :----------- | :----------------------- |
102
+ | copy-success | Fired after copy success |
103
+ | copy-error | Fired after copy failure |
104
+ | toggle | Node expand/collapse |
105
+
106
+ ## Slots
107
+
108
+ Custom copy button:
109
+
110
+ ```html
111
+ <json-viewer copyable>
112
+ <button slot="copy-button">Copy JSON</button>
113
+ </json-viewer>
114
+ ```
115
+
116
+ ## License
117
+
118
+ [MIT](https://opensource.org/licenses/MIT)
119
+
120
+ Copyright (c) 2025-present [Lruihao](https://github.com/Lruihao)
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # &lt;json-viewer&gt; Element
2
+
3
+ > 🌈 一个轻量、现代的 JSON 可视化与交互 Web 组件
4
+
5
+ ## 功能特性
6
+
7
+ - 🌟 **Web 组件**:原生,无框架依赖
8
+ - 🎨 **主题**:支持明暗模式
9
+ - 📦 **盒装**:可选边框与内边距
10
+ - 📋 **可复制**:一键复制 JSON
11
+ - 🔑 **排序**:支持键排序
12
+ - 🔍 **展开深度**:可控初始展开层级
13
+ - 🧩 **自定义复制按钮**:slot 插槽支持
14
+ - 🧬 **类型高亮**:丰富多彩的类型高亮
15
+ - 🛠️ **自定义事件**:支持 copy/toggle 事件监听
16
+
17
+ ## 使用方法
18
+
19
+ ### 安装
20
+
21
+ ```bash
22
+ npm install json-viewer-element
23
+ ```
24
+
25
+ ### 引入
26
+
27
+ #### 作为模块
28
+
29
+ ```js
30
+ import 'json-viewer-element'
31
+ ```
32
+
33
+ #### UMD (CDN)
34
+
35
+ ```html
36
+ <script src="https://unpkg.com/json-viewer-element/dist/json-viewer-element.umd.js"></script>
37
+ ```
38
+
39
+ ### 基本用法
40
+
41
+ 手动绑定 value:
42
+
43
+ ```html
44
+ <json-viewer id="viewer" boxed copyable sort expand-depth="2" theme="dark"></json-viewer>
45
+ <script>
46
+ document.getElementById('viewer').value = { hello: "world", arr: [1,2,3] };
47
+ </script>
48
+ ```
49
+
50
+ 直接在标签上绑定 value:
51
+
52
+ ```html
53
+ <json-viewer value='{"hello":"world","arr":[1,2,3]}' boxed copyable sort expand-depth="2" theme="dark"></json-viewer>
54
+ ```
55
+
56
+ 在 Vue 框架中使用:
57
+
58
+ ```vue
59
+ <template>
60
+ <json-viewer :value="JSON.stringify(json)" boxed copyable sort expand-depth="2" theme="dark"></json-viewer>
61
+ </template>
62
+
63
+ <script>
64
+ export default {
65
+ data() {
66
+ return {
67
+ json: { hello: "world", arr: [1,2,3] },
68
+ }
69
+ },
70
+ }
71
+ </script>
72
+ ```
73
+
74
+ ## 属性
75
+
76
+ > [!TIP]
77
+ > 在 Vue 等框架中使用时,value 和 copyable 对象的值需要转成字符串传入。
78
+
79
+ | 属性 | 类型 | 默认值 | 说明 |
80
+ | :----------- | :----------------------------------------- | :------ | :----------------------------------------- |
81
+ | value | object / array / string / number / boolean | null | JSON 数据 |
82
+ | expand-depth | number | 1 | 初始展开层级 |
83
+ | copyable | boolean / CopyableOptions | false | 启用复制按钮或自定义复制按钮配置(见下表) |
84
+ | sort | boolean | false | 是否对对象键排序 |
85
+ | boxed | boolean | false | 是否显示边框和内边距 |
86
+ | theme | 'light' / 'dark' | 'light' | 主题 |
87
+ | parse | boolean | true | 字符串值是否自动解析为 JSON |
88
+
89
+ ### CopyableOptions
90
+
91
+ | 属性 | 类型 | 默认值 | 说明 |
92
+ | :---------- | :---------------- | :-------- | :------------------------- |
93
+ | copyText | string | Copy | 复制按钮显示的文本 |
94
+ | copiedText | string | Copied | 复制成功后显示的文本 |
95
+ | timeout | number | 2000 | 显示 copiedText 的时长 (ms) |
96
+ | align | 'left' / 'right' | right | 复制按钮对齐方式 |
97
+
98
+ ## 事件
99
+
100
+ | 事件 | 说明 |
101
+ | :----------- | :------------------ |
102
+ | copy-success | 复制成功后触发 |
103
+ | copy-error | 复制失败后触发 |
104
+ | toggle | 节点折叠/展开时触发 |
105
+
106
+ ## 插槽
107
+
108
+ 自定义复制按钮:
109
+
110
+ ```html
111
+ <json-viewer copyable>
112
+ <button slot="copy-button">复制 JSON</button>
113
+ </json-viewer>
114
+ ```
115
+
116
+ ## License
117
+
118
+ [MIT](https://opensource.org/licenses/MIT)
119
+
120
+ Copyright (c) 2025-present [Lruihao](https://github.com/Lruihao)
@@ -0,0 +1 @@
1
+ export * from './json-viewer-element';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Options for copyable feature.
3
+ * @property {string} [copyText] Text shown on the copy button
4
+ * @property {string} [copiedText] Text shown after successful copy
5
+ * @property {number} [timeout] How long to show copiedText (ms)
6
+ * @property {'left'|'right'} [align] Copy button alignment
7
+ */
8
+ export interface CopyableOptions {
9
+ copyText?: string;
10
+ copiedText?: string;
11
+ timeout?: number;
12
+ align?: 'left' | 'right';
13
+ }
14
+ /**
15
+ * Props for JsonViewerElement.
16
+ * @property {object | Array<any> | string | number | boolean} value JSON data to display
17
+ * @property {number} [expandDepth] Initial expand depth
18
+ * @property {boolean|CopyableOptions} [copyable] Enable copy button
19
+ * @property {boolean} [sort] Whether to sort object keys
20
+ * @property {boolean} [boxed] Whether to show a box around the viewer
21
+ * @property {'light'|'dark'} [theme] Theme
22
+ * @property {boolean} [parse] Whether to parse string value as JSON
23
+ */
24
+ export interface JsonViewerElementProps {
25
+ value: object | Array<any> | string | number | boolean;
26
+ expandDepth?: number;
27
+ copyable?: boolean | CopyableOptions;
28
+ sort?: boolean;
29
+ boxed?: boolean;
30
+ theme?: 'light' | 'dark';
31
+ parse?: boolean;
32
+ }
33
+ export declare class JsonViewerElement extends HTMLElement {
34
+ static get observedAttributes(): string[];
35
+ private _value;
36
+ private root;
37
+ private container;
38
+ constructor();
39
+ connectedCallback(): void;
40
+ attributeChangedCallback(): void;
41
+ set value(v: any);
42
+ get value(): any;
43
+ private get expandDepth();
44
+ private get sort();
45
+ private get theme();
46
+ private get parse();
47
+ private get copyable();
48
+ /**
49
+ * Copy text to clipboard. Uses Clipboard API if available, otherwise fallback.
50
+ */
51
+ private copyText;
52
+ /**
53
+ * Render the JSON viewer.
54
+ */
55
+ private render;
56
+ /**
57
+ * Recursively build the JSON tree.
58
+ */
59
+ private build;
60
+ /**
61
+ * Create a leaf node for primitive values.
62
+ */
63
+ private leaf;
64
+ }
65
+ declare global {
66
+ interface HTMLElementTagNameMap {
67
+ 'json-viewer': JsonViewerElement;
68
+ }
69
+ }
@@ -0,0 +1,416 @@
1
+ const togglerSvg = "data:image/svg+xml;charset=utf-8;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMCAwIDggOC04IDh6IiBmaWxsPSIjNjY2Ii8+PC9zdmc+";
2
+ const tpl = document.createElement("template");
3
+ tpl.innerHTML = `
4
+ <style>
5
+ /* Light Theme (default) */
6
+ :host {
7
+ --jv-bg-color: #ffffff;
8
+ --jv-border-color: #ddd;
9
+ --jv-text-color: #111;
10
+ --jv-key-color: #111;
11
+ --jv-string-color: #42b983;
12
+ --jv-number-color: #fc1e70;
13
+ --jv-boolean-color: #fc1e70;
14
+ --jv-null-color: #e08331;
15
+ --jv-undefined-color: #b0b0b0;
16
+ --jv-function-color: #067bca;
17
+ --jv-regexp-color: #fc1e70;
18
+ --jv-copy-bg: #eee;
19
+ --jv-copy-text: #333;
20
+ --jv-ellipsis-color: #999999;
21
+ --jv-ellipsis-bg: #eeeeee;
22
+ --jv-hover-shadow: rgba(0,0,0,0.1);
23
+ }
24
+
25
+ /* Dark Theme */
26
+ :host([theme="dark"]) {
27
+ --jv-bg-color: #23272f;
28
+ --jv-border-color: #2c313a;
29
+ --jv-text-color: #d4d4d4;
30
+ --jv-key-color: #79c0ff;
31
+ --jv-string-color: #a5d6a7;
32
+ --jv-number-color: #e2b86b;
33
+ --jv-boolean-color: #ff7b72;
34
+ --jv-null-color: #ffab70;
35
+ --jv-undefined-color: #d2a8ff;
36
+ --jv-function-color: #c678dd;
37
+ --jv-regexp-color: #56b6c2;
38
+ --jv-copy-bg: #3a3f4b;
39
+ --jv-copy-text: #fff;
40
+ --jv-ellipsis-color: #6e7681;
41
+ --jv-ellipsis-bg: #2c313a;
42
+ --jv-hover-shadow: rgba(0,0,0,0.4);
43
+ }
44
+
45
+ :host {
46
+ display: block;
47
+ width: 100%;
48
+ max-width: 100%;
49
+ font-family: Consolas, Menlo, Courier, monospace;
50
+ font-size: 14px;
51
+ padding: 8px;
52
+ overflow-x: auto;
53
+ box-sizing: border-box;
54
+ position: relative;
55
+ background-color: var(--jv-bg-color);
56
+ color: var(--jv-text-color);
57
+ }
58
+
59
+ :host([boxed]) {
60
+ border: 1px solid var(--jv-border-color);
61
+ border-radius: 4px;
62
+ padding: 16px;
63
+ transition: box-shadow 0.2s ease;
64
+ }
65
+ :host([boxed]:hover) {
66
+ box-shadow: 0 2px 8px var(--jv-hover-shadow);
67
+ }
68
+ #root:has(+.align-left) {
69
+ margin-top: 16px;
70
+ }
71
+ .jv-copy {
72
+ cursor: pointer;
73
+ font-size: 12px;
74
+ background: var(--jv-copy-bg);
75
+ color: var(--jv-copy-text);
76
+ padding: 4px 8px;
77
+ border-radius: 3px;
78
+ opacity: 0;
79
+ transition: opacity 0.2s ease;
80
+ }
81
+ :host(:hover) .jv-copy {
82
+ opacity: 1;
83
+ }
84
+ slot[name="copy-button"] {
85
+ position: absolute;
86
+ top: 8px;
87
+ right: 8px;
88
+ z-index: 10;
89
+ opacity: 0;
90
+ transition: opacity 0.2s ease;
91
+ display: block !important;
92
+ }
93
+ slot[name="copy-button"][hidden] {
94
+ display: none !important;
95
+ }
96
+ slot[name="copy-button"].align-left {
97
+ left: 8px;
98
+ right: auto;
99
+ }
100
+ slot[name="copy-button"].align-right {
101
+ right: 8px;
102
+ left: auto;
103
+ }
104
+ :host(:hover) slot[name="copy-button"] {
105
+ opacity: 1;
106
+ }
107
+ .jv-toggle {
108
+ background-image: url("${togglerSvg}");
109
+ background-repeat: no-repeat;
110
+ background-size: contain;
111
+ background-position: center center;
112
+ cursor: pointer;
113
+ width: 10px;
114
+ height: 10px;
115
+ margin-right: 2px;
116
+ display: inline-block;
117
+ transition: rotate .1s;
118
+ }
119
+ .jv-toggle.open {
120
+ rotate: 90deg;
121
+ }
122
+ .jv-key {
123
+ color: var(--jv-key-color);
124
+ }
125
+ .jv-string {
126
+ color: var(--jv-string-color);
127
+ }
128
+ .jv-number {
129
+ color: var(--jv-number-color);
130
+ }
131
+ .jv-boolean {
132
+ color: var(--jv-boolean-color);
133
+ }
134
+ .jv-null {
135
+ color: var(--jv-null-color);
136
+ }
137
+ .jv-undefined {
138
+ color: var(--jv-undefined-color);
139
+ }
140
+ .jv-function {
141
+ color: var(--jv-function-color);
142
+ }
143
+ .jv-regexp {
144
+ color: var(--jv-regexp-color);
145
+ }
146
+ .jv-list {
147
+ margin-left: 16px;
148
+ }
149
+ .jv-item:not(:has(.jv-toggle)) .jv-key {
150
+ margin-left: 12px;
151
+ }
152
+ .jv-item:not(:last-child):after {
153
+ content: ',';
154
+ }
155
+ .jv-node > .jv-ellipsis {
156
+ display: none;
157
+ }
158
+ .jv-node.empty > .jv-list {
159
+ display: inline-block;
160
+ margin-inline: 4px;
161
+ }
162
+ .jv-node.collapsed > .jv-list,
163
+ .jv-node.collapsed.empty > .jv-ellipsis {
164
+ display: none;
165
+ }
166
+ .jv-node.collapsed > .jv-ellipsis {
167
+ color: var(--jv-ellipsis-color);
168
+ background-color: var(--jv-ellipsis-bg);
169
+ display: inline-block;
170
+ line-height: 0.9;
171
+ font-size: 0.85em;
172
+ vertical-align: 2px;
173
+ cursor: pointer;
174
+ user-select: none;
175
+ padding: 2px 4px;
176
+ margin: 0px 4px;
177
+ border-radius: 3px;
178
+ }
179
+ </style>
180
+ <div id="root" part="root"></div>
181
+ <slot name="copy-button" part="copy-button" hidden>
182
+ <span class="jv-copy"></span>
183
+ </slot>
184
+ `;
185
+ class JsonViewerElement extends HTMLElement {
186
+ constructor() {
187
+ super();
188
+ this._value = null;
189
+ this.root = this.attachShadow({ mode: "open" });
190
+ this.root.appendChild(tpl.content.cloneNode(true));
191
+ this.container = this.root.getElementById("root");
192
+ }
193
+ static get observedAttributes() {
194
+ return [
195
+ "value",
196
+ "expand-depth",
197
+ "copyable",
198
+ "sort",
199
+ "boxed",
200
+ "theme",
201
+ "parse"
202
+ ];
203
+ }
204
+ connectedCallback() {
205
+ this.render();
206
+ }
207
+ attributeChangedCallback() {
208
+ this.render();
209
+ }
210
+ // ----- Public property: value -----
211
+ set value(v) {
212
+ if (v === this._value)
213
+ return;
214
+ this._value = v;
215
+ this.render();
216
+ }
217
+ get value() {
218
+ return this._value ?? this.getAttribute("value");
219
+ }
220
+ // ----- Private getters for props -----
221
+ get expandDepth() {
222
+ return Number(this.getAttribute("expand-depth") ?? 1);
223
+ }
224
+ get sort() {
225
+ return this.hasAttribute("sort");
226
+ }
227
+ get theme() {
228
+ return this.getAttribute("theme") || "light";
229
+ }
230
+ get parse() {
231
+ return this.getAttribute("parse") !== "false";
232
+ }
233
+ get copyable() {
234
+ if (!this.hasAttribute("copyable"))
235
+ return false;
236
+ const attr = this.getAttribute("copyable");
237
+ const defaultCopyableOptions = { copyText: "Copy", copiedText: "Copied", timeout: 2e3, align: "right" };
238
+ if (attr === "" || attr === null)
239
+ return defaultCopyableOptions;
240
+ try {
241
+ return {
242
+ ...defaultCopyableOptions,
243
+ ...JSON.parse(attr)
244
+ };
245
+ } catch {
246
+ return defaultCopyableOptions;
247
+ }
248
+ }
249
+ /**
250
+ * Copy text to clipboard. Uses Clipboard API if available, otherwise fallback.
251
+ */
252
+ copyText(text) {
253
+ if (navigator.clipboard) {
254
+ this.copyText = (text2) => navigator.clipboard.writeText(text2);
255
+ return this.copyText(text);
256
+ }
257
+ this.copyText = (text2) => new Promise((resolve, reject) => {
258
+ const input = document.createElement("input");
259
+ input.value = text2;
260
+ document.body.appendChild(input);
261
+ input.select();
262
+ if (document.execCommand("copy")) {
263
+ document.body.removeChild(input);
264
+ resolve();
265
+ } else {
266
+ document.body.removeChild(input);
267
+ reject(new Error("Copy failed"));
268
+ }
269
+ });
270
+ return this.copyText(text);
271
+ }
272
+ /**
273
+ * Render the JSON viewer.
274
+ */
275
+ render() {
276
+ if (typeof this.value === "string" && this.parse) {
277
+ try {
278
+ this._value = JSON.parse(this.value);
279
+ } catch {
280
+ }
281
+ }
282
+ this.container.innerHTML = "";
283
+ this.container.appendChild(this.build(this._value, 0));
284
+ const copyableOptions = this.copyable;
285
+ if (copyableOptions) {
286
+ const align = copyableOptions.align || "right";
287
+ const copySlot = this.root.querySelector('slot[name="copy-button"]');
288
+ const customCopyButton = copySlot.assignedElements()[0];
289
+ const defaultCopyBtn = this.root.querySelector(".jv-copy");
290
+ copySlot.hidden = false;
291
+ copySlot.className = `align-${align}`;
292
+ if (!customCopyButton) {
293
+ let copyTimeout;
294
+ defaultCopyBtn.textContent = copyableOptions.copyText;
295
+ const newBtn = defaultCopyBtn.cloneNode(true);
296
+ defaultCopyBtn.replaceWith(newBtn);
297
+ newBtn.textContent = copyableOptions.copyText;
298
+ newBtn.addEventListener("click", () => {
299
+ const textToCopy = JSON.stringify(this._value, null, 2);
300
+ this.copyText(textToCopy).then(() => {
301
+ newBtn.textContent = copyableOptions.copiedText;
302
+ copyTimeout = window.setTimeout(() => {
303
+ newBtn.textContent = copyableOptions.copyText;
304
+ clearTimeout(copyTimeout);
305
+ }, copyableOptions.timeout);
306
+ this.dispatchEvent(new CustomEvent("copy-success", {
307
+ detail: { text: textToCopy, options: copyableOptions }
308
+ }));
309
+ }).catch(() => {
310
+ console.warn("Failed to copy text to clipboard");
311
+ this.dispatchEvent(new CustomEvent("copy-error", {
312
+ detail: { text: textToCopy, options: copyableOptions }
313
+ }));
314
+ });
315
+ });
316
+ }
317
+ }
318
+ }
319
+ /**
320
+ * Recursively build the JSON tree.
321
+ */
322
+ build(data, depth) {
323
+ if (data === null)
324
+ return this.leaf("null", "jv-null");
325
+ if (data === void 0)
326
+ return this.leaf("undefined", "jv-undefined");
327
+ if (typeof data === "boolean")
328
+ return this.leaf(String(data), "jv-boolean");
329
+ if (typeof data === "number")
330
+ return this.leaf(String(data), "jv-number");
331
+ if (typeof data === "string")
332
+ return this.leaf(`"${data}"`, "jv-string");
333
+ if (typeof data === "function")
334
+ return this.leaf("<function>", "jv-function");
335
+ if (data instanceof RegExp)
336
+ return this.leaf("<regexp>", "jv-regexp");
337
+ if (data instanceof Date)
338
+ return this.leaf(`"${data.toLocaleString()}"`, "jv-string");
339
+ const isArr = Array.isArray(data);
340
+ const node = document.createElement("span");
341
+ node.className = "jv-node";
342
+ node.setAttribute("part", "node");
343
+ const list = document.createElement("div");
344
+ list.className = "jv-list";
345
+ list.setAttribute("part", "list");
346
+ const keys = isArr ? this.sort ? [...data.keys()].sort((a, b) => a - b) : [...data.keys()] : this.sort ? Object.keys(data).sort() : Object.keys(data);
347
+ for (const k of keys) {
348
+ const item = document.createElement("div");
349
+ const childNode = this.build(data[k], depth + 1);
350
+ item.className = "jv-item";
351
+ if (childNode instanceof Element && childNode.classList.contains("jv-node")) {
352
+ const childToggle = childNode.querySelector(".jv-toggle");
353
+ if (childToggle) {
354
+ childToggle.remove();
355
+ item.append(childToggle);
356
+ }
357
+ }
358
+ if (!isArr) {
359
+ const keySpan = document.createElement("span");
360
+ keySpan.className = "jv-key";
361
+ keySpan.setAttribute("part", "key");
362
+ keySpan.textContent = `"${k}": `;
363
+ item.append(keySpan);
364
+ }
365
+ item.append(childNode);
366
+ list.append(item);
367
+ }
368
+ const toggle = document.createElement("span");
369
+ toggle.className = "jv-toggle";
370
+ toggle.setAttribute("part", "toggle");
371
+ if (!node.classList.contains("collapsed")) {
372
+ toggle.classList.add("open");
373
+ }
374
+ toggle.addEventListener("click", () => {
375
+ node.classList.toggle("collapsed");
376
+ toggle.classList.toggle("open");
377
+ this.dispatchEvent(new CustomEvent("toggle", {
378
+ detail: {
379
+ node,
380
+ data,
381
+ isCollapsed: node.classList.contains("collapsed")
382
+ }
383
+ }));
384
+ });
385
+ const ellipsis = document.createElement("span");
386
+ ellipsis.className = "jv-ellipsis";
387
+ ellipsis.setAttribute("part", "ellipsis");
388
+ ellipsis.textContent = `...${keys.length}`;
389
+ ellipsis.addEventListener("click", () => {
390
+ node.classList.remove("collapsed");
391
+ toggle.classList.add("open");
392
+ });
393
+ if (depth >= this.expandDepth) {
394
+ node.classList.add("collapsed");
395
+ toggle.classList.remove("open");
396
+ }
397
+ if (!keys.length)
398
+ node.classList.add("empty");
399
+ node.append(toggle, isArr ? "[" : "{", ellipsis, list, isArr ? "]" : "}");
400
+ return node;
401
+ }
402
+ /**
403
+ * Create a leaf node for primitive values.
404
+ */
405
+ leaf(text, cls) {
406
+ const s = document.createElement("span");
407
+ s.className = `jv-value ${cls}`;
408
+ s.setAttribute("part", `value ${cls.replace("jv-", "")}`);
409
+ s.textContent = text;
410
+ return s;
411
+ }
412
+ }
413
+ customElements.define("json-viewer", JsonViewerElement);
414
+ export {
415
+ JsonViewerElement
416
+ };
@@ -0,0 +1 @@
1
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).JsonViewerElement={})}(this,function(e){"use strict";const t=document.createElement("template");t.innerHTML='\n<style>\n/* Light Theme (default) */\n:host {\n --jv-bg-color: #ffffff;\n --jv-border-color: #ddd;\n --jv-text-color: #111;\n --jv-key-color: #111;\n --jv-string-color: #42b983;\n --jv-number-color: #fc1e70;\n --jv-boolean-color: #fc1e70;\n --jv-null-color: #e08331;\n --jv-undefined-color: #b0b0b0;\n --jv-function-color: #067bca;\n --jv-regexp-color: #fc1e70;\n --jv-copy-bg: #eee;\n --jv-copy-text: #333;\n --jv-ellipsis-color: #999999;\n --jv-ellipsis-bg: #eeeeee;\n --jv-hover-shadow: rgba(0,0,0,0.1);\n}\n\n/* Dark Theme */\n:host([theme="dark"]) {\n --jv-bg-color: #23272f;\n --jv-border-color: #2c313a;\n --jv-text-color: #d4d4d4;\n --jv-key-color: #79c0ff;\n --jv-string-color: #a5d6a7;\n --jv-number-color: #e2b86b;\n --jv-boolean-color: #ff7b72;\n --jv-null-color: #ffab70;\n --jv-undefined-color: #d2a8ff;\n --jv-function-color: #c678dd;\n --jv-regexp-color: #56b6c2;\n --jv-copy-bg: #3a3f4b;\n --jv-copy-text: #fff;\n --jv-ellipsis-color: #6e7681;\n --jv-ellipsis-bg: #2c313a;\n --jv-hover-shadow: rgba(0,0,0,0.4);\n}\n\n:host {\n display: block;\n width: 100%;\n max-width: 100%;\n font-family: Consolas, Menlo, Courier, monospace;\n font-size: 14px;\n padding: 8px;\n overflow-x: auto;\n box-sizing: border-box;\n position: relative;\n background-color: var(--jv-bg-color);\n color: var(--jv-text-color);\n}\n\n:host([boxed]) {\n border: 1px solid var(--jv-border-color);\n border-radius: 4px;\n padding: 16px;\n transition: box-shadow 0.2s ease;\n}\n:host([boxed]:hover) {\n box-shadow: 0 2px 8px var(--jv-hover-shadow);\n}\n#root:has(+.align-left) {\n margin-top: 16px;\n}\n.jv-copy {\n cursor: pointer;\n font-size: 12px;\n background: var(--jv-copy-bg);\n color: var(--jv-copy-text);\n padding: 4px 8px;\n border-radius: 3px;\n opacity: 0;\n transition: opacity 0.2s ease;\n}\n:host(:hover) .jv-copy {\n opacity: 1;\n}\nslot[name="copy-button"] {\n position: absolute;\n top: 8px;\n right: 8px;\n z-index: 10;\n opacity: 0;\n transition: opacity 0.2s ease;\n display: block !important;\n}\nslot[name="copy-button"][hidden] {\n display: none !important;\n}\nslot[name="copy-button"].align-left {\n left: 8px;\n right: auto;\n}\nslot[name="copy-button"].align-right {\n right: 8px;\n left: auto;\n}\n:host(:hover) slot[name="copy-button"] {\n opacity: 1;\n}\n.jv-toggle {\n background-image: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyBoZWlnaHQ9IjE2IiB3aWR0aD0iOCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMCAwIDggOC04IDh6IiBmaWxsPSIjNjY2Ii8+PC9zdmc+");\n background-repeat: no-repeat;\n background-size: contain;\n background-position: center center;\n cursor: pointer;\n width: 10px;\n height: 10px;\n margin-right: 2px;\n display: inline-block;\n transition: rotate .1s;\n}\n.jv-toggle.open {\n rotate: 90deg;\n}\n.jv-key {\n color: var(--jv-key-color);\n}\n.jv-string {\n color: var(--jv-string-color);\n}\n.jv-number {\n color: var(--jv-number-color);\n}\n.jv-boolean {\n color: var(--jv-boolean-color);\n}\n.jv-null {\n color: var(--jv-null-color);\n}\n.jv-undefined {\n color: var(--jv-undefined-color);\n}\n.jv-function {\n color: var(--jv-function-color);\n}\n.jv-regexp {\n color: var(--jv-regexp-color);\n}\n.jv-list {\n margin-left: 16px;\n}\n.jv-item:not(:has(.jv-toggle)) .jv-key {\n margin-left: 12px;\n}\n.jv-item:not(:last-child):after {\n content: \',\';\n}\n.jv-node > .jv-ellipsis {\n display: none;\n}\n.jv-node.empty > .jv-list {\n display: inline-block;\n margin-inline: 4px;\n}\n.jv-node.collapsed > .jv-list,\n.jv-node.collapsed.empty > .jv-ellipsis {\n display: none;\n}\n.jv-node.collapsed > .jv-ellipsis {\n color: var(--jv-ellipsis-color);\n background-color: var(--jv-ellipsis-bg);\n display: inline-block;\n line-height: 0.9;\n font-size: 0.85em;\n vertical-align: 2px;\n cursor: pointer;\n user-select: none;\n padding: 2px 4px;\n margin: 0px 4px;\n border-radius: 3px;\n}\n</style>\n<div id="root" part="root"></div>\n<slot name="copy-button" part="copy-button" hidden>\n <span class="jv-copy"></span>\n</slot>\n';class n extends HTMLElement{constructor(){super(),this._value=null,this.root=this.attachShadow({mode:"open"}),this.root.appendChild(t.content.cloneNode(!0)),this.container=this.root.getElementById("root")}static get observedAttributes(){return["value","expand-depth","copyable","sort","boxed","theme","parse"]}connectedCallback(){this.render()}attributeChangedCallback(){this.render()}set value(e){e!==this._value&&(this._value=e,this.render())}get value(){return this._value??this.getAttribute("value")}get expandDepth(){return Number(this.getAttribute("expand-depth")??1)}get sort(){return this.hasAttribute("sort")}get theme(){return this.getAttribute("theme")||"light"}get parse(){return"false"!==this.getAttribute("parse")}get copyable(){if(!this.hasAttribute("copyable"))return!1;const e=this.getAttribute("copyable"),t={copyText:"Copy",copiedText:"Copied",timeout:2e3,align:"right"};if(""===e||null===e)return t;try{return{...t,...JSON.parse(e)}}catch{return t}}copyText(e){return navigator.clipboard?(this.copyText=e=>navigator.clipboard.writeText(e),this.copyText(e)):(this.copyText=e=>new Promise((t,n)=>{const o=document.createElement("input");o.value=e,document.body.appendChild(o),o.select(),document.execCommand("copy")?(document.body.removeChild(o),t()):(document.body.removeChild(o),n(new Error("Copy failed")))}),this.copyText(e))}render(){if("string"==typeof this.value&&this.parse)try{this._value=JSON.parse(this.value)}catch{}this.container.innerHTML="",this.container.appendChild(this.build(this._value,0));const e=this.copyable;if(e){const t=e.align||"right",n=this.root.querySelector('slot[name="copy-button"]'),o=n.assignedElements()[0],r=this.root.querySelector(".jv-copy");if(n.hidden=!1,n.className=`align-${t}`,!o){let t;r.textContent=e.copyText;const n=r.cloneNode(!0);r.replaceWith(n),n.textContent=e.copyText,n.addEventListener("click",()=>{const o=JSON.stringify(this._value,null,2);this.copyText(o).then(()=>{n.textContent=e.copiedText,t=window.setTimeout(()=>{n.textContent=e.copyText,clearTimeout(t)},e.timeout),this.dispatchEvent(new CustomEvent("copy-success",{detail:{text:o,options:e}}))}).catch(()=>{console.warn("Failed to copy text to clipboard"),this.dispatchEvent(new CustomEvent("copy-error",{detail:{text:o,options:e}}))})})}}}build(e,t){if(null===e)return this.leaf("null","jv-null");if(void 0===e)return this.leaf("undefined","jv-undefined");if("boolean"==typeof e)return this.leaf(String(e),"jv-boolean");if("number"==typeof e)return this.leaf(String(e),"jv-number");if("string"==typeof e)return this.leaf(`"${e}"`,"jv-string");if("function"==typeof e)return this.leaf("<function>","jv-function");if(e instanceof RegExp)return this.leaf("<regexp>","jv-regexp");if(e instanceof Date)return this.leaf(`"${e.toLocaleString()}"`,"jv-string");const n=Array.isArray(e),o=document.createElement("span");o.className="jv-node",o.setAttribute("part","node");const r=document.createElement("div");r.className="jv-list",r.setAttribute("part","list");const i=n?this.sort?[...e.keys()].sort((e,t)=>e-t):[...e.keys()]:this.sort?Object.keys(e).sort():Object.keys(e);for(const l of i){const o=document.createElement("div"),i=this.build(e[l],t+1);if(o.className="jv-item",i instanceof Element&&i.classList.contains("jv-node")){const e=i.querySelector(".jv-toggle");e&&(e.remove(),o.append(e))}if(!n){const e=document.createElement("span");e.className="jv-key",e.setAttribute("part","key"),e.textContent=`"${l}": `,o.append(e)}o.append(i),r.append(o)}const s=document.createElement("span");s.className="jv-toggle",s.setAttribute("part","toggle"),o.classList.contains("collapsed")||s.classList.add("open"),s.addEventListener("click",()=>{o.classList.toggle("collapsed"),s.classList.toggle("open"),this.dispatchEvent(new CustomEvent("toggle",{detail:{node:o,data:e,isCollapsed:o.classList.contains("collapsed")}}))});const a=document.createElement("span");return a.className="jv-ellipsis",a.setAttribute("part","ellipsis"),a.textContent=`...${i.length}`,a.addEventListener("click",()=>{o.classList.remove("collapsed"),s.classList.add("open")}),t>=this.expandDepth&&(o.classList.add("collapsed"),s.classList.remove("open")),i.length||o.classList.add("empty"),o.append(s,n?"[":"{",a,r,n?"]":"}"),o}leaf(e,t){const n=document.createElement("span");return n.className=`jv-value ${t}`,n.setAttribute("part",`value ${t.replace("jv-","")}`),n.textContent=e,n}}customElements.define("json-viewer",n),e.JsonViewerElement=n,Object.defineProperty(e,Symbol.toStringTag,{value:"Module"})});
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "json-viewer-element",
3
+ "type": "module",
4
+ "version": "1.0.0",
5
+ "packageManager": "pnpm@10.18.2",
6
+ "description": "A custom element for viewing and interacting with JSON data.",
7
+ "author": "Lruihao (https://lruihao.cn)",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/Lruihao/json-viewer-element#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Lruihao/json-viewer-element.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Lruihao/json-viewer-element/issues"
16
+ },
17
+ "keywords": [
18
+ "json-viewer",
19
+ "web-component",
20
+ "custom-element"
21
+ ],
22
+ "main": "dist/json-viewer-element.es.js",
23
+ "types": "dist/json-viewer-element.d.ts",
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "dev": "vite demo",
29
+ "build": "vite build && npm run build:dts",
30
+ "build:demo": "vite build demo --base /json-viewer-element",
31
+ "build:dts": "tsc --emitDeclarationOnly --outDir dist",
32
+ "lint": "eslint .",
33
+ "version": "auto-changelog-plus -p && git add CHANGELOG.md"
34
+ },
35
+ "devDependencies": {
36
+ "@antfu/eslint-config": "^5.4.1",
37
+ "auto-changelog-plus": "^1.2.1",
38
+ "eslint": "^9.36.0",
39
+ "terser": "^5.44.0",
40
+ "typescript": "^5.9.3",
41
+ "vite": "^7.1.9",
42
+ "vite-plugin-dts": "^4.5.4"
43
+ }
44
+ }