diff --git a/README.md b/README.md index 612d9c1b..5826b456 100644 --- a/README.md +++ b/README.md @@ -1149,11 +1149,13 @@ A few helper functions, components and types that might be useful in your own im - `LinkCustomComponent`: the component used to render [hyperlinks](#active-hyperlinks) - `LinkCustomNodeDefinition`: custom node definition for [hyperlinks](#active-hyperlinks) - `StringDisplay`: main component used to display a string value, re-used in the above "Link" Custom Component +- `StringEdit`: component used when editing a string value, can be useful for custom components - `IconAdd`, `IconEdit`, `IconDelete`, `IconCopy`, `IconOk`, `IconCancel`, `IconChevron`: all the built-in [icon](#icons) components - `matchNode`, `matchNodeKey`: helpers for defining custom [Search](#searchfiltering) functions - `extract`: function to extract a deeply nested object value from a string path. See [here](https://github.com/CarlosNZ/object-property-extractor) - `assign`: function to set a deep object value from a string path. See [here](https://github.com/CarlosNZ/object-property-assigner) - `isCollection`: simple utility that returns `true` if input is a "Collection" (i.e. an Object or Array) +- `toPathString`: transforms a path array to a string representation, e.g. `["data", 0, "property1", "name"] => "data.0.property1.name"` - `defaultTheme`, `githubDarkTheme`, `monoDarkTheme`, `monoLightTheme`, `candyWrapperTheme`, `psychedelicTheme`: all built-in themes - `standardDataTypes`: array containing all standard data types: `[ 'string','number', 'boolean', 'null', 'object', 'array' ]` @@ -1194,6 +1196,7 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s ## Changelog +- **1.25.2**: Don't treat Date objects as collections, so they can be handled by custom components (#187)[https://github.com/CarlosNZ/json-edit-react/issues/187] - **1.25.1**: Small bug fix for incorrect resetting of cancelled edits (#184)[https://github.com/CarlosNZ/json-edit-react/issues/184] - **1.25.0**: - Implement [External control](#external-control) via event callbacks and triggers ([#138](https://github.com/CarlosNZ/json-edit-react/issues/138), [#145](https://github.com/CarlosNZ/json-edit-react/issues/145)) diff --git a/demo/src/demoData/data.tsx b/demo/src/demoData/data.tsx index ba8af63d..6731d642 100644 --- a/demo/src/demoData/data.tsx +++ b/demo/src/demoData/data.tsx @@ -5,6 +5,7 @@ export const data: Record = { boolean: true, nothing: null, enum: 'Option B', + // Date: new Date(), Usage: [ 'Edit a value by clicking the "edit" icon, or double-clicking the value.', 'You can change the type of any value', diff --git a/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index b9493027..9caa531b 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -9,6 +9,9 @@ import { LinkCustomNodeDefinition, assign, matchNode, + StringDisplay, + StringEdit, + toPathString, } from '../_imports' import { DefaultValueFunction, @@ -95,7 +98,38 @@ export const demoDataDefinitions: Record = { rootName: 'data', collapse: 2, data: data.intro, - customNodeDefinitions: [dateNodeDefinition], + customNodeDefinitions: [ + dateNodeDefinition, + // { + // condition: (nodeData) => nodeData.value instanceof Date, + // element: (props) => { + // const { nodeData, isEditing, setValue, getStyles, canEdit, value, handleEdit } = props + // return isEditing ? ( + // >} + // handleEdit={() => { + // const newDate = new Date(value as string) + // handleEdit(newDate as any) + // }} + // /> + // ) : ( + // + // ) + // }, + // showEditTools: true, + // showOnEdit: true, + // }, + ], // restrictEdit: ({ key }) => key === 'number', customTextEditorAvailable: true, restrictTypeSelection: ({ key }) => { diff --git a/src/AutogrowTextArea.tsx b/src/AutogrowTextArea.tsx index 113913f0..a6828e6d 100644 --- a/src/AutogrowTextArea.tsx +++ b/src/AutogrowTextArea.tsx @@ -15,7 +15,6 @@ interface TextAreaProps { name: string value: string setValue: React.Dispatch> - isEditing: boolean handleKeyPress: (e: React.KeyboardEvent) => void styles: React.CSSProperties textAreaRef?: React.MutableRefObject diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 66f1bd6a..4da3245c 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -343,7 +343,6 @@ export const CollectionNode: React.FC = (props) => { name={pathString} value={stringifiedValue} setValue={setStringifiedValue} - isEditing={isEditing} handleKeyPress={handleKeyPressEdit} styles={getStyles('input', nodeData)} /> @@ -382,6 +381,8 @@ export const CollectionNode: React.FC = (props) => { setIsEditing: () => setCurrentlyEditingElement(path), getStyles, canDragOnto: canEdit, + canEdit, + keyboardCommon: {}, } const CollectionContents = showCustomNodeContents ? ( diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 30c6f3e9..6f8bf5f1 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -202,25 +202,28 @@ export const ValueNodeWrapper: React.FC = (props) => { }) } - const handleEdit = () => { + const handleEdit = (inputValue?: unknown) => { setCurrentlyEditingElement(null) setPreviousValue(null) let newValue: JsonData - switch (dataType) { - case 'object': - newValue = { [translate('DEFAULT_NEW_KEY', nodeData)]: value } - break - case 'array': - newValue = value ?? [] - break - case 'number': { - const n = Number(value) - if (isNaN(n)) newValue = 0 - else newValue = n - break + if (inputValue !== undefined) newValue = inputValue as JsonData + else { + switch (dataType) { + case 'object': + newValue = { [translate('DEFAULT_NEW_KEY', nodeData)]: value } + break + case 'array': + newValue = value ?? [] + break + case 'number': { + const n = Number(value) + if (isNaN(n)) newValue = 0 + else newValue = n + break + } + default: + newValue = value } - default: - newValue = value } onEdit(newValue, path).then((error) => { if (error) onError({ code: 'UPDATE_ERROR', message: error }, newValue) @@ -247,7 +250,7 @@ export const ValueNodeWrapper: React.FC = (props) => { const { isEditingKey, canEditKey } = derivedValues const showErrorString = !isEditing && error const showTypeSelector = isEditing && allowedDataTypes.length > 1 - const showEditButtons = dataType !== 'invalid' && !error && showEditTools + const showEditButtons = (dataType !== 'invalid' || CustomNode) && !error && showEditTools const showKey = showLabel && !hideKey const showCustomNode = CustomNode && ((isEditing && showOnEdit) || (!isEditing && showOnView)) @@ -320,6 +323,8 @@ export const ValueNodeWrapper: React.FC = (props) => { getStyles={getStyles} originalNode={passOriginalNode ? getInputComponent(data, inputProps) : undefined} originalNodeKey={passOriginalNode ? : undefined} + canEdit={canEdit} + keyboardCommon={inputProps.keyboardCommon} /> ) : ( // Need to re-fetch data type to make sure it's one of the "core" ones diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index fde8d48a..b25f27e4 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -2,7 +2,12 @@ import React, { useEffect, useRef, useState } from 'react' import { AutogrowTextArea } from './AutogrowTextArea' import { insertCharInTextArea, toPathString } from './helpers' import { useTheme } from './contexts' -import { type NodeData, type InputProps, type EnumDefinition } from './types' +import { + type NodeData, + type InputProps, + type EnumDefinition, + type KeyboardControlsFull, +} from './types' import { type TranslateFunction } from './localisation' export const INVALID_FUNCTION_STRING = '**INVALID_FUNCTION**' @@ -16,6 +21,9 @@ interface StringDisplayProps { canEdit: boolean setIsEditing: (value: React.SetStateAction) => void translate: TranslateFunction + // Can override nodeDate.value if we need to modify it for specific display + // purposes + value?: string } export const StringDisplay: React.FC = ({ nodeData, @@ -26,8 +34,9 @@ export const StringDisplay: React.FC = ({ setIsEditing, styles, translate, + value: displayValue, }) => { - const value = nodeData.value as string + const value = displayValue ?? (nodeData.value as string) const [isExpanded, setIsExpanded] = useState(false) const quoteChar = showStringQuotes ? '"' : '' @@ -75,24 +84,69 @@ export const StringDisplay: React.FC = ({ ) } -export const StringValue: React.FC = ({ +interface StringEditProps { + styles: React.CSSProperties + pathString: string + value: string + setValue: React.Dispatch> + handleEdit: () => void + handleKeyboard: ( + e: React.KeyboardEvent, + eventMap: Partial void>> + ) => void + keyboardCommon: Partial void>> +} +export const StringEdit: React.FC = ({ + styles, + pathString, value, setValue, - isEditing, - path, handleEdit, - nodeData, handleKeyboard, keyboardCommon, +}) => { + const textAreaRef = useRef(null) + + return ( + { + handleKeyboard(e, { + stringConfirm: handleEdit, + stringLineBreak: () => { + // Simulates standard text-area line break behaviour. Only + // required when control key is not "standard" text-area + // behaviour ("Shift-Enter" or "Enter") + const newValue = insertCharInTextArea( + textAreaRef as React.MutableRefObject, + '\n' + ) + setValue(newValue) + }, + ...keyboardCommon, + }) + }} + styles={styles} + /> + ) +} + +export const StringValue: React.FC = ({ + isEditing, + path, enumType, ...props }) => { const { getStyles } = useTheme() - const textAreaRef = useRef(null) - const pathString = toPathString(path) + const { value, setValue, nodeData, handleEdit, handleKeyboard, keyboardCommon } = props + if (isEditing && enumType) { return (
@@ -123,38 +177,14 @@ export const StringValue: React.FC>} - isEditing={isEditing} - handleKeyPress={(e) => { - handleKeyboard(e, { - stringConfirm: handleEdit, - stringLineBreak: () => { - // Simulates standard text-area line break behaviour. Only - // required when control key is not "standard" text-area - // behaviour ("Shift-Enter" or "Enter") - const newValue = insertCharInTextArea( - textAreaRef as React.MutableRefObject, - '\n' - ) - setValue(newValue) - }, - ...keyboardCommon, - }) - }} + - ) : ( - >} /> + ) : ( + ) } diff --git a/src/customComponents/ActiveHyperlinks.tsx b/src/customComponents/ActiveHyperlinks.tsx index d0a0a8b9..270aadd3 100644 --- a/src/customComponents/ActiveHyperlinks.tsx +++ b/src/customComponents/ActiveHyperlinks.tsx @@ -10,14 +10,12 @@ import React from 'react' import { StringDisplay } from '../../src/ValueNodes' import { toPathString } from '../helpers' import { type CustomNodeProps, type CustomNodeDefinition, type ValueNodeProps } from '../types' -import { useCommon } from '../hooks' export const LinkCustomComponent: React.FC< CustomNodeProps<{ stringTruncate?: number }> & ValueNodeProps > = (props) => { const { value, setIsEditing, getStyles, nodeData } = props const styles = getStyles('string', nodeData) - const { canEdit } = useCommon({ props }) return (
setIsEditing(true)} @@ -35,10 +33,9 @@ export const LinkCustomComponent: React.FC< >
diff --git a/src/helpers.ts b/src/helpers.ts index f89cb927..0fc48243 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -17,7 +17,7 @@ import { } from './types' export const isCollection = (value: unknown): value is Record | unknown[] => - value !== null && typeof value === 'object' + value !== null && typeof value === 'object' && !(value instanceof Date) /** * FILTERING diff --git a/src/index.ts b/src/index.ts index 02dfed14..6883ae48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ export { JsonEditor } from './JsonEditor' export { defaultTheme } from './contexts/ThemeProvider' export { IconAdd, IconEdit, IconDelete, IconCopy, IconOk, IconCancel, IconChevron } from './Icons' -export { StringDisplay } from './ValueNodes' +export { StringDisplay, StringEdit } from './ValueNodes' export { LinkCustomComponent, LinkCustomNodeDefinition } from './customComponents' -export { matchNode, matchNodeKey, isCollection } from './helpers' +export { matchNode, matchNodeKey, isCollection, toPathString } from './helpers' export { default as assign } from 'object-property-assigner' export { default as extract } from 'object-property-extractor' export { diff --git a/src/types.ts b/src/types.ts index 39304e6f..038328e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -310,7 +310,7 @@ export interface CustomNodeProps> extends BaseNodePr customNodeProps?: T parentData: CollectionData | null setValue: (value: ValueData) => void - handleEdit: () => void + handleEdit: (value?: unknown) => void handleCancel: () => void handleKeyPress: (e: React.KeyboardEvent) => void isEditing: boolean @@ -319,6 +319,8 @@ export interface CustomNodeProps> extends BaseNodePr children?: JSX.Element | JSX.Element[] | null originalNode?: JSX.Element originalNodeKey?: JSX.Element + canEdit: boolean + keyboardCommon: Partial void>> } export interface CustomNodeDefinition, U = Record> {