Skip to content

#157 Custom Text Editor #161

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 28 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
eb39408
Tab functionality
CarlosNZ Dec 23, 2024
6fbedeb
Pass common keyboard events from Wrapper
CarlosNZ Dec 23, 2024
5ed4119
TIdy
CarlosNZ Dec 23, 2024
7bd6c17
Merge branch 'main' into 64-tab-key-new
CarlosNZ Dec 23, 2024
c6dcd27
Skip over hidden elements when tabbing
CarlosNZ Dec 23, 2024
ad23ec3
Expand collection if children are editing
CarlosNZ Dec 26, 2024
3431c9a
Fix for key edit
CarlosNZ Dec 26, 2024
99f536b
Merge branch 'main' into 64-tab-key-new
CarlosNZ Jan 23, 2025
89cf321
WIP
CarlosNZ Jan 23, 2025
771f895
Correctly handle hidden elements
CarlosNZ Jan 23, 2025
4c2d4ac
Fix a number of problems
CarlosNZ Jan 24, 2025
f3bb4df
Generalise "sort" function
CarlosNZ Jan 25, 2025
c336722
Update App.tsx
CarlosNZ Jan 25, 2025
d89c0ff
Fix tests
CarlosNZ Jan 25, 2025
48e8883
Update README.md
CarlosNZ Jan 25, 2025
249802f
Handle "Tab" key correctly in JSON editing Text Area
CarlosNZ Jan 25, 2025
05e6f15
Merge branch 'main' into 157-custom-text-editor
CarlosNZ Jan 25, 2025
022a703
Basic implementation
CarlosNZ Jan 27, 2025
201ac96
Merge branch 'main' into 157-custom-text-editor
CarlosNZ Jan 27, 2025
cc7ffeb
Update CollectionNode.tsx
CarlosNZ Jan 27, 2025
2a51269
Lazy load code editor
CarlosNZ Jan 29, 2025
217e830
Merge branch 'main' into 157-custom-text-editor
CarlosNZ Jan 29, 2025
6e3d21d
Add Loading component
CarlosNZ Jan 29, 2025
235eb7f
Update README.md
CarlosNZ Jan 29, 2025
0e13685
Restore nested div to fix button positioning
CarlosNZ Jan 29, 2025
e70a4e2
Add themes to Code Editor
CarlosNZ Jan 29, 2025
a519c8c
Fix layout
CarlosNZ Jan 29, 2025
db71e5e
Update style.css
CarlosNZ Jan 29, 2025
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
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ A [React](https://github.com/facebook/react) component for editing or viewing JS
- [Examples](#examples-1)
- [JSON Schema validation](#json-schema-validation)
- [Drag-n-drop](#drag-n-drop)
- [Full object editing](#full-object-editing)
- [Search/Filtering](#searchfiltering)
- [Themes \& Styles](#themes--styles)
- [Fragments](#fragments)
Expand Down Expand Up @@ -157,13 +158,14 @@ The only *required* value is `data` (although you will need to provide a `setDat
| `customButtons` | `CustomButtonDefinition[]` | `[]` | You can add your own buttons to the Edit Buttons panel if you'd like to be able to perform a custom operation on the data. See [Custom Buttons](#custom-buttons) |
| `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. |
| `TextEditor` | `ReactComponent<TextEditorProps>` | | Pass a component to offer a custom text/code editor when editing full JSON object as text. [See details](#full-object-editing) |
| `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. | |
| `insertAtTop` | `boolean\| "object \| "array"` | `false` | If `true`, inserts new values at the *top* rather than bottom. Can set the behaviour just for arrays or objects by setting to `"object"` or `"array"` respectively. | |

## Managing state

It is recommended that you manage the `data` state yourself outside this component -- just pass in a `setData` method, which is called internally to update your `data`. However, this is not compulsory -- if you don't provide a `setData` method, the data will be managed internally, which would be fine if you're not doing anything with the data. The alternative is to use the [Update functions](#update-functions) to update your `data` externally, but this is not recommended except in special circumstances as you can run into issues keeping your data in sync with the internal state (which is what is displayed), as well as unnecessary re-renders. Update functions should be ideally be used only for implementing side effects, checking for errors, or mutating the data before setting it with `setData`.
It is recommended that you manage the `data` state yourself outside this component -- just pass in a `setData` method, which is called internally to update your `data`. However, this is not compulsory -- if you don't provide a `setData` method, the data will be managed internally, which would be fine if you're not doing anything with the data. The alternative is to use the [Update functions](#update-functions) to update your `data` externally, but this is not recommended except in special circumstances as you can run into issues keeping your data in sync with the internal state (which is what is displayed), as well as unnecessary re-renders. Update functions should be ideally be used only for implementing side effects (e.g. notifications), validation, or mutating the data before setting it with `setData`.

## Update functions

Expand Down Expand Up @@ -396,6 +398,24 @@ The `restrictDrag` property controls which items (if any) can be dragged into ne
- To be draggable, the node must *also* be delete-able (via the `restrictDelete` prop), as dragging a node to a new destination is essentially just deleting it and adding it back elsewhere.
- Similarly, the destination collection must be editable in order to drop it in there. This means that, if you've gone to the trouble of configuring restrictive editing constraints using Filter functions, you can be confident that they can't be circumvented via drag-n-drop.

## Full object editing

The user can edit the entire JSON object (or a sub-node) as raw text (provided you haven't restricted it using a [`restrictEdit` function](#filter-functions)). By default, we just display a native HTML [textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) element for plain-text editing. However, you can offer a more sophisticated text/code editor by passing the component into the `TextEditor` prop. Your component must provide the following props for json-edit-react to use:

- `value: string` // the current text
- `onChange: (value: string) => void` // should be called on every keystroke to update `value`
- `onKeyDown: (e: React.KeyboardEvent) => void` // should be called on every keystroke to detect "Accept"/"Cancel" keys

You can see an example in the [demo](https://carlosnz.github.io/json-edit-react/) where I have implemented [**CodeMirror**](https://codemirror.net/) when the "Custom Text Editor" option is checked. It changes the native editor (on the left) into the one shown on the right:

<img height="350" alt="Plain text editor" src="image/text_edit-normal.png">
<img height="350" alt="Plain text editor" src="image/text_edit-new.png">

See the codebase for the exact implementation details:

- [Simple component that wraps CodeMirror](https://github.com/CarlosNZ/json-edit-react/blob/157-custom-text-editor/demo/src/CodeEditor.tsx)
- [Prop passed to json-edit-react](https://github.com/CarlosNZ/json-edit-react/blob/6e3d21d20750b4a6519eea1f472be9a2a41b8a7c/demo/src/App.tsx#L441-L454)

## Search/Filtering

The displayed data can be filtered based on search input from a user. The user input should be captured independently (we don't provide a UI here) and passed in with the `searchText` prop. This input is debounced internally (time can be set with the `searchDebounceTime` prop), so no need for that as well. The values that the `searchText` are tested against is specified with the `searchFilter` prop. By default (no `searchFilter` defined), it will match against the data *values* (with case-insensitive partial matching -- i.e. input "Ilb", will match value "Bilbo").
Expand Down
6 changes: 6 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"dependencies": {
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.2",
"@codemirror/lang-json": "^6.0.1",
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@testing-library/jest-dom": "^6.3.0",
Expand All @@ -15,6 +16,11 @@
"@types/node": "^20.11.6",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@uiw/codemirror-theme-console": "^4.23.7",
"@uiw/codemirror-theme-github": "^4.23.7",
"@uiw/codemirror-theme-monokai": "^4.23.7",
"@uiw/codemirror-theme-quietlight": "^4.23.7",
"@uiw/react-codemirror": "^4.23.7",
"ajv": "^8.16.0",
"firebase": "^10.13.0",
"framer-motion": "^11.0.3",
Expand Down
41 changes: 39 additions & 2 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react'
import React, { useEffect, useRef, lazy, Suspense } from 'react'
import { useSearch, useLocation } from 'wouter'
import JSON5 from 'json5'
import 'react-datepicker/dist/react-datepicker.css'
Expand Down Expand Up @@ -45,14 +45,17 @@ import {
NumberIncrementStepper,
NumberDecrementStepper,
useToast,
Tooltip,
} from '@chakra-ui/react'
import logo from './image/logo_400.png'
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons'
import { ArrowBackIcon, ArrowForwardIcon, InfoIcon } from '@chakra-ui/icons'
import { demoDataDefinitions } from './demoData'
import { useDatabase } from './useDatabase'
import './style.css'
import { version } from './version'

const CodeEditor = lazy(() => import('./CodeEditor'))

interface AppState {
rootName: string
indent: number
Expand All @@ -69,6 +72,7 @@ interface AppState {
showStringQuotes: boolean
defaultNewValue: string
searchText: string
customTextEditor: boolean
}

const themes = [
Expand Down Expand Up @@ -104,6 +108,7 @@ function App() {
showStringQuotes: true,
defaultNewValue: 'New data!',
searchText: '',
customTextEditor: false,
})

const [isSaving, setIsSaving] = useState(false)
Expand Down Expand Up @@ -144,6 +149,7 @@ function App() {
allowEdit,
allowDelete,
allowAdd,
customTextEditor,
} = state

const restrictEdit: FilterFunction | boolean = (() => {
Expand Down Expand Up @@ -178,6 +184,7 @@ function App() {
searchText: '',
collapseLevel: newDataDefinition.collapse ?? state.collapseLevel,
rootName: newDataDefinition.rootName ?? 'data',
customTextEditor: false,
})

switch (selected) {
Expand Down Expand Up @@ -431,6 +438,21 @@ function App() {
// }}
// insertAtBeginning="object"
// rootFontSize={20}
TextEditor={
customTextEditor
? (props) => (
<Suspense
fallback={
<div className="loading" style={{ height: `${getLineHeight(data)}lh` }}>
Loading code editor...
</div>
}
>
<CodeEditor {...props} theme={theme?.displayName ?? ''} />
</Suspense>
)
: undefined
}
/>
</Box>
<VStack w="100%" align="flex-end" gap={4}>
Expand Down Expand Up @@ -666,6 +688,19 @@ function App() {
>
Sort Object keys
</Checkbox>
<HStack>
<Checkbox
id="customEditorCheckbox"
isChecked={customTextEditor}
onChange={() => toggleState('customTextEditor')}
disabled={!dataDefinition.customTextEditorAvailable}
>
Custom Text Editor
</Checkbox>
<Tooltip label="When in full JSON object edit">
<InfoIcon color="primaryScheme.500" />
</Tooltip>
</HStack>
</Flex>
<HStack className="inputRow" pt={2}>
<FormLabel className="labelWidth" textAlign="right">
Expand Down Expand Up @@ -700,3 +735,5 @@ export default App

export const truncate = (string: string, length = 200) =>
string.length < length ? string : `${string.slice(0, length - 2).trim()}...`

const getLineHeight = (data: JsonData) => JSON.stringify(data, null, 2).split('\n').length
39 changes: 39 additions & 0 deletions demo/src/CodeEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react'
import CodeMirror from '@uiw/react-codemirror'
import { json } from '@codemirror/lang-json'
import { TextEditorProps } from './_imports'
import { githubLight, githubDark } from '@uiw/codemirror-theme-github'
import { consoleDark } from '@uiw/codemirror-theme-console/dark'
import { consoleLight } from '@uiw/codemirror-theme-console/light'
import { quietlight } from '@uiw/codemirror-theme-quietlight'
import { monokai } from '@uiw/codemirror-theme-monokai'

const themeMap = {
Default: undefined,
'Github Light': githubLight,
'Github Dark': githubDark,
'White & Black': consoleLight,
'Black & White': consoleDark,
'Candy Wrapper': quietlight,
Psychedelic: monokai,
}

const CodeEditor: React.FC<TextEditorProps & { theme: string }> = ({
value,
onChange,
onKeyDown,
theme,
}) => {
return (
<CodeMirror
theme={themeMap?.[theme]}
value={value}
width="100%"
extensions={[json()]}
onChange={onChange}
onKeyDown={onKeyDown}
/>
)
}

export default CodeEditor
4 changes: 2 additions & 2 deletions demo/src/_imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 6 additions & 0 deletions demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface DemoData {
customNodeDefinitions?: CustomNodeDefinition[]
customTextDefinitions?: CustomTextDefinitions
styles?: Partial<ThemeStyles>
customTextEditorAvailable?: boolean
}

export const demoDataDefinitions: Record<string, DemoData> = {
Expand Down Expand Up @@ -91,6 +92,8 @@ export const demoDataDefinitions: Record<string, DemoData> = {
collapse: 2,
data: data.intro,
customNodeDefinitions: [dateNodeDefinition],
// restrictEdit: ({ key }) => key === 'number',
customTextEditorAvailable: true,
},
starWars: {
name: '🚀 Star Wars',
Expand Down Expand Up @@ -279,6 +282,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
return 'JSON Schema error'
}
},
customTextEditorAvailable: true,
},
liveData: {
name: '📖 Live Data (from database)',
Expand Down Expand Up @@ -440,6 +444,7 @@ export const demoDataDefinitions: Record<string, DemoData> = {
searchFilter: 'key',
searchPlaceholder: 'Search Theme keys',
data: {},
customTextEditorAvailable: true,
},
customNodes: {
name: '🔧 Custom Nodes',
Expand Down Expand Up @@ -625,5 +630,6 @@ export const demoDataDefinitions: Record<string, DemoData> = {
styles: {
string: ({ key }) => (key === 'name' ? { fontWeight: 'bold', fontSize: '120%' } : null),
},
customTextEditorAvailable: true,
},
}
19 changes: 19 additions & 0 deletions demo/src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,22 @@ footer {
font-weight: 600;
color: dimgray;
}

/* For CodeMirror */
.cm-theme-light,
.cm-theme {
width: 100%;
}

.cm-content,
.cm-gutters {
font-size: 80%;
}

/* Loading component for CodeMirror */
.loading {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
Loading