Skip to content

#138 #145 Event triggers & callbacks #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
223a25f
WIP
CarlosNZ Feb 14, 2025
2641e6f
Merge branch 'main' into 122-cancel-on-type-change-NEW
CarlosNZ Feb 14, 2025
395fea7
Merge branch 'main' into 122-cancel-on-type-change-NEW
CarlosNZ Feb 20, 2025
e0ef953
Initial mechanism implementation
CarlosNZ Feb 24, 2025
9fab91f
Try using "shouldUpdateUndo"
CarlosNZ Feb 26, 2025
05d1fc5
Revert attempt to handle undo
CarlosNZ Feb 28, 2025
3f0b200
Update JsonEditor.tsx
CarlosNZ Feb 28, 2025
7cefe11
Handle revert for Collection nodes
CarlosNZ Mar 1, 2025
005ee75
Update App.tsx
CarlosNZ Mar 1, 2025
4f18102
Revert App
CarlosNZ Mar 1, 2025
83c86dd
Revert some other stuff
CarlosNZ Mar 1, 2025
4cf31fc
Update TreeStateProvider.tsx
CarlosNZ Mar 1, 2025
eb9fe8e
Add onCollapse and onEditEvent functions
CarlosNZ Mar 1, 2025
eb98af7
Merge branch 'external-control-test' into 138-145-event-triggers
CarlosNZ Mar 1, 2025
88e2f6e
Update App
CarlosNZ Mar 1, 2025
23884b4
Merge branch 'external-control-test' into 138-145-event-triggers
CarlosNZ Mar 1, 2025
158c5e1
Tweak naming
CarlosNZ Mar 1, 2025
d31e7fb
Merge branch '176-additional-event-functions' into 138-145-event-trig…
CarlosNZ Mar 1, 2025
daf710a
Get edit controls working
CarlosNZ Mar 1, 2025
cd91bba
Update useTriggers.ts
CarlosNZ Mar 1, 2025
f227c63
Update useTriggers.ts
CarlosNZ Mar 1, 2025
f581cfc
Get multi-collapse working
CarlosNZ Mar 3, 2025
a63d60d
Clarify onCollapse props
CarlosNZ Mar 3, 2025
a4c9cd6
Remove initial Context state
CarlosNZ Mar 3, 2025
f0e0895
Merge branch 'main' into 138-145-event-triggers
CarlosNZ Mar 9, 2025
db3ae5d
Finesse triggers
CarlosNZ Mar 10, 2025
1886fd6
Further tweaks
CarlosNZ Mar 10, 2025
2158bf4
Merge branch 'main' into 138-145-event-triggers
CarlosNZ Mar 20, 2025
465f9ad
Update docs, test in App
CarlosNZ Mar 20, 2025
f4f6ca7
Tidy up formatting
CarlosNZ Mar 20, 2025
7c72340
Update README.md
CarlosNZ Mar 20, 2025
46c31d1
Update README.md
CarlosNZ Mar 20, 2025
9bcba71
Comment out unused in App
CarlosNZ Mar 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 90 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,24 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS

### Features include:

