From d454ce1f861bd5f629a1b41ffea43c1e16e7aa25 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Fri, 13 Dec 2024 20:17:35 +1300 Subject: [PATCH 01/14] Add initial functionality on Collection Nodes and String --- demo/src/App.tsx | 5 +++ demo/src/_imports.ts | 4 +- src/CollectionNode.tsx | 32 +++++++++----- src/JsonEditor.tsx | 4 +- src/ValueNodeWrapper.tsx | 2 + src/ValueNodes.tsx | 12 ++--- src/helpers.ts | 96 +++++++++++++++++++++++++++++++++++++++- src/index.ts | 1 + src/types.ts | 30 +++++++++++++ 9 files changed, 165 insertions(+), 21 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index f8e4add2..c617edb8 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -385,6 +385,11 @@ function App() { // ]} onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} + keyboardControls={{ + cancel: 'Tab', + objectConfirm: { key: 'Enter', modifier: 'Shift' }, + stringConfirm: 'Backspace', + }} /> diff --git a/demo/src/_imports.ts b/demo/src/_imports.ts index 951d5913..c39e3044 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 2c4c9809..8f19b671 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -5,7 +5,7 @@ import { EditButtons, InputButtons } from './ButtonPanels' import { getCustomNode } from './CustomNode' import { type CollectionNodeProps, type NodeData, type CollectionData } from './types' import { Icon } from './Icons' -import { filterNode, isCollection } from './helpers' +import { filterNode, handleKeyPress as handleKeyPressFn, isCollection } from './helpers' import { AutogrowTextArea } from './AutogrowTextArea' import { useTheme } from './theme' import { useTreeState } from './TreeStateProvider' @@ -44,6 +44,7 @@ export const CollectionNode: React.FC = (props) => { customNodeDefinitions, jsonParse, jsonStringify, + keyboardControls, } = props const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data)) @@ -126,10 +127,15 @@ export const CollectionNode: React.FC = (props) => { const brackets = collectionType === 'array' ? { open: '[', close: ']' } : { open: '{', close: '}' } - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && (e.metaKey || e.shiftKey || e.ctrlKey)) handleEdit() - else if (e.key === 'Escape') handleCancel() - } + const handleKeyPress = (e: React.KeyboardEvent) => + handleKeyPressFn( + keyboardControls, + { + stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), + cancel: handleCancel, + }, + e + ) const handleCollapse = (e: React.MouseEvent) => { if (e.getModifierState('Alt')) { @@ -163,11 +169,6 @@ export const CollectionNode: React.FC = (props) => { } } - const handleKeyPressKeyEdit = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') handleEditKey((e.target as HTMLInputElement).value) - else if (e.key === 'Escape') handleCancel() - } - const handleAdd = (key: string) => { animateCollapse(false) const newValue = getDefaultNewValue(nodeData, key) @@ -325,7 +326,16 @@ export const CollectionNode: React.FC = (props) => { defaultValue={name} autoFocus onFocus={(e) => e.target.select()} - onKeyDown={handleKeyPressKeyEdit} + onKeyDown={(e) => + handleKeyPressFn( + keyboardControls, + { + stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), + cancel: handleCancel, + }, + e + ) + } style={{ width: `${String(name).length / 1.5 + 0.5}em` }} /> ) : ( diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 313dbd63..677a6200 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import assign, { type Input } from 'object-property-assigner' import extract from 'object-property-extractor' import { CollectionNode } from './CollectionNode' -import { isCollection, matchNode, matchNodeKey } from './helpers' +import { getFullKeyboardControlMap, isCollection, matchNode, matchNodeKey } from './helpers' import { type CollectionData, type JsonEditorProps, @@ -64,6 +64,7 @@ const Editor: React.FC = ({ jsonParse = JSON.parse, jsonStringify = (data: JsonData) => JSON.stringify(data, null, 2), errorMessageTimeout = 2500, + keyboardControls = {}, }) => { const { getStyles } = useTheme() const collapseFilter = useCallback(getFilterFunction(collapse), [collapse]) @@ -283,6 +284,7 @@ const Editor: React.FC = ({ jsonParse, jsonStringify, errorMessageTimeout, + keyboardControls: getFullKeyboardControlMap(keyboardControls), } const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth } diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 21f0e170..8d1b5818 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -42,6 +42,7 @@ export const ValueNodeWrapper: React.FC = (props) => { indent, translate, customNodeDefinitions, + keyboardControls, } = props const { getStyles } = useTheme() const { setCurrentlyEditingElement, setCollapseState } = useTreeState() @@ -217,6 +218,7 @@ export const ValueNodeWrapper: React.FC = (props) => { showStringQuotes, nodeData, translate, + keyboardControls, } const ValueComponent = showCustomNode ? ( diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index c4603c4c..e15df219 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react' import { AutogrowTextArea } from './AutogrowTextArea' -import { toPathString, truncate } from './helpers' +import { handleKeyPress, toPathString, truncate } from './helpers' import { useTheme } from './theme' import { type InputProps } from './types' @@ -17,12 +17,10 @@ export const StringValue: React.FC = ({ stringTruncate, showStringQuotes, nodeData, + keyboardControls, }) => { const { getStyles } = useTheme() - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) handleEdit() - else if (e.key === 'Escape') handleCancel() - } + const pathString = toPathString(path) const quoteChar = showStringQuotes ? '"' : '' @@ -34,7 +32,9 @@ export const StringValue: React.FC = ({ value={value} setValue={setValue as React.Dispatch>} isEditing={isEditing} - handleKeyPress={handleKeyPress} + handleKeyPress={(e) => + handleKeyPress(keyboardControls, { stringConfirm: handleEdit, cancel: handleCancel }, e) + } styles={getStyles('input', nodeData)} /> ) : ( diff --git a/src/helpers.ts b/src/helpers.ts index 24bc12b3..39726622 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,4 +1,11 @@ -import { type SearchFilterFunction, type NodeData, type SearchFilterInputFunction } from './types' +import { + type SearchFilterFunction, + type NodeData, + type SearchFilterInputFunction, + type KeyboardControls, + type KeyEvent, + type KeyboardControlsFull, +} from './types' export const isCollection = (value: unknown): value is Record | unknown[] => value !== null && typeof value === 'object' @@ -117,3 +124,90 @@ export const toPathString = (path: Array) => // non-printable char .map((part) => (part === '' ? String.fromCharCode(0) : part)) .join('.') + +/** + * KEYBOARD INTERACTION + */ + +// A general keyboard handler function +export const handleKeyPress = ( + controls: KeyboardControlsFull, + eventMap: Partial void>>, + e: React.KeyboardEvent +) => { + console.log('keyboardControls', controls) + const definitions = Object.entries(eventMap) + + for (const [definition, action] of definitions) { + if (eventMatch(e, controls[definition as keyof KeyboardControlsFull])) { + console.log('Match', e.key, getModifier(e)) + e.preventDefault() + action() + break + } + } +} + +// Returns the currently pressed modifier key. Only returns one, so the first +// match in the list is returned +const getModifier = (e: React.KeyboardEvent): React.ModifierKey | undefined => { + if (e.shiftKey) return 'Shift' + if (e.metaKey) return 'Meta' + if (e.ctrlKey) return 'Control' + if (e.altKey) return 'Alt' + return undefined +} + +// Determines whether a keyboard event matches a predefined value +const eventMatch = (e: React.KeyboardEvent, definition: KeyEvent | React.ModifierKey[]) => { + const eventKey = e.key + const eventModifier = getModifier(e) + console.log(eventKey, eventModifier) + if (Array.isArray(definition)) return eventModifier ? definition.includes(eventModifier) : false + const { key, modifier } = definition + console.log(definition) + + return ( + eventKey === key && + (modifier === eventModifier || + (Array.isArray(modifier) && modifier.includes(eventModifier as React.ModifierKey))) + ) +} + +const ENTER = { key: 'Enter' } + +const defaultKeyboardControls: KeyboardControlsFull = { + confirm: ENTER, + cancel: { key: 'Escape' }, + objectConfirm: { ...ENTER, modifier: ['Meta', 'Shift', 'Control'] }, + objectLineBreak: ENTER, + stringConfirm: ENTER, + stringLineBreak: { ...ENTER, modifier: ['Shift'] }, + numberConfirm: ENTER, + booleanConfirm: ENTER, + numberUp: { key: 'ArrowUp' }, + numberDown: { key: 'ArrowDown' }, + clipboardModifier: ['Meta', 'Control'], + collapseModifier: ['Alt'], +} + +export const getFullKeyboardControlMap = (userControls: KeyboardControls): KeyboardControlsFull => { + const controls = { ...defaultKeyboardControls } + for (const key of Object.keys(defaultKeyboardControls)) { + const typedKey = key as keyof KeyboardControls + if (userControls[typedKey]) { + const value = userControls[typedKey] + + const definition = (() => { + if (['clipboardModifier', 'collapseModifier'].includes(key)) + return Array.isArray(value) ? value : [value] + if (typeof value === 'string') return { key: value } + return value + })() as KeyEvent & React.ModifierKey[] + + controls[typedKey] = definition + } + } + + return controls +} diff --git a/src/index.ts b/src/index.ts index 9d22d7ca..5e8e3969 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,5 +31,6 @@ export { type ThemeStyles, type NodeData, type JsonData, + type KeyboardControls, } from './types' export { type LocalisedStrings, type TranslateFunction } from './localisation' diff --git a/src/types.ts b/src/types.ts index 0f33f79e..c707573d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,7 @@ export interface JsonEditorProps { jsonParse?: (input: string) => JsonData jsonStringify?: (input: JsonData) => string errorMessageTimeout?: number // ms + keyboardControls?: KeyboardControls } const ValueDataTypes = ['string', 'number', 'boolean', 'null'] as const @@ -150,6 +151,33 @@ export type InternalMoveFunction = ( position: Position ) => Promise +export interface KeyEvent { + key: string + modifier?: React.ModifierKey | React.ModifierKey[] +} +export interface KeyboardControls { + confirm?: KeyEvent | string // buttons, and value node default + cancel?: KeyEvent | string // all "Cancel" operations + objectConfirm?: KeyEvent | string + objectLineBreak?: KeyEvent | string + stringConfirm?: KeyEvent | string + stringLineBreak?: KeyEvent | string // for Value nodes + numberConfirm?: KeyEvent | string + booleanConfirm?: KeyEvent | string + numberUp?: KeyEvent | string + numberDown?: KeyEvent | string + clipboardModifier?: React.ModifierKey | React.ModifierKey[] + collapseModifier?: React.ModifierKey | React.ModifierKey[] +} + +export type KeyboardControlsFull = Omit< + Required<{ [Property in keyof KeyboardControls]: KeyEvent }>, + 'clipboardModifier' | 'collapseModifier' +> & { + clipboardModifier: React.ModifierKey[] + collapseModifier: React.ModifierKey[] +} + /** * NODES */ @@ -189,6 +217,7 @@ interface BaseNodeProps { customNodeDefinitions: CustomNodeDefinition[] customButtons: CustomButtonDefinition[] errorMessageTimeout: number + keyboardControls: KeyboardControlsFull } export interface CollectionNodeProps extends BaseNodeProps { @@ -263,6 +292,7 @@ export interface InputProps { showStringQuotes: boolean nodeData: NodeData translate: TranslateFunction + keyboardControls: KeyboardControlsFull } /** From 525c352219f42478f15d980c47d08b185ce10036 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Fri, 13 Dec 2024 22:25:06 +1300 Subject: [PATCH 02/14] More implementation --- src/CollectionNode.tsx | 34 ++++++++++-------------- src/JsonEditor.tsx | 19 ++++++++++++-- src/ValueNodeWrapper.tsx | 23 ++++++++--------- src/ValueNodes.tsx | 56 ++++++++++++++++------------------------ src/types.ts | 12 ++++++--- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index 8f19b671..b8bb3ae1 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -5,7 +5,7 @@ import { EditButtons, InputButtons } from './ButtonPanels' import { getCustomNode } from './CustomNode' import { type CollectionNodeProps, type NodeData, type CollectionData } from './types' import { Icon } from './Icons' -import { filterNode, handleKeyPress as handleKeyPressFn, isCollection } from './helpers' +import { filterNode, isCollection } from './helpers' import { AutogrowTextArea } from './AutogrowTextArea' import { useTheme } from './theme' import { useTreeState } from './TreeStateProvider' @@ -44,7 +44,7 @@ export const CollectionNode: React.FC = (props) => { customNodeDefinitions, jsonParse, jsonStringify, - keyboardControls, + handleKeyPress, } = props const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data)) @@ -127,15 +127,11 @@ export const CollectionNode: React.FC = (props) => { const brackets = collectionType === 'array' ? { open: '[', close: ']' } : { open: '{', close: '}' } - const handleKeyPress = (e: React.KeyboardEvent) => - handleKeyPressFn( - keyboardControls, - { - stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), - cancel: handleCancel, - }, - e - ) + const handleKeyPressFn = (e: React.KeyboardEvent) => + handleKeyPress(e, { + stringConfirm: handleEdit, + cancel: handleCancel, + }) const handleCollapse = (e: React.MouseEvent) => { if (e.getModifierState('Alt')) { @@ -277,7 +273,7 @@ export const CollectionNode: React.FC = (props) => { value={stringifiedValue} setValue={setStringifiedValue} isEditing={isEditing} - handleKeyPress={handleKeyPress} + handleKeyPress={handleKeyPressFn} styles={getStyles('input', nodeData)} />
@@ -303,7 +299,7 @@ export const CollectionNode: React.FC = (props) => { setValue: async (val: unknown) => await onEdit(val, path), handleEdit, handleCancel, - handleKeyPress, + handleKeyPress: handleKeyPressFn, isEditing, setIsEditing: () => setCurrentlyEditingElement(pathString), getStyles, @@ -327,14 +323,10 @@ export const CollectionNode: React.FC = (props) => { autoFocus onFocus={(e) => e.target.select()} onKeyDown={(e) => - handleKeyPressFn( - keyboardControls, - { - stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), - cancel: handleCancel, - }, - e - ) + handleKeyPress(e, { + stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), + cancel: handleCancel, + }) } style={{ width: `${String(name).length / 1.5 + 0.5}em` }} /> diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 677a6200..1badf4ac 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -2,7 +2,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react' import assign, { type Input } from 'object-property-assigner' import extract from 'object-property-extractor' import { CollectionNode } from './CollectionNode' -import { getFullKeyboardControlMap, isCollection, matchNode, matchNodeKey } from './helpers' +import { + getFullKeyboardControlMap, + handleKeyPress, + isCollection, + matchNode, + matchNodeKey, +} from './helpers' import { type CollectionData, type JsonEditorProps, @@ -15,6 +21,7 @@ import { type UpdateFunction, type UpdateFunctionProps, type JsonData, + type KeyboardControls, } from './types' import { useTheme, ThemeProvider } from './theme' import { TreeStateProvider } from './TreeStateProvider' @@ -249,6 +256,11 @@ const Editor: React.FC = ({ const restrictDragFilter = useMemo(() => getFilterFunction(restrictDrag), [restrictDrag]) const searchFilter = useMemo(() => getSearchFilter(searchFilterInput), [searchFilterInput]) + const fullKeyboardControls = useMemo( + () => getFullKeyboardControlMap(keyboardControls), + [keyboardControls] + ) + const otherProps = { name: rootName, nodeData, @@ -284,7 +296,10 @@ const Editor: React.FC = ({ jsonParse, jsonStringify, errorMessageTimeout, - keyboardControls: getFullKeyboardControlMap(keyboardControls), + handleKeyPress: ( + e: React.KeyboardEvent, + eventMap: Partial void>> + ) => handleKeyPress(fullKeyboardControls, eventMap, e), } const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth } diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 8d1b5818..105bbbfc 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -42,7 +42,7 @@ export const ValueNodeWrapper: React.FC = (props) => { indent, translate, customNodeDefinitions, - keyboardControls, + handleKeyPress, } = props const { getStyles } = useTheme() const { setCurrentlyEditingElement, setCollapseState } = useTreeState() @@ -179,11 +179,6 @@ export const ValueNodeWrapper: React.FC = (props) => { }) } - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') handleEditKey((e.target as HTMLInputElement).value) - else if (e.key === 'Escape') handleCancel() - } - const handleCancel = () => { setCurrentlyEditingElement(null) setValue(data) @@ -218,7 +213,7 @@ export const ValueNodeWrapper: React.FC = (props) => { showStringQuotes, nodeData, translate, - keyboardControls, + handleKeyPress, } const ValueComponent = showCustomNode ? ( @@ -229,10 +224,9 @@ export const ValueNodeWrapper: React.FC = (props) => { setValue={updateValue} handleEdit={handleEdit} handleCancel={handleCancel} - handleKeyPress={(e: React.KeyboardEvent) => { - if (e.key === 'Enter') handleEdit() - else if (e.key === 'Escape') handleCancel() - }} + handleKeyPress={(e: React.KeyboardEvent) => + handleKeyPress(e, { stringConfirm: handleEdit, cancel: handleCancel }) + } isEditing={isEditing} setIsEditing={() => setCurrentlyEditingElement(pathString)} getStyles={getStyles} @@ -289,7 +283,12 @@ export const ValueNodeWrapper: React.FC = (props) => { defaultValue={name} autoFocus onFocus={(e) => e.target.select()} - onKeyDown={handleKeyPress} + onKeyDown={(e: React.KeyboardEvent) => + handleKeyPress(e, { + stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), + cancel: handleCancel, + }) + } style={{ width: `${String(name).length / 1.5 + 0.5}em` }} /> )} diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index e15df219..7593276e 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react' import { AutogrowTextArea } from './AutogrowTextArea' -import { handleKeyPress, toPathString, truncate } from './helpers' +import { toPathString, truncate } from './helpers' import { useTheme } from './theme' import { type InputProps } from './types' @@ -17,7 +17,7 @@ export const StringValue: React.FC = ({ stringTruncate, showStringQuotes, nodeData, - keyboardControls, + handleKeyPress, }) => { const { getStyles } = useTheme() @@ -32,9 +32,7 @@ export const StringValue: React.FC = ({ value={value} setValue={setValue as React.Dispatch>} isEditing={isEditing} - handleKeyPress={(e) => - handleKeyPress(keyboardControls, { stringConfirm: handleEdit, cancel: handleCancel }, e) - } + handleKeyPress={(e) => handleKeyPress(e, { stringConfirm: handleEdit, cancel: handleCancel })} styles={getStyles('input', nodeData)} /> ) : ( @@ -63,25 +61,9 @@ export const NumberValue: React.FC = ({ handleEdit, handleCancel, nodeData, + handleKeyPress, }) => { const { getStyles } = useTheme() - const handleKeyPress = (e: React.KeyboardEvent) => { - switch (e.key) { - case 'Enter': - handleEdit() - break - case 'Escape': - handleCancel() - break - case 'ArrowUp': - e.preventDefault() - setValue(Number(value) + 1) - break - case 'ArrowDown': - e.preventDefault() - setValue(Number(value) - 1) - } - } const validateNumber = (input: string) => { return input.replace(/[^0-9.-]/g, '') @@ -96,7 +78,14 @@ export const NumberValue: React.FC = ({ onChange={(e) => setValue(validateNumber(e.target.value))} autoFocus onFocus={(e) => e.target.select()} - onKeyDown={handleKeyPress} + onKeyDown={(e) => + handleKeyPress(e, { + numberConfirm: handleEdit, + cancel: handleCancel, + numberUp: () => setValue(Number(value) + 1), + numberDown: () => setValue(Number(value) - 1), + }) + } style={{ width: `${String(value).length / 1.5 + 2}em`, ...getStyles('input', nodeData) }} /> ) : ( @@ -119,14 +108,10 @@ export const BooleanValue: React.FC = ({ handleEdit, handleCancel, nodeData, + handleKeyPress, }) => { const { getStyles } = useTheme() - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') handleEdit() - else if (e.key === 'Escape') handleCancel() - } - return isEditing ? ( = ({ name={toPathString(path)} checked={value} onChange={() => setValue(!value)} - onKeyDown={handleKeyPress} + onKeyDown={(e) => + handleKeyPress(e, { + booleanConfirm: handleEdit, + cancel: handleCancel, + }) + } autoFocus /> ) : ( @@ -155,6 +145,7 @@ export const NullValue: React.FC = ({ handleEdit, handleCancel, nodeData, + handleKeyPress, }) => { const { getStyles } = useTheme() @@ -163,11 +154,8 @@ export const NullValue: React.FC = ({ return () => document.removeEventListener('keydown', listenForSubmit) }, [isEditing]) - const listenForSubmit = (event: any) => { - if (event.key === 'Enter') { - handleEdit() - } else if (event.key === 'Escape') handleCancel() - } + const listenForSubmit = (e: unknown) => + handleKeyPress(e as React.KeyboardEvent, { confirm: handleEdit, cancel: handleCancel }) return (
void>> + ) => void } export interface CollectionNodeProps extends BaseNodeProps { @@ -292,7 +295,10 @@ export interface InputProps { showStringQuotes: boolean nodeData: NodeData translate: TranslateFunction - keyboardControls: KeyboardControlsFull + handleKeyPress: ( + e: React.KeyboardEvent, + eventMap: Partial void>> + ) => void } /** From d3c057f5f1dab412705096966abd810cb3695d78 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Fri, 13 Dec 2024 23:20:07 +1300 Subject: [PATCH 03/14] Tidy up, handle modifiers --- demo/src/App.tsx | 10 ++++----- src/ButtonPanels.tsx | 47 +++++++++++++++++++++++++--------------- src/CollectionNode.tsx | 22 +++++++++++-------- src/JsonEditor.tsx | 12 ++++++---- src/ValueNodeWrapper.tsx | 11 ++++++---- src/ValueNodes.tsx | 16 +++++++------- src/helpers.ts | 8 +++---- src/types.ts | 5 +++-- 8 files changed, 77 insertions(+), 54 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index c617edb8..b64bae58 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -385,11 +385,11 @@ function App() { // ]} onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} - keyboardControls={{ - cancel: 'Tab', - objectConfirm: { key: 'Enter', modifier: 'Shift' }, - stringConfirm: 'Backspace', - }} + // keyboardControls={{ + // cancel: 'Tab', + // objectConfirm: { key: 'Enter', modifier: 'Shift' }, + // stringConfirm: 'Backspace', + // }} /> diff --git a/src/ButtonPanels.tsx b/src/ButtonPanels.tsx index 5cb79892..f687ba23 100644 --- a/src/ButtonPanels.tsx +++ b/src/ButtonPanels.tsx @@ -9,7 +9,9 @@ import { type CopyType, type NodeData, type CustomButtonDefinition, + type KeyboardControlsFull, } from './types' +import { getModifier } from './helpers' interface EditButtonProps { startEdit?: () => void @@ -20,6 +22,11 @@ interface EditButtonProps { nodeData: NodeData translate: TranslateFunction customButtons: CustomButtonDefinition[] + keyboardControls: KeyboardControlsFull + handleKeyboard: ( + e: React.KeyboardEvent, + eventMap: Partial void>> + ) => void } export const EditButtons: React.FC = ({ @@ -31,6 +38,8 @@ export const EditButtons: React.FC = ({ customButtons, nodeData, translate, + keyboardControls, + handleKeyboard, }) => { const { getStyles } = useTheme() const NEW_KEY_PROMPT = translate('KEY_NEW', nodeData) @@ -40,14 +49,19 @@ export const EditButtons: React.FC = ({ const { key, path, value: data } = nodeData const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && handleAdd) { - setIsAdding(false) - handleAdd(newKey) - setNewKey(NEW_KEY_PROMPT) - } else if (e.key === 'Escape') { - setIsAdding(false) - setNewKey(NEW_KEY_PROMPT) - } + handleKeyboard(e, { + stringConfirm: () => { + if (handleAdd) { + setIsAdding(false) + handleAdd(newKey) + setNewKey(NEW_KEY_PROMPT) + } + }, + cancel: () => { + setIsAdding(false) + setNewKey(NEW_KEY_PROMPT) + }, + }) } const handleCopy = (e: React.MouseEvent) => { @@ -56,15 +70,14 @@ export const EditButtons: React.FC = ({ let value let stringValue = '' if (enableClipboard) { - switch (e.ctrlKey || e.metaKey) { - case true: - value = stringifyPath(path) - stringValue = value - copyType = 'path' - break - default: - value = data - stringValue = type ? JSON.stringify(data, null, 2) : String(value) + const modifier = getModifier(e) + if (modifier && keyboardControls.clipboardModifier.includes(modifier)) { + value = stringifyPath(path) + stringValue = value + copyType = 'path' + } else { + value = data + stringValue = type ? JSON.stringify(data, null, 2) : String(value) } void navigator.clipboard.writeText(stringValue) } diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index b8bb3ae1..6413f4a7 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -5,7 +5,7 @@ import { EditButtons, InputButtons } from './ButtonPanels' import { getCustomNode } from './CustomNode' import { type CollectionNodeProps, type NodeData, type CollectionData } from './types' import { Icon } from './Icons' -import { filterNode, isCollection } from './helpers' +import { filterNode, getModifier, isCollection } from './helpers' import { AutogrowTextArea } from './AutogrowTextArea' import { useTheme } from './theme' import { useTreeState } from './TreeStateProvider' @@ -44,7 +44,8 @@ export const CollectionNode: React.FC = (props) => { customNodeDefinitions, jsonParse, jsonStringify, - handleKeyPress, + keyboardControls, + handleKeyboard, } = props const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data)) @@ -127,14 +128,15 @@ export const CollectionNode: React.FC = (props) => { const brackets = collectionType === 'array' ? { open: '[', close: ']' } : { open: '{', close: '}' } - const handleKeyPressFn = (e: React.KeyboardEvent) => - handleKeyPress(e, { - stringConfirm: handleEdit, + const handleKeyPressEdit = (e: React.KeyboardEvent) => + handleKeyboard(e, { + objectConfirm: handleEdit, cancel: handleCancel, }) const handleCollapse = (e: React.MouseEvent) => { - if (e.getModifierState('Alt')) { + const modifier = getModifier(e) + if (modifier && keyboardControls.collapseModifier.includes(modifier)) { hasBeenOpened.current = true setCollapseState({ collapsed: !collapsed, path }) return @@ -273,7 +275,7 @@ export const CollectionNode: React.FC = (props) => { value={stringifiedValue} setValue={setStringifiedValue} isEditing={isEditing} - handleKeyPress={handleKeyPressFn} + handleKeyPress={handleKeyPressEdit} styles={getStyles('input', nodeData)} />
@@ -299,7 +301,7 @@ export const CollectionNode: React.FC = (props) => { setValue: async (val: unknown) => await onEdit(val, path), handleEdit, handleCancel, - handleKeyPress: handleKeyPressFn, + handleKeyPress: handleKeyPressEdit, isEditing, setIsEditing: () => setCurrentlyEditingElement(pathString), getStyles, @@ -323,7 +325,7 @@ export const CollectionNode: React.FC = (props) => { autoFocus onFocus={(e) => e.target.select()} onKeyDown={(e) => - handleKeyPress(e, { + handleKeyboard(e, { stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), cancel: handleCancel, }) @@ -366,6 +368,8 @@ export const CollectionNode: React.FC = (props) => { nodeData={nodeData} translate={translate} customButtons={props.customButtons} + keyboardControls={keyboardControls} + handleKeyboard={handleKeyboard} /> ) diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 1badf4ac..2cb283bf 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -261,6 +261,12 @@ const Editor: React.FC = ({ [keyboardControls] ) + const handleKeyboardCallback = useCallback( + (e: React.KeyboardEvent, eventMap: Partial void>>) => + handleKeyPress(fullKeyboardControls, eventMap, e), + [keyboardControls] + ) + const otherProps = { name: rootName, nodeData, @@ -296,10 +302,8 @@ const Editor: React.FC = ({ jsonParse, jsonStringify, errorMessageTimeout, - handleKeyPress: ( - e: React.KeyboardEvent, - eventMap: Partial void>> - ) => handleKeyPress(fullKeyboardControls, eventMap, e), + handleKeyboard: handleKeyboardCallback, + keyboardControls: fullKeyboardControls, } const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth } diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index 105bbbfc..a58df5ee 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -42,7 +42,8 @@ export const ValueNodeWrapper: React.FC = (props) => { indent, translate, customNodeDefinitions, - handleKeyPress, + handleKeyboard, + keyboardControls, } = props const { getStyles } = useTheme() const { setCurrentlyEditingElement, setCollapseState } = useTreeState() @@ -213,7 +214,7 @@ export const ValueNodeWrapper: React.FC = (props) => { showStringQuotes, nodeData, translate, - handleKeyPress, + handleKeyboard, } const ValueComponent = showCustomNode ? ( @@ -225,7 +226,7 @@ export const ValueNodeWrapper: React.FC = (props) => { handleEdit={handleEdit} handleCancel={handleCancel} handleKeyPress={(e: React.KeyboardEvent) => - handleKeyPress(e, { stringConfirm: handleEdit, cancel: handleCancel }) + handleKeyboard(e, { stringConfirm: handleEdit, cancel: handleCancel }) } isEditing={isEditing} setIsEditing={() => setCurrentlyEditingElement(pathString)} @@ -284,7 +285,7 @@ export const ValueNodeWrapper: React.FC = (props) => { autoFocus onFocus={(e) => e.target.select()} onKeyDown={(e: React.KeyboardEvent) => - handleKeyPress(e, { + handleKeyboard(e, { stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value), cancel: handleCancel, }) @@ -305,6 +306,8 @@ export const ValueNodeWrapper: React.FC = (props) => { translate={translate} customButtons={props.customButtons} nodeData={nodeData} + handleKeyboard={handleKeyboard} + keyboardControls={keyboardControls} /> ) )} diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index 7593276e..bc504c01 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -17,7 +17,7 @@ export const StringValue: React.FC = ({ stringTruncate, showStringQuotes, nodeData, - handleKeyPress, + handleKeyboard, }) => { const { getStyles } = useTheme() @@ -32,7 +32,7 @@ export const StringValue: React.FC = ({ value={value} setValue={setValue as React.Dispatch>} isEditing={isEditing} - handleKeyPress={(e) => handleKeyPress(e, { stringConfirm: handleEdit, cancel: handleCancel })} + handleKeyPress={(e) => handleKeyboard(e, { stringConfirm: handleEdit, cancel: handleCancel })} styles={getStyles('input', nodeData)} /> ) : ( @@ -61,7 +61,7 @@ export const NumberValue: React.FC = ({ handleEdit, handleCancel, nodeData, - handleKeyPress, + handleKeyboard, }) => { const { getStyles } = useTheme() @@ -79,7 +79,7 @@ export const NumberValue: React.FC = ({ autoFocus onFocus={(e) => e.target.select()} onKeyDown={(e) => - handleKeyPress(e, { + handleKeyboard(e, { numberConfirm: handleEdit, cancel: handleCancel, numberUp: () => setValue(Number(value) + 1), @@ -108,7 +108,7 @@ export const BooleanValue: React.FC = ({ handleEdit, handleCancel, nodeData, - handleKeyPress, + handleKeyboard, }) => { const { getStyles } = useTheme() @@ -120,7 +120,7 @@ export const BooleanValue: React.FC = ({ checked={value} onChange={() => setValue(!value)} onKeyDown={(e) => - handleKeyPress(e, { + handleKeyboard(e, { booleanConfirm: handleEdit, cancel: handleCancel, }) @@ -145,7 +145,7 @@ export const NullValue: React.FC = ({ handleEdit, handleCancel, nodeData, - handleKeyPress, + handleKeyboard, }) => { const { getStyles } = useTheme() @@ -155,7 +155,7 @@ export const NullValue: React.FC = ({ }, [isEditing]) const listenForSubmit = (e: unknown) => - handleKeyPress(e as React.KeyboardEvent, { confirm: handleEdit, cancel: handleCancel }) + handleKeyboard(e as React.KeyboardEvent, { confirm: handleEdit, cancel: handleCancel }) return (
void>>, e: React.KeyboardEvent ) => { - console.log('keyboardControls', controls) const definitions = Object.entries(eventMap) for (const [definition, action] of definitions) { if (eventMatch(e, controls[definition as keyof KeyboardControlsFull])) { - console.log('Match', e.key, getModifier(e)) e.preventDefault() action() break @@ -150,7 +148,9 @@ export const handleKeyPress = ( // Returns the currently pressed modifier key. Only returns one, so the first // match in the list is returned -const getModifier = (e: React.KeyboardEvent): React.ModifierKey | undefined => { +export const getModifier = ( + e: React.KeyboardEvent | React.MouseEvent +): React.ModifierKey | undefined => { if (e.shiftKey) return 'Shift' if (e.metaKey) return 'Meta' if (e.ctrlKey) return 'Control' @@ -162,10 +162,8 @@ const getModifier = (e: React.KeyboardEvent): React.ModifierKey | undefined => { const eventMatch = (e: React.KeyboardEvent, definition: KeyEvent | React.ModifierKey[]) => { const eventKey = e.key const eventModifier = getModifier(e) - console.log(eventKey, eventModifier) if (Array.isArray(definition)) return eventModifier ? definition.includes(eventModifier) : false const { key, modifier } = definition - console.log(definition) return ( eventKey === key && diff --git a/src/types.ts b/src/types.ts index 4b46acf4..4ff46141 100644 --- a/src/types.ts +++ b/src/types.ts @@ -217,7 +217,8 @@ interface BaseNodeProps { customNodeDefinitions: CustomNodeDefinition[] customButtons: CustomButtonDefinition[] errorMessageTimeout: number - handleKeyPress: ( + keyboardControls: KeyboardControlsFull + handleKeyboard: ( e: React.KeyboardEvent, eventMap: Partial void>> ) => void @@ -295,7 +296,7 @@ export interface InputProps { showStringQuotes: boolean nodeData: NodeData translate: TranslateFunction - handleKeyPress: ( + handleKeyboard: ( e: React.KeyboardEvent, eventMap: Partial void>> ) => void From ed7af871be71aab2a0f4bd44c9045ea4750e018c Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sat, 14 Dec 2024 00:03:02 +1300 Subject: [PATCH 04/14] Improvements --- demo/src/App.tsx | 13 ++++++++----- src/helpers.ts | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index b64bae58..69436bbf 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -385,11 +385,14 @@ function App() { // ]} onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} - // keyboardControls={{ - // cancel: 'Tab', - // objectConfirm: { key: 'Enter', modifier: 'Shift' }, - // stringConfirm: 'Backspace', - // }} + keyboardControls={{ + cancel: 'Tab', + // confirm: 'Backspace', + objectConfirm: { key: 'Enter', modifier: 'Shift' }, + // stringConfirm: { key: 'Monkey', modifier: 'Shift' }, + clipboardModifier: ['Alt', 'Shift'], + collapseModifier: 'Meta', + }} /> diff --git a/src/helpers.ts b/src/helpers.ts index 40a5abc1..e256c144 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -129,7 +129,9 @@ export const toPathString = (path: Array) => * KEYBOARD INTERACTION */ -// A general keyboard handler function +// A general keyboard handler. Matches keyboard events against the predefined +// keyboard controls (defaults, or user-defined), and maps them to specific +// actions, provided via the "eventMap" export const handleKeyPress = ( controls: KeyboardControlsFull, eventMap: Partial void>>, @@ -182,9 +184,9 @@ const defaultKeyboardControls: KeyboardControlsFull = { stringConfirm: ENTER, stringLineBreak: { ...ENTER, modifier: ['Shift'] }, numberConfirm: ENTER, - booleanConfirm: ENTER, numberUp: { key: 'ArrowUp' }, numberDown: { key: 'ArrowDown' }, + booleanConfirm: ENTER, clipboardModifier: ['Meta', 'Control'], collapseModifier: ['Alt'], } @@ -204,6 +206,18 @@ export const getFullKeyboardControlMap = (userControls: KeyboardControls): Keybo })() as KeyEvent & React.ModifierKey[] controls[typedKey] = definition + + // Set value node defaults to generic "confirm" if not specifically + // defined. + const fallbackKeys: Array = [ + 'stringConfirm', + 'numberConfirm', + 'booleanConfirm', + ] + fallbackKeys.forEach((key) => { + if (!userControls[key] && userControls.confirm) + controls[key] = controls.confirm as KeyEvent & React.ModifierKey[] + }) } } From dad1d8bcea6b90cf690161ac8ab942c631a8e44a Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 00:14:26 +1300 Subject: [PATCH 05/14] Suppress linebreak logic when key control is default --- demo/src/App.tsx | 3 ++- src/ValueNodes.tsx | 23 ++++++++++++++++++++++- src/helpers.ts | 24 ++++++++++++++++++++---- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 69436bbf..ad2d5227 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -386,9 +386,10 @@ function App() { onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} keyboardControls={{ - cancel: 'Tab', + // cancel: 'Tab', // confirm: 'Backspace', objectConfirm: { key: 'Enter', modifier: 'Shift' }, + stringLineBreak: { key: 'Tab' }, // stringConfirm: { key: 'Monkey', modifier: 'Shift' }, clipboardModifier: ['Alt', 'Shift'], collapseModifier: 'Meta', diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index bc504c01..98195bfa 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -32,7 +32,28 @@ export const StringValue: React.FC = ({ value={value} setValue={setValue as React.Dispatch>} isEditing={isEditing} - handleKeyPress={(e) => handleKeyboard(e, { stringConfirm: handleEdit, cancel: handleCancel })} + handleKeyPress={(e) => { + handleKeyboard(e, { + stringConfirm: handleEdit, + cancel: handleCancel, + stringLineBreak: () => { + const textArea = document.getElementById( + `${pathString}_textarea` + ) as HTMLTextAreaElement + if (textArea) { + // Simulates standard text-area line break behaviour. Only + // required when control key is not the default (Shift-Enter) + const startPos: number = textArea?.selectionStart ?? Infinity + const endPos: number = textArea?.selectionEnd ?? Infinity + const strStart = value.slice(0, startPos) + const strEnd = value.slice(endPos) + ;(e.target as HTMLInputElement).value = strStart + '\n' + strEnd + textArea.setSelectionRange(startPos + 1, startPos + 1) + setValue(strStart + '\n' + strEnd) + } + }, + }) + }} styles={getStyles('input', nodeData)} /> ) : ( diff --git a/src/helpers.ts b/src/helpers.ts index e256c144..4d2a5bd0 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -140,7 +140,7 @@ export const handleKeyPress = ( const definitions = Object.entries(eventMap) for (const [definition, action] of definitions) { - if (eventMatch(e, controls[definition as keyof KeyboardControlsFull])) { + if (eventMatch(e, controls[definition as keyof KeyboardControlsFull], definition)) { e.preventDefault() action() break @@ -161,11 +161,27 @@ export const getModifier = ( } // Determines whether a keyboard event matches a predefined value -const eventMatch = (e: React.KeyboardEvent, definition: KeyEvent | React.ModifierKey[]) => { +const eventMatch = ( + e: React.KeyboardEvent, + keyEvent: KeyEvent | React.ModifierKey[], + definition: string +) => { const eventKey = e.key const eventModifier = getModifier(e) - if (Array.isArray(definition)) return eventModifier ? definition.includes(eventModifier) : false - const { key, modifier } = definition + if (Array.isArray(keyEvent)) return eventModifier ? keyEvent.includes(eventModifier) : false + const { key, modifier } = keyEvent + + if ( + // If the stringLineBreak control is the default (Shift-Enter), don't do + // anything, just let normal text-area behaviour occur. This allows normal + // "Undo" behaviour for the text area to continue as normal + definition === 'stringLineBreak' && + eventKey === 'Enter' && + eventModifier === 'Shift' && + key === 'Enter' && + modifier?.includes('Shift') + ) + return false return ( eventKey === key && From e29edbe23072e46721103223b9ab277a92ffc243 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 00:24:55 +1300 Subject: [PATCH 06/14] Start docs --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index dba34177..3a1fdc99 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS - [Active hyperlinks](#active-hyperlinks) - [Custom Collection nodes](#custom-collection-nodes) - [Custom Text](#custom-text) +- [Keyboard customisation](#keyboard-customisation) - [Undo functionality](#undo-functionality) - [Exported helpers](#exported-helpers) - [Functions \& Components](#functions--components) @@ -148,6 +149,7 @@ The only *required* value is `data` (although you will need to provide a `setDat | `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. | | `errorMessageTimeout` | `number` | `2500` | Time (in milliseconds) to display the error message in the UI. | | +| `keyboardControls` | `KeyboardControls` | As explained [above](#usage) | Override some or all of the keyboard controls. See [Keyboard customisation](#keyboard-customisation) for details. | | ## Managing state @@ -655,6 +657,10 @@ customText = { } ``` +## Keyboard customisation + + + ## Undo functionality @@ -709,6 +715,7 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s ## Changelog +- **1.18.0**: Ability to customise keyboard controls - **1.17.0**: `defaultValue` function takes the new `key` as second parameter - **1.16.0**: Extend the "click" zone for collapsing nodes to the header bar and left margin (not just the collapse icon) - **1.15.12**: From d41cdf3c5836003827d73fb795b1eba05f2ba139 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:25:16 +1300 Subject: [PATCH 07/14] Add docs --- README.md | 29 ++++++++++++++++++++++++++++- demo/src/App.tsx | 2 +- src/helpers.ts | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3a1fdc99..a12a7046 100644 --- a/README.md +++ b/README.md @@ -659,8 +659,35 @@ customText = { ## Keyboard customisation - +The default keyboard controls are [outlined above](#usage), but it's possible to customise/override these. Just pass in a `keyboardControls` prop with the actions you wish to override defined. The default config object is: +```ts +{ + confirm: 'Enter', // default for all Value nodes, and key entry + cancel: 'Escape', + objectConfirm: { key: 'Enter', modifier: ['Meta', 'Shift', 'Control'] }, + objectLineBreak: 'Enter', + stringConfirm: 'Enter', + stringLineBreak: { key: 'Enter', modifier: 'Shift' }, + numberConfirm: 'Enter', + numberUp: 'ArrowUp', + numberDown: 'ArrowDown', + booleanConfirm: 'Enter', + clipboardModifier: ['Meta', 'Control'], + collapseModifier: 'Alt', +} +``` + +If (for example), you just wish to change the general "confirmation" action to "Cmd-Enter" (on Mac), or "Ctrl-Enter", you'd just pass in: +```ts + keyboardControls = {confirm: "Enter", modifier: [ "Meta", "Control" ]} +``` +- Key names come from [this list](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) +- Accepted modifiers are "Meta", "Control", "Alt", "Shift" +- On Mac, "Meta" refers to the "Cmd" key, and "Alt" refers to "Option" +- 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`). ## Undo functionality diff --git a/demo/src/App.tsx b/demo/src/App.tsx index ad2d5227..9dc16339 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -392,7 +392,7 @@ function App() { stringLineBreak: { key: 'Tab' }, // stringConfirm: { key: 'Monkey', modifier: 'Shift' }, clipboardModifier: ['Alt', 'Shift'], - collapseModifier: 'Meta', + collapseModifier: 'Control', }} /> diff --git a/src/helpers.ts b/src/helpers.ts index 4d2a5bd0..47764b30 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -193,7 +193,7 @@ const eventMatch = ( const ENTER = { key: 'Enter' } const defaultKeyboardControls: KeyboardControlsFull = { - confirm: ENTER, + confirm: ENTER, // default for all Value nodes, and key entry cancel: { key: 'Escape' }, objectConfirm: { ...ENTER, modifier: ['Meta', 'Shift', 'Control'] }, objectLineBreak: ENTER, From b166bbe3822fd2ee736be72b2544efbab64530a1 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:31:37 +1300 Subject: [PATCH 08/14] Update README.md --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a12a7046..c24353b0 100644 --- a/README.md +++ b/README.md @@ -679,9 +679,16 @@ The default keyboard controls are [outlined above](#usage), but it's possible to If (for example), you just wish to change the general "confirmation" action to "Cmd-Enter" (on Mac), or "Ctrl-Enter", you'd just pass in: ```ts - keyboardControls = {confirm: "Enter", modifier: [ "Meta", "Control" ]} + keyboardControls = { + confirm: { + key: "Enter", + modifier: [ "Meta", "Control" ] + } + } ``` +**Considerations**: + - Key names come from [this list](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values) - Accepted modifiers are "Meta", "Control", "Alt", "Shift" - On Mac, "Meta" refers to the "Cmd" key, and "Alt" refers to "Option" From bb9594c5f2f1fdcb5f56053c6cae6b391ab80a1c Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:43:08 +1300 Subject: [PATCH 09/14] Update App.tsx --- demo/src/App.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 9dc16339..fd14189c 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -385,15 +385,16 @@ function App() { // ]} onChange={dataDefinition?.onChange ?? undefined} jsonParse={JSON5.parse} - keyboardControls={{ - // cancel: 'Tab', - // confirm: 'Backspace', - objectConfirm: { key: 'Enter', modifier: 'Shift' }, - stringLineBreak: { key: 'Tab' }, - // stringConfirm: { key: 'Monkey', modifier: 'Shift' }, - clipboardModifier: ['Alt', 'Shift'], - collapseModifier: 'Control', - }} + // keyboardControls={{ + // cancel: 'Tab', + // confirm: { key: 'Enter', modifier: 'Meta' }, + // objectConfirm: { key: 'Enter', modifier: 'Shift' }, + // stringLineBreak: { key: 'Enter' }, + // stringConfirm: { key: 'Enter', modifier: 'Meta' }, + // clipboardModifier: ['Alt', 'Shift'], + // collapseModifier: 'Control', + // booleanConfirm: 'Enter', + // }} /> From fc989533030c3068e999a52dc2e3c1481efc1806 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:44:20 +1300 Subject: [PATCH 10/14] Update _imports.ts --- demo/src/_imports.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/src/_imports.ts b/demo/src/_imports.ts index c39e3044..951d5913 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 ccf120ba4089be162a0ff63e7c10f458d1e3ec4f Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 13:46:11 +1300 Subject: [PATCH 11/14] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c24353b0..98a01783 100644 --- a/README.md +++ b/README.md @@ -731,6 +731,7 @@ A few helper functions, components and types that might be useful in your own im - `ValueNodeProps`: all props passed internally to "value" nodes (i.e. *not* objects/arrays) - `CustomNodeProps`: all props passed internally to [Custom nodes](#custom-nodes); basically the same as `CollectionNodeProps` with an extra `customNodeProps` field for passing props unique to your component` - `DataType`: `"string"` | `"number"` | `"boolean"` | `"null"` | `"object"` | `"array"` +- `KeyboardControls`: structure for [keyboard customisation](#keyboard-customisation) prop ## Issues, bugs, suggestions? @@ -749,7 +750,7 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s ## Changelog -- **1.18.0**: Ability to customise keyboard controls +- **1.18.0**: Ability to [customise keyboard controls](#keyboard-customisation) - **1.17.0**: `defaultValue` function takes the new `key` as second parameter - **1.16.0**: Extend the "click" zone for collapsing nodes to the header bar and left margin (not just the collapse icon) - **1.15.12**: From bc4dff27b65333a634b55a12363757bbafa27ac9 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:03:56 +1300 Subject: [PATCH 12/14] Improve comment --- src/ValueNodes.tsx | 3 ++- src/types.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index 98195bfa..9338f61c 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -42,7 +42,8 @@ export const StringValue: React.FC = ({ ) as HTMLTextAreaElement if (textArea) { // Simulates standard text-area line break behaviour. Only - // required when control key is not the default (Shift-Enter) + // required when control key is not "standard" text-area + // behaviour ("Shift-Enter" or "Enter") const startPos: number = textArea?.selectionStart ?? Infinity const endPos: number = textArea?.selectionEnd ?? Infinity const strStart = value.slice(0, startPos) diff --git a/src/types.ts b/src/types.ts index 4ff46141..2b39c1d3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -156,7 +156,7 @@ export interface KeyEvent { modifier?: React.ModifierKey | React.ModifierKey[] } export interface KeyboardControls { - confirm?: KeyEvent | string // buttons, and value node default + confirm?: KeyEvent | string // value node defaults, key entry cancel?: KeyEvent | string // all "Cancel" operations objectConfirm?: KeyEvent | string objectLineBreak?: KeyEvent | string From f865196313073dda39a2fe87daf217a6cd1afe5d Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:08:17 +1300 Subject: [PATCH 13/14] v1.18.0-beta1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba392529..9e2b8cee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-edit-react", - "version": "1.17.3", + "version": "1.18.0-beta1", "description": "React component for editing or viewing JSON/object data", "main": "build/index.cjs.js", "module": "build/index.esm.js", From 5d095329387281c9cd03af62a8f46cea44173465 Mon Sep 17 00:00:00 2001 From: Carl Smith <5456533+CarlosNZ@users.noreply.github.com> Date: Sun, 15 Dec 2024 14:13:59 +1300 Subject: [PATCH 14/14] Update demo --- demo/package.json | 4 ++-- demo/yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/package.json b/demo/package.json index e763973f..c254163d 100644 --- a/demo/package.json +++ b/demo/package.json @@ -1,6 +1,6 @@ { "name": "json-edit-react-demo", - "version": "0.1.0", + "version": "1.17.3", "private": true, "homepage": "https://carlosnz.github.io/json-edit-react", "dependencies": { @@ -18,7 +18,7 @@ "ajv": "^8.16.0", "firebase": "^10.13.0", "framer-motion": "^11.0.3", - "json-edit-react": "^1.17.3", + "json-edit-react": "^1.18.0-beta1", "json5": "^2.2.3", "react": "^18.2.0", "react-datepicker": "^5.0.0", diff --git a/demo/yarn.lock b/demo/yarn.lock index 015efa08..a436a298 100644 --- a/demo/yarn.lock +++ b/demo/yarn.lock @@ -8109,10 +8109,10 @@ json-buffer@3.0.1: resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-edit-react@^1.17.3: - version "1.17.3" - resolved "https://registry.yarnpkg.com/json-edit-react/-/json-edit-react-1.17.3.tgz#3300de1230b8f3e5bf51fd4a471c9e3ebc6c9df3" - integrity sha512-XehDsD3oRV1Uwb9ErX0R/DkZWhkpYmtJWsvzU4p9uL1ZvlBKtOMrF50eEh4gLwlDeM2mXD0xGyLsWCQYwtrY/A== +json-edit-react@^1.18.0-beta1: + version "1.18.0-beta1" + resolved "https://registry.yarnpkg.com/json-edit-react/-/json-edit-react-1.18.0-beta1.tgz#a9ba51e0eff458a1221e907d20ac7b38a5877ff4" + integrity sha512-J+ug/e1AnsgN8uDjFppGZtwWszMZL/57u3J65qz3ccsXkcL/V2slSfS5MeswV9LpYDlsJjGtILw1pGDkAs509g== dependencies: object-property-assigner "^1.3.0" object-property-extractor "^1.0.11"