Skip to content

Commit fa1c1e5

Browse files
committed
feat: add objectSortKeys props.
1 parent 7bab486 commit fa1c1e5

File tree

6 files changed

+121
-88
lines changed

6 files changed

+121
-88
lines changed

core/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ export interface JsonViewProps<T> extends React.DetailedHTMLProps<React.HTMLAttr
487487
enableClipboard?: boolean;
488488
/** Whether to highlight updates. @default true */
489489
highlightUpdates?: boolean;
490+
/** Whether sort keys through `String.prototype.localeCompare()` @default false */
491+
objectSortKeys?: boolean | ((a: string, b: string) => number);
490492
/** Display for quotes in object-key @default " */
491493
quotes?: "'" | '"' | '';
492494
/** When set to true, all nodes will be collapsed by default. Use an integer value to collapse at a particular depth. @default false */
@@ -498,13 +500,16 @@ export interface JsonViewProps<T> extends React.DetailedHTMLProps<React.HTMLAttr
498500
keyid: string;
499501
keyName?: string | number;
500502
}) => void;
503+
/** Fires event when you copy */
504+
onCopied?: CopiedProps<T>['onCopied'];
501505
/** Redefine interface elements to re-render. */
502506
components?: {
503507
braces?: MetaProps['render'];
504508
ellipsis?: EllipsisProps['render'];
505509
arrow?: JSX.Element;
506510
objectKey?: SemicolonProps['render'];
507511
value?: ValueViewProps<T>['renderValue'];
512+
copied?: CopiedProps<T>['render'];
508513
};
509514
}
510515
declare const JsonView: React.ForwardRefExoticComponent<Omit<JsonViewProps<object>, "ref"> & React.RefAttributes<HTMLDivElement>>;

core/src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useId } from 'react';
22
import { forwardRef } from 'react';
3-
import { RooNode, MetaProps, SemicolonProps, EllipsisProps } from './node';
3+
import { RooNode, MetaProps, EllipsisProps } from './node';
4+
import { SemicolonProps } from './semicolon';
45
import { ValueViewProps } from './value';
56
import { CopiedProps } from './copied';
67

@@ -22,6 +23,8 @@ export interface JsonViewProps<T extends object>
2223
enableClipboard?: boolean;
2324
/** Whether to highlight updates. @default true */
2425
highlightUpdates?: boolean;
26+
/** Whether sort keys through `String.prototype.localeCompare()` @default false */
27+
objectSortKeys?: boolean | ((a: string, b: string) => number);
2528
/** Display for quotes in object-key @default " */
2629
quotes?: "'" | '"' | '';
2730
/** When set to true, all nodes will be collapsed by default. Use an integer value to collapse at a particular depth. @default false */

core/src/node.tsx

Lines changed: 17 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
1-
import { FC, Fragment, PropsWithChildren, useId, cloneElement, useState, useMemo, useRef, useEffect } from 'react';
1+
import { FC, Fragment, PropsWithChildren, useId, cloneElement, useState } from 'react';
22
import { ValueView, ValueViewProps, Colon, Label, LabelProps, Line, typeMap } from './value';
33
import { TriangleArrow } from './arrow/TriangleArrow';
44
import { useExpandsStatus, store } from './store';
55
import { JsonViewProps } from './';
66
import { Copied } from './copied';
7+
import { Semicolon } from './semicolon';
78

89
export interface MetaProps extends LabelProps {
910
isArray?: boolean;
1011
start?: boolean;
1112
render?: (props: Pick<MetaProps, 'start' | 'isArray' | 'className' | 'children'>) => JSX.Element;
1213
}
1314