- edit individual values, or whole objects as JSON text
- fine-grained control over which elements can be edited, deleted, or added to
- full [JSON Schema](https://json-schema.org/) validation (using 3rd-party validation library)
- customisable UI, through simple, pre-defined [themes](#themes--styles), specific CSS overrides for UI components, or by targeting CSS classes
- self-contained — rendered with plain HTML/CSS, so no dependence on external UI libraries
- search/filter data by key, value or custom function
- provide your own [custom component](#custom-nodes) to integrate specialised UI for certain data.
- Edit individual values, or whole objects as JSON text
- Fine-grained control over which elements can be edited, deleted, or added to
- Full [JSON Schema](https://json-schema.org/) validation (using 3rd-party validation library)
- Customisable UI, through simple, pre-defined [themes](#themes--styles), specific CSS overrides for UI components, or by targeting CSS classes
- Self-contained — rendered with plain HTML/CSS, so no dependence on external UI libraries
- Search/filter data by key, value or custom function
- Provide your own [custom component](#custom-nodes) to integrate specialised UI for certain data.
- [localisable](#localisation) UI
- [Drag-n-drop](#drag-n-drop) editing
- [Keyboard customisation](#keyboard-customisation)
- [External control](#external-control-1) via callbacks and triggers

<img width="392" alt="screenshot" src="image/screenshot.png">

> [!IMPORTANT]
> Breaking changes:
> - **Version 1.19.0** has a change to the `theme` input. Built-in themes must now
> be imported separately and passed in, rather than just naming the theme as a
> string. This is better for tree-shaking, so unused themes won't be bundled
> with your build. See [Themes & Styles](#themes--styles)
> - **Version 1.14.0** has a change which recommends you provide a `setData` prop
> and not use `onUpdate` for updating your data externally. See [Managing
> state](#managing-state).
> - **Version 1.19.0** has a change to the `theme` input. Built-in themes must now be imported separately and passed in, rather than just naming the theme as a string. This is better for tree-shaking, so unused themes won't be bundled with your build. See [Themes & Styles](#themes--styles).
> - **Version 1.14.0** has a change which recommends you provide a `setData` prop and not use `onUpdate` for updating your data externally. See [Managing state](#managing-state).

## Contents <!-- omit in toc -->
- [Installation](#installation)
Expand All @@ -46,6 +42,7 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
- [Look and Feel / UI](#look-and-feel--ui)
- [Search and Filtering](#search-and-filtering)
- [Custom components \& overrides (incl. Localisation)](#custom-components--overrides-incl-localisation)
- [External control](#external-control)
- [Miscellaneous](#miscellaneous)
- [Managing state](#managing-state)
- [Update functions](#update-functions)
Expand All @@ -70,6 +67,9 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
- [Custom Text](#custom-text)
- [Custom Buttons](#custom-buttons)
- [Keyboard customisation](#keyboard-customisation)
- [External control](#external-control-1)
- [Event callbacks](#event-callbacks)
- [Event triggers](#event-triggers)
- [Undo functionality](#undo-functionality)
- [Exported helpers](#exported-helpers)
- [Functions \& Components](#functions--components)
Expand Down Expand Up @@ -195,7 +195,16 @@ The only *required* property is `data` (although you will need to provide a `set
| `TextEditor` | `ReactComponent<TextEditorProps>` | | Pass a component to offer a custom text/code editor when editing full JSON object as text. [See details](#full-object-editing) |
| `jsonParse` | `(input: string) => JsonData` | `JSON.parse` | When editing a block of JSON directly, you may wish to allow some "looser" input -- e.g. 'single quotes', trailing commas, or unquoted field names. In this case, you can provide a third-party JSON parsing method. I recommend [JSON5](https://json5.org/), which is what is used in the [Demo](https://carlosnz.github.io/json-edit-react/) |
| `jsonStringify` | `(data: JsonData) => string` | `(data) => JSON.stringify(data, null, 2)` | Similarly, you can override the default presentation of the JSON string when starting editing JSON. You can supply different formatting parameters to the native `JSON.stringify()`, or provide a third-party option, like the aforementioned JSON5. |
| `keyboardControls` | `KeyboardControls` | As explained [above](#usage) | Override some or all of the keyboard controls. See [Keyboard customisation](#keyboard-customisation) for details. | |
| `keyboardControls` | `KeyboardControls` | As explained [above](#usage) | Override some or all of the keyboard controls. See [Keyboard customisation](#keyboard-customisation) for details. |
### External control

More detail [below](#external-control-1)

| Prop | Type | Default | Description |
| ------------------ | --------------------- | ------- | -------------------------------------------------------------------- |
| `onEditEvent` | `OnEditEventFunction` | none | Callback to execute whenever the user starts or stops editing a node |
| `onCollapse` | `OnCollapseFunction` | none | Callback to execute whenever the user collapses or opens a node |
| `externalTriggers` | `ExternalTriggers` | none | Specify a node to collapse/open, or to start/stop editing | |

### Miscellaneous

Expand Down Expand Up @@ -798,7 +807,71 @@ If (for example), you just wish to change the general "confirmation" action to "
- If multiple modifiers are specified (in an array), *any* of them will be accepted (multi-modifier commands not currently supported)
- You only need to specify values for `stringConfirm`, `numberConfirm`, and `booleanConfirm` if they should *differ* from your `confirm` value.
- You won't be able to override system or browser behaviours: for example, on Mac "Ctrl-click" will perform a right-click, so using it as a click modifier won't work (hence we also accept "Meta"/"Cmd" as the default `clipboardModifier`).


## External control

You can interact with the component externally, with event callbacks and triggers to set/get the collapse or editing state of any node.

### Event callbacks

Pass in a function to the props `onEditEvent` and `onCollapse` if you want your app to be able to respond to these events.

The `onEditEvent` callback is executed whenever the user starts or stops editing a node, and has the following signature:

```ts
type OnEditEventFunction =
(path: CollectionKey[] | null, isKey: boolean) => void
```

The `path` will be an array representing the path components when starting to edit, and `null` when ending the edit. The `isKey` indicates whether the edit is for the property `key` rather than `value`.

The `onCollapse` callback is executed when user opens or collapses a node, and has the following signature:

```ts
type OnCollapseFunction = (
{
path: CollectionKey[],
collapsed: boolean, // closing = true, opening = false
includeChildren: boolean // if was clicked with Modifier key to
// open/close all descendants as well
}
) => void
```

### Event triggers

You can *trigger* collapse and editing actions by changing the the `externalTriggers` prop.

The shape of the `externalTriggers` object is:

```ts
interface ExternalTriggers {
collapse?: CollapseState | CollapseState[]
edit?: EditState
}

// CollapseState same as `onCollapseFunction` (above) input
interface CollapseState {
path: CollectionKey[]
collapsed: boolean
includeChildren: boolean
}

interface EditState {
path?: CollectionKey[]
action?: 'accept' | 'cancel'
}
```

For the `edit` trigger, the `path` is only required when *starting* to edit, and
the `action` is only required when *stopping* the edit, to determine whether the
component should cancel or submit the current changes.

> [!WARNING]
> Ensure that your `externalTriggers` object is stable (i.e. doesn't create new instances on each render) so as to not cause unwanted triggering -- you may need to wrap it in `useMemo`.
> You should also be careful that your event callbacks and triggers don't cause an infinite loop!


## Undo functionality

Even though Undo/Redo functionality is probably desirable in most cases, this is not built in to the component, for two main reasons:
Expand Down Expand Up @@ -826,7 +899,7 @@ A few helper functions, components and types that might be useful in your own im
- `ThemeInput`: input type for the `theme` prop
- `JsonEditorProps`: all input props for the Json Editor component
- `JsonData`: main `data` object -- any valid JSON structure
- [`UpdateFunction`](#update-functions), [`OnChangeFunction`](#onchange-function), [`OnErrorFunction`](#onerror-function) [`FilterFunction`](#filter-functions), [`CopyFunction`](#copy-function), [`SearchFilterFunction`](#searchfiltering), [`CompareFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort),[`TypeFilterFunction`](#filter-functions), [`LocalisedString`](#localisation), [`CustomNodeDefinition`](#custom-nodes), [`CustomTextDefinitions`](#custom-text), [`CustomTextFunction`](#custom-text),
- [`UpdateFunction`](#update-functions), [`OnChangeFunction`](#onchange-function), [`OnErrorFunction`](#onerror-function) [`FilterFunction`](#filter-functions), [`CopyFunction`](#copy-function), [`SearchFilterFunction`](#searchfiltering), [`OnEditEventFunction`](#event-callbacks), [`OnCollapseFunction`](#event-callbacks), [`CompareFunction`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort),[`TypeFilterFunction`](#filter-functions), [`LocalisedString`](#localisation), [`CustomNodeDefinition`](#custom-nodes), [`CustomTextDefinitions`](#custom-text), [`CustomTextFunction`](#custom-text), [`ExternalTriggers`](#event-triggers)
- `TranslateFunction`: function that takes a [localisation](#localisation) key and returns a translated string
- `IconReplacements`: input type for the `icons` prop
- `CollectionNodeProps`: all props passed internally to "collection" nodes (i.e. objects/arrays)
Expand Down
37 changes: 35 additions & 2 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
monoDarkTheme,
candyWrapperTheme,
psychedelicTheme,
// ExternalTriggers,
// type CollapseState
} from './_imports'
import { FaNpm, FaExternalLinkAlt, FaGithub } from 'react-icons/fa'
import { BiReset } from 'react-icons/bi'
Expand Down Expand Up @@ -111,6 +113,11 @@ function App() {
customTextEditor: false,
})

// const [isEditing, setIsEditing] = useState(false)
// const collapseState = useRef<Record<string, CollapseState>>({})
// const [collapseData, setCollapseData] = useState<CollapseState[]>()
// const [triggers, setTriggers] = useState<ExternalTriggers>()

const [isSaving, setIsSaving] = useState(false)
const previousTheme = useRef<Theme>() // Used when resetting after theme editing
const toast = useToast()
Expand All @@ -129,6 +136,19 @@ function App() {
if (selectedDataSet === 'liveData' && !loading && liveData) reset(liveData)
}, [loading, liveData, reset, selectedDataSet])

// useEffect(() => {
// const localStorageState = localStorage.getItem('collapseState')
// if (localStorageState) {
// setTimeout(() => {
// const data = JSON.parse(localStorageState) as Record<string, CollapseState>
// collapseState.current = data
// const collapseArray = Object.values(data)
// setCollapseData(collapseArray)
// // console.log('collapseArray', collapseArray)
// }, 500)
// }
// }, [])

const updateState = (patch: Partial<AppState>) => setState({ ...state, ...patch })

const toggleState = (field: keyof AppState) => updateState({ [field]: !state[field] })
Expand Down Expand Up @@ -486,11 +506,23 @@ function App() {
)
: undefined
}
// onEditEvent={(path, isKey) => console.log('Editing path', path, isKey)}
// onCollapse={(input) => console.log('Collapse', input)}
// collapseClickZones={['property', 'header']}
// onEditEvent={(path) => {
// console.log(path)
// setIsEditing(path ? true : false)
// }}
// onCollapse={(input) => {
// const path = JSON.stringify(input.path)
// const newCollapseState = { ...collapseState.current, [path]: input }
// collapseState.current = newCollapseState
// localStorage.setItem('collapseState', JSON.stringify(newCollapseState))
// }}
// externalTriggers={triggers}
/>
</Box>
{/* <Button onClick={() => setTriggers({ edit: { action: 'accept' } })}>
Click to stop edit
</Button> */}
<VStack w="100%" align="flex-end" gap={4}>
<HStack w="100%" justify="space-between" mt={4}>
<Button
Expand Down Expand Up @@ -525,6 +557,7 @@ function App() {
onClick={handleReset}
visibility={canUndo ? 'visible' : 'hidden'}
isLoading={isSaving}
// isDisabled={isEditing}
>
{selectedDataSet === 'liveData' ? 'Push to the cloud' : 'Reset'}
</Button>
Expand Down
8 changes: 6 additions & 2 deletions src/ButtonPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface EditButtonProps {
e: React.KeyboardEvent,
eventMap: Partial<Record<keyof KeyboardControlsFull, () => void>>
) => void
editConfirmRef: React.RefObject<HTMLDivElement>
}

export const EditButtons: React.FC<EditButtonProps> = ({
Expand All @@ -41,6 +42,7 @@ export const EditButtons: React.FC<EditButtonProps> = ({
translate,
keyboardControls,
handleKeyboard,
editConfirmRef,
}) => {
const { getStyles } = useTheme()
const NEW_KEY_PROMPT = translate('KEY_NEW', nodeData)
Expand Down Expand Up @@ -189,6 +191,7 @@ export const EditButtons: React.FC<EditButtonProps> = ({
setIsAdding(false)
}}
nodeData={nodeData}
editConfirmRef={editConfirmRef}
/>
</>
)}
Expand All @@ -200,10 +203,11 @@ export const InputButtons: React.FC<{
onOk: (e: React.MouseEvent<HTMLElement>) => void
onCancel: (e: React.MouseEvent<HTMLElement>) => void
nodeData: NodeData
}> = ({ onOk, onCancel, nodeData }) => {
editConfirmRef: React.RefObject<HTMLDivElement>
}> = ({ onOk, onCancel, nodeData, editConfirmRef }) => {
return (
<div className="jer-confirm-buttons">
<div onClick={onOk}>
<div onClick={onOk} ref={editConfirmRef}>
<Icon name="ok" nodeData={nodeData} />
</div>
<div onClick={onCancel}>
Expand Down
24 changes: 17 additions & 7 deletions src/CollectionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
const {
collapseState,
setCollapseState,
doesPathMatch,
getMatchingCollapseState,
currentlyEditingElement,
setCurrentlyEditingElement,
areChildrenBeingEdited,
Expand Down Expand Up @@ -62,6 +62,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
handleKeyboard,
insertAtTop,
onCollapse,
editConfirmRef,
collapseClickZones,
} = props
const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data))
Expand Down Expand Up @@ -113,9 +114,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
}, [collapseFilter])

useEffect(() => {
if (collapseState !== null && doesPathMatch(path)) {
hasBeenOpened.current = true
animateCollapse(collapseState.collapsed)
if (collapseState !== null) {
const matchingCollapse = getMatchingCollapseState(path)
if (matchingCollapse) {
hasBeenOpened.current = true
animateCollapse(matchingCollapse.collapsed)
}
}
}, [collapseState])

Expand Down Expand Up @@ -180,13 +184,13 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
const modifier = getModifier(e)
if (modifier && keyboardControls.collapseModifier.includes(modifier)) {
hasBeenOpened.current = true
setCollapseState({ collapsed: !collapsed, path })
setCollapseState({ collapsed: !collapsed, path, includeChildren: true })
return
}
if (!(currentlyEditingElement && currentlyEditingElement.includes(pathString))) {
hasBeenOpened.current = true
setCollapseState(null)
if (onCollapse) onCollapse({ path, collapsed: !collapsed, includesChildren: false })
if (onCollapse) onCollapse({ path, collapsed: !collapsed, includeChildren: false })
animateCollapse(!collapsed)
}
}
Expand Down Expand Up @@ -334,7 +338,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
/>
)}
<div className="jer-collection-input-button-row">
<InputButtons onOk={handleEdit} onCancel={handleCancel} nodeData={nodeData} />
<InputButtons
onOk={handleEdit}
onCancel={handleCancel}
nodeData={nodeData}
editConfirmRef={editConfirmRef}
/>
</div>
</div>
)
Expand Down Expand Up @@ -392,6 +401,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
customButtons={props.customButtons}
keyboardControls={keyboardControls}
handleKeyboard={handleKeyboard}
editConfirmRef={editConfirmRef}
/>
)

Expand Down
7 changes: 6 additions & 1 deletion src/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
type KeyboardControls,
} from './types'
import { useTheme, ThemeProvider, TreeStateProvider, defaultTheme, useTreeState } from './contexts'
import { useData } from './hooks/useData'
import { useData, useTriggers } from './hooks'
import { getTranslateFunction } from './localisation'
import { ValueNodeWrapper } from './ValueNodeWrapper'

Expand Down Expand Up @@ -74,6 +74,7 @@ const Editor: React.FC<JsonEditorProps> = ({
TextEditor,
errorMessageTimeout = 2500,
keyboardControls = {},
externalTriggers,
insertAtTop = false,
onCollapse,
collapseClickZones = ['header', 'left'],
Expand Down Expand Up @@ -281,6 +282,9 @@ const Editor: React.FC<JsonEditorProps> = ({
[keyboardControls]
)

const editConfirmRef = useRef<HTMLDivElement>(null)
useTriggers(externalTriggers, editConfirmRef)

// Common "sort" method for ordering nodes, based on the `keySort` prop
// - If it's false (the default), we do nothing
// - If true, use default array sort on the node's key
Expand Down Expand Up @@ -354,6 +358,7 @@ const Editor: React.FC<JsonEditorProps> = ({
array: insertAtTop === true || insertAtTop === 'array',
},
onCollapse,
editConfirmRef,
collapseClickZones,
}

Expand Down
Loading