Skip to content

Commit b9490fe

Browse files
EYHNpionxzh
authored andcommitted
feat: highlight changed
1 parent 1e5169a commit b9490fe

File tree

6 files changed

+109
-21
lines changed

6 files changed

+109
-21
lines changed

docs/pages/full/index.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ const IndexPage: FC = () => {
120120
const [displayDataTypes, setDisplayDataTypes] = useState(true)
121121
const [displayObjectSize, setDisplayObjectSize] = useState(true)
122122
const [editable, setEditable] = useState(true)
123+
const [highlightUpdates, setHighlightUpdates] = useState(true)
123124
useEffect(() => {
124125
const loop = () => {
125126
setSrc(src => ({
@@ -166,6 +167,15 @@ const IndexPage: FC = () => {
166167
)}
167168
label='Editable'
168169
/>
170+
<FormControlLabel
171+
control={(
172+
<Switch
173+
checked={highlightUpdates}
174+
onChange={event => setHighlightUpdates(event.target.checked)}
175+
/>
176+
)}
177+
label='Highlight Updates'
178+
/>
169179
<FormControlLabel
170180
control={(
171181
<Switch
@@ -239,6 +249,7 @@ const IndexPage: FC = () => {
239249
<JsonViewer
240250
value={src}
241251
editable={editable}
252+
highlightUpdates={highlightUpdates}
242253
indentWidth={indent}
243254
theme={theme}
244255
displayDataTypes={displayDataTypes}

src/components/DataKeyPair.tsx

+66-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Box } from '@mui/material'
22
import type { ComponentProps, FC, MouseEvent } from 'react'
3-
import { useCallback, useMemo, useState } from 'react'
3+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
44

55
import { useTextColor } from '../hooks/useColor'
66
import { useClipboard } from '../hooks/useCopyToClipboard'
@@ -21,6 +21,7 @@ import { DataBox } from './mui/DataBox'
2121

2222
export type DataKeyPairProps = {
2323
value: unknown
24+
prevValue?: unknown
2425
nestedIndex?: number
2526
editable?: boolean
2627
path: (string | number)[]
@@ -41,7 +42,7 @@ const IconBox: FC<IconBoxProps> = (props) => (
4142
)
4243

4344
export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
44-
const { value, path, nestedIndex } = props
45+
const { value, prevValue, path, nestedIndex } = props
4546
const propsEditable = props.editable ?? undefined
4647
const storeEditable = useJsonViewerStore(store => store.editable)
4748
const editable = useMemo(() => {
@@ -73,6 +74,7 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
7374
const onChange = useJsonViewerStore(store => store.onChange)
7475
const keyColor = useTextColor()
7576
const numberKeyColor = useJsonViewerStore(store => store.colorspace.base0C)
77+
const highlightColor = useJsonViewerStore(store => store.colorspace.base0F)
7678
const { Component, PreComponent, PostComponent, Editor } = useTypeComponents(value, path)
7779
const quotesOnKeys = useJsonViewerStore(store => store.quotesOnKeys)
7880
const rootName = useJsonViewerStore(store => store.rootName)
@@ -82,6 +84,49 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
8284
const enableClipboard = useJsonViewerStore(store => store.enableClipboard)
8385
const { copy, copied } = useClipboard()
8486

87+
const highlightUpdates = useJsonViewerStore(store => store.highlightUpdates)
88+
const isHighlight = useMemo(() => {
89+
if (!highlightUpdates || prevValue === undefined) return false
90+
91+
// highlight if value type changed
92+
if (typeof value !== typeof prevValue) {
93+
return true
94+
}
95+
96+
// highlight if isArray changed
97+
if (Array.isArray(value) !== Array.isArray(prevValue)) {
98+
return true
99+
}
100+
101+
// not highlight object/function
102+
// deep compare they will be slow
103+
if (typeof value === 'object' || typeof value === 'function') {
104+
return false
105+
}
106+
107+
// highlight if not equal
108+
if (value !== prevValue) {
109+
return true
110+
}
111+
112+
return false
113+
}, [highlightUpdates, prevValue, value])
114+
const highlightContainer = useRef<HTMLElement>()
115+
useEffect(() => {
116+
if (highlightContainer.current && isHighlight && 'animate' in highlightContainer.current) {
117+
highlightContainer.current.animate(
118+
[
119+
{ backgroundColor: highlightColor },
120+
{ backgroundColor: '' }
121+
],
122+
{
123+
duration: 1000,
124+
easing: 'ease-in'
125+
}
126+
)
127+
}
128+
}, [highlightColor, isHighlight, prevValue, value])
129+
85130
const actionIcons = useMemo(() => {
86131
if (editing) {
87132
return (
@@ -166,8 +211,9 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
166211
path,
167212
inspect,
168213
setInspect,
169-
value
170-
}), [inspect, path, setInspect, value])
214+
value,
215+
prevValue
216+
}), [inspect, path, setInspect, value, prevValue])
171217
return (
172218
<Box
173219
className='data-key-pair'
@@ -220,20 +266,22 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
220266
)
221267
: null
222268
}
223-
{
224-
(isRoot
225-
? rootName !== false
226-
? (quotesOnKeys ? <>&quot;{rootName}&quot;</> : <>{rootName}</>)
227-
: null
228-
: KeyRenderer.when(downstreamProps)
229-
? <KeyRenderer {...downstreamProps} />
230-
: nestedIndex === undefined && (
231-
isNumberKey
232-
? <Box component='span' style={{ color: numberKeyColor }}>{key}</Box>
233-
: quotesOnKeys ? <>&quot;{key}&quot;</> : <>{key}</>
234-
)
235-
)
236-
}
269+
<Box ref={highlightContainer} component='span'>
270+
{
271+
(isRoot
272+
? rootName !== false
273+
? (quotesOnKeys ? <>&quot;{rootName}&quot;</> : <>{rootName}</>)
274+
: null
275+
: KeyRenderer.when(downstreamProps)
276+
? <KeyRenderer {...downstreamProps} />
277+
: nestedIndex === undefined && (
278+
isNumberKey
279+
? <Box component='span' style={{ color: numberKeyColor }}>{key}</Box>
280+
: quotesOnKeys ? <>&quot;{key}&quot;</> : <>{key}</>
281+
)
282+
)
283+
}
284+
</Box>
237285
{
238286
(
239287
isRoot

src/components/DataTypes/Object.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export const ObjectType: FC<DataItemProps<object>> = (props) => {
136136
key={key}
137137
path={path}
138138
value={value}
139+
prevValue={props.prevValue instanceof Map ? props.prevValue.get(k) : undefined}
139140
editable={false}
140141
/>
141142
)
@@ -167,7 +168,12 @@ export const ObjectType: FC<DataItemProps<object>> = (props) => {
167168
const elements = value.slice(0, displayLength).map((value, index) => {
168169
const path = [...props.path, index]
169170
return (
170-
<DataKeyPair key={index} path={path} value={value} />
171+
<DataKeyPair
172+
key={index}
173+
path={path}
174+
value={value}
175+
prevValue={Array.isArray(props.prevValue) ? props.prevValue[index] : undefined}
176+
/>
171177
)
172178
})
173179
if (value.length > displayLength) {
@@ -193,6 +199,7 @@ export const ObjectType: FC<DataItemProps<object>> = (props) => {
193199
}
194200

195201
const elements: unknown[][] = segmentArray(value, groupArraysAfterLength)
202+
const prevElements = Array.isArray(props.prevValue) ? segmentArray(props.prevValue, groupArraysAfterLength) : undefined
196203

197204
return elements.map((list, index) => {
198205
const path = [...props.path]
@@ -202,6 +209,7 @@ export const ObjectType: FC<DataItemProps<object>> = (props) => {
202209
path={path}
203210
value={list}
204211
nestedIndex={index}
212+
prevValue={prevElements?.[index]}
205213
/>
206214
)
207215
})
@@ -216,7 +224,7 @@ export const ObjectType: FC<DataItemProps<object>> = (props) => {
216224
const elements = entries.slice(0, displayLength).map(([key, value]) => {
217225
const path = [...props.path, key]
218226
return (
219-
<DataKeyPair key={key} path={path} value={value} />
227+
<DataKeyPair key={key} path={path} value={value} prevValue={(props.prevValue as any)?.[key]} />
220228
)
221229
})
222230
if (entries.length > displayLength) {
@@ -242,6 +250,7 @@ export const ObjectType: FC<DataItemProps<object>> = (props) => {
242250
}, [
243251
props.inspect,
244252
props.value,
253+
props.prevValue,
245254
props.path,
246255
groupArraysAfterLength,
247256
displayLength,

src/index.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,20 @@ function useSetIfNotUndefinedEffect<Key extends keyof JsonViewerProps> (
4747
*/
4848
const JsonViewerInner: FC<JsonViewerProps> = (props) => {
4949
const { setState } = useContext(JsonViewerStoreContext)
50-
useSetIfNotUndefinedEffect('value', props.value)
50+
useEffect(() => {
51+
setState((state) => ({
52+
prevValue: state.value,
53+
value: props.value
54+
}))
55+
}, [props.value, setState])
5156
useSetIfNotUndefinedEffect('editable', props.editable)
5257
useSetIfNotUndefinedEffect('indentWidth', props.indentWidth)
5358
useSetIfNotUndefinedEffect('onChange', props.onChange)
5459
useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
5560
useSetIfNotUndefinedEffect('keyRenderer', props.keyRenderer)
5661
useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
5762
useSetIfNotUndefinedEffect('enableClipboard', props.enableClipboard)
63+
useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
5864
useSetIfNotUndefinedEffect('rootName', props.rootName)
5965
useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
6066
useSetIfNotUndefinedEffect('displayObjectSize', props.displayObjectSize)
@@ -97,6 +103,7 @@ const JsonViewerInner: FC<JsonViewerProps> = (props) => {
97103
}, [props.valueTypes, predefinedTypes, registerTypes])
98104

99105
const value = useJsonViewerStore(store => store.value)
106+
const prevValue = useJsonViewerStore(store => store.prevValue)
100107
const setHover = useJsonViewerStore(store => store.setHover)
101108
const onMouseLeave = useCallback(() => setHover(null), [setHover])
102109
return (
@@ -114,6 +121,7 @@ const JsonViewerInner: FC<JsonViewerProps> = (props) => {
114121
>
115122
<DataKeyPair
116123
value={value}
124+
prevValue={prevValue}
117125
path={useMemo(() => [], [])}
118126
/>
119127
</Paper>

src/stores/JsonViewerStore.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export type JsonViewerState<T = unknown> = {
2323
indentWidth: number
2424
groupArraysAfterLength: number
2525
enableClipboard: boolean
26+
highlightUpdates: boolean
2627
maxDisplayLength: number
2728
defaultInspectDepth: number
2829
collapseStringsAfterLength: number
@@ -32,6 +33,7 @@ export type JsonViewerState<T = unknown> = {
3233
editable: boolean | (<U>(path: Path, currentValue: U) => boolean)
3334
displayDataTypes: boolean
3435
rootName: false | string
36+
prevValue: T | undefined
3537
value: T
3638
onChange: JsonViewerOnChange
3739
onCopy: JsonViewerOnCopy | undefined
@@ -49,6 +51,7 @@ export const createJsonViewerStore = <T = unknown> (props: JsonViewerProps<T>) =
4951
return create<JsonViewerState>()((set, get) => ({
5052
// provided by user
5153
enableClipboard: props.enableClipboard ?? true,
54+
highlightUpdates: props.highlightUpdates ?? false,
5255
indentWidth: props.indentWidth ?? 3,
5356
groupArraysAfterLength: props.groupArraysAfterLength ?? 100,
5457
collapseStringsAfterLength:
@@ -71,6 +74,7 @@ export const createJsonViewerStore = <T = unknown> (props: JsonViewerProps<T>) =
7174
hoverPath: null,
7275
colorspace: lightColorspace,
7376
value: props.value,
77+
prevValue: undefined,
7478
displayObjectSize: props.displayObjectSize ?? true,
7579

7680
getInspectCache: (path, nestedIndex) => {

src/type.ts

+8
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface DataItemProps<ValueType = unknown> {
3636
inspect: boolean
3737
setInspect: Dispatch<SetStateAction<boolean>>
3838
value: ValueType
39+
prevValue: ValueType | undefined
3940
path: Path
4041
}
4142

@@ -168,4 +169,11 @@ export type JsonViewerProps<T = unknown> = {
168169
* @default true
169170
*/
170171
displayObjectSize?: boolean
172+
173+
/**
174+
* Whether to highlight updates.
175+
*
176+
* @default false
177+
*/
178+
highlightUpdates?: boolean
171179
}

0 commit comments

Comments
 (0)