@gfazioli/mantine-json-tree 2.0.2 → 2.1.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.
@@ -1,8 +1,9 @@
1
1
  'use client';
2
- import React, { useMemo } from 'react';
2
+ import React, { useMemo, useEffect, useCallback } from 'react';
3
3
  import { IconCopy, IconLibraryMinus, IconLibraryPlus, IconChevronRight } from '@tabler/icons-react';
4
- import { createVarsResolver, rem, getFontSize, factory, useProps, useStyles, useMantineTheme, getTreeExpandedState, useTree, Box, Group, ActionIcon, Tree, Text, Code, Badge } from '@mantine/core';
5
- import { convertToTreeData, isExpandable, formatValue } from './lib/utils.mjs';
4
+ import { createVarsResolver, rem, factory, useProps, useStyles, useRandomClassName, getTreeExpandedState, useTree, Box, Group, ActionIcon, ScrollArea, Tree, Text, Code, Badge, Tooltip } from '@mantine/core';
5
+ import { JsonTreeMediaVariables } from './JsonTreeMediaVariables.mjs';
6
+ import { convertToTreeData, findNodeByPath, isExpandable, formatValue } from './lib/utils.mjs';
6
7
  import classes from './JsonTree.module.css.mjs';
7
8
 
8
9
  const defaultProps = {
@@ -12,18 +13,210 @@ const defaultProps = {
12
13
  showItemsCount: false,
13
14
  withCopyToClipboard: false,
14
15
  showIndentGuides: false,
16
+ showLineNumbers: false,
17
+ showPathOnHover: false,
15
18
  stickyHeader: false,
16
19
  displayFunctions: "as-string",
17
20
  expandAllControlIcon: /* @__PURE__ */ React.createElement(IconLibraryPlus, { size: 16 }),
18
21
  collapseAllControlIcon: /* @__PURE__ */ React.createElement(IconLibraryMinus, { size: 16 }),
19
22
  copyToClipboardIcon: /* @__PURE__ */ React.createElement(IconCopy, { size: 12 })
20
23
  };
24
+ function renderJSONNode({ node, expanded, hasChildren, elementProps, tree }, props, ctx, onNodeClick) {
25
+ const {
26
+ getStyles,
27
+ copyToClipboardIcon,
28
+ expandControlIcon,
29
+ collapseControlIcon,
30
+ onExpand,
31
+ onCollapse,
32
+ onExpandedChange
33
+ } = ctx;
34
+ const jsonNode = node;
35
+ const {
36
+ type,
37
+ value,
38
+ key,
39
+ path,
40
+ itemCount,
41
+ depth = 0
42
+ } = jsonNode.nodeData || {
43
+ type: "null",
44
+ value: null,
45
+ path: "unknown",
46
+ depth: 0
47
+ };
48
+ const {
49
+ showItemsCount,
50
+ withCopyToClipboard,
51
+ onCopy,
52
+ showIndentGuides,
53
+ showLineNumbers,
54
+ showPathOnHover,
55
+ tooltipProps
56
+ } = props;
57
+ const handleCopy = async (e) => {
58
+ e.stopPropagation();
59
+ try {
60
+ const copy = JSON.stringify(value, null, 2);
61
+ await navigator.clipboard.writeText(copy);
62
+ onCopy?.(copy, value);
63
+ } catch (error) {
64
+ }
65
+ };
66
+ const handleClick = () => {
67
+ if (onNodeClick) {
68
+ onNodeClick(path, value);
69
+ }
70
+ };
71
+ const handleToggleExpanded = (e) => {
72
+ e.stopPropagation();
73
+ if (expanded) {
74
+ onCollapse?.(node.value);
75
+ } else {
76
+ onExpand?.(node.value);
77
+ }
78
+ if (onExpandedChange) {
79
+ const newState = { ...tree.expandedState, [node.value]: !expanded };
80
+ onExpandedChange(Object.keys(newState).filter((k) => newState[k]));
81
+ } else {
82
+ tree.toggleExpanded(node.value);
83
+ }
84
+ };
85
+ const renderIndentGuides = () => {
86
+ if (!showIndentGuides || depth === 0) {
87
+ return null;
88
+ }
89
+ const guides = [];
90
+ for (let i = 0; i < depth; i++) {
91
+ const colorIndex = i % 5;
92
+ guides.push(
93
+ /* @__PURE__ */ React.createElement(
94
+ "div",
95
+ {
96
+ key: i,
97
+ ...getStyles("indentGuide", {
98
+ style: {
99
+ left: `${i * 32 + 8}px`
100
+ }
101
+ }),
102
+ "data-color-index": colorIndex
103
+ }
104
+ )
105
+ );
106
+ }
107
+ return guides;
108
+ };
109
+ const lineNumber = showLineNumbers ? /* @__PURE__ */ React.createElement("span", { ...getStyles("lineNumber") }) : null;
110
+ const wrapWithTooltip = (content) => showPathOnHover ? /* @__PURE__ */ React.createElement(Tooltip, { label: path, position: "top-start", withArrow: true, openDelay: 300, ...tooltipProps }, content) : content;
111
+ if (!hasChildren) {
112
+ return wrapWithTooltip(
113
+ /* @__PURE__ */ React.createElement(
114
+ Group,
115
+ {
116
+ gap: 4,
117
+ wrap: "nowrap",
118
+ ...elementProps,
119
+ onClick: handleClick,
120
+ style: { cursor: onNodeClick ? "pointer" : "default", position: "relative" }
121
+ },
122
+ lineNumber,
123
+ renderIndentGuides(),
124
+ key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("key"), "data-key": key }, key), /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
125
+ (() => {
126
+ const formattedValue = formatValue(value, type);
127
+ return /* @__PURE__ */ React.createElement(Code, { ...getStyles("value"), "data-type": type, "data-value": formattedValue }, formattedValue);
128
+ })(),
129
+ withCopyToClipboard && /* @__PURE__ */ React.createElement(
130
+ ActionIcon,
131
+ {
132
+ size: "xs",
133
+ variant: "subtle",
134
+ color: "gray",
135
+ onClick: handleCopy,
136
+ ...getStyles("copyButton")
137
+ },
138
+ copyToClipboardIcon
139
+ )
140
+ )
141
+ );
142
+ }
143
+ const openBracket = type === "array" ? "[" : "{";
144
+ const closeBracket = type === "array" ? "]" : "}";
145
+ const expandCollapseIcon = (() => {
146
+ if (!expandControlIcon && !collapseControlIcon) {
147
+ return /* @__PURE__ */ React.createElement(
148
+ IconChevronRight,
149
+ {
150
+ size: 14,
151
+ style: {
152
+ transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
153
+ transition: "transform 0.2s ease"
154
+ }
155
+ }
156
+ );
157
+ }
158
+ if (expandControlIcon && !collapseControlIcon) {
159
+ return React.cloneElement(expandControlIcon, {
160
+ style: {
161
+ ...expandControlIcon.props?.style,
162
+ transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
163
+ transition: "transform 0.2s ease"
164
+ }
165
+ });
166
+ }
167
+ if (!expandControlIcon && collapseControlIcon) {
168
+ return expanded ? collapseControlIcon : /* @__PURE__ */ React.createElement(IconChevronRight, { size: 14 });
169
+ }
170
+ return expanded ? collapseControlIcon : expandControlIcon;
171
+ })();
172
+ return wrapWithTooltip(
173
+ /* @__PURE__ */ React.createElement(
174
+ Group,
175
+ {
176
+ gap: 4,
177
+ wrap: "nowrap",
178
+ ...elementProps,
179
+ onClick: handleClick,
180
+ "data-expanded": expanded,
181
+ "data-has-children": hasChildren,
182
+ "data-type": type,
183
+ style: { cursor: onNodeClick ? "pointer" : "default", position: "relative" }
184
+ },
185
+ lineNumber,
186
+ renderIndentGuides(),
187
+ /* @__PURE__ */ React.createElement(
188
+ ActionIcon,
189
+ {
190
+ size: "xs",
191
+ variant: "subtle",
192
+ onClick: handleToggleExpanded,
193
+ ...getStyles("expandCollapse")
194
+ },
195
+ expandCollapseIcon
196
+ ),
197
+ key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("key") }, key), /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
198
+ /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("bracket") }, openBracket),
199
+ !expanded && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { component: "span", size: "xs", ...getStyles("ellipsis") }, "..."), /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("bracket") }, closeBracket), itemCount !== void 0 && showItemsCount && /* @__PURE__ */ React.createElement(Badge, { size: "xs", variant: "light", color: "gray", ...getStyles("itemsCount") }, itemCount)),
200
+ withCopyToClipboard && /* @__PURE__ */ React.createElement(
201
+ ActionIcon,
202
+ {
203
+ size: "xs",
204
+ variant: "subtle",
205
+ color: "gray",
206
+ onClick: handleCopy,
207
+ ...getStyles("copyButton")
208
+ },
209
+ copyToClipboardIcon
210
+ )
211
+ )
212
+ );
213
+ }
21
214
  const varsResolver = createVarsResolver(
22
- (_, { size, stickyHeader, stickyHeaderOffset }) => {
215
+ (_, { stickyHeader, stickyHeaderOffset }) => {
23
216
  return {
24
217
  root: {
25
218
  "--json-tree-font-family": "var(--mantine-font-family-monospace)",
26
- "--json-tree-font-size": getFontSize(size) || "var(--mantine-font-size-xs)"
219
+ "--json-tree-font-size": void 0
27
220
  },
28
221
  header: {
29
222
  "--json-tree-header-background-color": "inherit",
@@ -58,7 +251,8 @@ const varsResolver = createVarsResolver(
58
251
  },
59
252
  expandCollapse: {},
60
253
  keyValueSeparator: {},
61
- ellipsis: {},
254
+ ellipsis: { "--json-tree-color-ellipsis": "var(--mantine-color-dark-3)" },
255
+ lineNumber: { "--json-tree-color-line-number": "var(--mantine-color-gray-5)" },
62
256
  itemsCount: {},
63
257
  controls: {},
64
258
  copyButton: {}
@@ -73,12 +267,19 @@ const JsonTree = factory((_props, ref) => {
73
267
  maxDepth,
74
268
  onNodeClick,
75
269
  onCopy,
270
+ onExpand,
271
+ onCollapse,
76
272
  withExpandAll,
77
- variant,
78
273
  title,
79
274
  showItemsCount,
80
275
  withCopyToClipboard,
81
276
  showIndentGuides,
277
+ showLineNumbers,
278
+ showPathOnHover,
279
+ tooltipProps,
280
+ maxHeight,
281
+ expanded: controlledExpanded,
282
+ onExpandedChange,
82
283
  stickyHeaderOffset,
83
284
  stickyHeader,
84
285
  displayFunctions,
@@ -87,6 +288,7 @@ const JsonTree = factory((_props, ref) => {
87
288
  copyToClipboardIcon,
88
289
  expandControlIcon,
89
290
  collapseControlIcon,
291
+ size,
90
292
  classNames,
91
293
  style,
92
294
  styles,
@@ -107,12 +309,19 @@ const JsonTree = factory((_props, ref) => {
107
309
  vars,
108
310
  varsResolver
109
311
  });
110
- const theme = useMantineTheme();
312
+ const responsiveClassName = useRandomClassName();
111
313
  const treeData = useMemo(
112
314
  () => [convertToTreeData(data, void 0, "root", 0, displayFunctions)],
113
315
  [data, displayFunctions]
114
316
  );
115
317
  const initialExpandedState = useMemo(() => {
318
+ if (controlledExpanded) {
319
+ const state = {};
320
+ controlledExpanded.forEach((path) => {
321
+ state[path] = true;
322
+ });
323
+ return state;
324
+ }
116
325
  if (defaultExpanded) {
117
326
  if (maxDepth === -1) {
118
327
  return getTreeExpandedState(treeData, "*");
@@ -130,190 +339,104 @@ const JsonTree = factory((_props, ref) => {
130
339
  return getTreeExpandedState(treeData, expandedNodes);
131
340
  }
132
341
  return {};
133
- }, [treeData, defaultExpanded, maxDepth]);
342
+ }, [treeData, defaultExpanded, maxDepth, controlledExpanded]);
134
343
  const tree = useTree({
135
344
  initialExpandedState
136
345
  });
137
- function renderJSONNode({ node, expanded, hasChildren, elementProps, tree: tree2 }, _, props2, onNodeClick2) {
138
- const jsonNode = node;
139
- const {
140
- type,
141
- value,
142
- key,
143
- path,
144
- itemCount,
145
- depth = 0
146
- } = jsonNode.nodeData || {
147
- type: "null",
148
- value: null,
149
- path: "unknown",
150
- depth: 0
151
- };
152
- const { showItemsCount: showItemsCount2, withCopyToClipboard: withCopyToClipboard2, onCopy: onCopy2, showIndentGuides: showIndentGuides2 } = props2;
153
- const handleCopy = async (e) => {
154
- e.stopPropagation();
155
- try {
156
- const copy = JSON.stringify(value, null, 2);
157
- await navigator.clipboard.writeText(copy);
158
- onCopy2?.(copy, value);
159
- } catch (error) {
160
- }
161
- };
162
- const handleClick = () => {
163
- if (onNodeClick2) {
164
- onNodeClick2(path, value);
165
- }
166
- };
167
- const renderIndentGuides = () => {
168
- if (!showIndentGuides2 || depth === 0) {
169
- return null;
170
- }
171
- const guides = [];
172
- for (let i = 0; i < depth; i++) {
173
- const colorIndex = i % 5;
174
- guides.push(
175
- /* @__PURE__ */ React.createElement(
176
- "div",
177
- {
178
- key: i,
179
- ...getStyles("indentGuide", {
180
- style: {
181
- left: `${i * 32 + 8}px`
182
- }
183
- }),
184
- "data-color-index": colorIndex
185
- }
186
- )
187
- );
188
- }
189
- return guides;
190
- };
191
- if (!hasChildren) {
192
- return /* @__PURE__ */ React.createElement(
193
- Group,
194
- {
195
- gap: 4,
196
- wrap: "nowrap",
197
- ...elementProps,
198
- onClick: handleClick,
199
- style: { cursor: onNodeClick2 ? "pointer" : "default", position: "relative" }
200
- },
201
- renderIndentGuides(),
202
- key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("key"), "data-key": key }, key), /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
203
- (() => {
204
- const formattedValue = formatValue(value, type);
205
- return /* @__PURE__ */ React.createElement(Code, { ...getStyles("value"), "data-type": type, "data-value": formattedValue }, formattedValue);
206
- })(),
207
- withCopyToClipboard2 && /* @__PURE__ */ React.createElement(
208
- ActionIcon,
209
- {
210
- size: "xs",
211
- variant: "subtle",
212
- color: "gray",
213
- onClick: handleCopy,
214
- ...getStyles("copyButton")
215
- },
216
- copyToClipboardIcon
217
- )
218
- );
346
+ useEffect(() => {
347
+ if (controlledExpanded) {
348
+ const state = {};
349
+ controlledExpanded.forEach((path) => {
350
+ state[path] = true;
351
+ });
352
+ tree.setExpandedState(state);
219
353
  }
220
- const openBracket = type === "array" ? "[" : "{";
221
- const closeBracket = type === "array" ? "]" : "}";
222
- const expandCollapseIcon = (() => {
223
- if (!expandControlIcon && !collapseControlIcon) {
224
- return /* @__PURE__ */ React.createElement(
225
- IconChevronRight,
226
- {
227
- size: 14,
228
- style: {
229
- transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
230
- transition: "transform 0.2s ease"
231
- }
232
- }
354
+ }, [controlledExpanded, tree.setExpandedState]);
355
+ const handleKeyDown = useCallback(
356
+ async (e) => {
357
+ if ((e.metaKey || e.ctrlKey) && e.key === "c" && withCopyToClipboard) {
358
+ e.preventDefault();
359
+ const focused = e.currentTarget.querySelector(
360
+ '[data-value][tabindex="0"], [data-value]:focus'
233
361
  );
234
- }
235
- if (expandControlIcon && !collapseControlIcon) {
236
- return React.cloneElement(expandControlIcon, {
237
- style: {
238
- ...expandControlIcon.props?.style,
239
- transform: expanded ? "rotate(90deg)" : "rotate(0deg)",
240
- transition: "transform 0.2s ease"
362
+ if (focused) {
363
+ const nodePath = focused.getAttribute("data-value");
364
+ if (nodePath) {
365
+ const nodeData = findNodeByPath(treeData, nodePath);
366
+ if (nodeData?.nodeData) {
367
+ try {
368
+ const copy = JSON.stringify(nodeData.nodeData.value, null, 2);
369
+ await navigator.clipboard.writeText(copy);
370
+ onCopy?.(copy, nodeData.nodeData.value);
371
+ } catch {
372
+ }
373
+ }
241
374
  }
242
- });
243
- }
244
- if (!expandControlIcon && collapseControlIcon) {
245
- return expanded ? collapseControlIcon : /* @__PURE__ */ React.createElement(IconChevronRight, { size: 14 });
375
+ }
246
376
  }
247
- return expanded ? collapseControlIcon : expandControlIcon;
248
- })();
249
- return /* @__PURE__ */ React.createElement(
250
- Group,
251
- {
252
- gap: 4,
253
- wrap: "nowrap",
254
- ...elementProps,
255
- onClick: handleClick,
256
- "data-expanded": expanded,
257
- "data-has-children": hasChildren,
258
- "data-type": type,
259
- style: { cursor: onNodeClick2 ? "pointer" : "default", position: "relative" }
260
- },
261
- renderIndentGuides(),
262
- /* @__PURE__ */ React.createElement(
263
- ActionIcon,
264
- {
265
- size: "xs",
266
- variant: "subtle",
267
- onClick: (e) => {
268
- e.stopPropagation();
269
- tree2.toggleExpanded(node.value);
270
- },
271
- ...getStyles("expandCollapse")
272
- },
273
- expandCollapseIcon
274
- ),
275
- key !== void 0 && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("key") }, key), /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("keyValueSeparator") }, ":")),
276
- /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("bracket") }, openBracket),
277
- !expanded && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Text, { component: "span", size: "xs", ...getStyles("ellipsis") }, "..."), /* @__PURE__ */ React.createElement(Text, { component: "span", ...getStyles("bracket") }, closeBracket), itemCount !== void 0 && showItemsCount2 && /* @__PURE__ */ React.createElement(Badge, { size: "xs", variant: "light", color: "gray", ...getStyles("itemsCount") }, itemCount)),
278
- withCopyToClipboard2 && /* @__PURE__ */ React.createElement(
279
- ActionIcon,
280
- {
281
- size: "xs",
282
- variant: "subtle",
283
- color: "gray",
284
- onClick: handleCopy,
285
- ...getStyles("copyButton")
286
- },
287
- copyToClipboardIcon
288
- )
289
- );
290
- }
291
- return /* @__PURE__ */ React.createElement(Box, { ref, ...getStyles("root"), ...others }, (title || withExpandAll) && /* @__PURE__ */ React.createElement(Group, { ...getStyles("header"), justify: "space-between", mod: { sticky: stickyHeader } }, title || /* @__PURE__ */ React.createElement("div", null), withExpandAll && isExpandable(data) && /* @__PURE__ */ React.createElement(Group, { gap: "xs", style: { top: 10, zIndex: 1 } }, /* @__PURE__ */ React.createElement(
292
- ActionIcon,
293
- {
294
- size: "xs",
295
- variant: "transparent",
296
- onClick: () => tree.expandAllNodes(),
297
- ...getStyles("controls")
298
377
  },
299
- expandAllControlIcon
300
- ), /* @__PURE__ */ React.createElement(
301
- ActionIcon,
302
- {
303
- size: "xs",
304
- variant: "transparent",
305
- onClick: () => tree.collapseAllNodes(),
306
- ...getStyles("controls")
307
- },
308
- collapseAllControlIcon
309
- ))), /* @__PURE__ */ React.createElement(
378
+ [withCopyToClipboard, treeData, onCopy]
379
+ );
380
+ const renderCtx = {
381
+ getStyles,
382
+ copyToClipboardIcon,
383
+ expandControlIcon,
384
+ collapseControlIcon,
385
+ onExpand,
386
+ onCollapse,
387
+ onExpandedChange
388
+ };
389
+ const treeComponent = /* @__PURE__ */ React.createElement(
310
390
  Tree,
311
391
  {
312
392
  data: treeData,
313
393
  tree,
314
394
  levelOffset: 32,
315
- renderNode: (payload) => renderJSONNode(payload, theme, props, onNodeClick)
395
+ renderNode: (payload) => renderJSONNode(payload, props, renderCtx, onNodeClick)
316
396
  }
397
+ );
398
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(JsonTreeMediaVariables, { size, selector: `.${responsiveClassName}` }), /* @__PURE__ */ React.createElement(
399
+ Box,
400
+ {
401
+ ref,
402
+ ...getStyles("root", { className: responsiveClassName }),
403
+ ...others,
404
+ "data-line-numbers": showLineNumbers || void 0,
405
+ onKeyDown: handleKeyDown
406
+ },
407
+ (title || withExpandAll) && /* @__PURE__ */ React.createElement(Group, { ...getStyles("header"), justify: "space-between", mod: { sticky: stickyHeader } }, title || /* @__PURE__ */ React.createElement("div", null), withExpandAll && isExpandable(data) && /* @__PURE__ */ React.createElement(Group, { gap: "xs", style: { top: 10, zIndex: 1 } }, /* @__PURE__ */ React.createElement(
408
+ ActionIcon,
409
+ {
410
+ size: "xs",
411
+ variant: "transparent",
412
+ onClick: () => {
413
+ const allState = getTreeExpandedState(treeData, "*");
414
+ if (onExpandedChange) {
415
+ onExpandedChange(Object.keys(allState).filter((k) => allState[k]));
416
+ } else {
417
+ tree.expandAllNodes();
418
+ }
419
+ },
420
+ ...getStyles("controls")
421
+ },
422
+ expandAllControlIcon
423
+ ), /* @__PURE__ */ React.createElement(
424
+ ActionIcon,
425
+ {
426
+ size: "xs",
427
+ variant: "transparent",
428
+ onClick: () => {
429
+ if (onExpandedChange) {
430
+ onExpandedChange([]);
431
+ } else {
432
+ tree.collapseAllNodes();
433
+ }
434
+ },
435
+ ...getStyles("controls")
436
+ },
437
+ collapseAllControlIcon
438
+ ))),
439
+ maxHeight ? /* @__PURE__ */ React.createElement(ScrollArea.Autosize, { mah: maxHeight }, treeComponent) : treeComponent
317
440
  ));
318
441
  });
319
442
  JsonTree.classes = classes;