From e633caf02613eb1f28fea9ac2ee15d9a2e75cc84 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 8 Mar 2025 20:57:30 +1300 Subject: [PATCH 1/4] Abstract KeyDisplay into single component --- demo/src/App.tsx | 17 +++++++- demo/src/_imports.ts | 4 +- src/CollectionNode.tsx | 68 ++++++++--------------------- src/CustomNode.ts | 1 + src/KeyDisplay.tsx | 92 ++++++++++++++++++++++++++++++++++++++++ src/ValueNodeWrapper.tsx | 68 +++++++++-------------------- src/style.css | 2 +- src/types.ts | 3 ++ 8 files changed, 153 insertions(+), 102 deletions(-) create mode 100644 src/KeyDisplay.tsx diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 18356b40..20107cfd 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -414,7 +414,22 @@ function App() { maxWidth="min(670px, 90vw)" className="block-shadow" stringTruncate={90} - customNodeDefinitions={dataDefinition?.customNodeDefinitions} + // customNodeDefinitions={dataDefinition?.customNodeDefinitions} + customNodeDefinitions={[ + { + condition: ({ key }) => key === 'string', + element: ({ value, originalNode, originalKeyNode }) => ( +
+ {originalKeyNode} + ICON + {originalNode} +
+ ), + hideKey: true, + passOriginalNode: true, + showOnEdit: true, + }, + ]} customText={dataDefinition?.customTextDefinitions} // icons={{ chevron: }} // customButtons={[ 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 acc1715b..873cb0b4 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -17,6 +17,7 @@ import { isCollection, } from './helpers' import { AutogrowTextArea } from './AutogrowTextArea' +import { KeyDisplay } from './KeyDisplay' import { useTheme, useTreeState } from './contexts' import { useCollapseTransition, useCommon, useDragNDrop } from './hooks' @@ -359,54 +360,6 @@ export const CollectionNode: React.FC = (props) => { CollectionChildren ) - const KeyDisplay = isEditingKey ? ( - e.target.select()} - onKeyDown={(e) => - handleKeyboard(e, { - stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), - cancel: handleCancel, - tabForward: () => { - handleEditKey((e.target as HTMLInputElement).value) - const firstChildKey = keyValueArray?.[0][0] - setCurrentlyEditingElement( - firstChildKey - ? [...path, firstChildKey] - : getNextOrPrevious(nodeData.fullData, path, 'next', sort) - ) - }, - tabBack: () => { - handleEditKey((e.target as HTMLInputElement).value) - setCurrentlyEditingElement(getNextOrPrevious(nodeData.fullData, path, 'prev', sort)) - }, - }) - } - style={{ width: `${String(name).length / 1.5 + 0.5}em` }} - /> - ) : ( - showKey && ( - e.stopPropagation()} - onDoubleClick={() => canEditKey && setCurrentlyEditingElement(path, 'key')} - > - {name === '' ? ( - 0 ? 'jer-empty-string' : undefined}> - {/* display "" using pseudo class CSS */} - - ) : ( - `${name}:` - )} - - ) - ) - const EditButtonDisplay = showEditButtons && ( = (props) => { /> ) + const keyDisplayProps = { + canEditKey, + isEditingKey, + pathString, + path, + name: name as string, + handleKeyboard, + handleEditKey, + handleCancel, + keyValueArray, + styles: getStyles('property', nodeData), + getNextOrPrevious: (type: 'next' | 'prev') => + getNextOrPrevious(nodeData.fullData, path, type, sort), + } + const CollectionNodeComponent = (
= (props) => { >
- {KeyDisplay} + {showKey && } {!isEditing && ( = (props) => { <> ) : (
- {KeyDisplay} + {EditButtonDisplay}
)} diff --git a/src/CustomNode.ts b/src/CustomNode.ts index 5665bc17..b266b630 100644 --- a/src/CustomNode.ts +++ b/src/CustomNode.ts @@ -13,6 +13,7 @@ export interface CustomNodeData { showOnView?: boolean showEditTools?: boolean showCollectionWrapper?: boolean + passOriginalNode?: boolean } // Fetches matching custom nodes (based on condition filter) from custom node diff --git a/src/KeyDisplay.tsx b/src/KeyDisplay.tsx new file mode 100644 index 00000000..d0cbdb38 --- /dev/null +++ b/src/KeyDisplay.tsx @@ -0,0 +1,92 @@ +/** + * Component to display the "Property" value for both Collection and Value nodes + */ + +import React from 'react' +import { useTreeState } from './contexts' +import { type KeyboardControlsFull, type CollectionKey, type ValueData } from './types' + +interface KeyDisplayProps { + canEditKey: boolean + isEditingKey: boolean + pathString: string + path: CollectionKey[] + name: string + handleKeyboard: ( + e: React.KeyboardEvent, + eventMap: Partial void>> + ) => void + handleEditKey: (newKey: string) => void + handleCancel: () => void + keyValueArray?: Array<[string | number, ValueData]> + styles: React.CSSProperties + getNextOrPrevious: (type: 'next' | 'prev') => CollectionKey[] | null +} + +export const KeyDisplay: React.FC = ({ + isEditingKey, + canEditKey, + pathString, + path, + name, + handleKeyboard, + handleEditKey, + handleCancel, + keyValueArray, + styles, + getNextOrPrevious, +}) => { + const { setCurrentlyEditingElement } = useTreeState() + + if (!isEditingKey) + return ( + 10 ? 1 : 0, + }} + onDoubleClick={() => canEditKey && setCurrentlyEditingElement(path, 'key')} + > + {name === '' ? ( + 0 ? 'jer-empty-string' : undefined}> + {/* display "" using pseudo class CSS */} + + ) : ( + `${name}:` + )} + + ) + + return ( + e.target.select()} + onKeyDown={(e: React.KeyboardEvent) => + handleKeyboard(e, { + stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), + cancel: handleCancel, + tabForward: () => { + handleEditKey((e.target as HTMLInputElement).value) + if (keyValueArray) { + const firstChildKey = keyValueArray?.[0][0] + setCurrentlyEditingElement( + firstChildKey ? [...path, firstChildKey] : getNextOrPrevious('next') + ) + } else setCurrentlyEditingElement(path) + }, + tabBack: () => { + handleEditKey((e.target as HTMLInputElement).value) + setCurrentlyEditingElement(getNextOrPrevious('prev')) + }, + }) + } + style={{ width: `${String(name).length / 1.5 + 0.5}em` }} + /> + ) +} diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 9ead0355..b63d6e47 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -21,6 +21,7 @@ import { useTheme, useTreeState } from './contexts' import { getCustomNode, type CustomNodeData } from './CustomNode' import { filterNode, getNextOrPrevious } from './helpers' import { useCommon, useDragNDrop } from './hooks' +import { KeyDisplay } from './KeyDisplay' export const ValueNodeWrapper: React.FC = (props) => { const { @@ -111,6 +112,7 @@ export const ValueNodeWrapper: React.FC = (props) => { showEditTools = true, showOnEdit, showOnView, + passOriginalNode, } = customNodeData // Include custom node options in dataType list @@ -214,8 +216,7 @@ export const ValueNodeWrapper: React.FC = (props) => { const showErrorString = !isEditing && error const showTypeSelector = isEditing && allowedDataTypes.length > 0 const showEditButtons = dataType !== 'invalid' && !error && showEditTools - const showKeyEdit = showLabel && isEditingKey - const showKey = showLabel && !isEditingKey && !hideKey + const showKey = showLabel && !hideKey const showCustomNode = CustomNode && ((isEditing && showOnEdit) || (!isEditing && showOnView)) const inputProps = { @@ -270,6 +271,8 @@ export const ValueNodeWrapper: React.FC = (props) => { isEditing={isEditing} setIsEditing={() => setCurrentlyEditingElement(path)} getStyles={getStyles} + originalNode={passOriginalNode ? getInputComponent(data, inputProps) : undefined} + originalKeyNode={passOriginalNode ? KeyDisplay : undefined} /> ) : ( // Need to re-fetch data type to make sure it's one of the "core" ones @@ -277,6 +280,20 @@ export const ValueNodeWrapper: React.FC = (props) => { getInputComponent(data, inputProps) ) + const keyDisplayProps = { + canEditKey, + isEditingKey, + pathString, + path, + name: name as string, + handleKeyboard, + handleEditKey, + handleCancel, + styles: getStyles('property', nodeData), + getNextOrPrevious: (type: 'next' | 'prev') => + getNextOrPrevious(nodeData.fullData, path, type, sort), + } + return (
= (props) => { flexWrap: (name as string).length > 10 ? 'wrap' : 'nowrap', }} > - {showKey && ( - 10 ? 1 : 0, - }} - onDoubleClick={() => canEditKey && setCurrentlyEditingElement(path, 'key')} - > - {name === '' ? ( - 0 ? 'jer-empty-string' : undefined}> - {/* display "" using pseudo class CSS */} - - ) : ( - `${name}:` - )} - - )} - {showKeyEdit && ( - e.target.select()} - onKeyDown={(e: React.KeyboardEvent) => - handleKeyboard(e, { - stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), - cancel: handleCancel, - tabForward: () => { - handleEditKey((e.target as HTMLInputElement).value) - setCurrentlyEditingElement(path) - }, - tabBack: () => { - handleEditKey((e.target as HTMLInputElement).value) - setCurrentlyEditingElement( - getNextOrPrevious(nodeData.fullData, path, 'prev', sort) - ) - }, - }) - } - style={{ width: `${String(name).length / 1.5 + 0.5}em` }} - /> - )} + {showKey && }
{ValueComponent}
{isEditing ? ( diff --git a/src/style.css b/src/style.css index 61b19948..057f7f3c 100644 --- a/src/style.css +++ b/src/style.css @@ -399,7 +399,7 @@ select:focus + .focus { /* For displaying keys that are purely "" */ .jer-empty-string::after { - content: ':'; + content: ''; font-style: italic; font-size: 90%; } diff --git a/src/types.ts b/src/types.ts index 76bd08fd..6a5e9164 100644 --- a/src/types.ts +++ b/src/types.ts @@ -285,6 +285,8 @@ export interface CustomNodeProps> extends BaseNodePr setIsEditing: React.Dispatch> getStyles: (element: ThemeableElement, nodeData: NodeData) => React.CSSProperties children?: JSX.Element | JSX.Element[] | null + originalNode?: JSX.Element + originalKeyNode?: JSX.Element } export interface CustomNodeDefinition, U = Record> { @@ -298,6 +300,7 @@ export interface CustomNodeDefinition, U = Record Date: Sat, 8 Mar 2025 22:27:41 +1300 Subject: [PATCH 2/4] Hide colon via CSS if required --- demo/src/App.tsx | 40 ++++++++++++++++++++++++---------------- src/KeyDisplay.tsx | 3 ++- src/ValueNodeWrapper.tsx | 30 +++++++++++++++--------------- src/types.ts | 2 +- 4 files changed, 42 insertions(+), 33 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 20107cfd..c15302b8 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -414,22 +414,30 @@ function App() { maxWidth="min(670px, 90vw)" className="block-shadow" stringTruncate={90} - // customNodeDefinitions={dataDefinition?.customNodeDefinitions} - customNodeDefinitions={[ - { - condition: ({ key }) => key === 'string', - element: ({ value, originalNode, originalKeyNode }) => ( -
- {originalKeyNode} - ICON - {originalNode} -
- ), - hideKey: true, - passOriginalNode: true, - showOnEdit: true, - }, - ]} + customNodeDefinitions={dataDefinition?.customNodeDefinitions} + // customNodeDefinitions={[ + // { + // condition: ({ key }) => key === 'string', + // element: ({ nodeData, value, originalNode, originalNodeKey }) => ( + //
+ // {originalNodeKey} + // {/* {nodeData.key} */} + // ICON:{' '} + // {originalNode} + //
+ // ), + // hideKey: true, + // passOriginalNode: true, + // showOnEdit: true, + // }, + // ]} customText={dataDefinition?.customTextDefinitions} // icons={{ chevron: }} // customButtons={[ diff --git a/src/KeyDisplay.tsx b/src/KeyDisplay.tsx index d0cbdb38..45c2fc51 100644 --- a/src/KeyDisplay.tsx +++ b/src/KeyDisplay.tsx @@ -54,8 +54,9 @@ export const KeyDisplay: React.FC = ({ {/* display "" using pseudo class CSS */} ) : ( - `${name}:` + `${name}` )} + : ) diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index b63d6e47..4abbd0f1 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -257,6 +257,20 @@ export const ValueNodeWrapper: React.FC = (props) => { }, } + const keyDisplayProps = { + canEditKey, + isEditingKey, + pathString, + path, + name: name as string, + handleKeyboard, + handleEditKey, + handleCancel, + styles: getStyles('property', nodeData), + getNextOrPrevious: (type: 'next' | 'prev') => + getNextOrPrevious(nodeData.fullData, path, type, sort), + } + const ValueComponent = showCustomNode ? ( = (props) => { setIsEditing={() => setCurrentlyEditingElement(path)} getStyles={getStyles} originalNode={passOriginalNode ? getInputComponent(data, inputProps) : undefined} - originalKeyNode={passOriginalNode ? KeyDisplay : undefined} + originalNodeKey={passOriginalNode ? : undefined} /> ) : ( // Need to re-fetch data type to make sure it's one of the "core" ones @@ -280,20 +294,6 @@ export const ValueNodeWrapper: React.FC = (props) => { getInputComponent(data, inputProps) ) - const keyDisplayProps = { - canEditKey, - isEditingKey, - pathString, - path, - name: name as string, - handleKeyboard, - handleEditKey, - handleCancel, - styles: getStyles('property', nodeData), - getNextOrPrevious: (type: 'next' | 'prev') => - getNextOrPrevious(nodeData.fullData, path, type, sort), - } - return (
> extends BaseNodePr getStyles: (element: ThemeableElement, nodeData: NodeData) => React.CSSProperties children?: JSX.Element | JSX.Element[] | null originalNode?: JSX.Element - originalKeyNode?: JSX.Element + originalNodeKey?: JSX.Element } export interface CustomNodeDefinition, U = Record> { From 616f158952069d98ea9fa20a6a31e104aeb20b02 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 8 Mar 2025 22:43:01 +1300 Subject: [PATCH 3/4] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cc562dbe..2f728460 100644 --- a/README.md +++ b/README.md @@ -662,7 +662,9 @@ Custom nodes are provided in the `customNodeDefinitions` prop, as an array of ob showOnView // boolean, default true showEditTools // boolean, default true name // string (appears in Types selector) - showInTypesSelector, // boolean (optional), default false + showInTypesSelector // boolean (optional), default false + passOriginalNode // boolean (optional), default false -- if `true` makes the original node + // for rendering within Custom Node // Only affects Collection nodes: showCollectionWrapper // boolean (optional), default true @@ -673,7 +675,7 @@ Custom nodes are provided in the `customNodeDefinitions` prop, as an array of ob The `condition` is just a [Filter function](#filter-functions), with the same input parameters (`key`, `path`, `value`, etc.), and `element` is a React component. Every node in the data structure will be run through each condition function, and any that match will be replaced by your custom component. Note that if a node matches more than one custom definition conditions (from multiple components), the *first* one will be used, so place them in the array in priority order. -The component will receive *all* the same props as a standard node component plus some additional ones — see [BaseNodeProps](https://github.com/CarlosNZ/json-edit-react/blob/b085f6391dabf574809f1040b11401c13344923d/src/types.ts#L219-L265) (common to all nodes) and [CustomNodeProps](https://github.com/CarlosNZ/json-edit-react/blob/b085f6391dabf574809f1040b11401c13344923d/src/types.ts#L275-L287) type definitions. Specifically, if you want to update the data structure from your custom node, you'll need to call the `setValue` method on your node's data value. +The component will receive *all* the same props as a standard node component plus some additional ones — see [BaseNodeProps](https://github.com/CarlosNZ/json-edit-react/blob/b085f6391dabf574809f1040b11401c13344923d/src/types.ts#L219-L265) (common to all nodes) and [CustomNodeProps](https://github.com/CarlosNZ/json-edit-react/blob/b085f6391dabf574809f1040b11401c13344923d/src/types.ts#L275-L287) type definitions. Specifically, if you want to update the data structure from your custom node, you'll need to call the `setValue` method on your node's data value. And if you enable `passOriginalNode` above, you'll also have access to `originalNode` and `originalNodeKey` in order to render the standard content (i.e. what would have been rendered if it wasn't intercepted by this Custom Node) -- this can be helpful if you want your Custom Node to just be the default content with a little extra decoration. (*Note:* you may need a little custom CSS to render these original node components identically to the default display.) You can pass additional props specific to your component, if required, through the `customNodeProps` object. A thorough example of a custom **Date Picker** is used in the demo (along with a couple of other more basic presentational ones), which you can inspect to see how to utilise the standard props and a couple of custom props. View the source code [here](https://github.com/CarlosNZ/json-edit-react/blob/main/demo/src/customComponents/DateTimePicker.tsx). @@ -850,6 +852,8 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s ## Changelog +- **1.24.0**: + - Option to access (and render) the original node (and its key) within a [Custom Node](#custom-nodes) ([#180](https://github.com/CarlosNZ/json-edit-react/issues/180)) - **1.23.1**: Fix bug where you could collapse a node by clicking inside a "new key" input field [#175](https://github.com/CarlosNZ/json-edit-react/issues/175) - **1.23.0**: - Add `viewOnly` prop as a shorthand for restricting all editing [#168](https://github.com/CarlosNZ/json-edit-react/issues/168) From 64722118769c2c6cd4a87268d6f510e80cfaabc0 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 8 Mar 2025 22:49:27 +1300 Subject: [PATCH 4/4] Remove dupe assignation --- src/ValueNodeWrapper.tsx | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 572f0505..4abbd0f1 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -294,20 +294,6 @@ export const ValueNodeWrapper: React.FC = (props) => { getInputComponent(data, inputProps) ) - const keyDisplayProps = { - canEditKey, - isEditingKey, - pathString, - path, - name: name as string, - handleKeyboard, - handleEditKey, - handleCancel, - styles: getStyles('property', nodeData), - getNextOrPrevious: (type: 'next' | 'prev') => - getNextOrPrevious(nodeData.fullData, path, type, sort), - } - return (