Skip to content

Commit 1f61356

Browse files
committed
feat: add highlightUpdates props.
1 parent 78b05e4 commit 1f61356

File tree

7 files changed

+124
-13
lines changed

7 files changed

+124
-13
lines changed

core/README.md

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ A React component for displaying and editing javascript arrays and JSON objects.
2323
📚 Use Typescript to write, better code hints.
2424
🎨 Support theme customization & [`online editing`](https://uiwjs.github.io/react-json-view/#online-editing-theme) theme
2525
🌒 Support dark/light mode
26-
📦 Zero dependencies
26+
📦 Zero dependencies
27+
♻️ Whether to highlight updates.
2728

2829
## Quick Start
2930

@@ -306,6 +307,50 @@ export default function Demo() {
306307
}
307308
```
308309

310+
## Highlight Updates
311+
312+
```tsx mdx:preview
313+
import React, { useState, useEffect } from 'react';
314+
import JsonView from '@uiw/react-json-view';
315+
import { TriangleArrow } from '@uiw/react-json-view/triangle-arrow';
316+
import { TriangleSolidArrow } from '@uiw/react-json-view/triangle-solid-arrow';
317+
318+
const object = {
319+
string: 'Lorem ipsum dolor sit amet',
320+
integer: 42,
321+
timer: 0,
322+
object: { 'first-child': true, 'second-child': false, 'last-child': null },
323+
}
324+
export default function Demo() {
325+
const [src, setSrc] = useState({ ...object })
326+
useEffect(() => {
327+
const loop = () => {
328+
setSrc(src => ({
329+
...src,
330+
timer: src.timer + 1
331+
}))
332+
}
333+
const id = setInterval(loop, 1000)
334+
return () => clearInterval(id)
335+
}, []);
336+
337+
return (
338+
<JsonView
339+
value={src}
340+
keyName="root"
341+
style={{
342+
'--w-rjv-background-color': '#ffffff',
343+
'--w-rjv-border-left': '1px dashed #ebebeb',
344+
// ✅ Change default update background color ✅
345+
'--w-rjv-update-color': '#ff6ffd',
346+
}}
347+
/>
348+
)
349+
}
350+
```
351+
352+
This feature can be disabled with `highlightUpdates={false}`, and the default color can be changed with `--w-rjv-update-color`.
353+
309354
## Modify Icon Style
310355

311356
Use built-in default icons.
@@ -425,6 +470,8 @@ export interface JsonViewProps<T> extends React.DetailedHTMLProps<React.HTMLAttr
425470
keyName?: string | number;
426471
/** The user can copy objects and arrays to clipboard by clicking on the clipboard icon. @default true */
427472
enableClipboard?: boolean;
473+
/** Whether to highlight updates. @default true */
474+
highlightUpdates?: boolean;
428475
/** Display for quotes in object-key @default " */
429476
quotes?: "'" | '"' | '';
430477
/** 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/copied.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@ import { useState } from 'react';
33
export interface CopiedProps<T = object> extends React.SVGProps<SVGSVGElement> {
44
show?: boolean;
55
text?: T;
6-
onCopied?: (text: string, obj: T) => void;
6+
onCopied?: (text: string, obj: T | '') => void;
77
render?: (props: Omit<CopiedProps<T>, 'render'>) => JSX.Element;
88
}
99

1010
export function Copied<T>(props: CopiedProps<T>) {
11-
const { children, style, text = '', render, show, ...reset } = props;
11+
const { children, style, text = '', onCopied, render, show, ...reset } = props;
1212
if (!show) return null;
1313
const [copied, setCopied] = useState(false);
1414
const click = (event: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
1515
event.stopPropagation();
16-
let copyText = JSON.stringify(text, (key, value) => {
16+
let copyText = JSON.stringify(text || '', (key, value) => {
1717
if (typeof value === 'bigint') {
1818
return value.toString()
1919
}
@@ -24,6 +24,7 @@ export function Copied<T>(props: CopiedProps<T>) {
2424

2525
navigator.clipboard.writeText(copyText)
2626
.then(() => {
27+
onCopied && onCopied(copyText, text);
2728
setCopied(true);
2829
const timer = setTimeout(() => {
2930
setCopied(false);

core/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface JsonViewProps<T extends object>
2020
keyName?: string | number;
2121
/** The user can copy objects and arrays to clipboard by clicking on the clipboard icon. @default true */
2222
enableClipboard?: boolean;
23+
/** Whether to highlight updates. @default true */
24+
highlightUpdates?: boolean;
2325
/** Display for quotes in object-key @default " */
2426
quotes?: "'" | '"' | '';
2527
/** 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: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, Fragment, PropsWithChildren, useId, cloneElement, useState } from 'react';
1+
import { FC, Fragment, PropsWithChildren, useId, cloneElement, useState, useMemo, useRef, useEffect } 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';
@@ -11,6 +11,14 @@ export interface MetaProps extends LabelProps {
1111
render?: (props: Pick<MetaProps, 'start' | 'isArray' | 'className' | 'children'>) => JSX.Element;
1212
}
1313

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+
1422
export function Meta(props: MetaProps) {
1523
const { isArray = false, start = false, className, render, ...reset } = props;
1624
const mark = isArray ? '[]' : '{}';
@@ -46,6 +54,7 @@ export const Ellipsis: FC<PropsWithChildren<EllipsisProps>> = ({ style, render,
4654
};
4755
export interface SemicolonProps extends LabelProps {
4856
show?: boolean;
57+
highlightUpdates?: boolean;
4958
quotes?: JsonViewProps<object>['quotes'];
5059
value?: object;
5160
render?: (props: Omit<SemicolonProps, 'show'> & {}) => JSX.Element;
@@ -57,13 +66,60 @@ export const Semicolon: FC<PropsWithChildren<SemicolonProps>> = ({
5766
value,
5867
className = 'w-rjv-object-key',
5968
show,
69+
highlightUpdates,
6070
quotes,
6171
...props
6272
}) => {
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+
}
100+
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+
63119
const content = show ? `${quotes}${children}${quotes}` : children;
64120
if (render) return render({ className, ...props, value, style: { color }, children: content });
65121
return (
66-
<Label className={className} color={color} {...props}>
122+
<Label className={className} color={color} {...props} ref={highlightContainer}>
67123
{content}
68124
</Label>
69125
);
@@ -92,6 +148,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
92148
components = {},
93149
displayObjectSize = true,
94150
enableClipboard = true,
151+
highlightUpdates = true,
95152
indentWidth = 15,
96153
collapsed,
97154
level = 1,
@@ -117,6 +174,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
117174
displayDataTypes,
118175
displayObjectSize,
119176
enableClipboard,
177+
highlightUpdates,
120178
onCopied,
121179
onExpand,
122180
collapsed,
@@ -178,6 +236,7 @@ export function RooNode<T extends object>(props: RooNodeProps<T>) {
178236
<Semicolon
179237
value={item}
180238
quotes={quotes}
239+
highlightUpdates={highlightUpdates}
181240
render={components.objectKey}
182241
color={typeof key === 'number' ? typeMap['number'].color : ''}
183242
show={typeof key === 'string'}

core/src/theme/dark.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const darkTheme = {
66
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
77
'--w-rjv-info-color': '#656565',
88
'--w-rjv-copied-color': '#0184a6',
9+
'--w-rjv-update-color': '#ebcb8b',
910
'--w-rjv-copied-success-color': '#28a745',
1011

1112
'--w-rjv-curlybraces-color': '#1896b6',

core/src/theme/light.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const lightTheme = {
55
'--w-rjv-line-color': '#ebebeb',
66
'--w-rjv-arrow-color': 'var(--w-rjv-color)',
77
'--w-rjv-info-color': '#0000004d',
8+
'--w-rjv-update-color': '#ebcb8b',
89
'--w-rjv-copied-color': '#002b36',
910
'--w-rjv-copied-success-color': '#28a745',
1011

core/src/value.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FC, Fragment, PropsWithChildren, useState } from 'react';
1+
import { FC, Fragment, PropsWithChildren, PropsWithRef, forwardRef, useState } from 'react';
22
import { Meta, MetaProps, CountInfo } from './node';
33
import { Copied } from './copied';
44

@@ -136,7 +136,7 @@ export function ValueView<T = object>(props: ValueViewProps<T>) {
136136
);
137137
return (
138138
<Line {...eventProps}>
139-
<Label {...reset}>
139+
<Label {...reset} ref={null}>
140140
{renderKey}
141141
<Colon />
142142
{typeView}
@@ -156,7 +156,7 @@ export function ValueView<T = object>(props: ValueViewProps<T>) {
156156
)
157157
return (
158158
<Line {...eventProps}>
159-
<Label {...reset}>
159+
<Label {...reset} ref={null}>
160160
{renderKey}
161161
<Colon />
162162
{typeView}
@@ -174,19 +174,19 @@ export interface LabelProps extends React.HTMLAttributes<HTMLSpanElement> {
174174
paddingRight?: number;
175175
}
176176

177-
export const Label: FC<PropsWithChildren<LabelProps>> = ({
177+
export const Label = forwardRef<HTMLSpanElement, LabelProps>(({
178178
children,
179179
color,
180180
fontSize,
181181
opacity,
182182
paddingRight,
183183
style,
184184
...reset
185-
}) => (
186-
<span style={{ color, fontSize, opacity, paddingRight, ...style }} {...reset}>
185+
}, ref) => (
186+
<span style={{ color, fontSize, opacity, paddingRight, ...style }} {...reset} ref={ref}>
187187
{children}
188188
</span>
189-
);
189+
));
190190

191191
export interface TypeProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> {
192192
type: keyof typeof typeMap;

0 commit comments

Comments
 (0)