11import { ReactNode , useState } from 'react'
22import { cn } from '../../lib'
33import styles from './Json.module.css'
4- import { isPrimitive , shouldObjectCollapse } from './helpers.js'
4+ import { isPrimitive , shouldObjectCollapse , stringifyPrimitive } from './helpers.js'
55import { useWidth } from './useWidth.js'
66
7+ const defaultPageLimit = 100
8+
79interface JsonProps {
810 json : unknown
911 label ?: string
1012 className ?: string
1113 expandRoot ?: boolean // Expand the top-level object/array by default
14+ pageLimit ?: number // Max items to render before showing "Show more..."
1215}
1316
1417/**
1518 * JSON viewer component with collapsible objects and arrays.
1619 */
17- export default function Json ( { json, label, className, expandRoot = true } : JsonProps ) : ReactNode {
20+ export default function Json ( { json, label, className, expandRoot = true , pageLimit } : JsonProps ) : ReactNode {
1821 return < div className = { cn ( styles . json , className ) } role = "tree" >
19- < JsonContent json = { json } label = { label } expandRoot = { expandRoot } />
22+ < JsonContent json = { json } label = { label } expandRoot = { expandRoot } pageLimit = { pageLimit } />
2023 </ div >
2124}
2225
23- function JsonContent ( { json, label, expandRoot } : JsonProps ) : ReactNode {
26+ function JsonContent ( { json, label, expandRoot, pageLimit } : JsonProps ) : ReactNode {
2427 let div
2528 if ( Array . isArray ( json ) ) {
26- div = < JsonArray array = { json } label = { label } expandRoot = { expandRoot } />
29+ div = < JsonArray array = { json } label = { label } expandRoot = { expandRoot } pageLimit = { pageLimit } />
2730 } else if ( typeof json === 'object' && json !== null ) {
28- div = < JsonObject label = { label } obj = { json } expandRoot = { expandRoot } />
31+ div = < JsonObject label = { label } obj = { json } expandRoot = { expandRoot } pageLimit = { pageLimit } />
2932 } else {
3033 // primitive
3134 const key = label ? < span className = { styles . key } > { label } : </ span > : ''
@@ -52,7 +55,7 @@ function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
5255 const separator = ', '
5356
5457 const children : ReactNode [ ] = [ ]
55- let suffix : string | undefined = undefined
58+ let suffix : string | undefined
5659
5760 let characterCount = 0
5861 for ( const [ index , value ] of array . entries ( ) ) {
@@ -62,9 +65,7 @@ function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
6265 }
6366 // should we continue?
6467 if ( isPrimitive ( value ) ) {
65- const asString = typeof value === 'bigint' ? value . toString ( ) :
66- value === undefined ? 'undefined' /* see JsonContent - even if JSON.stringify([undefined]) === '[null]' */ :
67- JSON . stringify ( value )
68+ const asString = stringifyPrimitive ( value )
6869 characterCount += asString . length
6970 if ( characterCount < maxCharacterCount ) {
7071 children . push ( < JsonContent json = { value } key = { `value-${ index } ` } /> )
@@ -86,13 +87,14 @@ function CollapsedArray({ array }: {array: unknown[]}): ReactNode {
8687 )
8788}
8889
89- function JsonArray ( { array, label, expandRoot } : { array : unknown [ ] , label ?: string , expandRoot ?: boolean } ) : ReactNode {
90+ function JsonArray ( { array, label, expandRoot, pageLimit = defaultPageLimit } : { array : unknown [ ] , label ?: string , expandRoot ?: boolean , pageLimit ?: number } ) : ReactNode {
9091 const [ collapsed , setCollapsed ] = useState ( ! expandRoot && shouldObjectCollapse ( array ) )
92+ const [ limit , setLimit ] = useState ( pageLimit )
9193 const key = label ? < span className = { styles . key } > { label } : </ span > : ''
9294 if ( collapsed ) {
9395 return < div role = "treeitem" className = { styles . clickable } aria-expanded = "false" onClick = { ( ) => { setCollapsed ( false ) } } >
9496 { key }
95- < CollapsedArray array = { array } > </ CollapsedArray >
97+ < CollapsedArray array = { array } / >
9698 </ div >
9799 }
98100 return < >
@@ -101,7 +103,10 @@ function JsonArray({ array, label, expandRoot }: { array: unknown[], label?: str
101103 < span className = { styles . array } > { '[' } </ span >
102104 </ div >
103105 < ul role = "group" >
104- { array . map ( ( item , index ) => < li key = { index } > < JsonContent json = { item } /> </ li > ) }
106+ { array . slice ( 0 , limit ) . map ( ( item , index ) => < li key = { index } > < JsonContent json = { item } /> </ li > ) }
107+ { array . length > limit && < li >
108+ < button className = { styles . showMore } onClick = { ( ) => { setLimit ( limit + pageLimit ) } } > Show more...</ button >
109+ </ li > }
105110 </ ul >
106111 < div className = { styles . array } > { ']' } </ div >
107112 </ >
@@ -114,7 +119,7 @@ function CollapsedObject({ obj }: { obj: object }): ReactNode {
114119 const kvSeparator = ': '
115120
116121 const children : ReactNode [ ] = [ ]
117- let suffix : string | undefined = undefined
122+ let suffix : string | undefined
118123
119124 const entries = Object . entries ( obj )
120125 let characterCount = 0
@@ -125,9 +130,7 @@ function CollapsedObject({ obj }: { obj: object }): ReactNode {
125130 }
126131 // should we continue?
127132 if ( isPrimitive ( value ) ) {
128- const asString = typeof value === 'bigint' ? value . toString ( ) :
129- value === undefined ? 'undefined' /* see JsonContent - even if JSON.stringify([undefined]) === '[null]' */ :
130- JSON . stringify ( value )
133+ const asString = stringifyPrimitive ( value )
131134 characterCount += key . length + kvSeparator . length + asString . length
132135 if ( characterCount < maxCharacterCount ) {
133136 children . push ( < JsonContent json = { value as unknown } label = { key } key = { `value-${ index } ` } /> )
@@ -149,26 +152,31 @@ function CollapsedObject({ obj }: { obj: object }): ReactNode {
149152 )
150153}
151154
152- function JsonObject ( { obj, label, expandRoot } : { obj : object , label ?: string , expandRoot ?: boolean } ) : ReactNode {
155+ function JsonObject ( { obj, label, expandRoot, pageLimit = defaultPageLimit } : { obj : object , label ?: string , expandRoot ?: boolean , pageLimit ?: number } ) : ReactNode {
153156 const [ collapsed , setCollapsed ] = useState ( ! expandRoot && shouldObjectCollapse ( obj ) )
157+ const [ limit , setLimit ] = useState ( pageLimit )
154158 const key = label ? < span className = { styles . key } > { label } : </ span > : ''
155159 if ( collapsed ) {
156160 return < div role = "treeitem" className = { styles . clickable } aria-expanded = "false" onClick = { ( ) => { setCollapsed ( false ) } } >
157161 { key }
158162 < CollapsedObject obj = { obj } />
159163 </ div >
160164 }
165+ const entries = Object . entries ( obj )
161166 return < >
162167 < div role = "treeitem" className = { styles . clickable } aria-expanded = "true" onClick = { ( ) => { setCollapsed ( true ) } } >
163168 { key }
164169 < span className = { styles . object } > { '{' } </ span >
165170 </ div >
166171 < ul role = "group" >
167- { Object . entries ( obj ) . map ( ( [ key , value ] ) =>
172+ { entries . slice ( 0 , limit ) . map ( ( [ key , value ] ) =>
168173 < li key = { key } >
169174 < JsonContent json = { value as unknown } label = { key } />
170175 </ li >
171176 ) }
177+ { entries . length > limit && < li >
178+ < button className = { styles . showMore } onClick = { ( ) => { setLimit ( limit + pageLimit ) } } > Show more...</ button >
179+ </ li > }
172180 </ ul >
173181 < div className = { styles . object } > { '}' } </ div >
174182 </ >
0 commit comments