From 223a25f8d97ed4c188b3f851ae5c0adcbfb252b4 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Fri, 14 Feb 2025 13:41:08 +1300 Subject: [PATCH 01/26] WIP --- demo/src/App.tsx | 2 +- demo/src/_imports.ts | 4 ++-- src/CollectionNode.tsx | 3 ++- src/ValueNodeWrapper.tsx | 20 ++++++++++++++++---- src/contexts/TreeStateProvider.tsx | 13 ++++++++++++- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 6fd60a17..38d6be35 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -383,7 +383,7 @@ function App() { : false } restrictEdit={restrictEdit} - // restrictEdit={(nodeData) => !(typeof nodeData.value === 'string')} + // restrictEdit={(nodeData) => typeof nodeData.value === 'object'} restrictDelete={restrictDelete} restrictAdd={restrictAdd} restrictTypeSelection={dataDefinition?.restrictTypeSelection} diff --git a/demo/src/_imports.ts b/demo/src/_imports.ts index 295d8b6f..ccb43158 100644 --- a/demo/src/_imports.ts +++ b/demo/src/_imports.ts @@ -3,10 +3,10 @@ */ /* Installed package */ -export * from 'json-edit-react' +// export * from 'json-edit-react' /* Local src */ -// export * from './json-edit-react/src' +export * from './json-edit-react/src' /* Compiled local package */ // export * from './package/build' diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index b29af099..3081e59c 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -93,8 +93,9 @@ export const CollectionNode: React.FC = (props) => { const hasBeenOpened = useRef(!startCollapsed) useEffect(() => { + console.log('Change', data) setStringifiedValue(jsonStringify(data)) - if (isEditing) setCurrentlyEditingElement(null) + // if (isEditing) setCurrentlyEditingElement(null) }, [data]) useEffect(() => { diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index af6c2337..b4072f95 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -53,6 +53,8 @@ export const ValueNodeWrapper: React.FC = (props) => { setPreviouslyEditedElement, tabDirection, setTabDirection, + previousValue, + setPreviousValue, } = useTreeState() const [value, setValue] = useState( // Bad things happen when you put a function into useState @@ -176,6 +178,7 @@ export const ValueNodeWrapper: React.FC = (props) => { const handleEdit = () => { setCurrentlyEditingElement(null) + setPreviousValue(null) let newValue: JsonData switch (dataType) { case 'object': @@ -200,7 +203,12 @@ export const ValueNodeWrapper: React.FC = (props) => { const handleCancel = () => { setCurrentlyEditingElement(null) + if (previousValue !== null) { + onEdit(previousValue, path) + return + } setValue(data) + setPreviousValue(null) setDataType(getDataType(data, customNodeData)) } @@ -274,7 +282,7 @@ export const ValueNodeWrapper: React.FC = (props) => { ) : ( // Need to re-fetch data type to make sure it's one of the "core" ones // when fetching a non-custom component - getInputComponent(data, inputProps) + getInputComponent(dataType, inputProps) ) return ( @@ -350,7 +358,12 @@ export const ValueNodeWrapper: React.FC = (props) => { showEditButtons && ( setCurrentlyEditingElement(path, handleCancel) : undefined + canEdit + ? () => { + setPreviousValue(value) + setCurrentlyEditingElement(path, handleCancel) + } + : undefined } handleDelete={canDelete ? handleDelete : undefined} enableClipboard={enableClipboard} @@ -402,8 +415,7 @@ const getDataType = (value: unknown, customNodeData?: CustomNodeData) => { return 'invalid' } -const getInputComponent = (data: JsonData, inputProps: InputProps) => { - const dataType = getDataType(data) +const getInputComponent = (dataType: string, inputProps: InputProps) => { const { value } = inputProps switch (dataType) { case 'string': diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index 0fd4472b..a665b58c 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -8,7 +8,7 @@ */ import React, { createContext, useContext, useRef, useState } from 'react' -import { type TabDirection, type CollectionKey } from '../types' +import { type TabDirection, type CollectionKey, JsonData } from '../types' import { toPathString } from '../helpers' interface CollapseAllState { @@ -37,6 +37,8 @@ interface TreeStateContext { setDragSource: (newState: DragSource) => void tabDirection: TabDirection setTabDirection: (dir: TabDirection) => void + previousValue: JsonData | null + setPreviousValue: (value: JsonData | null) => void } const initialContext: TreeStateContext = { collapseState: null, @@ -51,6 +53,8 @@ const initialContext: TreeStateContext = { setDragSource: () => {}, tabDirection: 'next', setTabDirection: () => {}, + previousValue: null, + setPreviousValue: () => {}, } const TreeStateProviderContext = createContext(initialContext) @@ -58,6 +62,11 @@ const TreeStateProviderContext = createContext(initialContext) export const TreeStateProvider = ({ children }: { children: React.ReactNode }) => { const [collapseState, setCollapseState] = useState(null) const [currentlyEditingElement, setCurrentlyEditingElement] = useState(null) + + // This value holds the "previous" value when user changes type. Because + // changing data type causes a proper data update, cancelling afterwards + // doesn't revert to the previous type. This value allows us to do that. + const [previousValue, setPreviousValue] = useState(null) const [dragSource, setDragSource] = useState({ path: null, pathString: null, @@ -130,6 +139,8 @@ export const TreeStateProvider = ({ children }: { children: React.ReactNode }) = setTabDirection: (dir: TabDirection) => { tabDirection.current = dir }, + previousValue, + setPreviousValue, // Drag-n-drop dragSource, setDragSource, From e0ef9539cb7b8485cd97a63d11e0a6f706ea1e50 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Mon, 24 Feb 2025 23:54:58 +1300 Subject: [PATCH 02/26] Initial mechanism implementation --- src/ButtonPanels.tsx | 8 +++-- src/JsonEditor.tsx | 7 +++- src/ValueNodeWrapper.tsx | 9 ++++- src/contexts/TreeStateProvider.tsx | 2 +- src/hooks/index.ts | 1 + src/hooks/useTriggers.ts | 55 ++++++++++++++++++++++++++++++ src/types.ts | 2 ++ 7 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 src/hooks/useTriggers.ts diff --git a/src/ButtonPanels.tsx b/src/ButtonPanels.tsx index 01a0ef94..f06ab408 100644 --- a/src/ButtonPanels.tsx +++ b/src/ButtonPanels.tsx @@ -28,6 +28,7 @@ interface EditButtonProps { e: React.KeyboardEvent, eventMap: Partial void>> ) => void + editConfirmRef: React.RefObject } export const EditButtons: React.FC = ({ @@ -41,6 +42,7 @@ export const EditButtons: React.FC = ({ translate, keyboardControls, handleKeyboard, + editConfirmRef, }) => { const { getStyles } = useTheme() const NEW_KEY_PROMPT = translate('KEY_NEW', nodeData) @@ -188,6 +190,7 @@ export const EditButtons: React.FC = ({ setIsAdding(false) }} nodeData={nodeData} + editConfirmRef={editConfirmRef} /> )} @@ -199,10 +202,11 @@ export const InputButtons: React.FC<{ onOk: (e: React.MouseEvent) => void onCancel: (e: React.MouseEvent) => void nodeData: NodeData -}> = ({ onOk, onCancel, nodeData }) => { + editConfirmRef: React.RefObject +}> = ({ onOk, onCancel, nodeData, editConfirmRef }) => { return (
-
+
diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 26ed5d19..e3a33843 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -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' @@ -74,6 +74,7 @@ const Editor: React.FC = ({ TextEditor, errorMessageTimeout = 2500, keyboardControls = {}, + externalTriggers, insertAtTop = false, }) => { const { getStyles } = useTheme() @@ -279,6 +280,9 @@ const Editor: React.FC = ({ [keyboardControls] ) + const editConfirmRef = useRef(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 @@ -351,6 +355,7 @@ const Editor: React.FC = ({ object: insertAtTop === true || insertAtTop === 'object', array: insertAtTop === true || insertAtTop === 'array', }, + editConfirmRef, } const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth } diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 9ead0355..fd8e8b0f 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -44,6 +44,7 @@ export const ValueNodeWrapper: React.FC = (props) => { handleKeyboard, keyboardControls, sort, + editConfirmRef, } = props const { getStyles } = useTheme() const { @@ -345,7 +346,12 @@ export const ValueNodeWrapper: React.FC = (props) => {
{ValueComponent}
{isEditing ? ( - + ) : ( showEditButtons && ( = (props) => { nodeData={nodeData} handleKeyboard={handleKeyboard} keyboardControls={keyboardControls} + editConfirmRef={editConfirmRef} /> ) )} diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index 0fd4472b..e5680246 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -11,7 +11,7 @@ import React, { createContext, useContext, useRef, useState } from 'react' import { type TabDirection, type CollectionKey } from '../types' import { toPathString } from '../helpers' -interface CollapseAllState { +export interface CollapseAllState { path: CollectionKey[] collapsed: boolean } diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 0bac6903..917c7f20 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './useCommon' export * from './useData' export * from './useDragNDrop' export * from './useCollapseTransition' +export * from './useTriggers' diff --git a/src/hooks/useTriggers.ts b/src/hooks/useTriggers.ts new file mode 100644 index 00000000..97330c8b --- /dev/null +++ b/src/hooks/useTriggers.ts @@ -0,0 +1,55 @@ +import { useEffect } from 'react' +import { useTreeState, type CollapseAllState } from '../contexts' +import { type CollectionKey } from '../types' +import { toPathString } from '../helpers' + +export interface EditState { + path?: CollectionKey[] + action?: 'accept' | 'cancel' +} + +export interface ExternalTriggers { + collapse?: CollapseAllState | CollapseAllState[] + edit?: EditState +} + +export const useTriggers = ( + triggers: ExternalTriggers | null | undefined, + editConfirmRef: React.RefObject +) => { + const { setCurrentlyEditingElement, currentlyEditingElement, setCollapseState } = useTreeState() + + useEffect(() => { + console.log('Trigger....') + if (!triggers) return + + console.log('Processing triggers', triggers) + const { collapse, edit } = triggers + + if (Array.isArray(collapse)) { + return // For now + } + + if (collapse) setCollapseState(collapse) + + const isPathIncluded = edit?.path ? toPathString(edit.path) === currentlyEditingElement : true + + switch (edit?.action) { + case 'accept': { + if (isPathIncluded) { + if (editConfirmRef.current) editConfirmRef.current.click() + setCurrentlyEditingElement(null) + } + + break + } + case 'cancel': { + if (isPathIncluded) setCurrentlyEditingElement(null) + break + } + default: { + if (edit?.path) setCurrentlyEditingElement(edit.path) + } + } + }, [triggers]) +} diff --git a/src/types.ts b/src/types.ts index 76bd08fd..ca7b4159 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ import { type Options as AssignOptions } from 'object-property-assigner' import { type LocalisedStrings, type TranslateFunction } from './localisation' +import { ExternalTriggers } from './hooks' export type JsonData = CollectionData | ValueData @@ -49,6 +50,7 @@ export interface JsonEditorProps { TextEditor?: React.FC errorMessageTimeout?: number // ms keyboardControls?: KeyboardControls + externalTriggers?: ExternalTriggers insertAtTop?: boolean | 'array' | 'object' } From 9fab91f0db698637779351819d39bba24d3b9aee Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Wed, 26 Feb 2025 23:02:37 +1300 Subject: [PATCH 03/26] Try using "shouldUpdateUndo" --- demo/src/App.tsx | 15 ++++++++--- demo/src/demoData/dataDefinitions.tsx | 2 +- src/CollectionNode.tsx | 1 - src/JsonEditor.tsx | 38 ++++++++++++++++++--------- src/ValueNodeWrapper.tsx | 7 +++-- src/hooks/useData.ts | 11 +++++--- 6 files changed, 49 insertions(+), 25 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 8037e8b4..f231506d 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, lazy, Suspense } from 'react' +import React, { useEffect, useRef, lazy, Suspense, useCallback } from 'react' import { useSearch, useLocation } from 'wouter' import JSON5 from 'json5' import 'react-datepicker/dist/react-datepicker.css' @@ -119,12 +119,18 @@ function App() { const [ { present: data, past, future }, - { set: setData, reset, undo: undoData, redo: redoData, canUndo, canRedo }, - ] = useUndo(selectedDataSet === 'editTheme' ? defaultTheme : dataDefinition.data) + { set, reset, undo: undoData, redo: redoData, canUndo, canRedo }, + ] = useUndo(selectedDataSet === 'editTheme' ? defaultTheme : dataDefinition.data, { + useCheckpoints: true, + }) // Provides a named version of these methods (i.e undo.name = "undo") const undo = () => undoData() const redo = () => redoData() + console.log('Past', past) + + const setData = useCallback((value: any, noUndo: boolean = false) => set(value, !noUndo), [set]) + useEffect(() => { if (selectedDataSet === 'liveData' && !loading && liveData) reset(liveData) }, [loading, liveData, reset, selectedDataSet]) @@ -384,7 +390,7 @@ function App() { } // viewOnly restrictEdit={restrictEdit} - // restrictEdit={(nodeData) => typeof nodeData.value === 'object'} + // restrictEdit={(nodeData) => !(typeof nodeData.value === 'string')} restrictDelete={restrictDelete} restrictAdd={restrictAdd} restrictTypeSelection={dataDefinition?.restrictTypeSelection} @@ -435,6 +441,7 @@ function App() { // ]} onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} + // undo={undo} // keyboardControls={{ // cancel: 'Tab', // confirm: { key: 'Enter', modifier: 'Meta' }, diff --git a/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index cdc2a604..e4e9aa4f 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -89,7 +89,7 @@ export const demoDataDefinitions: Record = { ), rootName: 'data', - collapse: 2, + collapse: 1, data: data.intro, customNodeDefinitions: [dateNodeDefinition], // restrictEdit: ({ key }) => key === 'number', diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index a164b83d..2c0857d9 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -97,7 +97,6 @@ export const CollectionNode: React.FC = (props) => { const { isEditing, isEditingKey, isArray, canEditKey } = derivedValues useEffect(() => { - console.log('Change', data) setStringifiedValue(jsonStringify(data)) // if (isEditing) setCurrentlyEditingElement(null) }, [data]) diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 26ed5d19..8a1cb1ed 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -109,11 +109,18 @@ const Editor: React.FC = ({ // Common method for handling data update. It runs the updated data through // provided "onUpdate" function, then updates data state or returns error // information accordingly - const handleEdit = async (updateMethod: UpdateFunction, input: UpdateFunctionProps) => { + const handleEdit = async ( + updateMethod: UpdateFunction, + input: UpdateFunctionProps, + shouldUpdateUndo: boolean = true + ) => { const result = await updateMethod(input) + // const shouldUpdateUndo = typeof input.newValue === typeof input.currentValue + if (result === true || result === undefined) { - setData(input.newData) + console.log('Setting', input.newValue, shouldUpdateUndo) + setData(input.newData, !shouldUpdateUndo) return } @@ -128,23 +135,28 @@ const Editor: React.FC = ({ setData(resultValue) } - const onEdit: InternalUpdateFunction = async (value, path) => { + const onEdit: InternalUpdateFunction = async (value, path, shouldUpdateUndo: boolean = true) => { const { currentData, newData, currentValue, newValue } = updateDataObject( data, path, value, 'update' ) - if (currentValue === newValue) return - - return await handleEdit(srcEdit, { - currentData, - newData, - currentValue, - newValue, - name: path.slice(-1)[0], - path, - }) + console.log('EDIT', value, shouldUpdateUndo) + if (currentValue === newValue && !shouldUpdateUndo) return + + return await handleEdit( + srcEdit, + { + currentData, + newData, + currentValue, + newValue, + name: path.slice(-1)[0], + path, + }, + shouldUpdateUndo + ) } const onDelete: InternalUpdateFunction = async (value, path) => { diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index ef15bffc..8b071e3d 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -167,7 +167,8 @@ export const ValueNodeWrapper: React.FC = (props) => { // that won't match the custom node condition any more customNodeData?.CustomNode ? translate('DEFAULT_STRING', nodeData) : undefined ) - onEdit(newValue, path).then((error) => { + console.log('Change data type', newValue) + onEdit(newValue, path, false).then((error) => { if (error) { onError({ code: 'UPDATE_ERROR', message: error }, newValue as JsonData) setCurrentlyEditingElement(null) @@ -196,15 +197,17 @@ export const ValueNodeWrapper: React.FC = (props) => { default: newValue = value } + console.log('Normal edit', newValue) onEdit(newValue, path).then((error) => { if (error) onError({ code: 'UPDATE_ERROR', message: error }, newValue) }) } const handleCancel = () => { + console.log('previousValue', previousValue) setCurrentlyEditingElement(null) if (previousValue !== null) { - onEdit(previousValue, path) + onEdit(previousValue, path, false) return } setValue(data) diff --git a/src/hooks/useData.ts b/src/hooks/useData.ts index df7d7afa..ea796a45 100644 --- a/src/hooks/useData.ts +++ b/src/hooks/useData.ts @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react' interface UseDataProps { - setData?: (data: T) => void + setData?: (data: T, shouldUpdateUndo?: boolean) => void data: T } @@ -14,8 +14,8 @@ export const useData = ({ setData, data }: UseDataProps) => { const [localData, setLocalData] = useState(setData ? undefined : data) const setDataMethod = useCallback( - (data: T) => { - if (setData) setData(data) + (data: T, shouldUpdateUndo?: boolean) => { + if (setData) setData(data, shouldUpdateUndo) else setLocalData(data) }, [setData] @@ -25,5 +25,8 @@ export const useData = ({ setData, data }: UseDataProps) => { if (!setData) setLocalData(data) }, [data]) - return [setData ? data : localData, setDataMethod] as [T, (data: T) => void] + return [setData ? data : localData, setDataMethod] as [ + T, + (data: T, shouldUpdateUndo?: boolean) => void + ] } From 05d1fc550018a37145a5683ac301aafe20a1521f Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 11:28:15 +1300 Subject: [PATCH 04/26] Revert attempt to handle undo --- src/JsonEditor.tsx | 39 ++++++++++++++------------------------- src/ValueNodeWrapper.tsx | 4 ++-- src/hooks/useData.ts | 11 ++++------- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 8a1cb1ed..511c092b 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -109,18 +109,12 @@ const Editor: React.FC = ({ // Common method for handling data update. It runs the updated data through // provided "onUpdate" function, then updates data state or returns error // information accordingly - const handleEdit = async ( - updateMethod: UpdateFunction, - input: UpdateFunctionProps, - shouldUpdateUndo: boolean = true - ) => { + const handleEdit = async (updateMethod: UpdateFunction, input: UpdateFunctionProps) => { const result = await updateMethod(input) - // const shouldUpdateUndo = typeof input.newValue === typeof input.currentValue - if (result === true || result === undefined) { - console.log('Setting', input.newValue, shouldUpdateUndo) - setData(input.newData, !shouldUpdateUndo) + console.log('Setting', input.newValue) + setData(input.newData) return } @@ -135,28 +129,23 @@ const Editor: React.FC = ({ setData(resultValue) } - const onEdit: InternalUpdateFunction = async (value, path, shouldUpdateUndo: boolean = true) => { + const onEdit: InternalUpdateFunction = async (value, path) => { const { currentData, newData, currentValue, newValue } = updateDataObject( data, path, value, 'update' ) - console.log('EDIT', value, shouldUpdateUndo) - if (currentValue === newValue && !shouldUpdateUndo) return - - return await handleEdit( - srcEdit, - { - currentData, - newData, - currentValue, - newValue, - name: path.slice(-1)[0], - path, - }, - shouldUpdateUndo - ) + if (currentValue === newValue) return + + return await handleEdit(srcEdit, { + currentData, + newData, + currentValue, + newValue, + name: path.slice(-1)[0], + path, + }) } const onDelete: InternalUpdateFunction = async (value, path) => { diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 8b071e3d..3616076f 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -168,7 +168,7 @@ export const ValueNodeWrapper: React.FC = (props) => { customNodeData?.CustomNode ? translate('DEFAULT_STRING', nodeData) : undefined ) console.log('Change data type', newValue) - onEdit(newValue, path, false).then((error) => { + onEdit(newValue, path).then((error) => { if (error) { onError({ code: 'UPDATE_ERROR', message: error }, newValue as JsonData) setCurrentlyEditingElement(null) @@ -207,7 +207,7 @@ export const ValueNodeWrapper: React.FC = (props) => { console.log('previousValue', previousValue) setCurrentlyEditingElement(null) if (previousValue !== null) { - onEdit(previousValue, path, false) + onEdit(previousValue, path) return } setValue(data) diff --git a/src/hooks/useData.ts b/src/hooks/useData.ts index ea796a45..df7d7afa 100644 --- a/src/hooks/useData.ts +++ b/src/hooks/useData.ts @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react' interface UseDataProps { - setData?: (data: T, shouldUpdateUndo?: boolean) => void + setData?: (data: T) => void data: T } @@ -14,8 +14,8 @@ export const useData = ({ setData, data }: UseDataProps) => { const [localData, setLocalData] = useState(setData ? undefined : data) const setDataMethod = useCallback( - (data: T, shouldUpdateUndo?: boolean) => { - if (setData) setData(data, shouldUpdateUndo) + (data: T) => { + if (setData) setData(data) else setLocalData(data) }, [setData] @@ -25,8 +25,5 @@ export const useData = ({ setData, data }: UseDataProps) => { if (!setData) setLocalData(data) }, [data]) - return [setData ? data : localData, setDataMethod] as [ - T, - (data: T, shouldUpdateUndo?: boolean) => void - ] + return [setData ? data : localData, setDataMethod] as [T, (data: T) => void] } From 3f0b200df67e25262dc8da6da5f499bcf5fbcba7 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 11:29:14 +1300 Subject: [PATCH 05/26] Update JsonEditor.tsx --- src/JsonEditor.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 511c092b..26ed5d19 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -113,7 +113,6 @@ const Editor: React.FC = ({ const result = await updateMethod(input) if (result === true || result === undefined) { - console.log('Setting', input.newValue) setData(input.newData) return } From 7cefe11e71ec8352c1089a4dc825d3f2c1f2e350 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:14:58 +1300 Subject: [PATCH 06/26] Handle revert for Collection nodes --- src/CollectionNode.tsx | 9 +++++++++ src/ValueNodeWrapper.tsx | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 2c0857d9..39d9d619 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -29,6 +29,8 @@ export const CollectionNode: React.FC = (props) => { currentlyEditingElement, setCurrentlyEditingElement, areChildrenBeingEdited, + previousValue, + setPreviousValue, } = useTreeState() const { mainContainerRef, @@ -188,6 +190,7 @@ export const CollectionNode: React.FC = (props) => { try { const value = jsonParse(stringifiedValue) setCurrentlyEditingElement(null) + setPreviousValue(null) setError(null) if (JSON.stringify(value) === JSON.stringify(data)) return onEdit(value, path).then((error) => { @@ -235,8 +238,13 @@ export const CollectionNode: React.FC = (props) => { const handleCancel = () => { setCurrentlyEditingElement(null) + if (previousValue !== null) { + onEdit(previousValue, path) + return + } setError(null) setStringifiedValue(jsonStringify(data)) + setPreviousValue(null) } const showLabel = showArrayIndices || !isArray @@ -413,6 +421,7 @@ export const CollectionNode: React.FC = (props) => { canEdit ? () => { hasBeenOpened.current = true + setPreviousValue(null) setCurrentlyEditingElement(path) } : undefined diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 3616076f..c0659518 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -167,7 +167,6 @@ export const ValueNodeWrapper: React.FC = (props) => { // that won't match the custom node condition any more customNodeData?.CustomNode ? translate('DEFAULT_STRING', nodeData) : undefined ) - console.log('Change data type', newValue) onEdit(newValue, path).then((error) => { if (error) { onError({ code: 'UPDATE_ERROR', message: error }, newValue as JsonData) @@ -204,7 +203,6 @@ export const ValueNodeWrapper: React.FC = (props) => { } const handleCancel = () => { - console.log('previousValue', previousValue) setCurrentlyEditingElement(null) if (previousValue !== null) { onEdit(previousValue, path) From 005ee759ba51002b86ff6a3b5132d9b0ef7a2a61 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:22:17 +1300 Subject: [PATCH 07/26] Update App.tsx --- demo/src/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index f231506d..65c5fc68 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -127,8 +127,6 @@ function App() { const undo = () => undoData() const redo = () => redoData() - console.log('Past', past) - const setData = useCallback((value: any, noUndo: boolean = false) => set(value, !noUndo), [set]) useEffect(() => { From 4f18102fcd2d4bcd1db62644092aa5c5f32a5604 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:13:24 +1300 Subject: [PATCH 08/26] Revert App --- demo/src/App.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 65c5fc68..7bff00dc 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -119,16 +119,12 @@ function App() { const [ { present: data, past, future }, - { set, reset, undo: undoData, redo: redoData, canUndo, canRedo }, - ] = useUndo(selectedDataSet === 'editTheme' ? defaultTheme : dataDefinition.data, { - useCheckpoints: true, - }) + { set: setData, reset, undo: undoData, redo: redoData, canUndo, canRedo }, + ] = useUndo(selectedDataSet === 'editTheme' ? defaultTheme : dataDefinition.data) // Provides a named version of these methods (i.e undo.name = "undo") const undo = () => undoData() const redo = () => redoData() - const setData = useCallback((value: any, noUndo: boolean = false) => set(value, !noUndo), [set]) - useEffect(() => { if (selectedDataSet === 'liveData' && !loading && liveData) reset(liveData) }, [loading, liveData, reset, selectedDataSet]) @@ -439,7 +435,6 @@ function App() { // ]} onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} - // undo={undo} // keyboardControls={{ // cancel: 'Tab', // confirm: { key: 'Enter', modifier: 'Meta' }, From 83c86dd65d441e9c3f099bb0e892267715b3117f Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:14:26 +1300 Subject: [PATCH 09/26] Revert some other stuff --- demo/src/App.tsx | 2 +- demo/src/demoData/dataDefinitions.tsx | 2 +- src/ValueNodeWrapper.tsx | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 7bff00dc..18356b40 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, lazy, Suspense, useCallback } from 'react' +import React, { useEffect, useRef, lazy, Suspense } from 'react' import { useSearch, useLocation } from 'wouter' import JSON5 from 'json5' import 'react-datepicker/dist/react-datepicker.css' diff --git a/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index e4e9aa4f..cdc2a604 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -89,7 +89,7 @@ export const demoDataDefinitions: Record = { ), rootName: 'data', - collapse: 1, + collapse: 2, data: data.intro, customNodeDefinitions: [dateNodeDefinition], // restrictEdit: ({ key }) => key === 'number', diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index c0659518..ef15bffc 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -196,7 +196,6 @@ export const ValueNodeWrapper: React.FC = (props) => { default: newValue = value } - console.log('Normal edit', newValue) onEdit(newValue, path).then((error) => { if (error) onError({ code: 'UPDATE_ERROR', message: error }, newValue) }) From 4cf31fc11d75ed99a950972f3dc79bc5b664977d Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 15:17:03 +1300 Subject: [PATCH 10/26] Update TreeStateProvider.tsx --- src/contexts/TreeStateProvider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index a665b58c..89e0d59b 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -8,7 +8,7 @@ */ import React, { createContext, useContext, useRef, useState } from 'react' -import { type TabDirection, type CollectionKey, JsonData } from '../types' +import { type TabDirection, type CollectionKey, type JsonData } from '../types' import { toPathString } from '../helpers' interface CollapseAllState { From eb9fe8ec96163774ef92154149a6502d0318b433 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:23:15 +1300 Subject: [PATCH 11/26] Add onCollapse and onEditEvent functions --- demo/src/App.tsx | 2 ++ src/CollectionNode.tsx | 2 ++ src/JsonEditor.tsx | 4 +++- src/contexts/TreeStateProvider.tsx | 19 +++++++++++++++++-- src/types.ts | 12 ++++++++++++ 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 18356b40..b56e1b85 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -463,6 +463,8 @@ function App() { ) : undefined } + onEditEvent={(path) => console.log('Editing path', path)} + onCollapse={(input) => console.log('Collapse', input)} /> diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 39d9d619..775b8a63 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -60,6 +60,7 @@ export const CollectionNode: React.FC = (props) => { keyboardControls, handleKeyboard, insertAtTop, + onCollapse, } = props const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data)) @@ -182,6 +183,7 @@ export const CollectionNode: React.FC = (props) => { if (!(currentlyEditingElement && currentlyEditingElement.includes(pathString))) { hasBeenOpened.current = true setCollapseState(null) + if (onCollapse) onCollapse({ path, collapse: !collapsed, includeChildren: false }) animateCollapse(!collapsed) } } diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 26ed5d19..a7eb4f7a 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -75,6 +75,7 @@ const Editor: React.FC = ({ errorMessageTimeout = 2500, keyboardControls = {}, insertAtTop = false, + onCollapse, }) => { const { getStyles } = useTheme() const { setCurrentlyEditingElement } = useTreeState() @@ -351,6 +352,7 @@ const Editor: React.FC = ({ object: insertAtTop === true || insertAtTop === 'object', array: insertAtTop === true || insertAtTop === 'array', }, + onCollapse, } const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth } @@ -393,7 +395,7 @@ export const JsonEditor: React.FC = (props) => { return ( - + diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index 89e0d59b..eb16206d 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -8,7 +8,13 @@ */ import React, { createContext, useContext, useRef, useState } from 'react' -import { type TabDirection, type CollectionKey, type JsonData } from '../types' +import { + type TabDirection, + type CollectionKey, + type JsonData, + type OnEditEventFunction, + type OnCollapseFunction, +} from '../types' import { toPathString } from '../helpers' interface CollapseAllState { @@ -59,7 +65,13 @@ const initialContext: TreeStateContext = { const TreeStateProviderContext = createContext(initialContext) -export const TreeStateProvider = ({ children }: { children: React.ReactNode }) => { +interface TreeStateProps { + children: React.ReactNode + onEditEvent?: OnEditEventFunction + onCollapse?: OnCollapseFunction +} + +export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeStateProps) => { const [collapseState, setCollapseState] = useState(null) const [currentlyEditingElement, setCurrentlyEditingElement] = useState(null) @@ -98,6 +110,7 @@ export const TreeStateProvider = ({ children }: { children: React.ReactNode }) = cancelOp.current() } setCurrentlyEditingElement(pathString) + if (onEditEvent) onEditEvent(path) cancelOp.current = typeof newCancelOrKey === 'function' ? newCancelOrKey : null } @@ -121,6 +134,8 @@ export const TreeStateProvider = ({ children }: { children: React.ReactNode }) = collapseState, setCollapseState: (state) => { setCollapseState(state) + if (onCollapse && state !== null) + onCollapse({ path: state.path, collapse: state.collapsed, includeChildren: true }) // Reset after 2 seconds, which is enough time for all child nodes to // have opened/closed, but still allows collapse reset if data changes // externally diff --git a/src/types.ts b/src/types.ts index 76bd08fd..1658489c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,6 +50,9 @@ export interface JsonEditorProps { errorMessageTimeout?: number // ms keyboardControls?: KeyboardControls insertAtTop?: boolean | 'array' | 'object' + // Additional events + onEditEvent?: OnEditEventFunction + onCollapse?: OnCollapseFunction } const ValueDataTypes = ['string', 'number', 'boolean', 'null'] as const @@ -157,6 +160,14 @@ export type CompareFunction = ( export type SortFunction = (arr: T[], nodeMap: (input: T) => [string | number, unknown]) => void +export type OnEditEventFunction = (path: CollectionKey[] | string | null) => void + +export type OnCollapseFunction = (input: { + path: CollectionKey[] + collapse: boolean + includeChildren: boolean +}) => void + // Internal update export type InternalUpdateFunction = ( value: unknown, @@ -263,6 +274,7 @@ export interface CollectionNodeProps extends BaseNodeProps { jsonStringify: (data: JsonData) => string insertAtTop: { object: boolean; array: boolean } TextEditor?: React.FC + onCollapse?: OnCollapseFunction } export type ValueData = string | number | boolean From 88e2f6e654baaadf6055065cbe6829e6a6d4b885 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 19:03:09 +1300 Subject: [PATCH 12/26] Update App --- demo/src/App.tsx | 37 +++++++++++++++++++++++++++++++++++++ demo/src/_imports.ts | 4 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 18356b40..34e326f3 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -53,6 +53,7 @@ import { demoDataDefinitions } from './demoData' import { useDatabase } from './useDatabase' import './style.css' import { version } from './version' +import { ExternalTriggers } from './json-edit-react/src/types' const CodeEditor = lazy(() => import('./CodeEditor')) @@ -111,6 +112,9 @@ function App() { customTextEditor: false, }) + const [triggerText, setTriggerText] = useState('') + const [externalTriggers, setExternalTriggers] = useState | null>(null) + const [isSaving, setIsSaving] = useState(false) const previousTheme = useRef() // Used when resetting after theme editing const toast = useToast() @@ -463,6 +467,7 @@ function App() { ) : undefined } + externalTriggers={externalTriggers as ExternalTriggers | null} /> @@ -512,6 +517,38 @@ function App() { + + + Trigger Text + + setTriggerText(e.target.value)} + /> + + Demo data diff --git a/demo/src/_imports.ts b/demo/src/_imports.ts index 295d8b6f..ccb43158 100644 --- a/demo/src/_imports.ts +++ b/demo/src/_imports.ts @@ -3,10 +3,10 @@ */ /* Installed package */ -export * from 'json-edit-react' +// export * from 'json-edit-react' /* Local src */ -// export * from './json-edit-react/src' +export * from './json-edit-react/src' /* Compiled local package */ // export * from './package/build' From 158c5e171460d902db3d94f44d01cf4de8112ea7 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:09:04 +1300 Subject: [PATCH 13/26] Tweak naming --- src/CollectionNode.tsx | 2 +- src/contexts/TreeStateProvider.tsx | 2 +- src/types.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 775b8a63..078fbe28 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -183,7 +183,7 @@ export const CollectionNode: React.FC = (props) => { if (!(currentlyEditingElement && currentlyEditingElement.includes(pathString))) { hasBeenOpened.current = true setCollapseState(null) - if (onCollapse) onCollapse({ path, collapse: !collapsed, includeChildren: false }) + if (onCollapse) onCollapse({ path, collapsed: !collapsed, includesChildren: false }) animateCollapse(!collapsed) } } diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index eb16206d..d0d2c878 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -135,7 +135,7 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta setCollapseState: (state) => { setCollapseState(state) if (onCollapse && state !== null) - onCollapse({ path: state.path, collapse: state.collapsed, includeChildren: true }) + onCollapse({ path: state.path, collapsed: state.collapsed, includesChildren: true }) // Reset after 2 seconds, which is enough time for all child nodes to // have opened/closed, but still allows collapse reset if data changes // externally diff --git a/src/types.ts b/src/types.ts index 1658489c..16a1ca88 100644 --- a/src/types.ts +++ b/src/types.ts @@ -164,8 +164,8 @@ export type OnEditEventFunction = (path: CollectionKey[] | string | null) => voi export type OnCollapseFunction = (input: { path: CollectionKey[] - collapse: boolean - includeChildren: boolean + collapsed: boolean + includesChildren: boolean }) => void // Internal update From daf710a0bc1f9da1c562fc8251b1a5e194e3b6ab Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 1 Mar 2025 23:49:17 +1300 Subject: [PATCH 14/26] Get edit controls working --- src/CollectionNode.tsx | 9 ++++++++- src/contexts/TreeStateProvider.tsx | 2 +- src/types.ts | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 078fbe28..c05b7ae5 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -61,6 +61,7 @@ export const CollectionNode: React.FC = (props) => { handleKeyboard, insertAtTop, onCollapse, + editConfirmRef, } = props const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data)) @@ -331,7 +332,12 @@ export const CollectionNode: React.FC = (props) => { /> )}
- +
) @@ -437,6 +443,7 @@ export const CollectionNode: React.FC = (props) => { customButtons={props.customButtons} keyboardControls={keyboardControls} handleKeyboard={handleKeyboard} + editConfirmRef={editConfirmRef} /> ) diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index d1241291..c719481f 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -1,6 +1,6 @@ /** * Captures state that is required to be shared between nodes. In particular: - * - global collapse state for triggering whole tree expansions/closures + * - global collapse state for triggering whole tree expansions/collapses * - the currently editing node (to ensure only one node at a time can be * edited) * - the value of the node currently being dragged (so that the target it is diff --git a/src/types.ts b/src/types.ts index 448effc4..94abfd5e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -260,6 +260,7 @@ interface BaseNodeProps { e: React.KeyboardEvent, eventMap: Partial void>> ) => void + editConfirmRef: React.RefObject } export interface CollectionNodeProps extends BaseNodeProps { From cd91bba9a1b9ad48f41c04edfe063ac1acd49d99 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 2 Mar 2025 00:05:34 +1300 Subject: [PATCH 15/26] Update useTriggers.ts --- src/hooks/useTriggers.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/hooks/useTriggers.ts b/src/hooks/useTriggers.ts index 97330c8b..1f1c5352 100644 --- a/src/hooks/useTriggers.ts +++ b/src/hooks/useTriggers.ts @@ -32,11 +32,11 @@ export const useTriggers = ( if (collapse) setCollapseState(collapse) - const isPathIncluded = edit?.path ? toPathString(edit.path) === currentlyEditingElement : true + const doesPathMatch = edit?.path ? toPathString(edit.path) === currentlyEditingElement : true switch (edit?.action) { case 'accept': { - if (isPathIncluded) { + if (doesPathMatch) { if (editConfirmRef.current) editConfirmRef.current.click() setCurrentlyEditingElement(null) } @@ -44,7 +44,7 @@ export const useTriggers = ( break } case 'cancel': { - if (isPathIncluded) setCurrentlyEditingElement(null) + if (doesPathMatch) setCurrentlyEditingElement(null) break } default: { From f227c631e5317e3808b1bdec9082c967a94f4102 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 2 Mar 2025 12:07:54 +1300 Subject: [PATCH 16/26] Update useTriggers.ts --- src/hooks/useTriggers.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/hooks/useTriggers.ts b/src/hooks/useTriggers.ts index 1f1c5352..d61f1041 100644 --- a/src/hooks/useTriggers.ts +++ b/src/hooks/useTriggers.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useTreeState, type CollapseAllState } from '../contexts' +import { useTreeState } from '../contexts' import { type CollectionKey } from '../types' import { toPathString } from '../helpers' @@ -8,8 +8,14 @@ export interface EditState { action?: 'accept' | 'cancel' } +export interface CollapseState { + path: CollectionKey[] + collapsed: boolean + includeChildren: boolean +} + export interface ExternalTriggers { - collapse?: CollapseAllState | CollapseAllState[] + collapse?: CollapseState | CollapseState[] edit?: EditState } From f581cfc677639d1eb182a3cc5d5bea0c057739c3 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Mon, 3 Mar 2025 22:34:00 +1300 Subject: [PATCH 17/26] Get multi-collapse working --- src/CollectionNode.tsx | 11 ++++++---- src/contexts/TreeStateProvider.tsx | 34 ++++++++++++++++++++++++------ src/hooks/useTriggers.ts | 12 ++++++----- 3 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index c05b7ae5..3c0bdfaa 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -25,7 +25,7 @@ export const CollectionNode: React.FC = (props) => { const { collapseState, setCollapseState, - doesPathMatch, + getMatchingCollapseState, currentlyEditingElement, setCurrentlyEditingElement, areChildrenBeingEdited, @@ -112,9 +112,12 @@ export const CollectionNode: React.FC = (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]) diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index c719481f..37bf9c4b 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -20,6 +20,7 @@ import { toPathString } from '../helpers' export interface CollapseAllState { path: CollectionKey[] collapsed: boolean + includeChildren?: boolean } export interface DragSource { @@ -28,9 +29,9 @@ export interface DragSource { } interface TreeStateContext { - collapseState: CollapseAllState | null - setCollapseState: (collapseState: CollapseAllState | null) => void - doesPathMatch: (path: CollectionKey[]) => boolean + collapseState: CollapseAllState | CollapseAllState[] | null + setCollapseState: (collapseState: CollapseAllState | CollapseAllState[] | null) => void + getMatchingCollapseState: (path: CollectionKey[]) => CollapseAllState | null currentlyEditingElement: string | null setCurrentlyEditingElement: ( path: CollectionKey[] | string | null, @@ -49,7 +50,7 @@ interface TreeStateContext { const initialContext: TreeStateContext = { collapseState: null, setCollapseState: () => {}, - doesPathMatch: () => false, + getMatchingCollapseState: () => null, currentlyEditingElement: null, setCurrentlyEditingElement: () => {}, previouslyEditedElement: null, @@ -72,7 +73,9 @@ interface TreeStateProps { } export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeStateProps) => { - const [collapseState, setCollapseState] = useState(null) + const [collapseState, setCollapseState] = useState( + null + ) const [currentlyEditingElement, setCurrentlyEditingElement] = useState(null) // This value holds the "previous" value when user changes type. Because @@ -114,9 +117,26 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta cancelOp.current = typeof newCancelOrKey === 'function' ? newCancelOrKey : null } - const doesPathMatch = (path: CollectionKey[]) => { + const getMatchingCollapseState = (path: CollectionKey[]) => { + if (Array.isArray(collapseState)) { + for (const cs of collapseState) { + if (pathMatchSingle(path, cs)) return cs + } + return null + } + + return pathMatchSingle(path, collapseState) ? collapseState : null + } + + const pathMatchSingle = (path: CollectionKey[], collapseState: CollapseAllState | null) => { if (collapseState === null) return false + if (!collapseState.includeChildren) + return ( + collapseState.path.every((part, index) => path[index] === part) && + collapseState.path.length === path.length + ) + for (const [index, value] of collapseState.path.entries()) { if (value !== path[index]) return false } @@ -141,7 +161,7 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta // externally if (state !== null) setTimeout(() => setCollapseState(null), 2000) }, - doesPathMatch, + getMatchingCollapseState, // Editing currentlyEditingElement, setCurrentlyEditingElement: updateCurrentlyEditingElement, diff --git a/src/hooks/useTriggers.ts b/src/hooks/useTriggers.ts index d61f1041..4c1b3a75 100644 --- a/src/hooks/useTriggers.ts +++ b/src/hooks/useTriggers.ts @@ -26,17 +26,19 @@ export const useTriggers = ( const { setCurrentlyEditingElement, currentlyEditingElement, setCollapseState } = useTreeState() useEffect(() => { - console.log('Trigger....') + // console.log('Trigger....') if (!triggers) return - console.log('Processing triggers', triggers) + // console.log('Processing triggers', triggers) const { collapse, edit } = triggers - if (Array.isArray(collapse)) { - return // For now + // COLLAPSE + + if (collapse) { + setCollapseState(collapse) } - if (collapse) setCollapseState(collapse) + // EDIT const doesPathMatch = edit?.path ? toPathString(edit.path) === currentlyEditingElement : true From a63d60d62e9c1c7d077021aa64cb91621d29fe1c Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:10:35 +1300 Subject: [PATCH 18/26] Clarify onCollapse props --- src/CollectionNode.tsx | 4 ++-- src/contexts/TreeStateProvider.tsx | 4 +++- src/types.ts | 7 ++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 3c0bdfaa..6aa3cf8c 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -181,13 +181,13 @@ export const CollectionNode: React.FC = (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) } } diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index 37bf9c4b..221005ea 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -155,7 +155,9 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta setCollapseState: (state) => { setCollapseState(state) if (onCollapse && state !== null) - onCollapse({ path: state.path, collapsed: state.collapsed, includesChildren: true }) + if (Array.isArray(state)) { + state.forEach((cs) => onCollapse(cs)) + } else onCollapse(state) // Reset after 2 seconds, which is enough time for all child nodes to // have opened/closed, but still allows collapse reset if data changes // externally diff --git a/src/types.ts b/src/types.ts index 94abfd5e..a9b5ac64 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,7 @@ import { type Options as AssignOptions } from 'object-property-assigner' import { type LocalisedStrings, type TranslateFunction } from './localisation' import { ExternalTriggers } from './hooks' +import { CollapseAllState } from './contexts' export type JsonData = CollectionData | ValueData @@ -164,11 +165,7 @@ export type SortFunction = (arr: T[], nodeMap: (input: T) => [string | number export type OnEditEventFunction = (path: CollectionKey[] | string | null) => void -export type OnCollapseFunction = (input: { - path: CollectionKey[] - collapsed: boolean - includesChildren: boolean -}) => void +export type OnCollapseFunction = (input: CollapseAllState) => void // Internal update export type InternalUpdateFunction = ( From a4c9cd61f2a3d845e78d7c037bc4a856912a095c Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:20:09 +1300 Subject: [PATCH 19/26] Remove initial Context state --- src/contexts/TreeStateProvider.tsx | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index 221005ea..68f480f1 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -47,24 +47,8 @@ interface TreeStateContext { previousValue: JsonData | null setPreviousValue: (value: JsonData | null) => void } -const initialContext: TreeStateContext = { - collapseState: null, - setCollapseState: () => {}, - getMatchingCollapseState: () => null, - currentlyEditingElement: null, - setCurrentlyEditingElement: () => {}, - previouslyEditedElement: null, - setPreviouslyEditedElement: () => {}, - areChildrenBeingEdited: () => false, - dragSource: { path: null, pathString: null }, - setDragSource: () => {}, - tabDirection: 'next', - setTabDirection: () => {}, - previousValue: null, - setPreviousValue: () => {}, -} -const TreeStateProviderContext = createContext(initialContext) +const TreeStateProviderContext = createContext(null) interface TreeStateProps { children: React.ReactNode @@ -188,4 +172,8 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta ) } -export const useTreeState = () => useContext(TreeStateProviderContext) +export const useTreeState = () => { + const context = useContext(TreeStateProviderContext) + if (!context) throw new Error('Must be used within Context Provider') + return context +} From db3ae5dbc64594772323f8a641744911dd120a7c Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:23:15 +1300 Subject: [PATCH 20/26] Finesse triggers --- demo/src/App.tsx | 65 +++++++++++++----------------- src/ValueNodeWrapper.tsx | 2 +- src/contexts/TreeStateProvider.tsx | 19 +++------ src/hooks/useTriggers.ts | 10 +---- src/index.ts | 1 + src/types.ts | 13 ++++-- 6 files changed, 46 insertions(+), 64 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 9879d0cf..dd03694c 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -16,6 +16,7 @@ import { monoDarkTheme, candyWrapperTheme, psychedelicTheme, + // type CollapseState } from './_imports' import { FaNpm, FaExternalLinkAlt, FaGithub } from 'react-icons/fa' import { BiReset } from 'react-icons/bi' @@ -53,7 +54,6 @@ import { demoDataDefinitions } from './demoData' import { useDatabase } from './useDatabase' import './style.css' import { version } from './version' -import { ExternalTriggers } from './json-edit-react/src/types' const CodeEditor = lazy(() => import('./CodeEditor')) @@ -112,8 +112,10 @@ function App() { customTextEditor: false, }) - const [triggerText, setTriggerText] = useState('') - const [externalTriggers, setExternalTriggers] = useState | null>(null) + const [isEditing, setIsEditing] = useState(false) + + // const collapseState = useRef>({}) + // const [collapseData, setCollapseData] = useState() const [isSaving, setIsSaving] = useState(false) const previousTheme = useRef() // Used when resetting after theme editing @@ -133,6 +135,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 + // collapseState.current = data + // const collapseArray = Object.values(data) + // setCollapseData(collapseArray) + // // console.log('collapseArray', collapseArray) + // }, 500) + // } + // }, []) + const updateState = (patch: Partial) => setState({ ...state, ...patch }) const toggleState = (field: keyof AppState) => updateState({ [field]: !state[field] }) @@ -490,10 +505,15 @@ function App() { ) : undefined } - onEditEvent={(path, isKey) => console.log('Editing path', path, isKey)} - onCollapse={(input) => console.log('Collapse', input)} - externalTriggers={externalTriggers as ExternalTriggers | null} // collapseClickZones={['property', 'header']} + onEditEvent={(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={{ collapse: collapseData }} /> @@ -530,6 +550,7 @@ function App() { onClick={handleReset} visibility={canUndo ? 'visible' : 'hidden'} isLoading={isSaving} + isDisabled={isEditing} > {selectedDataSet === 'liveData' ? 'Push to the cloud' : 'Reset'} @@ -543,38 +564,6 @@ function App() { - - - Trigger Text - - setTriggerText(e.target.value)} - /> - - Demo data diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index b5b892d0..18c74a73 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -160,7 +160,7 @@ export const ValueNodeWrapper: React.FC = (props) => { setDataType(type) // Custom nodes will be instantiated expanded and NOT editing setCurrentlyEditingElement(null) - setCollapseState({ path, collapsed: false }) + setCollapseState({ path, collapsed: false, includeChildren: false }) } else { const newValue = convertValue( value, diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index eb429214..84479a08 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -14,24 +14,19 @@ import { type JsonData, type OnEditEventFunction, type OnCollapseFunction, + type CollapseState, } from '../types' import { toPathString } from '../helpers' -export interface CollapseAllState { - path: CollectionKey[] - collapsed: boolean - includeChildren?: boolean -} - export interface DragSource { path: CollectionKey[] | null pathString: string | null } interface TreeStateContext { - collapseState: CollapseAllState | CollapseAllState[] | null - setCollapseState: (collapseState: CollapseAllState | CollapseAllState[] | null) => void - getMatchingCollapseState: (path: CollectionKey[]) => CollapseAllState | null + collapseState: CollapseState | CollapseState[] | null + setCollapseState: (collapseState: CollapseState | CollapseState[] | null) => void + getMatchingCollapseState: (path: CollectionKey[]) => CollapseState | null currentlyEditingElement: string | null setCurrentlyEditingElement: ( path: CollectionKey[] | string | null, @@ -57,9 +52,7 @@ interface TreeStateProps { } export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeStateProps) => { - const [collapseState, setCollapseState] = useState( - null - ) + const [collapseState, setCollapseState] = useState(null) const [currentlyEditingElement, setCurrentlyEditingElement] = useState(null) // This value holds the "previous" value when user changes type. Because @@ -112,7 +105,7 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta return pathMatchSingle(path, collapseState) ? collapseState : null } - const pathMatchSingle = (path: CollectionKey[], collapseState: CollapseAllState | null) => { + const pathMatchSingle = (path: CollectionKey[], collapseState: CollapseState | null) => { if (collapseState === null) return false if (!collapseState.includeChildren) diff --git a/src/hooks/useTriggers.ts b/src/hooks/useTriggers.ts index 4c1b3a75..efad974d 100644 --- a/src/hooks/useTriggers.ts +++ b/src/hooks/useTriggers.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useTreeState } from '../contexts' -import { type CollectionKey } from '../types' +import { type CollectionKey, type CollapseState } from '../types' import { toPathString } from '../helpers' export interface EditState { @@ -8,12 +8,6 @@ export interface EditState { action?: 'accept' | 'cancel' } -export interface CollapseState { - path: CollectionKey[] - collapsed: boolean - includeChildren: boolean -} - export interface ExternalTriggers { collapse?: CollapseState | CollapseState[] edit?: EditState @@ -26,10 +20,8 @@ export const useTriggers = ( const { setCurrentlyEditingElement, currentlyEditingElement, setCollapseState } = useTreeState() useEffect(() => { - // console.log('Trigger....') if (!triggers) return - // console.log('Processing triggers', triggers) const { collapse, edit } = triggers // COLLAPSE diff --git a/src/index.ts b/src/index.ts index aa786db3..169bacf5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,6 +33,7 @@ export { type JsonData, type KeyboardControls, type TextEditorProps, + type CollapseState, } from './types' export { type LocalisedStrings, type TranslateFunction } from './localisation' diff --git a/src/types.ts b/src/types.ts index 0fa2ac4f..e466839a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,6 @@ import { type Options as AssignOptions } from 'object-property-assigner' import { type LocalisedStrings, type TranslateFunction } from './localisation' -import { ExternalTriggers } from './hooks' -import { CollapseAllState } from './contexts' +import { type ExternalTriggers } from './hooks' export type JsonData = CollectionData | ValueData @@ -166,7 +165,15 @@ export type SortFunction = (arr: T[], nodeMap: (input: T) => [string | number export type OnEditEventFunction = (path: CollectionKey[] | string | null, isKey: boolean) => void -export type OnCollapseFunction = (input: CollapseAllState) => void +// Definition to externally set Collapse state -- also passed to OnCollapse +// function +export interface CollapseState { + path: CollectionKey[] + collapsed: boolean + includeChildren: boolean +} + +export type OnCollapseFunction = (input: CollapseState) => void // Internal update export type InternalUpdateFunction = ( From 1886fd6fdac64aa65b4472b14ec63b3ea602cf4e Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Mon, 10 Mar 2025 23:37:57 +1300 Subject: [PATCH 21/26] Further tweaks --- src/contexts/TreeStateProvider.tsx | 41 ++++++++++++++++-------------- src/hooks/useTriggers.ts | 5 ++++ src/index.ts | 1 + 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/contexts/TreeStateProvider.tsx b/src/contexts/TreeStateProvider.tsx index 84479a08..45576417 100644 --- a/src/contexts/TreeStateProvider.tsx +++ b/src/contexts/TreeStateProvider.tsx @@ -94,31 +94,18 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta cancelOp.current = typeof newCancelOrKey === 'function' ? newCancelOrKey : null } + // Returns the current "CollapseState" value to Collection Node if it matches + // that node. If the current "CollapseState" is an array, will return the one + // matching one const getMatchingCollapseState = (path: CollectionKey[]) => { if (Array.isArray(collapseState)) { for (const cs of collapseState) { - if (pathMatchSingle(path, cs)) return cs + if (doesCollapseStateMatchPath(path, cs)) return cs } return null } - return pathMatchSingle(path, collapseState) ? collapseState : null - } - - const pathMatchSingle = (path: CollectionKey[], collapseState: CollapseState | null) => { - if (collapseState === null) return false - - if (!collapseState.includeChildren) - return ( - collapseState.path.every((part, index) => path[index] === part) && - collapseState.path.length === path.length - ) - - for (const [index, value] of collapseState.path.entries()) { - if (value !== path[index]) return false - } - - return true + return doesCollapseStateMatchPath(path, collapseState) ? collapseState : null } const areChildrenBeingEdited = (pathString: string) => @@ -167,6 +154,22 @@ export const TreeStateProvider = ({ children, onEditEvent, onCollapse }: TreeSta export const useTreeState = () => { const context = useContext(TreeStateProviderContext) - if (!context) throw new Error('Must be used within Context Provider') + if (!context) throw new Error('Missing Context Provider') return context } + +const doesCollapseStateMatchPath = (path: CollectionKey[], collapseState: CollapseState | null) => { + if (collapseState === null) return false + + if (!collapseState.includeChildren) + return ( + collapseState.path.every((part, index) => path[index] === part) && + collapseState.path.length === path.length + ) + + for (const [index, value] of collapseState.path.entries()) { + if (value !== path[index]) return false + } + + return true +} diff --git a/src/hooks/useTriggers.ts b/src/hooks/useTriggers.ts index efad974d..ca6e6cd2 100644 --- a/src/hooks/useTriggers.ts +++ b/src/hooks/useTriggers.ts @@ -1,3 +1,8 @@ +/** + * Hook to handle changes to the `externalTriggers` prop in order to set + * "collapse" state, as well as start/stop editing + */ + import { useEffect } from 'react' import { useTreeState } from '../contexts' import { type CollectionKey, type CollapseState } from '../types' diff --git a/src/index.ts b/src/index.ts index 169bacf5..d5f6e978 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ export { type TextEditorProps, type CollapseState, } from './types' +export { type EditState, type ExternalTriggers } from './hooks' export { type LocalisedStrings, type TranslateFunction } from './localisation' export { From 465f9ad1a396560c344b6e7fcde0322569eb3e10 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Fri, 21 Mar 2025 12:52:42 +1300 Subject: [PATCH 22/26] Update docs, test in App --- README.md | 96 +++++++++++++++++++++++++++++++++++++++++++----- demo/src/App.tsx | 12 +++++- src/index.ts | 2 + src/types.ts | 2 +- 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 149885fb..2fc7cc62 100644 --- a/README.md +++ b/README.md @@ -13,16 +13,17 @@ 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 screenshot @@ -46,6 +47,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) @@ -70,6 +72,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) @@ -195,7 +200,16 @@ The only *required* property is `data` (although you will need to provide a `set | `TextEditor` | `ReactComponent` | | 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 @@ -798,7 +812,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: diff --git a/demo/src/App.tsx b/demo/src/App.tsx index dd03694c..d9d7cb6a 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -16,6 +16,7 @@ import { monoDarkTheme, candyWrapperTheme, psychedelicTheme, + ExternalTriggers, // type CollapseState } from './_imports' import { FaNpm, FaExternalLinkAlt, FaGithub } from 'react-icons/fa' @@ -116,6 +117,7 @@ function App() { // const collapseState = useRef>({}) // const [collapseData, setCollapseData] = useState() + // const [triggers, setTriggers] = useState() const [isSaving, setIsSaving] = useState(false) const previousTheme = useRef() // Used when resetting after theme editing @@ -506,16 +508,22 @@ function App() { : undefined } // collapseClickZones={['property', 'header']} - onEditEvent={(path) => setIsEditing(path ? true : false)} + 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={{ collapse: collapseData }} + // externalTriggers={triggers} /> + {/* */}