Skip to content

Commit bd156df

Browse files
authored
133 Keyboard overrides (#134)
Keyboard overrides
1 parent 10f5c9f commit bd156df

File tree

13 files changed

+350
-84
lines changed

13 files changed

+350
-84
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
5353
- [Active hyperlinks](#active-hyperlinks)
5454
- [Custom Collection nodes](#custom-collection-nodes)
5555
- [Custom Text](#custom-text)
56+
- [Keyboard customisation](#keyboard-customisation)
5657
- [Undo functionality](#undo-functionality)
5758
- [Exported helpers](#exported-helpers)
5859
- [Functions \& Components](#functions--components)
@@ -148,6 +149,7 @@ The only *required* value is `data` (although you will need to provide a `setDat
148149
| `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/) |
149150
| `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. |
150151
| `errorMessageTimeout` | `number` | `2500` | Time (in milliseconds) to display the error message in the UI. | |
152+
| `keyboardControls` | `KeyboardControls` | As explained [above](#usage) | Override some or all of the keyboard controls. See [Keyboard customisation](#keyboard-customisation) for details. | |
151153

152154
## Managing state
153155

@@ -655,6 +657,44 @@ customText = {
655657
}
656658
```
657659
660+
## Keyboard customisation
661+
662+
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:
663+
```ts
664+
{
665+
confirm: 'Enter', // default for all Value nodes, and key entry
666+
cancel: 'Escape',
667+
objectConfirm: { key: 'Enter', modifier: ['Meta', 'Shift', 'Control'] },
668+
objectLineBreak: 'Enter',
669+
stringConfirm: 'Enter',
670+
stringLineBreak: { key: 'Enter', modifier: 'Shift' },
671+
numberConfirm: 'Enter',
672+
numberUp: 'ArrowUp',
673+
numberDown: 'ArrowDown',
674+
booleanConfirm: 'Enter',
675+
clipboardModifier: ['Meta', 'Control'],
676+
collapseModifier: 'Alt',
677+
}
678+
```
679+
680+
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:
681+
```ts
682+
keyboardControls = {
683+
confirm: {
684+
key: "Enter",
685+
modifier: [ "Meta", "Control" ]
686+
}
687+
}
688+
```
689+
690+
**Considerations**:
691+
692+
- Key names come from [this list](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values)
693+
- Accepted modifiers are "Meta", "Control", "Alt", "Shift"
694+
- On Mac, "Meta" refers to the "Cmd" key, and "Alt" refers to "Option"
695+
- If multiple modifiers are specified (in an array), *any* of them will be accepted (multi-modifier commands not currently supported)
696+
- You only need to specify values for `stringConfirm`, `numberConfirm`, and `booleanConfirm` if they should *differ* from your `confirm` value.
697+
- 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`).
658698
659699
## Undo functionality
660700
@@ -691,6 +731,7 @@ A few helper functions, components and types that might be useful in your own im
691731
- `ValueNodeProps`: all props passed internally to "value" nodes (i.e. *not* objects/arrays)
692732
- `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`
693733
- `DataType`: `"string"` | `"number"` | `"boolean"` | `"null"` | `"object"` | `"array"`
734+
- `KeyboardControls`: structure for [keyboard customisation](#keyboard-customisation) prop
694735
695736
## Issues, bugs, suggestions?
696737
@@ -709,6 +750,7 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s
709750
710751
## Changelog
711752
753+
- **1.18.0**: Ability to [customise keyboard controls](#keyboard-customisation)
712754
- **1.17.0**: `defaultValue` function takes the new `key` as second parameter
713755
- **1.16.0**: Extend the "click" zone for collapsing nodes to the header bar and left margin (not just the collapse icon)
714756
- **1.15.12**:

demo/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-edit-react-demo",
3-
"version": "0.1.0",
3+
"version": "1.17.3",
44
"private": true,
55
"homepage": "https://carlosnz.github.io/json-edit-react",
66
"dependencies": {
@@ -18,7 +18,7 @@
1818
"ajv": "^8.16.0",
1919
"firebase": "^10.13.0",
2020
"framer-motion": "^11.0.3",
21-
"json-edit-react": "^1.17.3",
21+
"json-edit-react": "^1.18.0-beta1",
2222
"json5": "^2.2.3",
2323
"react": "^18.2.0",
2424
"react-datepicker": "^5.0.0",

demo/src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,16 @@ function App() {
385385
// ]}
386386
onChange={dataDefinition?.onChange ?? undefined}
387387
jsonParse={JSON5.parse}
388+
// keyboardControls={{
389+
// cancel: 'Tab',
390+
// confirm: { key: 'Enter', modifier: 'Meta' },
391+
// objectConfirm: { key: 'Enter', modifier: 'Shift' },
392+
// stringLineBreak: { key: 'Enter' },
393+
// stringConfirm: { key: 'Enter', modifier: 'Meta' },
394+
// clipboardModifier: ['Alt', 'Shift'],
395+
// collapseModifier: 'Control',
396+
// booleanConfirm: 'Enter',
397+
// }}
388398
/>
389399
</Box>
390400
<VStack w="100%" align="flex-end" gap={4}>

demo/yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8109,10 +8109,10 @@ json-buffer@3.0.1:
81098109
resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13"
81108110
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
81118111

8112-
json-edit-react@^1.17.3:
8113-
version "1.17.3"
8114-
resolved "https://registry.yarnpkg.com/json-edit-react/-/json-edit-react-1.17.3.tgz#3300de1230b8f3e5bf51fd4a471c9e3ebc6c9df3"
8115-
integrity sha512-XehDsD3oRV1Uwb9ErX0R/DkZWhkpYmtJWsvzU4p9uL1ZvlBKtOMrF50eEh4gLwlDeM2mXD0xGyLsWCQYwtrY/A==
8112+
json-edit-react@^1.18.0-beta1:
8113+
version "1.18.0-beta1"
8114+
resolved "https://registry.yarnpkg.com/json-edit-react/-/json-edit-react-1.18.0-beta1.tgz#a9ba51e0eff458a1221e907d20ac7b38a5877ff4"
8115+
integrity sha512-J+ug/e1AnsgN8uDjFppGZtwWszMZL/57u3J65qz3ccsXkcL/V2slSfS5MeswV9LpYDlsJjGtILw1pGDkAs509g==
81168116
dependencies:
81178117
object-property-assigner "^1.3.0"
81188118
object-property-extractor "^1.0.11"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-edit-react",
3-
"version": "1.17.3",
3+
"version": "1.18.0-beta1",
44
"description": "React component for editing or viewing JSON/object data",
55
"main": "build/index.cjs.js",
66
"module": "build/index.esm.js",

src/ButtonPanels.tsx

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
type CopyType,
1010
type NodeData,
1111
type CustomButtonDefinition,
12+
type KeyboardControlsFull,
1213
} from './types'
14+
import { getModifier } from './helpers'
1315

1416
interface EditButtonProps {
1517
startEdit?: () => void
@@ -20,6 +22,11 @@ interface EditButtonProps {
2022
nodeData: NodeData
2123
translate: TranslateFunction
2224
customButtons: CustomButtonDefinition[]
25+
keyboardControls: KeyboardControlsFull
26+
handleKeyboard: (
27+
e: React.KeyboardEvent,
28+
eventMap: Partial<Record<keyof KeyboardControlsFull, () => void>>
29+
) => void
2330
}
2431

2532
export const EditButtons: React.FC<EditButtonProps> = ({
@@ -31,6 +38,8 @@ export const EditButtons: React.FC<EditButtonProps> = ({
3138
customButtons,
3239
nodeData,
3340
translate,
41+
keyboardControls,
42+
handleKeyboard,
3443
}) => {
3544
const { getStyles } = useTheme()
3645
const NEW_KEY_PROMPT = translate('KEY_NEW', nodeData)
@@ -40,14 +49,19 @@ export const EditButtons: React.FC<EditButtonProps> = ({
4049
const { key, path, value: data } = nodeData
4150

4251
const handleKeyPress = (e: React.KeyboardEvent) => {
43-
if (e.key === 'Enter' && handleAdd) {
44-
setIsAdding(false)
45-
handleAdd(newKey)
46-
setNewKey(NEW_KEY_PROMPT)
47-
} else if (e.key === 'Escape') {
48-
setIsAdding(false)
49-
setNewKey(NEW_KEY_PROMPT)
50-
}
52+
handleKeyboard(e, {
53+
stringConfirm: () => {
54+
if (handleAdd) {
55+
setIsAdding(false)
56+
handleAdd(newKey)
57+
setNewKey(NEW_KEY_PROMPT)
58+
}
59+
},
60+
cancel: () => {
61+
setIsAdding(false)
62+
setNewKey(NEW_KEY_PROMPT)
63+
},
64+
})
5165
}
5266

5367
const handleCopy = (e: React.MouseEvent<HTMLElement>) => {
@@ -56,15 +70,14 @@ export const EditButtons: React.FC<EditButtonProps> = ({
5670
let value
5771
let stringValue = ''
5872
if (enableClipboard) {
59-
switch (e.ctrlKey || e.metaKey) {
60-
case true:
61-
value = stringifyPath(path)
62-
stringValue = value
63-
copyType = 'path'
64-
break
65-
default:
66-
value = data
67-
stringValue = type ? JSON.stringify(data, null, 2) : String(value)
73+
const modifier = getModifier(e)
74+
if (modifier && keyboardControls.clipboardModifier.includes(modifier)) {
75+
value = stringifyPath(path)
76+
stringValue = value
77+
copyType = 'path'
78+
} else {
79+
value = data
80+
stringValue = type ? JSON.stringify(data, null, 2) : String(value)
6881
}
6982
void navigator.clipboard.writeText(stringValue)
7083
}

src/CollectionNode.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { EditButtons, InputButtons } from './ButtonPanels'
55
import { getCustomNode } from './CustomNode'
66
import { type CollectionNodeProps, type NodeData, type CollectionData } from './types'
77
import { Icon } from './Icons'
8-
import { filterNode, isCollection } from './helpers'
8+
import { filterNode, getModifier, isCollection } from './helpers'
99
import { AutogrowTextArea } from './AutogrowTextArea'
1010
import { useTheme } from './theme'
1111
import { useTreeState } from './TreeStateProvider'
@@ -44,6 +44,8 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
4444
customNodeDefinitions,
4545
jsonParse,
4646
jsonStringify,
47+
keyboardControls,
48+
handleKeyboard,
4749
} = props
4850
const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data))
4951

@@ -126,13 +128,15 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
126128
const brackets =
127129
collectionType === 'array' ? { open: '[', close: ']' } : { open: '{', close: '}' }
128130

129-
const handleKeyPress = (e: React.KeyboardEvent) => {
130-
if (e.key === 'Enter' && (e.metaKey || e.shiftKey || e.ctrlKey)) handleEdit()
131-
else if (e.key === 'Escape') handleCancel()
132-
}
131+
const handleKeyPressEdit = (e: React.KeyboardEvent) =>
132+
handleKeyboard(e, {
133+
objectConfirm: handleEdit,
134+
cancel: handleCancel,
135+
})
133136

134137
const handleCollapse = (e: React.MouseEvent) => {
135-
if (e.getModifierState('Alt')) {
138+
const modifier = getModifier(e)
139+
if (modifier && keyboardControls.collapseModifier.includes(modifier)) {
136140
hasBeenOpened.current = true
137141
setCollapseState({ collapsed: !collapsed, path })
138142
return
@@ -163,11 +167,6 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
163167
}
164168
}
165169

166-
const handleKeyPressKeyEdit = (e: React.KeyboardEvent) => {
167-
if (e.key === 'Enter') handleEditKey((e.target as HTMLInputElement).value)
168-
else if (e.key === 'Escape') handleCancel()
169-
}
170-
171170
const handleAdd = (key: string) => {
172171
animateCollapse(false)
173172
const newValue = getDefaultNewValue(nodeData, key)
@@ -276,7 +275,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
276275
value={stringifiedValue}
277276
setValue={setStringifiedValue}
278277
isEditing={isEditing}
279-
handleKeyPress={handleKeyPress}
278+
handleKeyPress={handleKeyPressEdit}
280279
styles={getStyles('input', nodeData)}
281280
/>
282281
<div className="jer-collection-input-button-row">
@@ -302,7 +301,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
302301
setValue: async (val: unknown) => await onEdit(val, path),
303302
handleEdit,
304303
handleCancel,
305-
handleKeyPress,
304+
handleKeyPress: handleKeyPressEdit,
306305
isEditing,
307306
setIsEditing: () => setCurrentlyEditingElement(pathString),
308307
getStyles,
@@ -325,7 +324,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
325324
defaultValue={name}
326325
autoFocus
327326
onFocus={(e) => e.target.select()}
328-
onKeyDown={handleKeyPressKeyEdit}
327+
onKeyDown={(e) =>
328+
handleKeyboard(e, {
329+
stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value),
330+
cancel: handleCancel,
331+
})
332+
}
329333
style={{ width: `${String(name).length / 1.5 + 0.5}em` }}
330334
/>
331335
) : (
@@ -364,6 +368,8 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
364368
nodeData={nodeData}
365369
translate={translate}
366370
customButtons={props.customButtons}
371+
keyboardControls={keyboardControls}
372+
handleKeyboard={handleKeyboard}
367373
/>
368374
)
369375

src/JsonEditor.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
22
import assign, { type Input } from 'object-property-assigner'
33
import extract from 'object-property-extractor'
44
import { CollectionNode } from './CollectionNode'
5-
import { isCollection, matchNode, matchNodeKey } from './helpers'
5+
import {
6+
getFullKeyboardControlMap,
7+
handleKeyPress,
8+
isCollection,
9+
matchNode,
10+
matchNodeKey,
11+
} from './helpers'
612
import {
713
type CollectionData,
814
type JsonEditorProps,
@@ -15,6 +21,7 @@ import {
1521
type UpdateFunction,
1622
type UpdateFunctionProps,
1723
type JsonData,
24+
type KeyboardControls,
1825
} from './types'
1926
import { useTheme, ThemeProvider } from './theme'
2027
import { TreeStateProvider } from './TreeStateProvider'
@@ -64,6 +71,7 @@ const Editor: React.FC<JsonEditorProps> = ({
6471
jsonParse = JSON.parse,
6572
jsonStringify = (data: JsonData) => JSON.stringify(data, null, 2),
6673
errorMessageTimeout = 2500,
74+
keyboardControls = {},
6775
}) => {
6876
const { getStyles } = useTheme()
6977
const collapseFilter = useCallback(getFilterFunction(collapse), [collapse])
@@ -248,6 +256,17 @@ const Editor: React.FC<JsonEditorProps> = ({
248256
const restrictDragFilter = useMemo(() => getFilterFunction(restrictDrag), [restrictDrag])
249257
const searchFilter = useMemo(() => getSearchFilter(searchFilterInput), [searchFilterInput])
250258

259+
const fullKeyboardControls = useMemo(
260+
() => getFullKeyboardControlMap(keyboardControls),
261+
[keyboardControls]
262+
)
263+
264+
const handleKeyboardCallback = useCallback(
265+
(e: React.KeyboardEvent, eventMap: Partial<Record<keyof KeyboardControls, () => void>>) =>
266+
handleKeyPress(fullKeyboardControls, eventMap, e),
267+
[keyboardControls]
268+
)
269+
251270
const otherProps = {
252271
name: rootName,
253272
nodeData,
@@ -283,6 +302,8 @@ const Editor: React.FC<JsonEditorProps> = ({
283302
jsonParse,
284303
jsonStringify,
285304
errorMessageTimeout,
305+
handleKeyboard: handleKeyboardCallback,
306+
keyboardControls: fullKeyboardControls,
286307
}
287308

288309
const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth }

0 commit comments

Comments
 (0)