14-
export function usePrevious<T>(value: T) {
15-
const ref = useRef<T>();
16-
useEffect(() => {
17-
ref.current = value;
18-
});
19-
return ref.current;
20-
}
21-
2215
export function Meta(props: MetaProps) {
2316
const { isArray = false, start = false, className, render, ...reset } = props;
2417
const mark = isArray ? '[]' : '{}';
@@ -52,78 +45,7 @@ export const Ellipsis: FC<PropsWithChildren<EllipsisProps>> = ({ style, render,
5245
</span>
5346
);
5447
};
55-
export interface SemicolonProps extends LabelProps {
56-
show?: boolean;
57-
highlightUpdates?: boolean;
58-
quotes?: JsonViewProps<object>['quotes'];
59-
value?: object;
60-
render?: (props: Omit<SemicolonProps, 'show'> & {}) => JSX.Element;
61-
}
62-
export const Semicolon: FC<PropsWithChildren<SemicolonProps>> = ({
63-
children,
64-
render,
65-
color,
66-
value,
67-
className = 'w-rjv-object-key',
68-
show,
69-
highlightUpdates,
70-
quotes,
71-
...props
72-
}) => {
73-
const prevValue = usePrevious(value);
74-
const highlightContainer = useRef<HTMLSpanElement>(null)
75-
const isHighlight = useMemo(() => {
76-
if (!highlightUpdates || prevValue === undefined) return false
77-
// highlight if value type changed
78-
if (typeof value !== typeof prevValue) {
79-
return true
80-
}
81-
if (typeof value === 'number') {
82-
// notice: NaN !== NaN
83-
if (isNaN(value) && isNaN(prevValue as unknown as number)) return false
84-
return value !== prevValue
85-
}
86-
// highlight if isArray changed
87-
if (Array.isArray(value) !== Array.isArray(prevValue)) {
88-
return true
89-
}
90-
// not highlight object/function
91-
// deep compare they will be slow
92-
if (typeof value === 'object' || typeof value === 'function') {
93-
return false
94-
}
95-
96-
// highlight if not equal
97-
if (value !== prevValue) {
98-
return true
99-
}
10048

101-
return false
102-
}, [highlightUpdates, value]);
103-
104-
useEffect(() => {
105-
if (highlightContainer.current && isHighlight && 'animate' in highlightContainer.current) {
106-
highlightContainer.current.animate(
107-
[
108-
{ backgroundColor: 'var(--w-rjv-update-color, #ebcb8b)' },
109-
{ backgroundColor: '' }
110-
],
111-
{
112-
duration: 1000,
113-
easing: 'ease-in'
114-
}
115-
)
116-
}
117-
}, [isHighlight, value]);
118-
119-
const content = show ? `${quotes}${children}${quotes}` : children;
120-
if (render) return render({ className, ...props, value, style: { color }, children: content });
121-
return (
122-
<Label className={className} color={color} {...props} ref={highlightContainer}>
123-
{content}
124-
</Label>
125-
);
126-
};
12749
export const CountInfo: FC<PropsWithChildren<LabelProps>> = ({ children }) => (
12850
<Label
12951
style={{ paddingLeft: 4, fontStyle: 'italic' }}
@@ -149,6 +71,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
14971
displayObjectSize = true,
15072
enableClipboard = true,
15173
highlightUpdates = true,
74+
objectSortKeys = false,
15275
indentWidth = 15,
15376
collapsed,
15477
level = 1,
@@ -159,7 +82,6 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
15982
...reset
16083
} = props;
16184
const isArray = Array.isArray(value);
162-
const nameKeys = (isArray ? Object.keys(value).map(m => Number(m)) : Object.keys(value)) as (keyof typeof value)[];
16385
const subkeyid = useId();
16486
const expands = useExpandsStatus();
16587
const expand = expands[keyid] ?? (typeof collapsed === 'boolean' ? collapsed : (typeof collapsed === 'number' ? level <= collapsed : true));
@@ -203,6 +125,15 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
203125
eventProps.onMouseEnter = () => setShowTools(true);
204126
eventProps.onMouseLeave = () => setShowTools(false);
205127
}
128+
const nameKeys = (isArray ? Object.keys(value).map(m => Number(m)) : Object.keys(value)) as (keyof typeof value)[];
129+
130+
// object
131+
let entries: [key: string | number, value: unknown][] = isArray ? Object.entries(value).map(m => [Number(m[0]), m[1]]) : Object.entries(value);
132+
if (objectSortKeys) {
133+
entries = objectSortKeys === true
134+
? entries.sort(([a], [b]) => typeof a === 'string' && typeof b === 'string' ? a.localeCompare(b) : 0)
135+
: entries.sort(([a], [b]) => typeof a === 'string' && typeof b === 'string' ? objectSortKeys(a, b) : 0)
136+
}
206137
return (
207138
<div {...reset} className={`${className} w-rjv-inner`} {...eventProps}>
208139
<Line style={{ display: 'inline-flex', alignItems: 'center' }} onClick={handle}>
@@ -229,9 +160,9 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
229160
</Line>
230161
{expand && (
231162
<Line className="w-rjv-content" style={{ borderLeft: 'var(--w-rjv-border-left-width, 1px) solid var(--w-rjv-line-color, #ebebeb)', marginLeft: 6 }}>
232-
{nameKeys.length > 0 &&
233-
nameKeys.map((key, idx) => {
234-
const item = value[key];
163+
{entries.length > 0 &&
164+
entries.map(([key, itemVal], idx) => {
165+
const item = itemVal as T;
235166
const renderKey = (
236167
<Semicolon
237168
value={item}
@@ -246,7 +177,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
246177
);
247178
const isEmpty = (Array.isArray(item) && (item as []).length === 0) || (typeof item === 'object' && item && !((item as any) instanceof Date) && Object.keys(item).length === 0);
248179
if (Array.isArray(item) && !isEmpty) {
249-
const label = isArray ? idx : key;
180+
const label = (isArray ? idx : key) as string;
250181
return (
251182
<Line key={label + idx} className="w-rjv-wrap">
252183
<RooNode value={item} keyid={keyid + subkeyid + label} keyName={label} {...subNodeProps} />
@@ -255,7 +186,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
255186
}
256187
if (typeof item === 'object' && item && !((item as any) instanceof Date) && !isEmpty) {
257188
return (
258-
<Line key={key + idx} className="w-rjv-wrap">
189+
<Line key={key + '' + idx} className="w-rjv-wrap">
259190
<RooNode keyid={keyid + subkeyid + key} value={item} keyName={key} {...subNodeProps} />
260191
</Line>
261192
);

core/src/semicolon.tsx

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { FC, PropsWithChildren, useId, cloneElement, useState, useMemo, useRef, useEffect } from 'react';
2+
import { Label, LabelProps } from './value';
3+
import { JsonViewProps } from './';
4+
5+
export function usePrevious<T>(value: T) {
6+
const ref = useRef<T>();
7+
useEffect(() => {
8+
ref.current = value;
9+
});
10+
return ref.current;
11+
}
12+
13+
export interface SemicolonProps extends LabelProps {
14+
show?: boolean;
15+
highlightUpdates?: boolean;
16+
quotes?: JsonViewProps<object>['quotes'];
17+
value?: object;
18+
render?: (props: Omit<SemicolonProps, 'show'> & {}) => JSX.Element;
19+
}
20+
export const Semicolon: FC<PropsWithChildren<SemicolonProps>> = ({
21+
children,
22+
render,
23+
color,
24+
value,
25+
className = 'w-rjv-object-key',
26+
show,
27+
highlightUpdates,
28+
quotes,
29+
...props
30+
}) => {
31+
const prevValue = usePrevious(value);
32+
const highlightContainer = useRef<HTMLSpanElement>(null)
33+
const isHighlight = useMemo(() => {
34+
if (!highlightUpdates || prevValue === undefined) return false
35+
// highlight if value type changed
36+
if (typeof value !== typeof prevValue) {
37+
return true
38+
}
39+
if (typeof value === 'number') {
40+
// notice: NaN !== NaN
41+
if (isNaN(value) && isNaN(prevValue as unknown as number)) return false
42+
return value !== prevValue
43+
}
44+
// highlight if isArray changed
45+
if (Array.isArray(value) !== Array.isArray(prevValue)) {
46+
return true
47+
}
48+
// not highlight object/function
49+
// deep compare they will be slow
50+
if (typeof value === 'object' || typeof value === 'function') {
51+
return false
52+
}
53+
54+
// highlight if not equal
55+
if (value !== prevValue) {
56+
return true
57+
}
58+
59+
return false
60+
}, [highlightUpdates, value]);
61+
62+
useEffect(() => {
63+
if (highlightContainer.current && isHighlight && 'animate' in highlightContainer.current) {
64+
highlightContainer.current.animate(
65+
[
66+
{ backgroundColor: 'var(--w-rjv-update-color, #ebcb8b)' },
67+
{ backgroundColor: '' }
68+
],
69+
{
70+
duration: 1000,
71+
easing: 'ease-in'
72+
}
73+
)
74+
}
75+
}, [isHighlight, value]);
76+
77+
const content = show ? `${quotes}${children}${quotes}` : children;
78+
if (render) return render({ className, ...props, value, style: { color }, children: content });
79+
return (
80+
<Label className={className} color={color} {...props} ref={highlightContainer}>
81+
{content}
82+
</Label>
83+
);
84+
};

core/src/value.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const Colon: FC<PropsWithChildren<React.HTMLAttributes<HTMLSpanElement>>>
5555

5656
export interface ValueViewProps<T>
5757
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> {
58-
keyName?: string;
58+
keyName?: string | number;
5959
value?: T;
6060
displayDataTypes: boolean;
6161
displayObjectSize: boolean;

www/src/Example.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function Example() {
6464
const [displayDataTypes, setDisplayDataTypes] = useState(true);
6565
const [displayObjectSize, setDisplayObjectSize] = useState(true);
6666
const [highlightUpdates, setHighlightUpdates] = useState(true);
67+
const [objectSortKeys, setObjectSortKeys] = useState(false);
6768
const [clipboard, setClipboard] = useState(true);
6869
const [quotes, setQuotes] = useState<JsonViewProps<object>['quotes']>("\"");
6970
const [collapsed, setCollapsed] = useState<JsonViewProps<object>['collapsed']>(true);
@@ -90,6 +91,7 @@ export function Example() {
9091
displayDataTypes={displayDataTypes}
9192
highlightUpdates={highlightUpdates}
9293
quotes={quotes}
94+
objectSortKeys={objectSortKeys}
9395
enableClipboard={clipboard}
9496
style={{ ...theme, padding: 6, borderRadius: 6 }}
9597
collapsed={collapsed}
@@ -158,6 +160,14 @@ export function Example() {
158160
onChange={(evn) => setHighlightUpdates(evn.target.checked)}
159161
/>
160162
</Label>
163+
<Label>
164+
<span>Sort Keys Through:</span>
165+
<input
166+
type="checkbox"
167+
checked={objectSortKeys}
168+
onChange={(evn) => setObjectSortKeys(evn.target.checked)}
169+
/>
170+
</Label>
161171
</Options>
162172
</Fragment>
163173
);

0 commit comments

Comments
 (0)