@@ -3,97 +3,19 @@ import clsx from 'clsx'
33import { css , useStyles } from '../styles/use-styles'
44import { CopiedCopier , Copier , ErrorCopier } from './icons'
55
6- export function JsonTree ( props : { value : any ; copyable ?: boolean } ) {
7- return < JsonValue isRoot value = { props . value } copyable = { props . copyable } />
8- }
9- type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
10-
11- const CopyButton = ( props : { value : unknown } ) => {
12- const styles = useStyles ( )
13- const [ copyState , setCopyState ] = createSignal < CopyState > ( 'NoCopy' )
14-
15- return (
16- < button
17- class = { styles ( ) . tree . actionButton }
18- title = "Copy object to clipboard"
19- aria-label = { `${
20- copyState ( ) === 'NoCopy'
21- ? 'Copy object to clipboard'
22- : copyState ( ) === 'SuccessCopy'
23- ? 'Object copied to clipboard'
24- : 'Error copying object to clipboard'
25- } `}
26- onClick = {
27- copyState ( ) === 'NoCopy'
28- ? ( ) => {
29- navigator . clipboard
30- . writeText ( JSON . stringify ( props . value , null , 2 ) )
31- . then (
32- ( ) => {
33- setCopyState ( 'SuccessCopy' )
34- setTimeout ( ( ) => {
35- setCopyState ( 'NoCopy' )
36- } , 1500 )
37- } ,
38- ( err ) => {
39- console . error ( 'Failed to copy: ' , err )
40- setCopyState ( 'ErrorCopy' )
41- setTimeout ( ( ) => {
42- setCopyState ( 'NoCopy' )
43- } , 1500 )
44- } ,
45- )
46- }
47- : undefined
48- }
49- >
50- < Switch >
51- < Match when = { copyState ( ) === 'NoCopy' } >
52- < Copier />
53- </ Match >
54- < Match when = { copyState ( ) === 'SuccessCopy' } >
55- < CopiedCopier theme = { 'dark' } />
56- </ Match >
57- < Match when = { copyState ( ) === 'ErrorCopy' } >
58- < ErrorCopier />
59- </ Match >
60- </ Switch >
61- </ button >
62- )
63- }
64-
65- const Expander = ( props : { expanded : boolean } ) => {
66- const styles = useStyles ( )
6+ export function JsonTree ( props : {
7+ value : any
8+ copyable ?: boolean
9+ defaultExpansionDepth ?: number
10+ } ) {
6711 return (
68- < span
69- class = { clsx (
70- styles ( ) . tree . expander ,
71- css `
72- transform : rotate (${ props . expanded ? 90 : 0 } deg);
73- ` ,
74- props . expanded &&
75- css `
76- & svg {
77- top : -1px ;
78- }
79- ` ,
80- ) }
81- >
82- < svg
83- width = "16"
84- height = "16"
85- viewBox = "0 0 16 16"
86- fill = "none"
87- xmlns = "http://www.w3.org/2000/svg"
88- >
89- < path
90- d = "M6 12L10 8L6 4"
91- stroke-width = "2"
92- stroke-linecap = "round"
93- stroke-linejoin = "round"
94- />
95- </ svg >
96- </ span >
12+ < JsonValue
13+ isRoot
14+ value = { props . value }
15+ copyable = { props . copyable }
16+ depth = { 0 }
17+ defaultExpansionDepth = { props . defaultExpansionDepth ?? 1 }
18+ />
9719 )
9820}
9921
@@ -103,8 +25,18 @@ function JsonValue(props: {
10325 isRoot ?: boolean
10426 isLastKey ?: boolean
10527 copyable ?: boolean
28+ defaultExpansionDepth : number
29+ depth : number
10630} ) {
107- const { value, keyName, isRoot = false , isLastKey, copyable } = props
31+ const {
32+ value,
33+ keyName,
34+ isRoot = false ,
35+ isLastKey,
36+ copyable,
37+ defaultExpansionDepth,
38+ depth,
39+ } = props
10840 const styles = useStyles ( )
10941
11042 return (
@@ -137,12 +69,24 @@ function JsonValue(props: {
13769 }
13870 if ( Array . isArray ( value ) ) {
13971 return (
140- < ArrayValue copyable = { copyable } keyName = { keyName } value = { value } />
72+ < ArrayValue
73+ defaultExpansionDepth = { defaultExpansionDepth }
74+ depth = { depth }
75+ copyable = { copyable }
76+ keyName = { keyName }
77+ value = { value }
78+ />
14179 )
14280 }
14381 if ( typeof value === 'object' ) {
14482 return (
145- < ObjectValue copyable = { copyable } keyName = { keyName } value = { value } />
83+ < ObjectValue
84+ defaultExpansionDepth = { defaultExpansionDepth }
85+ depth = { depth }
86+ copyable = { copyable }
87+ keyName = { keyName }
88+ value = { value }
89+ />
14690 )
14791 }
14892 return < span />
@@ -161,16 +105,36 @@ const ArrayValue = ({
161105 value,
162106 keyName,
163107 copyable,
108+ defaultExpansionDepth,
109+ depth,
164110} : {
165111 value : Array < any >
166112 copyable ?: boolean
167113 keyName ?: string
114+ defaultExpansionDepth : number
115+ depth : number
168116} ) => {
169117 const styles = useStyles ( )
170- const [ expanded , setExpanded ] = createSignal ( true )
118+ const [ expanded , setExpanded ] = createSignal ( depth <= defaultExpansionDepth )
119+
120+ if ( value . length === 0 ) {
121+ return (
122+ < span class = { styles ( ) . tree . expanderContainer } >
123+ { keyName && (
124+ < span class = { clsx ( styles ( ) . tree . valueKey , styles ( ) . tree . collapsible ) } >
125+ "{ keyName } ":{ ' ' }
126+ </ span >
127+ ) }
128+ < span class = { styles ( ) . tree . valueBraces } > []</ span >
129+ </ span >
130+ )
131+ }
171132 return (
172133 < span class = { styles ( ) . tree . expanderContainer } >
173- < Expander expanded = { expanded ( ) } />
134+ < Expander
135+ onClick = { ( ) => setExpanded ( ! expanded ( ) ) }
136+ expanded = { expanded ( ) }
137+ />
174138 { keyName && (
175139 < span
176140 onclick = { ( e ) => {
@@ -195,6 +159,8 @@ const ArrayValue = ({
195159 copyable = { copyable }
196160 value = { item }
197161 isLastKey = { isLastKey }
162+ defaultExpansionDepth = { defaultExpansionDepth }
163+ depth = { depth + 1 }
198164 />
199165 )
200166 } }
@@ -222,19 +188,40 @@ const ObjectValue = ({
222188 value,
223189 keyName,
224190 copyable,
191+ defaultExpansionDepth,
192+ depth,
225193} : {
226194 value : Record < string , any >
227195 keyName ?: string
228196 copyable ?: boolean
197+ defaultExpansionDepth : number
198+ depth : number
229199} ) => {
230200 const styles = useStyles ( )
231- const [ expanded , setExpanded ] = createSignal ( true )
201+ const [ expanded , setExpanded ] = createSignal ( depth <= defaultExpansionDepth )
232202 const keys = Object . keys ( value )
233203 const lastKeyName = keys [ keys . length - 1 ]
234204
205+ if ( keys . length === 0 ) {
206+ return (
207+ < span class = { styles ( ) . tree . expanderContainer } >
208+ { keyName && (
209+ < span class = { clsx ( styles ( ) . tree . valueKey , styles ( ) . tree . collapsible ) } >
210+ "{ keyName } ":{ ' ' }
211+ </ span >
212+ ) }
213+ < span class = { styles ( ) . tree . valueBraces } > { '{}' } </ span >
214+ </ span >
215+ )
216+ }
235217 return (
236218 < span class = { styles ( ) . tree . expanderContainer } >
237- { keyName && < Expander expanded = { expanded ( ) } /> }
219+ { keyName && (
220+ < Expander
221+ onClick = { ( ) => setExpanded ( ! expanded ( ) ) }
222+ expanded = { expanded ( ) }
223+ />
224+ ) }
238225 { keyName && (
239226 < span
240227 onClick = { ( e ) => {
@@ -259,6 +246,8 @@ const ObjectValue = ({
259246 keyName = { k }
260247 isLastKey = { lastKeyName === k }
261248 copyable = { copyable }
249+ defaultExpansionDepth = { defaultExpansionDepth }
250+ depth = { depth + 1 }
262251 />
263252 </ >
264253 ) }
@@ -281,3 +270,95 @@ const ObjectValue = ({
281270 </ span >
282271 )
283272}
273+
274+ type CopyState = 'NoCopy' | 'SuccessCopy' | 'ErrorCopy'
275+
276+ const CopyButton = ( props : { value : unknown } ) => {
277+ const styles = useStyles ( )
278+ const [ copyState , setCopyState ] = createSignal < CopyState > ( 'NoCopy' )
279+
280+ return (
281+ < button
282+ class = { styles ( ) . tree . actionButton }
283+ title = "Copy object to clipboard"
284+ aria-label = { `${
285+ copyState ( ) === 'NoCopy'
286+ ? 'Copy object to clipboard'
287+ : copyState ( ) === 'SuccessCopy'
288+ ? 'Object copied to clipboard'
289+ : 'Error copying object to clipboard'
290+ } `}
291+ onClick = {
292+ copyState ( ) === 'NoCopy'
293+ ? ( ) => {
294+ navigator . clipboard
295+ . writeText ( JSON . stringify ( props . value , null , 2 ) )
296+ . then (
297+ ( ) => {
298+ setCopyState ( 'SuccessCopy' )
299+ setTimeout ( ( ) => {
300+ setCopyState ( 'NoCopy' )
301+ } , 1500 )
302+ } ,
303+ ( err ) => {
304+ console . error ( 'Failed to copy: ' , err )
305+ setCopyState ( 'ErrorCopy' )
306+ setTimeout ( ( ) => {
307+ setCopyState ( 'NoCopy' )
308+ } , 1500 )
309+ } ,
310+ )
311+ }
312+ : undefined
313+ }
314+ >
315+ < Switch >
316+ < Match when = { copyState ( ) === 'NoCopy' } >
317+ < Copier />
318+ </ Match >
319+ < Match when = { copyState ( ) === 'SuccessCopy' } >
320+ < CopiedCopier theme = { 'dark' } />
321+ </ Match >
322+ < Match when = { copyState ( ) === 'ErrorCopy' } >
323+ < ErrorCopier />
324+ </ Match >
325+ </ Switch >
326+ </ button >
327+ )
328+ }
329+
330+ const Expander = ( props : { expanded : boolean ; onClick : ( ) => void } ) => {
331+ const styles = useStyles ( )
332+ return (
333+ < span
334+ onClick = { props . onClick }
335+ class = { clsx (
336+ styles ( ) . tree . expander ,
337+ css `
338+ transform : rotate (${ props . expanded ? 90 : 0 } deg);
339+ ` ,
340+ props . expanded &&
341+ css `
342+ & svg {
343+ top : -1px ;
344+ }
345+ ` ,
346+ ) }
347+ >
348+ < svg
349+ width = "16"
350+ height = "16"
351+ viewBox = "0 0 16 16"
352+ fill = "none"
353+ xmlns = "http://www.w3.org/2000/svg"
354+ >
355+ < path
356+ d = "M6 12L10 8L6 4"
357+ stroke-width = "2"
358+ stroke-linecap = "round"
359+ stroke-linejoin = "round"
360+ />
361+ </ svg >
362+ </ span >
363+ )
364+ }
0 commit comments