Skip to content

Commit

Permalink
feat: support add and delete feature (#438)
Browse files Browse the repository at this point in the history
* feat: support add and delete feature

* docs: add missing `enableDelete` document

* fix: prevent prototype polluting
  • Loading branch information
pionxzh authored Jan 16, 2024
1 parent a03362c commit 1709001
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 50 deletions.
10 changes: 7 additions & 3 deletions docs/pages/apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
| `sx` | `SxProps` | - | [The `sx` prop](https://mui.com/system/getting-started/the-sx-prop/) lets you style elements inline, using values from the theme. |
| `indentWidth` | `number` | 3 | Indent width for nested objects |
| `keyRenderer` | `{when: (props) => boolean}` | - | Customize a key, if `keyRenderer.when` returns `true`. |
| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) |
| `valueTypes` | `ValueTypes` | - | Customize the definition of data types. See [Defining Data Types](/how-to/data-types) |
| `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. |
| `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. |
| `enableClipboard` | `boolean` | `false` | Whether enable clipboard feature. |
| `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. |
| `onChange` | `(path, oldVal, newVal) => void` | - | Callback when value changed. |
| `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. |
| `onSelect` | `(path, value) => void` | - | Callback when value selected. |
| `enableClipboard` | `boolean` | `true` | Whether enable clipboard feature. |
| `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. |
| `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. |
| `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. |
| `defaultInspectDepth` | `number` | 5 | Default inspect depth for nested objects.<br /><br />_\* If the number is set too large, it could result in performance issues._ |
| `defaultInspectControl` | `(path, currentValue) => boolean` | - | Whether expand or collapse a field by default. Using this will override `defaultInspectDepth`. |
| `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._ |
Expand Down
23 changes: 23 additions & 0 deletions docs/pages/full/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,15 @@ import {
import type {
DataType,
JsonViewerKeyRenderer,
JsonViewerOnAdd,
JsonViewerOnChange,
JsonViewerOnDelete,
JsonViewerTheme
} from '@textea/json-viewer'
import {
applyValue,
defineDataType,
deleteValue,
JsonViewer,
stringType
} from '@textea/json-viewer'
Expand Down Expand Up @@ -336,6 +339,8 @@ const IndexPage: FC = () => {
highlightUpdates={highlightUpdates}
indentWidth={indent}
theme={theme}
enableAdd={true}
enableDelete={true}
displayDataTypes={displayDataTypes}
displaySize={displaySize}
groupArraysAfterLength={groupArraysAfterLength}
Expand All @@ -345,13 +350,31 @@ const IndexPage: FC = () => {
linkType,
imageDataType
]}
onAdd={
useCallback<JsonViewerOnAdd>(
(path) => {
const key = prompt('Key:')
if (key === null) return
const value = prompt('Value:')
if (value === null) return
setSrc(src => applyValue(src, [...path, key], value))
}, []
)
}
onChange={
useCallback<JsonViewerOnChange>(
(path, oldValue, newValue) => {
setSrc(src => applyValue(src, path, newValue))
}, []
)
}
onDelete={
useCallback<JsonViewerOnDelete>(
(path, value) => {
setSrc(src => deleteValue(src, path, value))
}, []
)
}
sx={{
paddingLeft: 2
}}
Expand Down
1 change: 1 addition & 0 deletions docs/pages/how-to/data-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The `is` function takes a value and a path and returns true if the value belongs

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:

- `props.path` - The path to the value.
- `props.value` - The value to render.
- `props.inspect` - A Boolean flag indicating whether the value is being inspected (expanded).
- `props.setInspect` - A function that can be used to toggle the inspect state.
Expand Down
73 changes: 72 additions & 1 deletion src/components/DataKeyPair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { useInspect } from '../hooks/useInspect'
import { useJsonViewerStore } from '../stores/JsonViewerStore'
import { useTypeComponents } from '../stores/typeRegistry'
import type { DataItemProps } from '../type'
import { copyString, getValueSize } from '../utils'
import { copyString, getValueSize, isPlainObject } from '../utils'
import {
AddBoxIcon,
CheckIcon,
ChevronRightIcon,
CloseIcon,
ContentCopyIcon,
DeleteIcon,
EditIcon,
ExpandMoreIcon
} from './Icons'
Expand Down Expand Up @@ -82,6 +84,51 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
const isRoot = root === value
const isNumberKey = Number.isInteger(Number(key))

const storeEnableAdd = useJsonViewerStore(store => store.enableAdd)
const onAdd = useJsonViewerStore(store => store.onAdd)
const enableAdd = useMemo(() => {
if (!onAdd || nestedIndex !== undefined) return false

if (storeEnableAdd === false) {
return false
}
if (propsEditable === false) {
// props.editable is false which means we cannot provide the suitable way to edit it
return false
}
if (typeof storeEnableAdd === 'function') {
return !!storeEnableAdd(path, value)
}

if (Array.isArray(value) || isPlainObject(value)) {
return true
}

return false
}, [onAdd, nestedIndex, path, storeEnableAdd, propsEditable, value])

const storeEnableDelete = useJsonViewerStore(store => store.enableDelete)
const onDelete = useJsonViewerStore(store => store.onDelete)
const enableDelete = useMemo(() => {
if (!onDelete || nestedIndex !== undefined) return false

if (isRoot) {
// don't allow delete root
return false
}
if (storeEnableDelete === false) {
return false
}
if (propsEditable === false) {
// props.editable is false which means we cannot provide the suitable way to edit it
return false
}
if (typeof storeEnableDelete === 'function') {
return !!storeEnableDelete(path, value)
}
return storeEnableDelete
}, [onDelete, nestedIndex, isRoot, path, storeEnableDelete, propsEditable, value])

const enableClipboard = useJsonViewerStore(store => store.enableClipboard)
const { copy, copied } = useClipboard()

Expand Down Expand Up @@ -205,6 +252,26 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
<EditIcon sx={{ fontSize: '.8rem' }} />
</IconBox>
)}
{enableAdd && (
<IconBox
onClick={event => {
event.preventDefault()
onAdd?.(path)
}}
>
<AddBoxIcon sx={{ fontSize: '.8rem' }} />
</IconBox>
)}
{enableDelete && (
<IconBox
onClick={event => {
event.preventDefault()
onDelete?.(path, value)
}}
>
<DeleteIcon sx={{ fontSize: '.9rem' }} />
</IconBox>
)}
</>
)
},
Expand All @@ -217,9 +284,13 @@ export const DataKeyPair: FC<DataKeyPairProps> = (props) => {
editable,
editing,
enableClipboard,
enableAdd,
enableDelete,
tempValue,
path,
value,
onAdd,
onDelete,
startEditing,
abortEditing,
commitEditing
Expand Down
10 changes: 10 additions & 0 deletions src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ const BaseIcon: FC<SvgIconProps> = ({ d, ...props }) => {
)
}

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'
const Check = 'M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'
const ChevronRight = 'M10 6 8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'
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'
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'
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'
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'
const ExpandMore = 'M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6z'
const Delete = 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zM8 9h8v10H8zm7.5-5l-1-1h-5l-1 1H5v2h14V4z'

