Line data Source code
1 : import * as React from 'react';
2 : import * as styles from './json_view.module.css';
3 :
4 : import { useNestedExpansionDispatch, useNestedExpansionState } from './json_nested_state.js';
5 : import { useJsonViewerState } from './json_view_state.js';
6 : import { JsonCopyButton } from './json_copy_button.js';
7 : import { JsonItemCount } from './json_item_count.js';
8 : import { JsonArrowSymbol } from './json_arrow_symbol.js';
9 : import { JsonEllipsisSymbol } from './json_ellipsis_symbol.js';
10 : import { JsonKeyName } from './json_key_name.js';
11 :
12 : export interface NestedOpenProps {
13 : expandKey: string;
14 : keyName?: (string | number);
15 : keyPath: (string | number)[];
16 : level: number;
17 : value: object;
18 : initialValue: object;
19 : parentValue?: object;
20 : }
21 :
22 1 : export function JsonNestedOpen(props: NestedOpenProps) {
23 : // Is the node expanded?
24 1 : const nodeExpansions = useNestedExpansionState();
25 1 : const dispatchExpands = useNestedExpansionDispatch();
26 1 : const { variant, onExpand, collapsed, shouldExpandNodeInitially } = useJsonViewerState();
27 1 : const defaultExpanded =
28 1 : typeof collapsed === 'boolean' ? !collapsed : typeof collapsed === 'number' ? props.level <= collapsed : true;
29 1 : let isExpanded = nodeExpansions[props.expandKey] ?? (shouldExpandNodeInitially ? true : defaultExpanded);
30 1 : const shouldExpand =
31 1 : shouldExpandNodeInitially && shouldExpandNodeInitially(isExpanded, {
32 1 : level: props.level,
33 1 : keyName: props.keyName,
34 1 : keyPath: props.keyPath,
35 1 : value: props.value,
36 1 : parentValue: props.parentValue
37 1 : });
38 1 : if (nodeExpansions[props.expandKey] === undefined && shouldExpandNodeInitially) {
39 1 : isExpanded = shouldExpand === true;
40 : }
41 :
42 : // Click handler
43 1 : const click = () => {
44 1 : const opt = { expand: !isExpanded, value: props.value, keyName: props.keyName };
45 1 : onExpand && onExpand(opt);
46 1 : dispatchExpands({ [props.expandKey]: opt.expand });
47 1 : };
48 :
49 1 : const style: React.CSSProperties = { display: 'inline-flex', alignItems: 'center' };
50 1 : const len = Object.keys(props.value ?? {}).length;
51 1 : const isObject = typeof props.value === 'object';
52 1 : const isArray = Array.isArray(props.value);
53 1 : const isMySet = props.value instanceof Set;
54 1 : const showArrow = len !== 0 && (isArray || isMySet || isObject);
55 1 : const childProps = {
56 1 : keyName: props.keyName,
57 1 : keyPath: props.keyPath,
58 1 : value: props.value,
59 1 : parentValue: props.parentValue
60 1 : };
61 1 : if (variant === 'explorer') {
62 : // The whole row toggles the node, the arrow button stays the accessible control
63 1 : return (
64 1 : <div className={styles.explorer_row} onClick={showArrow ? click : undefined}>
65 1 : {showArrow ? (
66 1 : <button
67 1 : type="button"
68 1 : className={styles.expand_button}
69 1 : aria-label={isExpanded ? 'Collapse value' : 'Expand value'}
70 1 : aria-expanded={isExpanded}
71 1 : onClick={(e) => { e.stopPropagation(); click(); }}
72 : >
73 1 : <JsonArrowSymbol isExpanded={isExpanded} />
74 1 : </button>
75 : ) : (
76 1 : <span className={styles.explorer_arrow_spacer} />
77 : )}
78 1 : {(props.keyName || typeof props.keyName === 'number') && (
79 1 : <JsonKeyName {...childProps} />
80 : )}
81 1 : <ExplorerSummary value={props.value} initialValue={props.initialValue} />
82 1 : <JsonCopyButton
83 1 : keyName={props.keyName!}
84 1 : value={props.value}
85 1 : expandKey={props.expandKey}
86 1 : parentValue={props.parentValue}
87 1 : keyPath={props.keyPath}
88 1 : />
89 1 : </div>
90 : );
91 : }
92 1 : return (
93 1 : <span style={style}>
94 1 : {showArrow && (
95 1 : <button
96 1 : type="button"
97 1 : className={styles.expand_button}
98 1 : aria-label={isExpanded ? 'Collapse JSON value' : 'Expand JSON value'}
99 1 : aria-expanded={isExpanded}
100 1 : onClick={click}
101 : >
102 1 : <JsonArrowSymbol isExpanded={isExpanded} />
103 1 : </button>
104 : )}
105 1 : {(props.keyName || typeof props.keyName === 'number') && (
106 1 : <JsonKeyName {...childProps} />
107 : )}
108 1 : <SetHeader value={props.initialValue} />
109 1 : <MapHeader value={props.initialValue} />
110 1 : <JsonBracketsOpen isBrackets={isArray || isMySet} />
111 1 : <JsonEllipsisSymbol
112 1 : keyName={props.keyName!}
113 1 : value={props.value}
114 1 : isExpanded={isExpanded}
115 1 : />
116 1 : <JsonBracketsClose isVisible={!isExpanded || !showArrow} isBrackets={isArray || isMySet} />
117 1 : <JsonItemCount value={props.value} />
118 1 : <JsonCopyButton
119 1 : keyName={props.keyName!}
120 1 : value={props.value}
121 1 : expandKey={props.expandKey}
122 1 : parentValue={props.parentValue}
123 1 : keyPath={props.keyPath}
124 1 : />
125 1 : </span>
126 : );
127 1 : };
128 :
129 : /// The explorer describes nested values instead of showing their brackets
130 1 : function ExplorerSummary(props: { value: object; initialValue: object }) {
131 1 : const len = Object.keys(props.value ?? {}).length;
132 1 : if (len === 0) {
133 1 : return <span className={styles.explorer_placeholder}>empty</span>;
134 : }
135 1 : let noun: string;
136 1 : if (props.initialValue instanceof Map) {
137 0 : noun = len === 1 ? 'entry' : 'entries';
138 1 : } else if (Array.isArray(props.value) || props.initialValue instanceof Set) {
139 1 : noun = len === 1 ? 'item' : 'items';
140 : } else {
141 1 : noun = len === 1 ? 'field' : 'fields';
142 : }
143 1 : return <span className={styles.explorer_summary}>{len} {noun}</span>;
144 1 : }
145 :
146 1 : function SetHeader(props: { value: unknown; }) {
147 1 : const isSet = props.value instanceof Set;
148 1 : if (!isSet) return null;
149 0 : return <span className={styles.object_header_set} data-type="set">Set</span>;
150 1 : };
151 :
152 1 : function MapHeader(props: { value: unknown; }) {
153 1 : const isMap = props.value instanceof Map;
154 1 : if (!isMap) return null;
155 0 : return <span className={styles.object_header_set} data-type="map">Map</span>;
156 1 : };
157 :
158 1 : export function JsonBracketsOpen(props: { isBrackets: boolean }) {
159 1 : return props.isBrackets
160 : ? (
161 1 : <span className={styles.object_square_brackets_open}>
162 : [
163 1 : </span>
164 : )
165 : : (
166 1 : <span className={styles.object_curly_brackets_open}>
167 1 : {'{'}
168 1 : </span>
169 : );
170 1 : };
171 :
172 1 : export function JsonBracketsClose(props: { isBrackets: boolean, isVisible: boolean }) {
173 1 : if (!props.isVisible) return null;
174 1 : return props.isBrackets
175 : ? (
176 1 : <span className={styles.object_curly_brackets_open}>
177 : ]
178 1 : </span>
179 : )
180 : : (
181 1 : <span className={styles.object_curly_brackets_close}>
182 1 : {'}'}
183 1 : </span>
184 : );
185 1 : };
|