Skip to content

Commit 1709001

Browse files
authored
feat: support add and delete feature (#438)
* feat: support add and delete feature * docs: add missing `enableDelete` document * fix: prevent prototype polluting
1 parent a03362c commit 1709001

File tree

10 files changed

+350
-50
lines changed

10 files changed

+350
-50
lines changed

docs/pages/apis.mdx

+7-3
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
| `sx` | `SxProps` | - | [The `sx` prop](https://mui.com/system/getting-started/the-sx-prop/) lets you style elements inline, using values from the theme. |
1111
| `indentWidth` | `number` | 3 | Indent width for nested objects |
1212
| `keyRenderer` | `{when: (props) => boolean}` | - | Customize a key, if `keyRenderer.when` returns `true`. |
13-
| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) |
13+
| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) |
14+
| `enableAdd` | `boolean` \|<br />`(path, currentValue) => boolean` | `false` | Whether enable add feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
15+
| `enableDelete` | `boolean` \|<br />`(path, currentValue) => boolean` | `false` | Whether enable delete feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
16+
| `enableClipboard` | `boolean` | `false` | Whether enable clipboard feature. |
17+
| `editable` | `boolean` \|<br />`(path, currentValue) => boolean` | `false` | Whether enable edit feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
1418
| `onChange` | `(path, oldVal, newVal) => void` | - | Callback when value changed. |
1519
| `onCopy` | `(path, value) => void` | - | Callback when value copied, you can use it to customize the copy behavior.<br />\*Note: you will have to write the data to the clipboard by yourself. |
1620
| `onSelect` | `(path, value) => void` | - | Callback when value selected. |
17-
| `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. |
18-
| `editable` | `boolean` \|<br />`(path, currentValue) => boolean` | `false` | Whether enable edit feature. Provide a function to customize this behavior by returning a boolean based on the value and path. |
21+
| `onAdd` | `(path) => void` | - | Callback when the add button is clicked. This is the function which implements the add feature. Please see the [DEMO](/full) for more details. |
22+
| `onDelete` | `(path) => void` | - | Callback when the delete button is clicked. This is the function which implements the delete feature. Please see the [DEMO](/full) for more details. |
1923
| `defaultInspectDepth` | `number` | 5 | Default inspect depth for nested objects.<br /><br />_\* If the number is set too large, it could result in performance issues._ |
2024
| `defaultInspectControl` | `(path, currentValue) => boolean` | - | Whether expand or collapse a field by default. Using this will override `defaultInspectDepth`. |
2125
| `maxDisplayLength` | `number` | 30 | Hide items after reaching the count.<br />`Array` and `Object` will be affected.<br /><br />_\* If the number is set too large, it could result in performance issues._ |

docs/pages/full/index.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@ import {
1616
import type {
1717
DataType,
1818
JsonViewerKeyRenderer,
19+
JsonViewerOnAdd,
1920
JsonViewerOnChange,
21+
JsonViewerOnDelete,
2022
JsonViewerTheme
2123
} from '@textea/json-viewer'
2224
import {
2325
applyValue,
2426
defineDataType,
27+
deleteValue,
2528
JsonViewer,
2629
stringType
2730
} from '@textea/json-viewer'
@@ -336,6 +339,8 @@ const IndexPage: FC = () => {
336339
highlightUpdates={highlightUpdates}
337340
indentWidth={indent}
338341
theme={theme}
342+
enableAdd={true}
343+
enableDelete={true}
339344
displayDataTypes={displayDataTypes}
340345
displaySize={displaySize}
341346
groupArraysAfterLength={groupArraysAfterLength}
@@ -345,13 +350,31 @@ const IndexPage: FC = () => {
345350
linkType,
346351
imageDataType
347352
]}
353+
onAdd={
354+
useCallback<JsonViewerOnAdd>(
355+
(path) => {
356+
const key = prompt('Key:')
357+
if (key === null) return
358+
const value = prompt('Value:')
359+
if (value === null) return
360+
setSrc(src => applyValue(src, [...path, key], value))
361+
}, []
362+
)
363+
}
348364
onChange={
349365
useCallback<JsonViewerOnChange>(
350366
(path, oldValue, newValue) => {
351367
setSrc(src => applyValue(src, path, newValue))
352368
}, []
353369
)
354370
}
371+
onDelete={
372+
useCallback<JsonViewerOnDelete>(
373+
(path, value) => {
374+
setSrc(src => deleteValue(src, path, value))
375+
}, []
376+
)
377+
}
355378
sx={{
356379
paddingLeft: 2
357380
}}

docs/pages/how-to/data-types.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The `is` function takes a value and a path and returns true if the value belongs
2525

2626
The `Component` prop is a React component that renders the value of the data type. It receives a `DataItemProps` object as a `prop`, which includes the following:
2727

28+
- `props.path` - The path to the value.
2829
- `props.value` - The value to render.
2930
- `props.inspect` - A Boolean flag indicating whether the value is being inspected (expanded).
3031
- `props.setInspect` - A function that can be used to toggle the inspect state.

src/components/DataKeyPair.tsx

+72-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ import { useInspect } from '../hooks/useInspect'
88
import { useJsonViewerStore } from '../stores/JsonViewerStore'
99
import { useTypeComponents } from '../stores/typeRegistry'
1010
import type { DataItemProps } from '../type'
11-
import { copyString, getValueSize } from '../utils'
11+
import { copyString, getValueSize, isPlainObject } from '../utils'
1212
import {
13+
AddBoxIcon,
1314
CheckIcon,
1415
ChevronRightIcon,
1516
CloseIcon,
1617
ContentCopyIcon,
18+
DeleteIcon,
1719
EditIcon,
1820
ExpandMoreIcon
1921
} from './Icons'
@@ -82,6 +84,51 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
8284
const isRoot = root === value
8385
const isNumberKey = Number.isInteger(Number(key))
8486

87+
const storeEnableAdd = useJsonViewerStore(store => store.enableAdd)
88+
const onAdd = useJsonViewerStore(store => store.onAdd)
89+
const enableAdd = useMemo(() => {
90+
if (!onAdd || nestedIndex !== undefined) return false
91+
92+
if (storeEnableAdd === false) {
93+
return false
94+
}
95+
if (propsEditable === false) {
96+
// props.editable is false which means we cannot provide the suitable way to edit it
97+
return false
98+
}
99+
if (typeof storeEnableAdd === 'function') {
100+
return !!storeEnableAdd(path, value)
101+
}
102+
103+
if (Array.isArray(value) || isPlainObject(value)) {
104+
return true
105+
}
106+
107+
return false
108+
}, [onAdd, nestedIndex, path, storeEnableAdd, propsEditable, value])
109+
110+
const storeEnableDelete = useJsonViewerStore(store => store.enableDelete)
111+
const onDelete = useJsonViewerStore(store => store.onDelete)
112+
const enableDelete = useMemo(() => {
113+
if (!onDelete || nestedIndex !== undefined) return false
114+
115+
if (isRoot) {
116+
// don't allow delete root
117+
return false
118+
}
119+
if (storeEnableDelete === false) {
120+
return false
121+
}
122+
if (propsEditable === false) {
123+
// props.editable is false which means we cannot provide the suitable way to edit it
124+
return false
125+
}
126+
if (typeof storeEnableDelete === 'function') {
127+
return !!storeEnableDelete(path, value)
128+
}
129+
return storeEnableDelete
130+
}, [onDelete, nestedIndex, isRoot, path, storeEnableDelete, propsEditable, value])
131+
85132
const enableClipboard = useJsonViewerStore(store => store.enableClipboard)
86133
const { copy, copied } = useClipboard()
87134

@@ -205,6 +252,26 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
205252
<EditIcon sx={{ fontSize: '.8rem' }} />
206253
</IconBox>
207254
)}
255+
{enableAdd && (
256+
<IconBox
257+
onClick={event => {
258+
event.preventDefault()
259+
onAdd?.(path)
260+
}}
261+
>
262+
<AddBoxIcon sx={{ fontSize: '.8rem' }} />
263+
</IconBox>
264+
)}
265+
{enableDelete && (
266+
<IconBox
267+
onClick={event => {
268+
event.preventDefault()
269+
onDelete?.(path, value)
270+
}}
271+
>
272+
<DeleteIcon sx={{ fontSize: '.9rem' }} />
273+
</IconBox>
274+
)}
208275
</>
209276
)
210277
},
@@ -217,9 +284,13 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
217284
editable,
218285
editing,
219286
enableClipboard,
287+
enableAdd,
288+
enableDelete,
220289
tempValue,
221290
path,
222291
value,
292+
onAdd,
293+
onDelete,
223294
startEditing,
224295
abortEditing,
225296
commitEditing

src/components/Icons.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ const BaseIcon: FC<SvgIconProps> = ({ d, ...props }) => {
1010
)
1111
}
1212

13+
const AddBox = 'M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m0 16H5V5h14zm-8-2h2v-4h4v-2h-4V7h-2v4H7v2h4z'
1314
const Check = 'M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'
1415
const ChevronRight = 'M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'
1516
const CircularArrows = 'M 12 2 C 10.615 1.998 9.214625 2.2867656 7.890625 2.8847656 L 8.9003906 4.6328125 C 9.9043906 4.2098125 10.957 3.998 12 4 C 15.080783 4 17.738521 5.7633175 19.074219 8.3222656 L 17.125 9 L 21.25 11 L 22.875 7 L 20.998047 7.6523438 C 19.377701 4.3110398 15.95585 2 12 2 z M 6.5097656 4.4882812 L 2.2324219 5.0820312 L 3.734375 6.3808594 C 1.6515335 9.4550558 1.3615962 13.574578 3.3398438 17 C 4.0308437 18.201 4.9801562 19.268234 6.1601562 20.115234 L 7.1699219 18.367188 C 6.3019219 17.710187 5.5922656 16.904 5.0722656 16 C 3.5320014 13.332354 3.729203 10.148679 5.2773438 7.7128906 L 6.8398438 9.0625 L 6.5097656 4.4882812 z M 19.929688 13 C 19.794687 14.08 19.450734 15.098 18.927734 16 C 17.386985 18.668487 14.531361 20.090637 11.646484 19.966797 L 12.035156 17.9375 L 8.2402344 20.511719 L 10.892578 23.917969 L 11.265625 21.966797 C 14.968963 22.233766 18.681899 20.426323 20.660156 17 C 21.355156 15.801 21.805219 14.445 21.949219 13 L 19.929688 13 z'
1617
const Close = 'M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z'
1718
const ContentCopy = 'M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'
1819
const Edit = 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'
1920
const ExpandMore = 'M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z'
21+
const Delete = 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zM8 9h8v10H8zm7.5-5l-1-1h-5l-1 1H5v2h14V4z'
22+
23+
export const AddBoxIcon: FC<SvgIconProps> = (props) => {
24+
return <BaseIcon d={AddBox} {...props} />
25+
}
2026

2127
export const CheckIcon: FC<SvgIconProps> = (props) => {
2228
return <BaseIcon d={Check} {...props} />
@@ -45,3 +51,7 @@ export const EditIcon: FC<SvgIconProps> = (props) => {
4551
export const ExpandMoreIcon: FC<SvgIconProps> = (props) => {
4652
return <BaseIcon d={ExpandMore} {...props} />
4753
}
54+
55+
export const DeleteIcon: FC<SvgIconProps> = (props) => {
56+
return <BaseIcon d={Delete} {...props} />
57+
}

src/index.tsx

+13-9
Original file line numberDiff line numberDiff line change
@@ -50,19 +50,23 @@ const JsonViewerInner: FC<JsonViewerProps> = (props) => {
5050
value: props.value
5151
}))
5252
}, [props.value, setState])
53-
useSetIfNotUndefinedEffect('editable', props.editable)
53+
useSetIfNotUndefinedEffect('rootName', props.rootName)
5454
useSetIfNotUndefinedEffect('indentWidth', props.indentWidth)
55-
useSetIfNotUndefinedEffect('onChange', props.onChange)
56-
useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
5755
useSetIfNotUndefinedEffect('keyRenderer', props.keyRenderer)
58-
useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
56+
useSetIfNotUndefinedEffect('enableAdd', props.enableAdd)
57+
useSetIfNotUndefinedEffect('enableDelete', props.enableDelete)
5958
useSetIfNotUndefinedEffect('enableClipboard', props.enableClipboard)
60-
useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
61-
useSetIfNotUndefinedEffect('rootName', props.rootName)
62-
useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
63-
useSetIfNotUndefinedEffect('displaySize', props.displaySize)
59+
useSetIfNotUndefinedEffect('editable', props.editable)
60+
useSetIfNotUndefinedEffect('onChange', props.onChange)
6461
useSetIfNotUndefinedEffect('onCopy', props.onCopy)
6562
useSetIfNotUndefinedEffect('onSelect', props.onSelect)
63+
useSetIfNotUndefinedEffect('onAdd', props.onAdd)
64+
useSetIfNotUndefinedEffect('onDelete', props.onDelete)
65+
useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
66+
useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
67+
useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
68+
useSetIfNotUndefinedEffect('displaySize', props.displaySize)
69+
useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
6670
useEffect(() => {
6771
if (props.theme === 'light') {
6872
setState({
@@ -179,4 +183,4 @@ export const JsonViewer = function JsonViewer<Value> (props: JsonViewerProps<Val
179183
export * from './components/DataTypes'
180184
export * from './theme/base16'
181185
export * from './type'
182-
export { applyValue, createDataType, defineDataType, isCycleReference, safeStringify } from './utils'
186+
export { applyValue, createDataType, defineDataType, deleteValue, isCycleReference, safeStringify } from './utils'

0 commit comments

Comments
 (0)