Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -655,6 +657,44 @@ 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: {
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"
- 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

Expand Down Expand Up @@ -691,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?

Expand All @@ -709,6 +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](#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**:
Expand Down
4 changes: 2 additions & 2 deletions demo/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,16 @@ function App() {
// ]}
onChange={dataDefinition?.onChange ?? undefined}
jsonParse={JSON5.parse}
// 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',
// }}
/>
</Box>
<VStack w="100%" align="flex-end" gap={4}>
Expand Down
8 changes: 4 additions & 4 deletions demo/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8109,10 +8109,10 @@ [email protected]:
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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
47 changes: 30 additions & 17 deletions src/ButtonPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
type CopyType,
type NodeData,
type CustomButtonDefinition,
type KeyboardControlsFull,
} from './types'
import { getModifier } from './helpers'

interface EditButtonProps {
startEdit?: () => void
Expand All @@ -20,6 +22,11 @@ interface EditButtonProps {
nodeData: NodeData
translate: TranslateFunction
customButtons: CustomButtonDefinition[]
keyboardControls: KeyboardControlsFull
handleKeyboard: (
e: React.KeyboardEvent,
eventMap: Partial<Record<keyof KeyboardControlsFull, () => void>>
) => void
}

export const EditButtons: React.FC<EditButtonProps> = ({
Expand All @@ -31,6 +38,8 @@ export const EditButtons: React.FC<EditButtonProps> = ({
customButtons,
nodeData,
translate,
keyboardControls,
handleKeyboard,
}) => {
const { getStyles } = useTheme()
const NEW_KEY_PROMPT = translate('KEY_NEW', nodeData)
Expand All @@ -40,14 +49,19 @@ export const EditButtons: React.FC<EditButtonProps> = ({
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<HTMLElement>) => {
Expand All @@ -56,15 +70,14 @@ export const EditButtons: React.FC<EditButtonProps> = ({
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)
}
Expand Down
34 changes: 20 additions & 14 deletions src/CollectionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -44,6 +44,8 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
customNodeDefinitions,
jsonParse,
jsonStringify,
keyboardControls,
handleKeyboard,
} = props
const [stringifiedValue, setStringifiedValue] = useState(jsonStringify(data))

Expand Down Expand Up @@ -126,13 +128,15 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (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 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
Expand Down Expand Up @@ -163,11 +167,6 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (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)
Expand Down Expand Up @@ -276,7 +275,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
value={stringifiedValue}
setValue={setStringifiedValue}
isEditing={isEditing}
handleKeyPress={handleKeyPress}
handleKeyPress={handleKeyPressEdit}
styles={getStyles('input', nodeData)}
/>
<div className="jer-collection-input-button-row">
Expand All @@ -302,7 +301,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
setValue: async (val: unknown) => await onEdit(val, path),
handleEdit,
handleCancel,
handleKeyPress,
handleKeyPress: handleKeyPressEdit,
isEditing,
setIsEditing: () => setCurrentlyEditingElement(pathString),
getStyles,
Expand All @@ -325,7 +324,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
defaultValue={name}
autoFocus
onFocus={(e) => e.target.select()}
onKeyDown={handleKeyPressKeyEdit}
onKeyDown={(e) =>
handleKeyboard(e, {
stringConfirm: () => handleEditKey((e.target as HTMLInputElement).value),
cancel: handleCancel,
})
}
style={{ width: `${String(name).length / 1.5 + 0.5}em` }}
/>
) : (
Expand Down Expand Up @@ -364,6 +368,8 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
nodeData={nodeData}
translate={translate}
customButtons={props.customButtons}
keyboardControls={keyboardControls}
handleKeyboard={handleKeyboard}
/>
)

Expand Down
23 changes: 22 additions & 1 deletion src/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isCollection, matchNode, matchNodeKey } from './helpers'
import {
getFullKeyboardControlMap,
handleKeyPress,
isCollection,
matchNode,
matchNodeKey,
} from './helpers'
import {
type CollectionData,
type JsonEditorProps,
Expand All @@ -15,6 +21,7 @@ import {
type UpdateFunction,
type UpdateFunctionProps,
type JsonData,
type KeyboardControls,
} from './types'
import { useTheme, ThemeProvider } from './theme'
import { TreeStateProvider } from './TreeStateProvider'
Expand Down Expand Up @@ -64,6 +71,7 @@ const Editor: React.FC<JsonEditorProps> = ({
jsonParse = JSON.parse,
jsonStringify = (data: JsonData) => JSON.stringify(data, null, 2),
errorMessageTimeout = 2500,
keyboardControls = {},
}) => {
const { getStyles } = useTheme()
const collapseFilter = useCallback(getFilterFunction(collapse), [collapse])
Expand Down Expand Up @@ -248,6 +256,17 @@ const Editor: React.FC<JsonEditorProps> = ({
const restrictDragFilter = useMemo(() => getFilterFunction(restrictDrag), [restrictDrag])
const searchFilter = useMemo(() => getSearchFilter(searchFilterInput), [searchFilterInput])

const fullKeyboardControls = useMemo(
() => getFullKeyboardControlMap(keyboardControls),
[keyboardControls]
)

const handleKeyboardCallback = useCallback(
(e: React.KeyboardEvent, eventMap: Partial<Record<keyof KeyboardControls, () => void>>) =>
handleKeyPress(fullKeyboardControls, eventMap, e),
[keyboardControls]
)

const otherProps = {
name: rootName,
nodeData,
Expand Down Expand Up @@ -283,6 +302,8 @@ const Editor: React.FC<JsonEditorProps> = ({
jsonParse,
jsonStringify,
errorMessageTimeout,
handleKeyboard: handleKeyboardCallback,
keyboardControls: fullKeyboardControls,
}

const mainContainerStyles = { ...getStyles('container', nodeData), minWidth, maxWidth }
Expand Down
Loading