Line data Source code
1 : import * as React from 'react';
2 : import * as styles from './json_view.module.css';
3 :
4 : import { useJsonViewerState } from './json_view_state.js';
5 : import { useNestedExpansionState } from './json_nested_state.js';
6 : import { useToolVisibilityDispatch } from './json_tool_state.js';
7 : import { JsonLiteral } from './json_literal.js';
8 : import { JsonKeyName } from './json_key_name.js';
9 : import { JsonValue } from './json_value.js';
10 : import { JsonCopyButton } from './json_copy_button.js';
11 : import { useUniqueKey } from './unique_key.js';
12 :
13 : interface KeyValuesProps<T extends object> {
14 : expandKey?: string;
15 : level: number;
16 : value: T;
17 : parentValue?: T;
18 : keyName?: (string | number);
19 : keyPath: (string | number)[];
20 : }
21 :
22 1 : export function JsonKeyValues<T extends object>(props: KeyValuesProps<T>) {
23 1 : const value = props.value ?? {};
24 1 : const { keyName, expandKey = '', level, keyPath: keyPath = [], parentValue } = props;
25 1 : const { objectSortKeys, indentWidth, arrayPageSize = 0, collapsed, shouldExpandNodeInitially } = useJsonViewerState();
26 1 : const [arrayPage, setArrayPage] = React.useState({ value, itemCount: arrayPageSize });
27 :
28 : // Is expanded?
29 1 : const expands = useNestedExpansionState();
30 1 : const defaultExpanded =
31 1 : typeof collapsed === 'boolean' ? !collapsed : typeof collapsed === 'number' ? level <= collapsed : true;
32 1 : const isExpanded = expands[expandKey] ?? (shouldExpandNodeInitially ? true : defaultExpanded);
33 1 : const shouldExpand =
34 1 : shouldExpandNodeInitially && shouldExpandNodeInitially(isExpanded, { value, keyPath, level, keyName, parentValue });
35 :
36 1 : if (expands[expandKey] === undefined && !shouldExpand) {
37 1 : return null;
38 : }
39 1 : if (!isExpanded) {
40 1 : return null;
41 : }
42 1 : const isMyArray = Array.isArray(value);
43 1 : let entries: [key: string | number, value: T][] = isMyArray
44 1 : ? Object.entries(value).map((m) => [Number(m[0]), m[1]])
45 1 : : Object.entries(value as T);
46 1 : if (objectSortKeys) {
47 0 : entries =
48 0 : objectSortKeys === true
49 0 : ? entries.sort(([a], [b]) => (typeof a === 'string' && typeof b === 'string' ? a.localeCompare(b) : 0))
50 0 : : entries.sort(([a, valA], [b, valB]) =>
51 0 : typeof a === 'string' && typeof b === 'string' ? objectSortKeys(a, b, valA, valB) : 0,
52 0 : );
53 : }
54 1 : const visibleItemCount = arrayPage.value === value ? arrayPage.itemCount : arrayPageSize;
55 1 : const hiddenItemCount = isMyArray && arrayPageSize > 0
56 1 : ? Math.max(0, entries.length - visibleItemCount)
57 1 : : 0;
58 1 : if (hiddenItemCount > 0) {
59 1 : entries = entries.slice(0, visibleItemCount);
60 : }
61 :
62 : const style = {
63 1 : borderLeft: 'var(--w-rjv-border-left-width, 1px) var(--w-rjv-line-style, solid) var(--w-rjv-line-color, #ebebeb)',
64 1 : paddingLeft: indentWidth,
65 1 : marginLeft: 6,
66 : };
67 1 : return (
68 1 : <div className="w-rjv-wrap" style={style}>
69 1 : {entries.map(([key, val], idx) => {
70 1 : return (
71 1 : <KeyValuesItem parentValue={value} keyName={key} keyPath={[...keyPath, key]} value={val} key={idx} level={level} />
72 : );
73 1 : })}
74 1 : {hiddenItemCount > 0 && (
75 1 : <button
76 1 : type="button"
77 1 : className={styles.array_load_more}
78 1 : data-testid="json-array-load-more"
79 1 : onClick={() => setArrayPage({ value, itemCount: visibleItemCount + arrayPageSize })}
80 1 : >
81 1 : Show {Math.min(arrayPageSize, hiddenItemCount)} more ({hiddenItemCount} remaining)
82 1 : </button>
83 : )}
84 1 : </div>
85 : );
86 1 : };
87 :
88 1 : export function KeyValuesItem<T extends object>(props: KeyValuesProps<T>) {
89 1 : const dispatch = useToolVisibilityDispatch();
90 1 : const subkeyid = useUniqueKey();
91 1 : const isMyArray = Array.isArray(props.value);
92 1 : const isMySet = props.value instanceof Set;
93 1 : const isMyMap = props.value instanceof Map;
94 1 : const isDate = props.value instanceof Date;
95 1 : const isUrl = props.value instanceof URL;
96 1 : const isMyObject = props.value && typeof props.value === 'object' && !isMyArray && !isMySet && !isMyMap && !isDate && !isUrl;
97 1 : const isNested = isMyObject || isMyArray || isMySet || isMyMap;
98 1 : if (isNested) {
99 1 : const mappedValue = isMySet
100 0 : ? Array.from(props.value as Set<any>)
101 1 : : (isMyMap ? Object.fromEntries(props.value as Iterable<[any, any]>) : props.value);
102 1 : return (
103 1 : <JsonValue
104 1 : keyName={props.keyName}
105 1 : keyPath={props.keyPath}
106 1 : value={mappedValue}
107 1 : parentValue={props.parentValue}
108 1 : initialValue={props.value}
109 1 : level={props.level + 1}
110 1 : />
111 : );
112 1 : }
113 1 : return (
114 1 : <div
115 1 : className={styles.object_entries_line}
116 1 : data-testid="json-entry-line"
117 1 : onMouseEnter={() => dispatch({ [subkeyid]: true })}
118 1 : onMouseLeave={() => dispatch({ [subkeyid]: false })}
119 : >
120 1 : <JsonKeyName
121 1 : keyName={props.keyName}
122 1 : keyPath={props.keyPath}
123 1 : />
124 1 : <JsonLiteral value={props.value} />
125 1 : <JsonCopyButton
126 1 : keyName={props.keyName}
127 1 : keyPath={props.keyPath}
128 1 : value={props.value as object}
129 1 : parentValue={props.parentValue}
130 1 : expandKey={subkeyid} />
131 1 : </div>
132 : );
133 1 : };
|