Skip to content

Commit e9485e8

Browse files
authored
#138 #145 Event triggers & callbacks (#182)
1 parent 3ef2069 commit e9485e8

11 files changed

+280
-70
lines changed

README.md

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,24 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
1313

1414
### Features include:
1515

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

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

2930
> [!IMPORTANT]
3031
> Breaking changes:
31-
> - **Version 1.19.0** has a change to the `theme` input. Built-in themes must now
32-
> be imported separately and passed in, rather than just naming the theme as a
33-
> string. This is better for tree-shaking, so unused themes won't be bundled
34-
> with your build. See [Themes & Styles](#themes--styles)
35-
> - **Version 1.14.0** has a change which recommends you provide a `setData` prop
36-
> and not use `onUpdate` for updating your data externally. See [Managing
37-
> state](#managing-state).
32+
> - **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).
33+
> - **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).
3834
3935
## Contents <!-- omit in toc -->
4036
- [Installation](#installation)
@@ -46,6 +42,7 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
4642
- [Look and Feel / UI](#look-and-feel--ui)
4743
- [Search and Filtering](#search-and-filtering)
4844
- [Custom components \& overrides (incl. Localisation)](#custom-components--overrides-incl-localisation)
45+
- [External control](#external-control)
4946
- [Miscellaneous](#miscellaneous)
5047
- [Managing state](#managing-state)
5148
- [Update functions](#update-functions)
@@ -70,6 +67,9 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
7067
- [Custom Text](#custom-text)
7168
- [Custom Buttons](#custom-buttons)
7269
- [Keyboard customisation](#keyboard-customisation)
70+
- [External control](#external-control-1)
71+
- [Event callbacks](#event-callbacks)
72+
- [Event triggers](#event-triggers)
7373
- [Undo functionality](#undo-functionality)
7474
- [Exported helpers](#exported-helpers)
7575
- [Functions \& Components](#functions--components)
@@ -195,7 +195,16 @@ The only *required* property is `data` (although you will need to provide a `set
195195
| `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) |
196196
| `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/) |
197197
| `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. |
198-
| `keyboardControls` | `KeyboardControls` | As explained [above](#usage) | Override some or all of the keyboard controls. See [Keyboard customisation](#keyboard-customisation) for details. | |
198+
| `keyboardControls` | `KeyboardControls` | As explained [above](#usage) | Override some or all of the keyboard controls. See [Keyboard customisation](#keyboard-customisation) for details. |
199+
### External control
200+
201+
More detail [below](#external-control-1)
202+
203+
| Prop | Type | Default | Description |
204+
| ------------------ | --------------------- | ------- | -------------------------------------------------------------------- |
205+
| `onEditEvent` | `OnEditEventFunction` | none | Callback to execute whenever the user starts or stops editing a node |
206+
| `onCollapse` | `OnCollapseFunction` | none | Callback to execute whenever the user collapses or opens a node |
207+
| `externalTriggers` | `ExternalTriggers` | none | Specify a node to collapse/open, or to start/stop editing | |
199208

200209
### Miscellaneous
201210

@@ -798,7 +807,71 @@ If (for example), you just wish to change the general "confirmation" action to "
798807
- If multiple modifiers are specified (in an array), *any* of them will be accepted (multi-modifier commands not currently supported)
799808
- You only need to specify values for `stringConfirm`, `numberConfirm`, and `booleanConfirm` if they should *differ* from your `confirm` value.
800809
- 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`).
801-
810+
811+
## External control
812+
813+
You can interact with the component externally, with event callbacks and triggers to set/get the collapse or editing state of any node.
814+
815+
### Event callbacks
816+
817+
Pass in a function to the props `onEditEvent` and `onCollapse` if you want your app to be able to respond to these events.
818+
819+
The `onEditEvent` callback is executed whenever the user starts or stops editing a node, and has the following signature:
820+
821+
```ts
822+
type OnEditEventFunction =
823+
(path: CollectionKey[] | null, isKey: boolean) => void
824+
```
825+
826+
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`.
827+
828+
The `onCollapse` callback is executed when user opens or collapses a node, and has the following signature:
829+
830+
```ts
831+
type OnCollapseFunction = (
832+
{
833+
path: CollectionKey[],
834+
collapsed: boolean, // closing = true, opening = false
835+
includeChildren: boolean // if was clicked with Modifier key to
836+
// open/close all descendants as well
837+
}
838+
) => void
839+
```
840+
841+
### Event triggers
842+
843+
You can *trigger* collapse and editing actions by changing the the `externalTriggers` prop.
844+
845+
The shape of the `externalTriggers` object is:
846+
847+
```ts
848+
interface ExternalTriggers {
849+
collapse?: CollapseState | CollapseState[]
850+
edit?: EditState
851+
}
852+
853+
// CollapseState same as `onCollapseFunction` (above) input
854+
interface CollapseState {
855+
path: CollectionKey[]
856+
collapsed: boolean
857+
includeChildren: boolean
858+
}
859+
860+
interface EditState {
861+
path?: CollectionKey[]
862+
action?: 'accept' | 'cancel'
863+
}
864+
```
865+
866+
For the `edit` trigger, the `path` is only required when *starting* to edit, and
867+
the `action` is only required when *stopping* the edit, to determine whether the
868+
component should cancel or submit the current changes.
869+
870+
> [!WARNING]
871+
> 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`.
872+
> You should also be careful that your event callbacks and triggers don't cause an infinite loop!
873+
874+
802875
## Undo functionality
803876
804877
Even though Undo/Redo functionality is probably desirable in most cases, this is not built in to the component, for two main reasons:
@@ -826,7 +899,7 @@ A few helper functions, components and types that might be useful in your own im
826899
- `ThemeInput`: input type for the `theme` prop
827900
- `JsonEditorProps`: all input props for the Json Editor component
828901
- `JsonData`: main `data` object -- any valid JSON structure
829-
- [`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),
902+
- [`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)
830903
- `TranslateFunction`: function that takes a [localisation](#localisation) key and returns a translated string
831904
- `IconReplacements`: input type for the `icons` prop
832905
- `CollectionNodeProps`: all props passed internally to "collection" nodes (i.e. objects/arrays)

demo/src/App.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
monoDarkTheme,
1717
candyWrapperTheme,
1818
psychedelicTheme,
19+
// ExternalTriggers,
20+
// type CollapseState
1921
} from './_imports'
2022
import { FaNpm, FaExternalLinkAlt, FaGithub } from 'react-icons/fa'
2123
import { BiReset } from 'react-icons/bi'
@@ -111,6 +113,11 @@ function App() {
111113
customTextEditor: false,
112114
})
113115

116+
// const [isEditing, setIsEditing] = useState(false)
117+
// const collapseState = useRef<Record<string, CollapseState>>({})
118+
// const [collapseData, setCollapseData] = useState<CollapseState[]>()
119+
// const [triggers, setTriggers] = useState<ExternalTriggers>()
120+
114121
const [isSaving, setIsSaving] = useState(false)
115122
const previousTheme = useRef<Theme>() // Used when resetting after theme editing
116123
const toast = useToast()
@@ -129,6 +136,19 @@ function App() {
129136
if (selectedDataSet === 'liveData' && !loading && liveData) reset(liveData)
130137
}, [loading, liveData, reset, selectedDataSet])
131138

139+
// useEffect(() => {
140+
// const localStorageState = localStorage.getItem('collapseState')
141+
// if (localStorageState) {
142+
// setTimeout(() => {
143+
// const data = JSON.parse(localStorageState) as Record<string, CollapseState>
144+
// collapseState.current = data
145+
// const collapseArray = Object.values(data)
146+
// setCollapseData(collapseArray)
147+
// // console.log('collapseArray', collapseArray)
148+
// }, 500)
149+
// }
150+
// }, [])
151+
132152
const updateState = (patch: Partial<AppState>) => setState({ ...state, ...patch })
133153

134154
const toggleState = (field: keyof AppState) => updateState({ [field]: !state[field] })
@@ -486,11 +506,23 @@ function App() {
486506
)
487507
: undefined
488508
}
489-
// onEditEvent={(path, isKey) => console.log('Editing path', path, isKey)}
490-
// onCollapse={(input) => console.log('Collapse', input)}
491509
// collapseClickZones={['property', 'header']}
510+
// onEditEvent={(path) => {
511+
// console.log(path)
512+
// setIsEditing(path ? true : false)
513+
// }}
514+
// onCollapse={(input) => {
515+
// const path = JSON.stringify(input.path)
516+
// const newCollapseState = { ...collapseState.current, [path]: input }
517+
// collapseState.current = newCollapseState
518+
// localStorage.setItem('collapseState', JSON.stringify(newCollapseState))
519+
// }}
520+
// externalTriggers={triggers}
492521
/>
493522
</Box>
523+
{/* <Button onClick={() => setTriggers({ edit: { action: 'accept' } })}>
524+
Click to stop edit
525+
</Button> */}
494526
<VStack w="100%" align="flex-end" gap={4}>
495527
<HStack w="100%" justify="space-between" mt={4}>
496528
<Button
@@ -525,6 +557,7 @@ function App() {
525557
onClick={handleReset}
526558
visibility={canUndo ? 'visible' : 'hidden'}
527559
isLoading={isSaving}
560+
// isDisabled={isEditing}
528561
>
529562
{selectedDataSet === 'liveData' ? 'Push to the cloud' : 'Reset'}
530563
</Button>

src/ButtonPanels.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ interface EditButtonProps {
2828
e: React.KeyboardEvent,
2929
eventMap: Partial<Record<keyof KeyboardControlsFull, () => void>>
3030
) => void
31+
editConfirmRef: React.RefObject<HTMLDivElement>
3132
}
3233

3334
export const EditButtons: React.FC<EditButtonProps> = ({
@@ -41,6 +42,7 @@ export const EditButtons: React.FC<EditButtonProps> = ({
4142
translate,
4243
keyboardControls,
4344
handleKeyboard,
45+
editConfirmRef,
4446
}) => {
4547
const { getStyles } = useTheme()
4648
const NEW_KEY_PROMPT = translate('KEY_NEW', nodeData)
@@ -189,6 +191,7 @@ export const EditButtons: React.FC<EditButtonProps> = ({
189191
setIsAdding(false)
190192
}}
191193
nodeData={nodeData}
194+
editConfirmRef={editConfirmRef}
192195
/>
193196
</>
194197
)}
@@ -200,10 +203,11 @@ export const InputButtons: React.FC<{
200203
onOk: (e: React.MouseEvent<HTMLElement>) => void
201204
onCancel: (e: React.MouseEvent<HTMLElement>) => void
202205
nodeData: NodeData
203-
}> = ({ onOk, onCancel, nodeData }) => {
206+
editConfirmRef: React.RefObject<HTMLDivElement>
207+
}> = ({ onOk, onCancel, nodeData, editConfirmRef }) => {
204208
return (
205209
<div className="jer-confirm-buttons">
206-
<div onClick={onOk}>
210+
<div onClick={onOk} ref={editConfirmRef}>
207211
<Icon name="ok" nodeData={nodeData} />
208212
</div>
209213
<div onClick={onCancel}>

src/CollectionNode.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
2626
const {
2727
collapseState,
2828
setCollapseState,
29-
doesPathMatch,
29+
getMatchingCollapseState,
3030
currentlyEditingElement,
3131
setCurrentlyEditingElement,
3232
areChildrenBeingEdited,
@@ -62,6 +62,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
6262
handleKeyboard,
6363
insertAtTop,
6464
onCollapse,
65+
editConfirmRef,
6566
collapseClickZones,
6667
} = props
6768
const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data))
@@ -113,9 +114,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
113114
}, [collapseFilter])
114115

115116
useEffect(() => {
116-
if (collapseState !== null && doesPathMatch(path)) {
117-
hasBeenOpened.current = true
118-
animateCollapse(collapseState.collapsed)
117+
if (collapseState !== null) {
118+
const matchingCollapse = getMatchingCollapseState(path)
119+
if (matchingCollapse) {
120+
hasBeenOpened.current = true
121+
animateCollapse(matchingCollapse.collapsed)
122+
}
119123
}
120124
}, [collapseState])
121125

@@ -180,13 +184,13 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
180184
const modifier = getModifier(e)
181185
if (modifier && keyboardControls.collapseModifier.includes(modifier)) {
182186
hasBeenOpened.current = true
183-
setCollapseState({ collapsed: !collapsed, path })
187+
setCollapseState({ collapsed: !collapsed, path, includeChildren: true })
184188
return
185189
}
186190
if (!(currentlyEditingElement && currentlyEditingElement.includes(pathString))) {
187191
hasBeenOpened.current = true
188192
setCollapseState(null)
189-
if (onCollapse) onCollapse({ path, collapsed: !collapsed, includesChildren: false })
193+
if (onCollapse) onCollapse({ path, collapsed: !collapsed, includeChildren: false })
190194
animateCollapse(!collapsed)
191195
}
192196
}
@@ -334,7 +338,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
334338
/>
335339
)}
336340
<div className="jer-collection-input-button-row">
337-
<InputButtons onOk={handleEdit} onCancel={handleCancel} nodeData={nodeData} />
341+
<InputButtons
342+
onOk={handleEdit}
343+
onCancel={handleCancel}
344+
nodeData={nodeData}
345+
editConfirmRef={editConfirmRef}
346+
/>
338347
</div>
339348
</div>
340349
)
@@ -392,6 +401,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
392401
customButtons={props.customButtons}
393402
keyboardControls={keyboardControls}
394403
handleKeyboard={handleKeyboard}
404+
editConfirmRef={editConfirmRef}
395405
/>
396406
)
397407

src/JsonEditor.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
type KeyboardControls,
2525
} from './types'
2626
import { useTheme, ThemeProvider, TreeStateProvider, defaultTheme, useTreeState } from './contexts'
27-
import { useData } from './hooks/useData'
27+
import { useData, useTriggers } from './hooks'
2828
import { getTranslateFunction } from './localisation'
2929
import { ValueNodeWrapper } from './ValueNodeWrapper'
3030

@@ -74,6 +74,7 @@ const Editor: React.FC<JsonEditorProps> = ({
7474
TextEditor,
7575
errorMessageTimeout = 2500,
7676
keyboardControls = {},
77+
externalTriggers,
7778
insertAtTop = false,
7879
onCollapse,
7980
collapseClickZones = ['header', 'left'],
@@ -281,6 +282,9 @@ const Editor: React.FC<JsonEditorProps> = ({
281282
[keyboardControls]
282283
)
283284

285+
const editConfirmRef = useRef<HTMLDivElement>(null)
286+
useTriggers(externalTriggers, editConfirmRef)
287+
284288
// Common "sort" method for ordering nodes, based on the `keySort` prop
285289
// - If it's false (the default), we do nothing
286290
// - If true, use default array sort on the node's key
@@ -354,6 +358,7 @@ const Editor: React.FC<JsonEditorProps> = ({
354358
array: insertAtTop === true || insertAtTop === 'array',
355359
},
356360
onCollapse,
361+
editConfirmRef,
357362
collapseClickZones,
358363
}
359364

0 commit comments

Comments
 (0)