export const AddBoxIcon: FC<SvgIconProps> = (props) => {
return <BaseIcon d={AddBox} {...props} />
}

export const CheckIcon: FC<SvgIconProps> = (props) => {
return <BaseIcon d={Check} {...props} />
Expand Down Expand Up @@ -45,3 +51,7 @@ export const EditIcon: FC<SvgIconProps> = (props) => {
export const ExpandMoreIcon: FC<SvgIconProps> = (props) => {
return <BaseIcon d={ExpandMore} {...props} />
}

export const DeleteIcon: FC<SvgIconProps> = (props) => {
return <BaseIcon d={Delete} {...props} />
}
22 changes: 13 additions & 9 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,23 @@ const JsonViewerInner: FC<JsonViewerProps> = (props) => {
value: props.value
}))
}, [props.value, setState])
useSetIfNotUndefinedEffect('editable', props.editable)
useSetIfNotUndefinedEffect('rootName', props.rootName)
useSetIfNotUndefinedEffect('indentWidth', props.indentWidth)
useSetIfNotUndefinedEffect('onChange', props.onChange)
useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
useSetIfNotUndefinedEffect('keyRenderer', props.keyRenderer)
useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
useSetIfNotUndefinedEffect('enableAdd', props.enableAdd)
useSetIfNotUndefinedEffect('enableDelete', props.enableDelete)
useSetIfNotUndefinedEffect('enableClipboard', props.enableClipboard)
useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
useSetIfNotUndefinedEffect('rootName', props.rootName)
useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
useSetIfNotUndefinedEffect('displaySize', props.displaySize)
useSetIfNotUndefinedEffect('editable', props.editable)
useSetIfNotUndefinedEffect('onChange', props.onChange)
useSetIfNotUndefinedEffect('onCopy', props.onCopy)
useSetIfNotUndefinedEffect('onSelect', props.onSelect)
useSetIfNotUndefinedEffect('onAdd', props.onAdd)
useSetIfNotUndefinedEffect('onDelete', props.onDelete)
useSetIfNotUndefinedEffect('maxDisplayLength', props.maxDisplayLength)
useSetIfNotUndefinedEffect('groupArraysAfterLength', props.groupArraysAfterLength)
useSetIfNotUndefinedEffect('displayDataTypes', props.displayDataTypes)
useSetIfNotUndefinedEffect('displaySize', props.displaySize)
useSetIfNotUndefinedEffect('highlightUpdates', props.highlightUpdates)
useEffect(() => {
if (props.theme === 'light') {
setState({
Expand Down Expand Up @@ -179,4 +183,4 @@ export const JsonViewer = function JsonViewer<Value> (props: JsonViewerProps<Val
export * from './components/DataTypes'
export * from './theme/base16'
export * from './type'
export { applyValue, createDataType, defineDataType, isCycleReference, safeStringify } from './utils'
export { applyValue, createDataType, defineDataType, deleteValue, isCycleReference, safeStringify } from './utils'
Loading

0 comments on commit 1709001

Please sign in to comment.