diff --git a/README.md b/README.md index a5074dfb..1ffa0129 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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` | | 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 @@ -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: + +Plain text editor +Plain text editor + +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"). diff --git a/demo/package.json b/demo/package.json index f8dec1af..ece7e65b 100644 --- a/demo/package.json +++ b/demo/package.json @@ -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", @@ -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", diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 1547db26..55af2290 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -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' @@ -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 @@ -69,6 +72,7 @@ interface AppState { showStringQuotes: boolean defaultNewValue: string searchText: string + customTextEditor: boolean } const themes = [ @@ -104,6 +108,7 @@ function App() { showStringQuotes: true, defaultNewValue: 'New data!', searchText: '', + customTextEditor: false, }) const [isSaving, setIsSaving] = useState(false) @@ -144,6 +149,7 @@ function App() { allowEdit, allowDelete, allowAdd, + customTextEditor, } = state const restrictEdit: FilterFunction | boolean = (() => { @@ -178,6 +184,7 @@ function App() { searchText: '', collapseLevel: newDataDefinition.collapse ?? state.collapseLevel, rootName: newDataDefinition.rootName ?? 'data', + customTextEditor: false, }) switch (selected) { @@ -431,6 +438,21 @@ function App() { // }} // insertAtBeginning="object" // rootFontSize={20} + TextEditor={ + customTextEditor + ? (props) => ( + + Loading code editor... + + } + > + + + ) + : undefined + } /> @@ -666,6 +688,19 @@ function App() { > Sort Object keys + + toggleState('customTextEditor')} + disabled={!dataDefinition.customTextEditorAvailable} + > + Custom Text Editor + + + + + @@ -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 diff --git a/demo/src/CodeEditor.tsx b/demo/src/CodeEditor.tsx new file mode 100644 index 00000000..98f83272 --- /dev/null +++ b/demo/src/CodeEditor.tsx @@ -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 = ({ + value, + onChange, + onKeyDown, + theme, +}) => { + return ( + + ) +} + +export default CodeEditor diff --git a/demo/src/_imports.ts b/demo/src/_imports.ts index 295d8b6f..ccb43158 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/demo/src/demoData/dataDefinitions.tsx b/demo/src/demoData/dataDefinitions.tsx index 0aab8748..fe423630 100644 --- a/demo/src/demoData/dataDefinitions.tsx +++ b/demo/src/demoData/dataDefinitions.tsx @@ -55,6 +55,7 @@ export interface DemoData { customNodeDefinitions?: CustomNodeDefinition[] customTextDefinitions?: CustomTextDefinitions styles?: Partial + customTextEditorAvailable?: boolean } export const demoDataDefinitions: Record = { @@ -91,6 +92,8 @@ export const demoDataDefinitions: Record = { collapse: 2, data: data.intro, customNodeDefinitions: [dateNodeDefinition], + // restrictEdit: ({ key }) => key === 'number', + customTextEditorAvailable: true, }, starWars: { name: '🚀 Star Wars', @@ -279,6 +282,7 @@ export const demoDataDefinitions: Record = { return 'JSON Schema error' } }, + customTextEditorAvailable: true, }, liveData: { name: '📖 Live Data (from database)', @@ -440,6 +444,7 @@ export const demoDataDefinitions: Record = { searchFilter: 'key', searchPlaceholder: 'Search Theme keys', data: {}, + customTextEditorAvailable: true, }, customNodes: { name: '🔧 Custom Nodes', @@ -625,5 +630,6 @@ export const demoDataDefinitions: Record = { styles: { string: ({ key }) => (key === 'name' ? { fontWeight: 'bold', fontSize: '120%' } : null), }, + customTextEditorAvailable: true, }, } diff --git a/demo/src/style.css b/demo/src/style.css index 81726876..479345f8 100644 --- a/demo/src/style.css +++ b/demo/src/style.css @@ -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; +} diff --git a/demo/yarn.lock b/demo/yarn.lock index c7fc10ea..7b5f941e 100644 --- a/demo/yarn.lock +++ b/demo/yarn.lock @@ -1168,6 +1168,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.18.6": + version "7.26.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.26.7.tgz#f4e7fe527cd710f8dc0618610b61b4b060c3c341" + integrity sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -2060,6 +2067,90 @@ resolved "https://registry.yarnpkg.com/@chakra-ui/visually-hidden/-/visually-hidden-2.2.0.tgz#9b0ecef8f01263ab808ba3bda7b36a0d91b4d5c1" integrity sha512-KmKDg01SrQ7VbTD3+cPWf/UfpF5MSwm3v7MWi0n5t8HnnadT13MF0MJCDSXbBWnzLv1ZKJ6zlyAOeARWX+DpjQ== +"@codemirror/autocomplete@^6.0.0": + version "6.18.4" + resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.4.tgz#4394f55d6771727179f2e28a871ef46bbbeb11b1" + integrity sha512-sFAphGQIqyQZfP2ZBsSHV7xQvo9Py0rV0dW7W3IMRdS+zDuNb2l3no78CvUaWKGfzFjI4FTrLdUSj86IGb2hRA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.17.0" + "@lezer/common" "^1.0.0" + +"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0": + version "6.8.0" + resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.8.0.tgz#92f200b66f852939bd6ebb90d48c2d9e9c813d64" + integrity sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.4.0" + "@codemirror/view" "^6.27.0" + "@lezer/common" "^1.1.0" + +"@codemirror/lang-json@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@codemirror/lang-json/-/lang-json-6.0.1.tgz#0a0be701a5619c4b0f8991f9b5e95fe33f462330" + integrity sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ== + dependencies: + "@codemirror/language" "^6.0.0" + "@lezer/json" "^1.0.0" + +"@codemirror/language@^6.0.0": + version "6.10.8" + resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.8.tgz#3e3a346a2b0a8cf63ee1cfe03349eb1965dce5f9" + integrity sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.23.0" + "@lezer/common" "^1.1.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + style-mod "^4.0.0" + +"@codemirror/lint@^6.0.0": + version "6.8.4" + resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.4.tgz#7d8aa5d1a6dec89ffcc23ad45ddca2e12e90982d" + integrity sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.35.0" + crelt "^1.0.5" + +"@codemirror/search@^6.0.0": + version "6.5.8" + resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.8.tgz#b59b3659b46184cc75d6108d7c050a4ca344c3a0" + integrity sha512-PoWtZvo7c1XFeZWmmyaOp2G0XVbOnm+fJzvghqGAktBW3cufwJUWvSCcNG0ppXiBEM05mZu6RhMtXPv2hpllig== + dependencies: + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + crelt "^1.0.5" + +"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0": + version "6.5.1" + resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.1.tgz#e5c0599f7b43cf03f19e05861317df5425c07904" + integrity sha512-3rA9lcwciEB47ZevqvD8qgbzhM9qMb8vCcQCNmDfVRPQG4JT9mSb0Jg8H7YjKGGQcFnLN323fj9jdnG59Kx6bg== + dependencies: + "@marijn/find-cluster-break" "^1.0.0" + +"@codemirror/theme-one-dark@^6.0.0": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8" + integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + "@lezer/highlight" "^1.0.0" + +"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0": + version "6.36.2" + resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.2.tgz#aeb644e161440734ac5a153bf6e5b4a4355047be" + integrity sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA== + dependencies: + "@codemirror/state" "^6.5.0" + style-mod "^4.1.0" + w3c-keyname "^2.2.4" + "@csstools/normalize.css@*": version "12.1.1" resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.1.1.tgz#f0ad221b7280f3fc814689786fd9ee092776ef8f" @@ -3076,6 +3167,39 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== +"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd" + integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA== + +"@lezer/highlight@^1.0.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b" + integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA== + dependencies: + "@lezer/common" "^1.0.0" + +"@lezer/json@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@lezer/json/-/json-1.0.3.tgz#e773a012ad0088fbf07ce49cfba875cc9e5bc05f" + integrity sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ== + dependencies: + "@lezer/common" "^1.2.0" + "@lezer/highlight" "^1.0.0" + "@lezer/lr" "^1.0.0" + +"@lezer/lr@^1.0.0": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727" + integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA== + dependencies: + "@lezer/common" "^1.0.0" + +"@marijn/find-cluster-break@^1.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8" + integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" @@ -3858,6 +3982,68 @@ "@typescript-eslint/types" "5.62.0" eslint-visitor-keys "^3.3.0" +"@uiw/codemirror-extensions-basic-setup@4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.7.tgz#8fce5d6190a755c889805d2edc5b85d7f29cd322" + integrity sha512-9/2EUa1Lck4kFKkR2BkxlZPpgD/EWuKHnOlysf1yHKZGraaZmZEaUw+utDK4QcuJc8Iz097vsLz4f4th5EU27g== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/commands" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/search" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + +"@uiw/codemirror-theme-console@^4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-console/-/codemirror-theme-console-4.23.7.tgz#60902d2b5f658fd677cff648174169811740eedb" + integrity sha512-5o23Pxer0DsuSTbzToUw73Y3b9sBI06e7e9c4SeBqI+tiYWdRefYu3M+hZZYi7L67b5bKlakMitCKvDNo3TqyQ== + dependencies: + "@uiw/codemirror-themes" "4.23.7" + +"@uiw/codemirror-theme-github@^4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-github/-/codemirror-theme-github-4.23.7.tgz#a52d4f4677e629956915c4a55d8dc9db496a1290" + integrity sha512-r9SstBZD7Ow1sQ8F0EpsRGx9b11K552M2FayvyLWTkal64YJmQMKW0S2KcWykgCMKLWhmDFi7LX+h8cg6nek8g== + dependencies: + "@uiw/codemirror-themes" "4.23.7" + +"@uiw/codemirror-theme-monokai@^4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-monokai/-/codemirror-theme-monokai-4.23.7.tgz#16c3b6f3b9ee54d466b7c6ec74fd35f470fcf4a0" + integrity sha512-IkflZncpj0rmQCXdDOn3O2wD516Isx12BQA/xkCfMQtgIQ7QgsqKKCPV83MiOiQFizioBNswjvXns7i5jlaJ7g== + dependencies: + "@uiw/codemirror-themes" "4.23.7" + +"@uiw/codemirror-theme-quietlight@^4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-quietlight/-/codemirror-theme-quietlight-4.23.7.tgz#426e1490d98c0c6dac7b6ece20a43e465bb6a2f5" + integrity sha512-C8C2vjk5uTkSvrqGdhwzGJN4a9vNai4YQrrRpJ3MscNi3KwLu4akE67U8kE5ZcThki9aHL9K2NXoP0SWt70Fag== + dependencies: + "@uiw/codemirror-themes" "4.23.7" + +"@uiw/codemirror-themes@4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.23.7.tgz#33d09a2d9df3eda3e3affcb68d91672e41bf646a" + integrity sha512-UNf1XOx1hG9OmJnrtT86PxKcdcwhaNhbrcD+nsk8WxRJ3n5c8nH6euDvgVPdVLPwbizsaQcZTILACgA/FjRpVg== + dependencies: + "@codemirror/language" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + +"@uiw/react-codemirror@^4.23.7": + version "4.23.7" + resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.7.tgz#b7fe2085936c593514f5e238865989bfef65e504" + integrity sha512-Nh/0P6W+kWta+ARp9YpnKPD9ick5teEnwmtNoPQnyd6NPv0EQP3Ui4YmRVNj1nkUEo+QjrAUaEfcejJ2up/HZA== + dependencies: + "@babel/runtime" "^7.18.6" + "@codemirror/commands" "^6.1.0" + "@codemirror/state" "^6.1.1" + "@codemirror/theme-one-dark" "^6.0.0" + "@uiw/codemirror-extensions-basic-setup" "4.23.7" + codemirror "^6.0.0" + "@ungap/structured-clone@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" @@ -4860,6 +5046,19 @@ coa@^2.0.2: chalk "^2.4.1" q "^1.1.2" +codemirror@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29" + integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg== + dependencies: + "@codemirror/autocomplete" "^6.0.0" + "@codemirror/commands" "^6.0.0" + "@codemirror/language" "^6.0.0" + "@codemirror/lint" "^6.0.0" + "@codemirror/search" "^6.0.0" + "@codemirror/state" "^6.0.0" + "@codemirror/view" "^6.0.0" + collect-v8-coverage@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" @@ -5084,6 +5283,11 @@ cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" +crelt@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" + integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -10866,6 +11070,11 @@ style-loader@^3.3.1: resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== +style-mod@^4.0.0, style-mod@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" + integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== + stylehacks@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" @@ -11500,6 +11709,11 @@ w3c-hr-time@^1.0.2: dependencies: browser-process-hrtime "^1.0.0" +w3c-keyname@^2.2.4: + version "2.2.8" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" + integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== + w3c-xmlserializer@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz#3e7104a05b75146cc60f564380b7f683acf1020a" diff --git a/image/text_edit-new.png b/image/text_edit-new.png new file mode 100644 index 00000000..22fd4c8c Binary files /dev/null and b/image/text_edit-new.png differ diff --git a/image/text_edit-normal.png b/image/text_edit-normal.png new file mode 100644 index 00000000..3805579d Binary files /dev/null and b/image/text_edit-normal.png differ diff --git a/src/CollectionNode.tsx b/src/CollectionNode.tsx index d37e4539..ebbc8fb4 100644 --- a/src/CollectionNode.tsx +++ b/src/CollectionNode.tsx @@ -54,6 +54,7 @@ export const CollectionNode: React.FC = (props) => { customNodeDefinitions, jsonParse, jsonStringify, + TextEditor, keyboardControls, handleKeyboard, insertAtTop, @@ -97,6 +98,7 @@ export const CollectionNode: React.FC = (props) => { useEffect(() => { setStringifiedValue(jsonStringify(data)) + if (isEditing) setCurrentlyEditingElement(null) }, [data]) useEffect(() => { @@ -297,7 +299,18 @@ export const CollectionNode: React.FC = (props) => { }) ) : (
-
+ {TextEditor ? ( + + handleKeyboard(e, { + objectConfirm: handleEdit, + cancel: handleCancel, + }) + } + /> + ) : ( = (props) => { handleKeyPress={handleKeyPressEdit} styles={getStyles('input', nodeData)} /> -
- -
+ )} +
+
) diff --git a/src/JsonEditor.tsx b/src/JsonEditor.tsx index 09a78836..c1133961 100644 --- a/src/JsonEditor.tsx +++ b/src/JsonEditor.tsx @@ -70,6 +70,7 @@ const Editor: React.FC = ({ customButtons = [], jsonParse = JSON.parse, jsonStringify = (data: JsonData) => JSON.stringify(data, null, 2), + TextEditor, errorMessageTimeout = 2500, keyboardControls = {}, insertAtTop = false, @@ -329,6 +330,7 @@ const Editor: React.FC = ({ parentData: null, jsonParse, jsonStringify, + TextEditor, errorMessageTimeout, handleKeyboard: handleKeyboardCallback, keyboardControls: fullKeyboardControls, diff --git a/src/index.ts b/src/index.ts index 1da9b40e..3b718028 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ export { type NodeData, type JsonData, type KeyboardControls, + type TextEditorProps, } from './types' export { type LocalisedStrings, type TranslateFunction } from './localisation' diff --git a/src/style.css b/src/style.css index d97c4c12..d14a54ac 100644 --- a/src/style.css +++ b/src/style.css @@ -182,6 +182,7 @@ select:focus + .focus { .jer-collection-input-button-row { display: flex; justify-content: flex-end; + width: 100%; font-size: 150%; margin-top: 0.4em; } diff --git a/src/types.ts b/src/types.ts index e24e7c1b..a67d298f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,7 @@ export interface JsonEditorProps { customButtons?: CustomButtonDefinition[] jsonParse?: (input: string) => JsonData jsonStringify?: (input: JsonData) => string + TextEditor?: React.FC errorMessageTimeout?: number // ms keyboardControls?: KeyboardControls insertAtTop?: boolean | 'array' | 'object' @@ -74,6 +75,12 @@ export interface IconReplacements { chevron?: JSX.Element } +export interface TextEditorProps { + value: string + onChange: (value: string) => void + onKeyDown: (e: React.KeyboardEvent) => void +} + /** * FUNCTIONS */ @@ -252,6 +259,7 @@ export interface CollectionNodeProps extends BaseNodeProps { jsonParse: (input: string) => JsonData jsonStringify: (data: JsonData) => string insertAtTop: { object: boolean; array: boolean } + TextEditor?: React.FC } export type ValueData = string | number | boolean