├── .README_npm.md ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── custom-component-library ├── .gitignore ├── README.md ├── components │ ├── BigInt │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── BooleanToggle │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── DateObject │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── DatePicker │ │ ├── Button.tsx │ │ ├── component.tsx │ │ ├── definition.ts │ │ ├── index.ts │ │ └── style.css │ ├── EnhancedLink │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── Hyperlink │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── Markdown │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── NaN │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── Symbol │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── Undefined │ │ ├── component.tsx │ │ ├── definition.ts │ │ └── index.ts │ ├── data.ts │ └── index.ts ├── eslint.config.js ├── image │ └── library_screenshot.png ├── index.html ├── package.json ├── public │ └── favicon_96.png ├── src │ ├── App.tsx │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── demo ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package.json ├── public │ ├── favicon.ico │ └── image │ │ ├── apple_touch_icon_180.png │ │ ├── favicon_16.png │ │ ├── favicon_32.png │ │ ├── favicon_96.png │ │ ├── icon_192.png │ │ ├── icon_512.png │ │ ├── json-edit-react_logo.png │ │ ├── logo192.png │ │ └── social_banner.png ├── scripts │ ├── output.json │ └── scrape_swapi.mjs ├── src │ ├── App.tsx │ ├── CodeEditor.tsx │ ├── LazyThemes.ts │ ├── SourceIndicator.tsx │ ├── chakra-theme │ │ ├── colours.ts │ │ ├── components.ts │ │ ├── fonts.ts │ │ ├── index.ts │ │ └── styles.css │ ├── demoData │ │ ├── customNodesSchema.json │ │ ├── data.tsx │ │ ├── dataDefinitions.tsx │ │ ├── index.ts │ │ ├── jsonSchema.json │ │ └── superheroExample.json5 │ ├── helpers.ts │ ├── image │ │ ├── logo.dark.svg │ │ ├── logo.svg │ │ ├── logo_400.png │ │ ├── logo_square.dark.svg │ │ └── logo_square.svg │ ├── index.css │ ├── main.tsx │ ├── react-app-env.d.ts │ ├── style.css │ ├── useDatabase.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock ├── eslint.config.mjs ├── image ├── admonition_npm.png ├── custom_component_levels.png ├── custom_text.png ├── enum_example.png ├── json-edit-react-logo.svg ├── key_select.png ├── logo192.png ├── screenshot.png ├── text-editor-comparison.png ├── theme_edit_after.png └── theme_edit_before.png ├── package.json ├── rollup.config.mjs ├── scripts ├── build_npm_readme.py ├── cleanBuildTypes.cjs ├── installLatestPackage.mjs └── use_npm_readme.py ├── src ├── AutogrowTextArea.tsx ├── ButtonPanels.tsx ├── CollectionNode.tsx ├── CustomNode.ts ├── Icons.tsx ├── JsonEditor.tsx ├── KeyDisplay.tsx ├── ValueNodeWrapper.tsx ├── ValueNodes.tsx ├── additionalThemes │ └── index.ts ├── contexts │ ├── ThemeProvider.tsx │ ├── TreeStateProvider.tsx │ └── index.ts ├── customComponents │ ├── ActiveHyperlinks.tsx │ └── index.ts ├── helpers.ts ├── hooks │ ├── index.ts │ ├── useCollapseTransition.ts │ ├── useCommon.ts │ ├── useData.ts │ ├── useDragNDrop.tsx │ └── useTriggers.ts ├── index.ts ├── localisation.ts ├── style.css └── types.ts ├── test └── nextPrevious.test.ts ├── tsconfig.json └── yarn.lock /.README_npm.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | {{NPM INTRO}} 10 | 11 | breaking changes 12 | 13 | {{NPM USAGE}} 14 | 15 | --- 16 | 17 | For **FULL DOCUMENTATION**, visit [https://github.com/CarlosNZ/json-edit-react](https://github.com/CarlosNZ/json-edit-react#json-edit-react) -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: CarlosNZ 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | # polar: # Replace with a single Polar username 13 | buy_me_a_coffee: carlsmith 14 | # thanks_dev: # Replace with a single thanks.dev username 15 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Reporting bugs for json-edit-react 4 | title: Bug report 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **Expected behavior** 14 | 15 | 16 | **Screenshots** 17 | 18 | 19 | **Online demo** 20 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request new features or enhancements 4 | title: Feature request 5 | labels: "user request" 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Suggestion** 11 | 12 | 13 | **Use case** 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Build folder 2 | build/ 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Compiled binary addons (https://nodejs.org/api/addons.html) 13 | build/Release 14 | 15 | # Dependency directories 16 | node_modules/ 17 | 18 | # TypeScript v1 declaration files 19 | typings/ 20 | 21 | # TypeScript cache 22 | *.tsbuildinfo 23 | 24 | # Optional npm cache directory 25 | .npm 26 | 27 | # Optional eslint cache 28 | .eslintcache 29 | 30 | # Yarn Integrity file 31 | .yarn-integrity 32 | 33 | # dotenv environment variables file 34 | .env 35 | .env.test 36 | 37 | #Misc 38 | .DS_Store 39 | 40 | # Duplicated source code for GUI app 41 | demo/src/json-edit-react 42 | 43 | # Dev playground file 44 | .npmrc 45 | temp.js 46 | .vscode/settings.json 47 | demo/src/firebaseConfig.json 48 | 49 | # Built package in demo/custom library 50 | build_package 51 | demo/src/package 52 | custom-component-library/components/package 53 | .original-readme.md 54 | .npm-readme.md 55 | demo/src/imports/import.ts 56 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | README_npm.md 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "semi": false 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Carl Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /custom-component-library/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /custom-component-library/README.md: -------------------------------------------------------------------------------- 1 | ## Custom Component Library 2 | 3 | A collection of [Custom Components](https://github.com/CarlosNZ/json-edit-react#custom-nodes) for **json-edit-react**. 4 | 5 | You can use these as-is or modify them for your own particular use case. 6 | 7 | screenshot 8 | 9 | Eventually, I'd like to publish these in a separate package so you can easily import them. But for now just copy the code out of this repo. 10 | 11 | Contains a [Vite](https://vite.dev/) web-app for previewing and developing components. 12 | 13 | The individual components are in the `/components` folder, along with demo data (in `data.ts`). 14 | 15 | > [!NOTE] 16 | > If you create a custom component that you think would be useful to others, please [create a PR](https://github.com/CarlosNZ/json-edit-react/pulls) for it. 17 | 18 | ## Components 19 | 20 | These are the ones currently available: 21 | 22 | - [x] Hyperlink/URL 23 | - [x] Undefined 24 | - [x] `Date` Object 25 | - [x] Date/Time Picker (with ISO string) 26 | - [x] Boolean Toggle 27 | - [x] `NaN` 28 | - [x] `BigInt` 29 | - [x] Markdown 30 | - [x] "Enhanced" link 31 | - [ ] Image (to-do) 32 | 33 | ## Development 34 | 35 | From within this folder: `/custom-component-library`: 36 | 37 | Install dependencies: 38 | 39 | ```js 40 | yarn install 41 | ``` 42 | 43 | Launch app: 44 | 45 | ```js 46 | yarn dev 47 | ``` 48 | 49 | ## Guidelines for development: 50 | 51 | Custom components should consider the following: 52 | 53 | - Must respect editing restrictions 54 | - If including CSS classes, please prefix with `jer-` 55 | - Handle keyboard input as much as possible: 56 | - Double-click to edit (if allowed) 57 | - `Tab`/`Shift-Tab` to navigate 58 | - `Enter` to submit 59 | - `Escape` to cancel 60 | - Provide customisation options, particularly styles 61 | - If the data contains non-JSON types, add a "stringify" and "reviver" function definition (see `BigInt`, `NaN` and `Symbol` components) 62 | 63 | If your custom component is "string-like", there are two helper components exported with the package: `StringDisplay` and `StringEdit` -- these are the same components used for the actual "string" elements in the main package. See the [Hyperlink](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/components/Hyperlink/component.tsx) and [BigInt](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/components/BigInt/component.tsx) components for examples of how to use them. 64 | 65 | -------------------------------------------------------------------------------- /custom-component-library/components/BigInt/component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { toPathString, StringEdit, type CustomNodeProps } from '@json-edit-react' 3 | 4 | export interface BigIntProps { 5 | style?: React.CSSProperties 6 | descriptionStyle?: React.CSSProperties 7 | } 8 | 9 | export const BigIntComponent: React.FC> = (props) => { 10 | const { 11 | setValue, 12 | isEditing, 13 | setIsEditing, 14 | getStyles, 15 | nodeData, 16 | customNodeProps = {}, 17 | value, 18 | handleEdit, 19 | ...rest 20 | } = props 21 | const { path } = nodeData 22 | const { style = { color: '#006291', fontSize: '90%' } } = customNodeProps 23 | 24 | const editDisplayValue = typeof value === 'bigint' ? String(value) : (value as string) 25 | 26 | return isEditing ? ( 27 | >} 32 | {...rest} 33 | handleEdit={() => { 34 | handleEdit(BigInt(nodeData.value as string)) 35 | }} 36 | /> 37 | ) : ( 38 | setIsEditing(true)} style={style}> 39 | {value as bigint} 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /custom-component-library/components/BigInt/definition.ts: -------------------------------------------------------------------------------- 1 | import { isCollection, type CustomNodeDefinition } from '@json-edit-react' 2 | import { BigIntComponent, BigIntProps } from './component' 3 | 4 | export const BigIntDefinition: CustomNodeDefinition = { 5 | condition: ({ value }) => typeof value === 'bigint', 6 | element: BigIntComponent, 7 | // customNodeProps: {}, 8 | showOnView: true, 9 | showEditTools: true, 10 | showOnEdit: true, 11 | name: 'BigInt', // shown in the Type selector menu 12 | showInTypesSelector: true, 13 | defaultValue: BigInt(9007199254740992), 14 | stringifyReplacer: (value) => 15 | typeof value === 'bigint' ? { __type: 'bigint', value: String(value) } : value, 16 | parseReviver: (value) => 17 | isCollection(value) && '__type' in value && 'value' in value && value.__type === 'bigint' 18 | ? BigInt(value.value as string) 19 | : value, 20 | } 21 | -------------------------------------------------------------------------------- /custom-component-library/components/BigInt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/BooleanToggle/component.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Boolean Toggle 3 | * 4 | * - Provides an alternative to the default boolean input -- a checkbox that can 5 | * be toggled on and off without having to explicity enable "editing" of the 6 | * element. 7 | */ 8 | 9 | import React from 'react' 10 | import { toPathString, type CustomNodeProps } from '@json-edit-react' 11 | 12 | export const BooleanToggleComponent: React.FC = (props) => { 13 | const { nodeData, value, handleEdit, canEdit } = props 14 | const { path } = nodeData 15 | return ( 16 | { 23 | // In this case we submit the data value immediately, not just the local 24 | // state 25 | handleEdit(!nodeData.value) 26 | // setValue(!value) 27 | }} 28 | /> 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /custom-component-library/components/BooleanToggle/definition.ts: -------------------------------------------------------------------------------- 1 | import { type CustomNodeDefinition } from '@json-edit-react' 2 | import { BooleanToggleComponent } from './component' 3 | 4 | export const BooleanToggleDefinition: CustomNodeDefinition<{ 5 | linkStyles?: React.CSSProperties 6 | stringTruncate?: number 7 | }> = { 8 | condition: ({ value }) => typeof value === 'boolean', 9 | element: BooleanToggleComponent, 10 | showOnView: true, 11 | showOnEdit: false, 12 | showEditTools: true, 13 | } 14 | -------------------------------------------------------------------------------- /custom-component-library/components/BooleanToggle/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/DateObject/component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import { StringDisplay, toPathString, StringEdit, type CustomNodeProps } from '@json-edit-react' 3 | 4 | export interface DateObjectProps { 5 | showTime?: boolean 6 | } 7 | 8 | export const DateObjectCustomComponent: React.FC> = (props) => { 9 | const { 10 | nodeData, 11 | isEditing, 12 | setValue, 13 | getStyles, 14 | canEdit, 15 | value, 16 | handleEdit, 17 | onError, 18 | customNodeProps = {}, 19 | } = props 20 | const lastValidDate = useRef(value) 21 | 22 | const { showTime = true } = customNodeProps 23 | 24 | if (value instanceof Date) lastValidDate.current = value 25 | 26 | const editDisplayValue = 27 | value instanceof Date 28 | ? showTime 29 | ? value.toISOString() 30 | : value.toDateString() 31 | : (value as string) 32 | const displayValue = showTime 33 | ? (nodeData.value as Date).toLocaleString() 34 | : (nodeData.value as Date).toLocaleDateString() 35 | 36 | return isEditing ? ( 37 | >} 43 | handleEdit={() => { 44 | const newDate = new Date(value as string) 45 | try { 46 | // Check if date is valid by trying to convert to ISO 47 | newDate.toISOString() 48 | handleEdit(newDate) 49 | } catch { 50 | handleEdit(lastValidDate.current) 51 | onError({ code: 'UPDATE_ERROR', message: 'Invalid Date' }, value) 52 | } 53 | }} 54 | /> 55 | ) : ( 56 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /custom-component-library/components/DateObject/definition.ts: -------------------------------------------------------------------------------- 1 | import { DateObjectCustomComponent, DateObjectProps } from './component' 2 | import { type CustomNodeDefinition } from '@json-edit-react' 3 | 4 | export const DateObjectDefinition: CustomNodeDefinition = { 5 | condition: (nodeData) => nodeData.value instanceof Date, 6 | element: DateObjectCustomComponent, 7 | showEditTools: true, 8 | showOnEdit: true, 9 | name: 'Date Object', // shown in the Type selector menu 10 | showInTypesSelector: true, 11 | defaultValue: new Date(), 12 | renderCollectionAsValue: true, 13 | // IMPORTANT: This component can't be used in conjunction with a ISO string 14 | // matcher (such as the DatePicker in this repo) -- because JSON.stringify 15 | // automatically serializes Date objects to ISO Strings, there's no way to 16 | // distinguish between them when re-parsing back to object. 17 | // There's also no point in providing a stringifyReplacer, as the 18 | // auto-serialisation gets done before passing to the string replacer function 19 | parseReviver: (value) => 20 | typeof value === 'string' && 21 | /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(value) 22 | ? new Date(value) 23 | : value, 24 | } 25 | -------------------------------------------------------------------------------- /custom-component-library/components/DateObject/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/DatePicker/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // Define the props interface for the Button component 4 | interface ButtonProps { 5 | color?: string 6 | textColor?: string 7 | text?: string 8 | onClick?: () => void 9 | } 10 | 11 | export const Button: React.FC = ({ 12 | color = 'rgb(49, 130, 206)', 13 | textColor = 'white', 14 | text = 'Button', 15 | onClick = () => {}, 16 | }) => { 17 | const buttonBaseStyles: React.CSSProperties = { 18 | backgroundColor: color, 19 | color: textColor, 20 | } 21 | 22 | return ( 23 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /custom-component-library/components/DatePicker/component.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An example Custom Component: 3 | * https://github.com/CarlosNZ/json-edit-react#custom-nodes 4 | * 5 | * A date/time picker which can be configure to show (using the 6 | * CustomNodeDefinitions at the bottom of this file) when an ISO date/time 7 | * string is present in the JSON data, and present a Date picker interface 8 | * rather than requiring the user to edit the ISO string directly. 9 | */ 10 | 11 | import React from 'react' 12 | import DatePicker from 'react-datepicker' 13 | import { Button } from './Button' 14 | import { CustomNodeProps } from '@json-edit-react' 15 | 16 | // Styles 17 | import 'react-datepicker/dist/react-datepicker.css' 18 | import './style.css' 19 | 20 | export interface DatePickerCustomProps { 21 | dateFormat?: string 22 | dateTimeFormat?: string 23 | showTime?: boolean 24 | } 25 | 26 | export const DateTimePicker: React.FC> = ({ 27 | value, 28 | setValue, 29 | handleEdit, 30 | handleCancel, 31 | handleKeyPress, 32 | isEditing, 33 | setIsEditing, 34 | getStyles, 35 | nodeData, 36 | customNodeProps, 37 | }) => { 38 | const { 39 | dateFormat = 'MMM d, yyyy', 40 | dateTimeFormat = 'MMM d, yyyy h:mm aa', 41 | showTime = true, 42 | } = customNodeProps ?? {} 43 | 44 | const date = new Date(value as string) 45 | 46 | const textColour = getStyles('container', nodeData).backgroundColor 47 | const okColour = getStyles('iconOk', nodeData).color 48 | const cancelColour = getStyles('iconCancel', nodeData).color 49 | const stringStyle = getStyles('string', nodeData) 50 | 51 | return isEditing ? ( 52 | // Picker only shows up when "editing". Due to the `showOnView: false` in 53 | // the definition below, this component will not show at all when viewing 54 | // (and so will show raw ISO strings). However, we've defined an alternative 55 | // here too, when showOnView == true, in which case the date/time string is 56 | // shown as a localised date/time. 57 | date && setValue(date.toISOString())} 66 | open={true} 67 | onKeyDown={handleKeyPress} 68 | > 69 |
70 | {/* These buttons are not really necessary -- you can either use the 71 | standard Ok/Cancel icons, or keyboard Enter/Esc, but shown for demo 72 | purposes */} 73 |
76 |
77 | ) : ( 78 |
setIsEditing(true)} 81 | className="jer-value-string" 82 | style={stringStyle} 83 | > 84 | "{showTime ? date.toLocaleString() : date.toLocaleDateString()}" 85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /custom-component-library/components/DatePicker/definition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An example Custom Component: 3 | * https://github.com/CarlosNZ/json-edit-react#custom-nodes 4 | * 5 | * A date/time picker which can be configure to show (using the 6 | * CustomNodeDefinitions at the bottom of this file) when an ISO date/time 7 | * string is present in the JSON data, and present a Date picker interface 8 | * rather than requiring the user to edit the ISO string directly. 9 | */ 10 | 11 | import { CustomNodeDefinition } from '@json-edit-react' 12 | import { DatePickerCustomProps, DateTimePicker } from './component' 13 | 14 | // Styles 15 | import 'react-datepicker/dist/react-datepicker.css' 16 | // For better matching with Chakra-UI 17 | import './style.css' 18 | 19 | // Definition for custom node behaviour 20 | export const DatePickerDefinition: CustomNodeDefinition = { 21 | // Condition is a regex to match ISO strings 22 | condition: ({ value }) => 23 | typeof value === 'string' && 24 | /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(value), 25 | element: DateTimePicker, 26 | showOnView: true, 27 | showOnEdit: true, 28 | name: 'Date (ISO)', // shown in the Type selector menu 29 | showInTypesSelector: true, 30 | defaultValue: new Date().toISOString(), // when instantiated, default to the current date/time 31 | customNodeProps: { showTime: true }, 32 | } 33 | -------------------------------------------------------------------------------- /custom-component-library/components/DatePicker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/DatePicker/style.css: -------------------------------------------------------------------------------- 1 | /* Styles to make Date picker more like Chakra-UI 2 | From https://github.com/chakra-ui/chakra-ui/issues/580#issuecomment-653527951 3 | */ 4 | 5 | .react-datepicker { 6 | font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', 7 | sans-serif; 8 | overflow: hidden; 9 | } 10 | 11 | react-datepicker__navigation--next--with-time:not( 12 | .react-datepicker__navigation--next--with-today-button 13 | ) { 14 | right: 90px; 15 | } 16 | 17 | /* .react-datepicker__navigation--previous, 18 | .react-datepicker__navigation--next { 19 | height: 8px; 20 | } */ 21 | 22 | .react-datepicker__navigation--previous { 23 | border-right-color: #cbd5e0; 24 | 25 | &:hover { 26 | border-right-color: #a0aec0; 27 | } 28 | } 29 | 30 | .react-datepicker__navigation--next { 31 | border-left-color: #cbd5e0; 32 | 33 | &:hover { 34 | border-left-color: #a0aec0; 35 | } 36 | } 37 | 38 | .react-datepicker-wrapper, 39 | .react-datepicker__input-container { 40 | display: block; 41 | } 42 | 43 | .react-datepicker__input-container input { 44 | padding-left: 1em; 45 | padding-right: 1em; 46 | color: darkslategrey; 47 | } 48 | 49 | .react-datepicker__header { 50 | border-radius: 0; 51 | background: #f7fafc; 52 | } 53 | 54 | .react-datepicker, 55 | .react-datepicker__header, 56 | .react-datepicker__time-container { 57 | border-color: #e2e8f0; 58 | } 59 | 60 | .react-datepicker__current-month, 61 | .react-datepicker-time__header, 62 | .react-datepicker-year-header { 63 | font-size: inherit; 64 | font-weight: 600; 65 | } 66 | 67 | .react-datepicker__time-container 68 | .react-datepicker__time 69 | .react-datepicker__time-box 70 | ul.react-datepicker__time-list 71 | li.react-datepicker__time-list-item { 72 | margin: 0 1px 0 0; 73 | height: auto; 74 | padding: 7px 10px; 75 | 76 | &:hover { 77 | background: #edf2f7; 78 | } 79 | } 80 | 81 | .react-datepicker__time-list { 82 | height: 210px !important; 83 | } 84 | 85 | .react-datepicker__day:hover { 86 | background: #edf2f7; 87 | } 88 | 89 | .react-datepicker__day--selected, 90 | .react-datepicker__day--in-selecting-range, 91 | .react-datepicker__day--in-range, 92 | .react-datepicker__month-text--selected, 93 | .react-datepicker__month-text--in-selecting-range, 94 | .react-datepicker__month-text--in-range, 95 | .react-datepicker__time-container 96 | .react-datepicker__time 97 | .react-datepicker__time-box 98 | ul.react-datepicker__time-list 99 | li.react-datepicker__time-list-item--selected { 100 | background: #3182ce; 101 | font-weight: normal; 102 | 103 | &:hover { 104 | background: #2a69ac; 105 | } 106 | } 107 | 108 | /* Button styles */ 109 | 110 | /* ChakraButton.css */ 111 | .jer-button { 112 | padding: 0.5rem 1rem; 113 | border-radius: 0.375rem; 114 | font-weight: 500; 115 | border: none; 116 | cursor: pointer; 117 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); 118 | transition: all 0.2s ease; 119 | } 120 | 121 | .jer-button:hover { 122 | filter: brightness(1.15); 123 | } 124 | 125 | .jer-button:active { 126 | filter: brightness(0.9); 127 | } 128 | 129 | .jer-button:focus { 130 | outline: none; 131 | box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.2); 132 | } 133 | -------------------------------------------------------------------------------- /custom-component-library/components/EnhancedLink/component.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * A custom "URL" renderer -- an object that has "text" and "url" properties, 3 | * but is displayed as a clickable string 4 | */ 5 | 6 | import React, { useState } from 'react' 7 | import { toPathString, StringDisplay, StringEdit, type CustomNodeProps } from '@json-edit-react' 8 | 9 | export interface EnhancedLinkProps { 10 | linkStyles?: React.CSSProperties 11 | propertyStyles?: React.CSSProperties 12 | labels?: { text: string; url: string } 13 | fieldNames?: { text: string; url: string } 14 | stringTruncate?: number 15 | [key: string]: unknown 16 | } 17 | 18 | type EnhancedLink = { 19 | [key: string]: string 20 | } 21 | 22 | export const EnhancedLinkCustomComponent: React.FC> = ( 23 | props 24 | ) => { 25 | const { setIsEditing, getStyles, nodeData, customNodeProps = {}, isEditing, handleEdit } = props 26 | const { 27 | linkStyles = { fontWeight: 'bold', textDecoration: 'underline' }, 28 | propertyStyles = {}, 29 | labels: { text: textLabel, url: urlLabel } = { text: 'Text', url: 'Link' }, 30 | fieldNames: { text: textField, url: urlField } = { text: 'text', url: 'url' }, 31 | stringTruncate = 120, 32 | } = customNodeProps 33 | const [text, setText] = useState((nodeData.value as EnhancedLink)[textField]) 34 | const [url, setUrl] = useState((nodeData.value as EnhancedLink)[urlField]) 35 | 36 | const styles = getStyles('string', nodeData) 37 | 38 | return ( 39 |
{ 41 | if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true) 42 | }} 43 | style={styles} 44 | > 45 | {isEditing ? ( 46 |
47 |
48 | {textLabel}: 49 | setText(val)} 55 | /> 56 |
57 |
58 | {urlLabel}: 59 | setUrl(val)} 65 | handleEdit={() => { 66 | handleEdit({ [textField]: text, [urlField]: url }) 67 | }} 68 | /> 69 |
70 |
71 | ) : ( 72 | { 79 | return ( 80 | 86 | {children} 87 | 88 | ) 89 | }} 90 | /> 91 | )} 92 |
93 | ) 94 | } 95 | -------------------------------------------------------------------------------- /custom-component-library/components/EnhancedLink/definition.ts: -------------------------------------------------------------------------------- 1 | import { isCollection, type CustomNodeDefinition } from '@json-edit-react' 2 | import { EnhancedLinkCustomComponent, EnhancedLinkProps } from './component' 3 | 4 | const TEXT_FIELD = 'text' 5 | const URL_FIELD = 'url' 6 | 7 | export const EnhancedLinkCustomNodeDefinition: CustomNodeDefinition = { 8 | condition: ({ value }) => isCollection(value) && TEXT_FIELD in value && URL_FIELD in value, 9 | element: EnhancedLinkCustomComponent, 10 | name: 'Enhanced Link', // shown in the Type selector menu 11 | showInTypesSelector: true, 12 | defaultValue: { 13 | [TEXT_FIELD]: 'This is the text that is displayed', 14 | [URL_FIELD]: 'https://link.goes.here', 15 | }, 16 | customNodeProps: { fieldNames: { text: TEXT_FIELD, url: URL_FIELD } }, 17 | showOnEdit: true, 18 | renderCollectionAsValue: true, 19 | } 20 | -------------------------------------------------------------------------------- /custom-component-library/components/EnhancedLink/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/Hyperlink/component.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An URL display Custom Component 3 | * 4 | * A simple custom node which detects urls in data and makes them active 5 | * hyperlinks. 6 | */ 7 | 8 | import React from 'react' 9 | import { toPathString, StringDisplay, type CustomNodeProps } from '@json-edit-react' 10 | 11 | export interface LinkProps { 12 | linkStyles?: React.CSSProperties 13 | stringTruncate?: number 14 | [key: string]: unknown 15 | } 16 | 17 | export const LinkCustomComponent: React.FC> = (props) => { 18 | const { setIsEditing, getStyles, nodeData, customNodeProps = {} } = props 19 | const styles = getStyles('string', nodeData) 20 | const { linkStyles = { fontWeight: 'bold', textDecoration: 'underline' }, stringTruncate = 60 } = 21 | customNodeProps 22 | return ( 23 |
{ 25 | if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true) 26 | }} 27 | style={styles} 28 | > 29 | { 36 | return ( 37 | 43 | {children} 44 | 45 | ) 46 | }} 47 | /> 48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /custom-component-library/components/Hyperlink/definition.ts: -------------------------------------------------------------------------------- 1 | import { type CustomNodeDefinition } from '@json-edit-react' 2 | import { LinkCustomComponent, LinkProps } from './component' 3 | 4 | export const LinkCustomNodeDefinition: CustomNodeDefinition = { 5 | // Condition is a regex to match url strings 6 | condition: ({ value }) => typeof value === 'string' && /^https?:\/\/.+\..+$/.test(value), 7 | element: LinkCustomComponent, 8 | customNodeProps: { stringTruncate: 80 }, 9 | showOnView: true, 10 | showOnEdit: false, 11 | } 12 | -------------------------------------------------------------------------------- /custom-component-library/components/Hyperlink/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/Markdown/component.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An Markdown display Custom Component 3 | * 4 | * Uses react-markdown to render the markdown content 5 | */ 6 | 7 | import React from 'react' 8 | import { type CustomNodeProps } from '@json-edit-react' 9 | import Markdown from 'react-markdown' 10 | 11 | export interface LinkProps { 12 | linkStyles?: React.CSSProperties 13 | stringTruncate?: number 14 | [key: string]: unknown 15 | } 16 | 17 | export const MarkdownComponent: React.FC> = (props) => { 18 | const { setIsEditing, getStyles, nodeData } = props 19 | const styles = getStyles('string', nodeData) 20 | 21 | return ( 22 |
{ 24 | if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true) 25 | }} 26 | onDoubleClick={() => setIsEditing(true)} 27 | style={styles} 28 | > 29 | {/* TO-DO: Style over-rides */} 30 | {nodeData.value as string} 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /custom-component-library/components/Markdown/definition.ts: -------------------------------------------------------------------------------- 1 | import { type CustomNodeDefinition } from '@json-edit-react' 2 | import { MarkdownComponent, LinkProps } from './component' 3 | 4 | export const MarkdownNodeDefinition: CustomNodeDefinition = { 5 | condition: () => false, // Over-ride this for specific cases 6 | element: MarkdownComponent, 7 | // customNodeProps: {}, 8 | showOnView: true, 9 | showOnEdit: false, 10 | } 11 | -------------------------------------------------------------------------------- /custom-component-library/components/Markdown/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/NaN/component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useKeyboardListener, type CustomNodeProps } from '@json-edit-react' 3 | 4 | export interface NaNProps { 5 | style?: React.CSSProperties 6 | } 7 | 8 | export const NotANumberComponent: React.FC> = ({ 9 | isEditing, 10 | setIsEditing, 11 | handleKeyboard, 12 | handleEdit, 13 | keyboardCommon, 14 | customNodeProps = {}, 15 | }) => { 16 | const listenForSubmit = (e: unknown) => 17 | handleKeyboard(e as React.KeyboardEvent, { 18 | confirm: handleEdit, 19 | ...keyboardCommon, 20 | }) 21 | 22 | useKeyboardListener(isEditing, listenForSubmit) 23 | 24 | return ( 25 |
setIsEditing(true)} 28 | > 29 | NaN 30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /custom-component-library/components/NaN/definition.ts: -------------------------------------------------------------------------------- 1 | import { NotANumberComponent, NaNProps } from './component' 2 | import { isCollection, type CustomNodeDefinition } from '@json-edit-react' 3 | 4 | export const NanDefinition: CustomNodeDefinition = { 5 | condition: ({ value }) => Number.isNaN(value), 6 | element: NotANumberComponent, 7 | showEditTools: true, 8 | showOnEdit: true, 9 | name: 'NaN', // shown in the Type selector menu 10 | showInTypesSelector: true, 11 | defaultValue: NaN, 12 | stringifyReplacer: (value) => (Number.isNaN(value) ? { __type: 'NaN', value: 'NaN' } : value), 13 | parseReviver: (value) => 14 | isCollection(value) && '__type' in value && 'value' in value && value.__type === 'NaN' 15 | ? NaN 16 | : value, 17 | } 18 | -------------------------------------------------------------------------------- /custom-component-library/components/NaN/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/Symbol/component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { toPathString, StringEdit, type CustomNodeProps } from '@json-edit-react' 3 | 4 | export interface SymbolProps { 5 | style?: React.CSSProperties 6 | descriptionStyle?: React.CSSProperties 7 | } 8 | 9 | export const SymbolComponent: React.FC> = (props) => { 10 | const { 11 | setValue, 12 | isEditing, 13 | setIsEditing, 14 | getStyles, 15 | nodeData, 16 | customNodeProps = {}, 17 | value, 18 | handleEdit, 19 | ...rest 20 | } = props 21 | const { path } = nodeData 22 | const { style = { color: '#006291', fontSize: '90%' }, descriptionStyle = { color: '#ff9300' } } = 23 | customNodeProps 24 | 25 | const editDisplayValue = typeof value === 'symbol' ? value.description ?? '' : (value as string) 26 | 27 | return isEditing ? ( 28 | >} 33 | {...rest} 34 | handleEdit={() => { 35 | handleEdit(Symbol(editDisplayValue)) 36 | }} 37 | /> 38 | ) : ( 39 | setIsEditing(true)}> 40 | Symbol("{(nodeData.value as symbol).description}") 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /custom-component-library/components/Symbol/definition.ts: -------------------------------------------------------------------------------- 1 | import { isCollection, type CustomNodeDefinition } from '@json-edit-react' 2 | import { SymbolComponent, SymbolProps } from './component' 3 | 4 | export const SymbolDefinition: CustomNodeDefinition = { 5 | condition: ({ value }) => typeof value === 'symbol', 6 | element: SymbolComponent, 7 | // customNodeProps: {}, 8 | showOnView: true, 9 | showEditTools: true, 10 | showOnEdit: true, 11 | name: 'Symbol', // shown in the Type selector menu 12 | showInTypesSelector: true, 13 | defaultValue: Symbol('New symbol'), 14 | stringifyReplacer: (value) => 15 | typeof value === 'symbol' ? { __type: 'Symbol', value: value.description ?? '' } : value, 16 | parseReviver: (value) => 17 | isCollection(value) && '__type' in value && 'value' in value && value.__type === 'Symbol' 18 | ? Symbol((value.value as string) ?? null) 19 | : value, 20 | } 21 | -------------------------------------------------------------------------------- /custom-component-library/components/Symbol/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/Undefined/component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useKeyboardListener, type CustomNodeProps } from '@json-edit-react' 3 | 4 | export interface UndefinedProps { 5 | style?: React.CSSProperties 6 | } 7 | 8 | export const UndefinedCustomComponent: React.FC> = ({ 9 | isEditing, 10 | setIsEditing, 11 | handleKeyboard, 12 | handleEdit, 13 | keyboardCommon, 14 | customNodeProps = {}, 15 | }) => { 16 | const listenForSubmit = (e: unknown) => 17 | handleKeyboard(e as React.KeyboardEvent, { 18 | confirm: handleEdit, 19 | ...keyboardCommon, 20 | }) 21 | 22 | useKeyboardListener(isEditing, listenForSubmit) 23 | 24 | return ( 25 |
setIsEditing(true)} 27 | className="jer-value-undefined" 28 | style={{ fontStyle: 'italic', color: '#9b9b9b', ...customNodeProps?.style }} 29 | > 30 | undefined 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /custom-component-library/components/Undefined/definition.ts: -------------------------------------------------------------------------------- 1 | import { UndefinedCustomComponent, UndefinedProps } from './component' 2 | import { type CustomNodeDefinition } from '@json-edit-react' 3 | 4 | export const UndefinedDefinition: CustomNodeDefinition = { 5 | condition: ({ value }) => value === undefined, 6 | element: UndefinedCustomComponent, 7 | showEditTools: true, 8 | showOnEdit: true, 9 | name: 'undefined', // shown in the Type selector menu 10 | showInTypesSelector: true, 11 | defaultValue: undefined, 12 | // These not required as "undefined" is a special case which won't work with a 13 | // standard reviver, so is handled internally 14 | // stringifyReplacer: 15 | // parseReviver: 16 | } 17 | -------------------------------------------------------------------------------- /custom-component-library/components/Undefined/index.ts: -------------------------------------------------------------------------------- 1 | export * from './definition' 2 | -------------------------------------------------------------------------------- /custom-component-library/components/data.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The data to be shown in the json-edit-react component, which showcases the 3 | * custom components defined in here. 4 | */ 5 | 6 | export const testData = { 7 | Intro: `# json-edit-react 8 | 9 | ## Custom Component library 10 | 11 | ### Components available: 12 | - Hyperlink 13 | - DatePicker 14 | - DateObject 15 | - Undefined 16 | - Markdown 17 | - BigInt 18 | - BooleanToggle 19 | - NaN 20 | - Symbol 21 | 22 | Click [here](https://github.com/CarlosNZ/json-edit-react/blob/main/custom-component-library/README.md) for more info 23 | `, 24 | 'Active Links': { 25 | Url: 'https://carlosnz.github.io/json-edit-react/', 26 | 'Long URL': 27 | 'https://www.google.com/maps/place/Sky+Tower/@-36.8465603,174.7609398,818m/data=!3m1!1e3!4m6!3m5!1s0x6d0d47f06d4bdc25:0x2d1b5c380ad9387!8m2!3d-36.848448!4d174.762191!16zL20vMDFuNXM2?entry=ttu&g_ep=EgoyMDI1MDQwOS4wIKXMDSoASAFQAw%3D%3D', 28 | 'Enhanced Link': { 29 | text: 'This link displays custom text', 30 | url: 'https://github.com/CarlosNZ/json-edit-react/tree/main/custom-component-library#custom-component-library', 31 | }, 32 | }, 33 | 'Date & Time': { 34 | Date: new Date().toISOString(), 35 | 'Show Time in Date?': true, 36 | info: 'Replaced in App.tsx', 37 | }, 38 | 39 | 'Non-JSON types': { 40 | undefined: undefined, 41 | 'Not a Number': NaN, 42 | Symbol1: Symbol('First one'), 43 | Symbol2: Symbol('Second one'), 44 | BigInt: 1234567890123456789012345678901234567890n, 45 | }, 46 | Markdown: 'This text is **bold** and this is *italic*', 47 | } 48 | -------------------------------------------------------------------------------- /custom-component-library/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Hyperlink' 2 | export * from './EnhancedLink' 3 | export * from './DateObject' 4 | export * from './Undefined' 5 | export * from './DatePicker' 6 | export * from './BooleanToggle' 7 | export * from './NaN' 8 | export * from './Symbol' 9 | export * from './BigInt' 10 | export * from './Markdown' 11 | -------------------------------------------------------------------------------- /custom-component-library/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /custom-component-library/image/library_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/custom-component-library/image/library_screenshot.png -------------------------------------------------------------------------------- /custom-component-library/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | JSON Edit React custom components 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /custom-component-library/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-component-library", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "dev:local": "VITE_JRE_SOURCE=local vite", 9 | "dev:build": "VITE_JRE_SOURCE=build vite", 10 | "build": "tsc -b && vite build", 11 | "lint": "eslint .", 12 | "preview": "vite preview" 13 | }, 14 | "dependencies": { 15 | "json-edit-react": "1.27.0", 16 | "react": "^19.0.0", 17 | "react-datepicker": "^7.5.0", 18 | "react-dom": "^19.0.0", 19 | "react-markdown": "^10.1.0" 20 | }, 21 | "devDependencies": { 22 | "@eslint/js": "^9.21.0", 23 | "@types/react": "^19.0.10", 24 | "@types/react-dom": "^19.0.4", 25 | "@vitejs/plugin-react": "^4.3.4", 26 | "eslint": "^9.21.0", 27 | "eslint-plugin-react-hooks": "^5.1.0", 28 | "eslint-plugin-react-refresh": "^0.4.19", 29 | "globals": "^15.15.0", 30 | "typescript": "~5.7.2", 31 | "typescript-eslint": "^8.24.1", 32 | "vite": "^6.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /custom-component-library/public/favicon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/custom-component-library/public/favicon_96.png -------------------------------------------------------------------------------- /custom-component-library/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Set to true to store date as Date object, false to store as ISO string 2 | const STORE_DATE_AS_DATE_OBJECT = true 3 | 4 | import { useState } from 'react' 5 | import { 6 | LinkCustomNodeDefinition, 7 | DateObjectDefinition, 8 | UndefinedDefinition, 9 | DatePickerDefinition, 10 | BooleanToggleDefinition, 11 | NanDefinition, 12 | SymbolDefinition, 13 | BigIntDefinition, 14 | MarkdownNodeDefinition, 15 | EnhancedLinkCustomNodeDefinition, 16 | } from '../components' 17 | import { testData } from '../components/data' 18 | import { JsonData, JsonEditor } from '@json-edit-react' 19 | 20 | if (testData?.['Date & Time']) { 21 | // @ts-expect-error redefine after initialisation 22 | testData['Date & Time'].Date = STORE_DATE_AS_DATE_OBJECT ? new Date() : new Date().toISOString() 23 | 24 | testData['Date & Time'].info = STORE_DATE_AS_DATE_OBJECT 25 | ? 'Date is stored a JS Date object. To use ISO string, set STORE_DATE_AS_DATE_OBJECT to false in App.tsx.' 26 | : 'Date is stored as ISO string. To use JS Date objects, set STORE_DATE_AS_DATE_OBJECT to true in App.tsx.' 27 | } 28 | 29 | type TestData = typeof testData 30 | 31 | function App() { 32 | const [data, setData] = useState(testData) 33 | 34 | console.log('Current data', data) 35 | 36 | return ( 37 |
38 | key === 'Markdown', 63 | }, 64 | { 65 | ...MarkdownNodeDefinition, 66 | condition: ({ key }) => key === 'Intro', 67 | hideKey: true, 68 | }, 69 | ]} 70 | rootName="" 71 | showCollectionCount="when-closed" 72 | /> 73 |
74 | ) 75 | } 76 | 77 | export default App 78 | -------------------------------------------------------------------------------- /custom-component-library/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | /* color-scheme: light dark; */ 7 | color: #213547; 8 | background-color: #cef1ce; 9 | /* color: rgba(255, 255, 255, 0.87); 10 | background-color: #242424; */ 11 | 12 | font-synthesis: none; 13 | text-rendering: optimizeLegibility; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | 17 | display: flex; 18 | justify-content: center; 19 | } 20 | 21 | #container { 22 | display: flex; 23 | flex-direction: column; 24 | justify-content: center; 25 | align-items: center; 26 | margin-bottom: 7em; 27 | margin-top: 1em; 28 | } 29 | 30 | .jer-editor-container { 31 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 32 | } 33 | 34 | a { 35 | font-weight: 500; 36 | /* color: #646cff; */ 37 | text-decoration: underline; 38 | } 39 | a:hover { 40 | /* color: #535bf2; */ 41 | color: #747bff; 42 | } 43 | 44 | body { 45 | margin: 0; 46 | display: flex; 47 | min-width: 320px; 48 | min-height: 100vh; 49 | display: flex; 50 | place-items: center; 51 | } 52 | 53 | h1, 54 | h2, 55 | h3 { 56 | font-family: system-ui, Helvetica, Arial, sans-serif; 57 | } 58 | 59 | h1 { 60 | font-size: 2em; 61 | line-height: 1.2; 62 | margin: 0; 63 | color: #3f5e7a; 64 | } 65 | 66 | h2 { 67 | font-size: 1.4em; 68 | line-height: 1.8; 69 | margin: 0; 70 | } 71 | 72 | h3 { 73 | font-size: 1.1em; 74 | line-height: 1.3; 75 | margin: 0; 76 | /* margin-bottom: 0.5em; */ 77 | } 78 | 79 | p { 80 | margin-top: 0; 81 | margin-bottom: 0; 82 | } 83 | 84 | ul { 85 | margin-top: 0.3em; 86 | margin-bottom: 0.3em; 87 | } 88 | 89 | button { 90 | border-radius: 8px; 91 | border: 1px solid transparent; 92 | padding: 0.6em 1.2em; 93 | font-size: 1em; 94 | font-weight: 500; 95 | font-family: inherit; 96 | /* background-color: #1a1a1a; */ 97 | background-color: #f9f9f9; 98 | cursor: pointer; 99 | transition: border-color 0.25s; 100 | } 101 | button:hover { 102 | border-color: #646cff; 103 | } 104 | button:focus, 105 | button:focus-visible { 106 | outline: 4px auto -webkit-focus-ring-color; 107 | } 108 | 109 | /* @media (prefers-color-scheme: light) { 110 | :root { 111 | color: #213547; 112 | background-color: #ffffff; 113 | } 114 | a:hover { 115 | color: #747bff; 116 | } 117 | button { 118 | background-color: #f9f9f9; 119 | } 120 | } */ 121 | -------------------------------------------------------------------------------- /custom-component-library/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /custom-component-library/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /custom-component-library/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@json-edit-react/*": ["../src/*"], 20 | "@json-edit-react": ["../src/"] 21 | }, 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "noUncheckedSideEffectImports": true 29 | }, 30 | "include": ["src", "components"] 31 | } 32 | -------------------------------------------------------------------------------- /custom-component-library/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /custom-component-library/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /custom-component-library/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | 5 | const packageSource = process.env.VITE_JRE_SOURCE || 'npm' 6 | console.log(`Using json-edit-react from: ${packageSource}`) 7 | 8 | const srcMap: Record = { 9 | npm: 'json-edit-react', 10 | local: path.resolve(__dirname, '../src'), 11 | build: path.resolve(__dirname, '../build_package'), 12 | } 13 | 14 | const jsonEditReactPath = srcMap[packageSource] ?? 'json-edit-react' 15 | 16 | // https://vite.dev/config/ 17 | 18 | export default defineConfig({ 19 | plugins: [react()], 20 | resolve: { 21 | alias: { '@json-edit-react': jsonEditReactPath }, 22 | }, 23 | server: { 24 | port: 5176, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 13 | 14 | ```js 15 | export default tseslint.config({ 16 | extends: [ 17 | // Remove ...tseslint.configs.recommended and replace with this 18 | ...tseslint.configs.recommendedTypeChecked, 19 | // Alternatively, use this for stricter rules 20 | ...tseslint.configs.strictTypeChecked, 21 | // Optionally, add this for stylistic rules 22 | ...tseslint.configs.stylisticTypeChecked, 23 | ], 24 | languageOptions: { 25 | // other options... 26 | parserOptions: { 27 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 28 | tsconfigRootDir: import.meta.dirname, 29 | }, 30 | }, 31 | }) 32 | ``` 33 | 34 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 35 | 36 | ```js 37 | // eslint.config.js 38 | import reactX from 'eslint-plugin-react-x' 39 | import reactDom from 'eslint-plugin-react-dom' 40 | 41 | export default tseslint.config({ 42 | plugins: { 43 | // Add the react-x and react-dom plugins 44 | 'react-x': reactX, 45 | 'react-dom': reactDom, 46 | }, 47 | rules: { 48 | // other rules... 49 | // Enable its recommended typescript rules 50 | ...reactX.configs['recommended-typescript'].rules, 51 | ...reactDom.configs.recommended.rules, 52 | }, 53 | }) 54 | ``` 55 | -------------------------------------------------------------------------------- /demo/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['build', 'src/json-edit-react', 'src/package'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 23 | }, 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JSON•Edit•React 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 82 | 83 | 84 | 85 | 86 | 87 |
88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-edit-react-demo", 3 | "version": "1.25.0", 4 | "private": true, 5 | "type": "module", 6 | "homepage": "https://carlosnz.github.io/json-edit-react", 7 | "scripts": { 8 | "start": "vite", 9 | "start:local": "VITE_JRE_SOURCE=local vite", 10 | "start:build": "VITE_JRE_SOURCE=build vite", 11 | "dev": "yarn start:local", 12 | "prebuild": "yarn lint", 13 | "build": "tsc -b && vite build", 14 | "build:local": "yarn build --base=/", 15 | "predeploy": "yarn build", 16 | "deploy": "gh-pages -d build", 17 | "lint": "eslint .", 18 | "serve": "vite preview", 19 | "serve:local": "vite preview --base=/", 20 | "compile": "tsc -b --noEmit" 21 | }, 22 | "dependencies": { 23 | "@chakra-ui/icons": "^2.1.1", 24 | "@chakra-ui/react": "^2.8.2", 25 | "@codemirror/lang-json": "^6.0.1", 26 | "@emotion/react": "^11.11.3", 27 | "@emotion/styled": "^11.11.0", 28 | "@uiw/codemirror-theme-console": "^4.23.7", 29 | "@uiw/codemirror-theme-github": "^4.23.7", 30 | "@uiw/codemirror-theme-monokai": "^4.23.7", 31 | "@uiw/codemirror-theme-quietlight": "^4.23.7", 32 | "@uiw/react-codemirror": "^4.23.7", 33 | "ajv": "^8.16.0", 34 | "firebase": "^10.13.0", 35 | "framer-motion": "^11.0.3", 36 | "json-edit-react": "1.27.0", 37 | "json5": "^2.2.3", 38 | "react": "^19.0.0", 39 | "react-dom": "^19.0.0", 40 | "react-firebase-hooks": "^5.1.1", 41 | "react-icons": "^5.0.1", 42 | "use-undo": "^1.1.1", 43 | "wouter": "^3.3.1" 44 | }, 45 | "devDependencies": { 46 | "@eslint/js": "^9.21.0", 47 | "@types/fs-extra": "^11.0.4", 48 | "@types/node": "^20.11.6", 49 | "@types/react": "^19.0.0", 50 | "@types/react-dom": "^19.0.0", 51 | "@vitejs/plugin-react": "^4.3.4", 52 | "cross-env": "^7.0.3", 53 | "eslint": "^9.21.0", 54 | "eslint-plugin-react-hooks": "^5.1.0", 55 | "eslint-plugin-react-refresh": "^0.4.19", 56 | "fs-extra": "^11.3.0", 57 | "gh-pages": "^6.1.1", 58 | "globals": "^15.15.0", 59 | "node-fetch": "^3.3.2", 60 | "typescript": "~5.7.2", 61 | "typescript-eslint": "^8.24.1", 62 | "vite": "^6.2.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/image/apple_touch_icon_180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/apple_touch_icon_180.png -------------------------------------------------------------------------------- /demo/public/image/favicon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/favicon_16.png -------------------------------------------------------------------------------- /demo/public/image/favicon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/favicon_32.png -------------------------------------------------------------------------------- /demo/public/image/favicon_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/favicon_96.png -------------------------------------------------------------------------------- /demo/public/image/icon_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/icon_192.png -------------------------------------------------------------------------------- /demo/public/image/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/icon_512.png -------------------------------------------------------------------------------- /demo/public/image/json-edit-react_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/json-edit-react_logo.png -------------------------------------------------------------------------------- /demo/public/image/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/logo192.png -------------------------------------------------------------------------------- /demo/public/image/social_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/public/image/social_banner.png -------------------------------------------------------------------------------- /demo/scripts/scrape_swapi.mjs: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch' 2 | import fs from 'fs' 3 | import { fileURLToPath } from 'url' 4 | import path from 'path' 5 | 6 | const BASE_URL = 'https://swapi.dev/api/people/1/' 7 | const DEPTH = 3 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = path.dirname(__filename) 11 | 12 | const memo = {} 13 | 14 | const getData = async (url) => { 15 | console.log('Fetching...', url) 16 | const response = await fetch(url) 17 | return await response.json() 18 | } 19 | 20 | const getCategory = (url) => { 21 | const matches = /^https:\/\/swapi.dev\/api\/(.+)\/[0-9]/.exec(url) 22 | return matches[1] 23 | } 24 | 25 | const propertyMutations = { 26 | gender: (data) => ['isMale', data === 'male'], 27 | url: (data) => ['url', data], 28 | } 29 | 30 | const keyProperties = { 31 | people: 'name', 32 | films: 'title', 33 | starships: 'name', 34 | vehicles: 'name', 35 | planets: 'name', 36 | species: 'name', 37 | } 38 | 39 | const retrieveSingle = async (url) => { 40 | if (url in memo) { 41 | const result = memo[url] 42 | const category = getCategory(url) 43 | const returnValue = result[keyProperties[category]] 44 | console.log('Already retrieved:', url, 'Returning', returnValue) 45 | return returnValue 46 | } else { 47 | const result = await getData(url) 48 | const category = getCategory(url) 49 | return result[keyProperties[category]] 50 | } 51 | } 52 | 53 | const runScript = async (url, depth, currentDepth = 1) => { 54 | if (url in memo) { 55 | const savedResult = memo[url] 56 | const category = getCategory(url) 57 | const result = savedResult[keyProperties[category]] 58 | console.log('Already retrieved:', url, 'Returning', result) 59 | return result 60 | } 61 | const result = await getData(url) 62 | memo[url] = result 63 | 64 | const output = [] 65 | 66 | for (const [key, value] of Object.entries(result)) { 67 | if (key in propertyMutations) { 68 | output.push(propertyMutations[key](value)) 69 | continue 70 | } 71 | 72 | if (/^[0-9]+$/.test(value)) { 73 | output.push([key, Number(value)]) 74 | continue 75 | } 76 | 77 | if (depth === currentDepth) { 78 | if (typeof value === 'string' && /^https:\/\/swapi\.dev/.test(value)) { 79 | // console.log('ValueMatch', value) 80 | output.push([key, await retrieveSingle(value)]) 81 | continue 82 | } 83 | if (Array.isArray(value)) { 84 | const result = 85 | value.length === 0 ? null : await Promise.all(value.map((v) => retrieveSingle(v))) 86 | output.push([key, result]) 87 | continue 88 | } 89 | output.push([key, value]) 90 | continue 91 | } 92 | 93 | if (Array.isArray(value)) { 94 | output.push([ 95 | key, 96 | value.length === 0 97 | ? null 98 | : await Promise.all(value.map((val) => runScript(val, depth, currentDepth + 1))), 99 | ]) 100 | continue 101 | } 102 | 103 | if (/^https:\/\/swapi\.dev/.test(value)) { 104 | output.push([key, await runScript(value, depth, currentDepth + 1)]) 105 | continue 106 | } 107 | 108 | // Remaining 109 | output.push([key, value]) 110 | } 111 | 112 | return Object.fromEntries(await Promise.all(output)) 113 | } 114 | 115 | runScript(BASE_URL, DEPTH).then((output) => { 116 | console.log(JSON.stringify(output, null, 2)) 117 | fs.writeFile(path.join(__dirname, './output.json'), JSON.stringify(output, null, 2), () => 118 | console.log('DONE!') 119 | ) 120 | }) 121 | -------------------------------------------------------------------------------- /demo/src/CodeEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TextEditorProps } from '@json-edit-react' 3 | import CodeMirror, { Extension } from '@uiw/react-codemirror' 4 | import { json } from '@codemirror/lang-json' 5 | import { githubLight, githubDark } from '@uiw/codemirror-theme-github' 6 | import { consoleDark } from '@uiw/codemirror-theme-console/dark' 7 | import { consoleLight } from '@uiw/codemirror-theme-console/light' 8 | import { quietlight } from '@uiw/codemirror-theme-quietlight' 9 | import { monokai } from '@uiw/codemirror-theme-monokai' 10 | 11 | const themeMap: Record = { 12 | Default: undefined, 13 | 'Github Light': githubLight, 14 | 'Github Dark': githubDark, 15 | 'White & Black': consoleLight, 16 | 'Black & White': consoleDark, 17 | 'Candy Wrapper': quietlight, 18 | Psychedelic: monokai, 19 | } 20 | 21 | // Styles defined in /demo/src/style.css 22 | 23 | const CodeEditor: React.FC = ({ 24 | value, 25 | onChange, 26 | onKeyDown, 27 | theme, 28 | }) => { 29 | return ( 30 | 38 | ) 39 | } 40 | 41 | export default CodeEditor 42 | -------------------------------------------------------------------------------- /demo/src/LazyThemes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | githubDarkTheme, 3 | githubLightTheme, 4 | monoLightTheme, 5 | monoDarkTheme, 6 | candyWrapperTheme, 7 | psychedelicTheme, 8 | Theme, 9 | } from '@json-edit-react' 10 | 11 | // This file contains functions that return theme objects 12 | // Each function is dynamically imported when needed to reduce initial bundle size 13 | 14 | export const getGithubDarkTheme = (): Theme => githubDarkTheme 15 | export const getGithubLightTheme = (): Theme => githubLightTheme 16 | export const getWhiteBlackTheme = (): Theme => monoLightTheme 17 | export const getBlackWhiteTheme = (): Theme => monoDarkTheme 18 | export const getCandyWrapperTheme = (): Theme => candyWrapperTheme 19 | export const getPsychedelicTheme = (): Theme => psychedelicTheme 20 | 21 | // Allow dynamic accessing of theme getter functions 22 | interface ThemeGetters { 23 | [key: string]: () => Theme 24 | } 25 | 26 | // Export a map for safer dynamic access 27 | export const themeGetters: ThemeGetters = { 28 | getGithubDarkTheme, 29 | getGithubLightTheme, 30 | getWhiteBlackTheme, 31 | getBlackWhiteTheme, 32 | getCandyWrapperTheme, 33 | getPsychedelicTheme, 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/SourceIndicator.tsx: -------------------------------------------------------------------------------- 1 | const SourceIndicator = () => { 2 | // Determine json-edit-react source from Env variable 3 | // Default is "published", so don't display anything additional in that case 4 | if (!['local', 'package'].includes(import.meta.env.VITE_JRE_SOURCE)) return null 5 | 6 | const backgroundColor = import.meta.env.VITE_JRE_SOURCE === 'package' ? '#ff51ff' : '#ef4444' 7 | const text = import.meta.env.VITE_JRE_SOURCE === 'package' ? 'BUILD' : 'LOCAL' 8 | 9 | return ( 10 |
21 |
32 | {text} 33 |
34 |
35 | ) 36 | } 37 | 38 | export default SourceIndicator 39 | -------------------------------------------------------------------------------- /demo/src/chakra-theme/colours.ts: -------------------------------------------------------------------------------- 1 | import { theme as base } from '@chakra-ui/react' 2 | 3 | const colors = { 4 | background: '#DDEAED', 5 | primary: '#073446', 6 | secondary: '#00567A', 7 | accent: '#EA3788', 8 | // altAccent: '#3FA34D', 9 | jetBlack: '#2B2B2B', 10 | offWhite: '#f2f2f2', 11 | accentScheme: { 12 | 50: '#ffe4f3', 13 | 100: '#fcb8d7', 14 | 200: '#f58bba', 15 | 300: '#ef5c9f', 16 | 400: '#e92f83', 17 | 500: '#d0166a', 18 | 600: '#a30e52', 19 | 700: '#75073b', 20 | 800: '#480223', 21 | 900: '#1e000e', 22 | }, 23 | primaryScheme: { 24 | 50: '#e6f7fe', 25 | 100: '#bbe7f9', 26 | 200: '#91d7f5', 27 | 300: '#69c7f3', 28 | 400: '#4cb8ef', 29 | 500: '#3ea0d6', 30 | 600: '#307ca6', 31 | 700: '#225875', 32 | 800: '#123547', 33 | 900: '#011219', 34 | }, 35 | textPrimary: 'black', 36 | textSecondary: base.colors.gray['600'], 37 | } 38 | 39 | export default colors 40 | -------------------------------------------------------------------------------- /demo/src/chakra-theme/components.ts: -------------------------------------------------------------------------------- 1 | // import { theme as base } from '@chakra-ui/react' 2 | 3 | const components = { 4 | Heading: { 5 | baseStyle: { color: 'primary' }, 6 | variants: { 7 | sub: { 8 | fontFamily: 'Work Sans, sans-serif', 9 | color: 'secondary', 10 | fontWeight: 'bold', 11 | fontSize: '1.4em', 12 | }, 13 | accent: { color: 'accent' }, 14 | }, 15 | }, 16 | Text: { 17 | baseStyle: { color: 'secondary', fontSize: 16 }, 18 | variants: { 19 | primary: { color: 'primary' }, 20 | accent: { color: 'accent' }, 21 | altAccent: { color: 'jetBlack' }, 22 | }, 23 | }, 24 | Button: { 25 | // baseStyle: { colorScheme: 'accentScheme' }, 26 | }, 27 | } 28 | export default components 29 | -------------------------------------------------------------------------------- /demo/src/chakra-theme/fonts.ts: -------------------------------------------------------------------------------- 1 | // import { theme as base } from '@chakra-ui/react' 2 | import './styles.css' 3 | 4 | const fonts = { 5 | body: 'Work Sans, sans-serif', 6 | heading: 'Montserrat Alternates, sans-serif', 7 | } 8 | 9 | export default fonts 10 | -------------------------------------------------------------------------------- /demo/src/chakra-theme/index.ts: -------------------------------------------------------------------------------- 1 | import { extendTheme } from '@chakra-ui/react' 2 | 3 | import colors from './colours' 4 | import fonts from './fonts' 5 | import components from './components' 6 | 7 | const theme = extendTheme({ 8 | styles: { 9 | global: { 10 | 'html, body': { 11 | // background: 12 | // 'radial-gradient(circle, hsla(191, 53%, 85%, 1) 0%, hsla(192, 45%, 67%, 1) 100%);', 13 | backgroundColor: 'background', 14 | height: '100vh', 15 | fontSize: '14px', 16 | }, 17 | a: { 18 | // color: 'brandDark.700', 19 | fontWeight: 600, 20 | _hover: { textDecoration: 'underline' }, 21 | }, 22 | }, 23 | }, 24 | config: { 25 | initialColorMode: 'light', 26 | }, 27 | textStyles: { 28 | mono: { 29 | fontFamily: 'mono', 30 | color: 'gray.600', 31 | }, 32 | }, 33 | colors, 34 | fonts, 35 | components, 36 | }) 37 | 38 | export default theme 39 | -------------------------------------------------------------------------------- /demo/src/chakra-theme/styles.css: -------------------------------------------------------------------------------- 1 | /* Put Google font imports here */ 2 | @import url('https://fonts.googleapis.com/css2?family=Work+Sans:ital,wght@0,300;0,400;0,600;1,300;1,400;1,500&display=swap'); 3 | @import url('https://fonts.googleapis.com/css2?family=Montserrat+Alternates:wght@500;700&display=swap'); 4 | -------------------------------------------------------------------------------- /demo/src/demoData/customNodesSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "Superhero array schema", 3 | "type": "array", 4 | "items": { 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string" 9 | }, 10 | "dateOfBirth": { 11 | "type": "string" 12 | }, 13 | "aliases": { 14 | "type": "array", 15 | "items": { 16 | "type": "string" 17 | } 18 | }, 19 | "logo": { 20 | "type": "string" 21 | }, 22 | "portrayedBy": { 23 | "type": "array", 24 | "items": { 25 | "type": "string" 26 | } 27 | }, 28 | "publisher": { 29 | "type": "string" 30 | } 31 | }, 32 | "required": ["name", "dateOfBirth", "aliases", "logo", "portrayedBy", "publisher"] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/demoData/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dataDefinitions' 2 | -------------------------------------------------------------------------------- /demo/src/demoData/jsonSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "https://example.com/complex-object.schema.json", 3 | "title": "Complex Object", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | }, 9 | "age": { 10 | "type": "integer", 11 | "minimum": 0 12 | }, 13 | "address": { 14 | "type": "object", 15 | "properties": { 16 | "street": { 17 | "type": "string" 18 | }, 19 | "suburb": { 20 | "type": "string" 21 | }, 22 | "city": { 23 | "type": "string" 24 | }, 25 | "state": { 26 | "type": "string" 27 | }, 28 | "postalCode": { 29 | "type": "string", 30 | "pattern": "\\d{5}" 31 | }, 32 | "country": { 33 | "type": "string" 34 | } 35 | }, 36 | "required": ["street", "city", "state", "postalCode"], 37 | "additionalProperties": false 38 | }, 39 | "hobbies": { 40 | "type": "array", 41 | "items": { 42 | "type": "string" 43 | } 44 | }, 45 | "category": { 46 | "type": "string", 47 | "enum": ["human", "enhanced human", "extra-terrestrial"] 48 | }, 49 | "isAlive": { 50 | "type": "boolean" 51 | } 52 | }, 53 | "additionalProperties": false, 54 | "required": ["name", "age"] 55 | } 56 | -------------------------------------------------------------------------------- /demo/src/demoData/superheroExample.json5: -------------------------------------------------------------------------------- 1 | // Data used in main screenshot 2 | { 3 | squadName: 'Super Hero Squad', 4 | homeTown: 'Metro City', 5 | formed: 2016, 6 | secretBase: 'Super tower', 7 | active: true, 8 | members: [ 9 | { 10 | name: 'Molecule Man', 11 | age: 29, 12 | secretIdentity: 'Dan Jukes', 13 | powers: ['Radiation resistance', 'Turning tiny', 'Radiation blast'], 14 | }, 15 | { 16 | name: 'Madame Uppercut', 17 | age: 39, 18 | secretIdentity: 'Jane Wilson', 19 | powers: ['Million tonne punch', 'Damage resistance', 'Superhuman reflexes'], 20 | }, 21 | { 22 | name: 'Eternal Flame', 23 | age: 1000000, 24 | secretIdentity: 'Unknown', 25 | powers: [ 26 | 'Immortality', 27 | 'Heat Immunity', 28 | 'Inferno', 29 | 'Teleportation', 30 | 'Interdimensional travel', 31 | ], 32 | }, 33 | ], 34 | } 35 | -------------------------------------------------------------------------------- /demo/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { JsonData } from '@json-edit-react' 2 | 3 | export const truncate = (string: string, length = 200) => 4 | string.length < length ? string : `${string.slice(0, length - 2).trim()}...` 5 | 6 | export const getLineHeight = (data: JsonData) => JSON.stringify(data, null, 2).split('\n').length 7 | -------------------------------------------------------------------------------- /demo/src/image/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/src/image/logo_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/demo/src/image/logo_400.png -------------------------------------------------------------------------------- /demo/src/image/logo_square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | import { ChakraProvider } from '@chakra-ui/react' 6 | import theme from './chakra-theme/index.ts' 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /demo/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/src/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | margin: 0; 5 | } 6 | 7 | /* .App { 8 | text-align: center; 9 | } */ 10 | 11 | footer { 12 | height: 50px; 13 | display: flex; 14 | padding: 10px; 15 | justify-content: flex-end; 16 | align-items: flex-end; 17 | } 18 | 19 | .inputRow { 20 | display: flex; 21 | width: 100%; 22 | justify-content: flex-start; 23 | } 24 | 25 | .inputWidth { 26 | max-width: 200px; 27 | } 28 | 29 | .labelWidth { 30 | width: 30%; 31 | } 32 | 33 | .chakra-checkbox__control { 34 | margin-left: 5px; 35 | } 36 | 37 | .block-shadow { 38 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; 39 | } 40 | 41 | .code { 42 | font-family: monospace; 43 | font-size: 90%; 44 | font-weight: 600; 45 | color: dimgray; 46 | } 47 | 48 | /* For CodeMirror */ 49 | .cm-theme-light, 50 | .cm-theme { 51 | width: 100%; 52 | } 53 | 54 | .cm-content, 55 | .cm-gutters { 56 | font-size: 80%; 57 | } 58 | 59 | /* Loading component for CodeMirror */ 60 | .loading { 61 | width: 100%; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | } 66 | -------------------------------------------------------------------------------- /demo/src/useDatabase.ts: -------------------------------------------------------------------------------- 1 | import { getFirestore, doc, setDoc } from 'firebase/firestore' 2 | import { initializeApp } from 'firebase/app' 3 | import { useDocument } from 'react-firebase-hooks/firestore' 4 | import firebaseConfig from './firebaseConfig.json' 5 | import { useMemo } from 'react' 6 | 7 | const firebaseApp = initializeApp(firebaseConfig) 8 | 9 | const db = getFirestore(firebaseApp) 10 | 11 | interface Message { 12 | timeStamp?: string 13 | name?: string 14 | message?: string 15 | } 16 | 17 | export const useDatabase = () => { 18 | const [value, loading, error] = useDocument( 19 | doc(getFirestore(firebaseApp), 'json-edit-react', 'live_json_data') 20 | ) 21 | 22 | const liveData = useMemo(() => { 23 | const { Guestbook, lastEdited, messages } = value?.data() ?? {} 24 | 25 | const messagesTidied = messages 26 | ? messages.map(({ timeStamp, name, message, ...rest }: Message) => ({ 27 | message, 28 | name, 29 | ...rest, 30 | timeStamp, 31 | })) 32 | : [] 33 | 34 | return Guestbook 35 | ? { 36 | Guestbook, 37 | lastEdited, 38 | messages: messagesTidied, 39 | } 40 | : null 41 | }, [value]) 42 | 43 | const updateLiveData = async (data: object) => { 44 | await setDoc( 45 | doc(db, 'json-edit-react', 'live_json_data'), 46 | { ...data, lastEdited: new Date().toISOString() }, 47 | { merge: true } 48 | ) 49 | } 50 | 51 | return { 52 | liveData, 53 | loading, 54 | error, 55 | updateLiveData, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare const __BUILD_TIME__: string 4 | declare const __VERSION__: string 5 | -------------------------------------------------------------------------------- /demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@json-edit-react/*": ["../src/*"], 20 | "@json-edit-react": ["../src/"] 21 | }, 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noUncheckedSideEffectImports": true 28 | }, 29 | "include": ["src", "../custom-component-library/components/DatePicker/DateTimePicker.tsx"] 30 | // "exclude": ["src/json-edit-react/**/*"] 31 | } 32 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /demo/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "resolveJsonModule": true, 9 | "esModuleInterop": true, 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | // "baseUrl": ".", 18 | // "paths": { 19 | // "@json-edit-react/*": ["../src/*"], 20 | // "@json-edit-react": ["../src/"] 21 | // }, 22 | 23 | /* Linting */ 24 | "strict": true, 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noFallthroughCasesInSwitch": true, 28 | "noUncheckedSideEffectImports": true 29 | }, 30 | "include": ["vite.config.ts"] 31 | // "exclude": ["src/json-edit-react/**/*"] 32 | } 33 | -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import path from 'path' 4 | import fs from 'fs-extra' 5 | 6 | type PackageOption = 'npm' | 'local' | 'build' 7 | 8 | // Get the appropriate package.json so we can determine the version 9 | const provider: PackageOption = (process.env.VITE_JRE_SOURCE as PackageOption) ?? 'npm' 10 | 11 | console.log(`Using json-edit-react from: ${provider}`) 12 | 13 | const srcMap: Record = { 14 | npm: { 15 | pkgJson: path.join('node_modules', 'json-edit-react', 'package.json'), 16 | jsonEditReactSrc: 'json-edit-react', 17 | }, 18 | local: { 19 | pkgJson: path.join('..', 'package.json'), 20 | jsonEditReactSrc: path.resolve(__dirname, '../src'), 21 | }, 22 | build: { 23 | pkgJson: path.join('..', 'build_package', 'package.json'), 24 | jsonEditReactSrc: path.resolve(__dirname, '../build_package'), 25 | }, 26 | } 27 | const packageFile = srcMap[provider].pkgJson 28 | const jsonEditReactPath = srcMap[provider].jsonEditReactSrc 29 | const pkg = fs.readJsonSync(packageFile) 30 | 31 | // https://vite.dev/config/ 32 | export default defineConfig({ 33 | plugins: [react()], 34 | base: 'https://carlosnz.github.io/json-edit-react/', 35 | resolve: { 36 | alias: { '@json-edit-react': jsonEditReactPath }, 37 | }, 38 | server: { 39 | port: 5175, 40 | }, 41 | build: { 42 | outDir: 'build', 43 | emptyOutDir: true, 44 | rollupOptions: { 45 | output: { 46 | manualChunks: { 47 | // UI library chunks 48 | chakra: [ 49 | '@chakra-ui/react', 50 | '@chakra-ui/icons', 51 | '@emotion/react', 52 | '@emotion/styled', 53 | 'framer-motion', 54 | ], 55 | // Code editor and related packages 56 | codemirror: [ 57 | '@uiw/react-codemirror', 58 | '@codemirror/lang-json', 59 | '@uiw/codemirror-theme-github', 60 | '@uiw/codemirror-theme-console', 61 | '@uiw/codemirror-theme-quietlight', 62 | '@uiw/codemirror-theme-monokai', 63 | ], 64 | // Icons library 65 | icons: ['react-icons/fa', 'react-icons/bi', 'react-icons/ai'], 66 | // Core React packages 67 | vendor: ['react', 'react-dom', 'wouter', 'use-undo'], 68 | // JSON utilities 69 | json: ['json5', 'ajv'], 70 | }, 71 | }, 72 | }, 73 | chunkSizeWarningLimit: 800, 74 | }, 75 | define: { 76 | __BUILD_TIME__: JSON.stringify( 77 | new Date().toLocaleString('en-NZ', { timeZone: 'Pacific/Auckland' }) 78 | ), 79 | __VERSION__: JSON.stringify(pkg.version), 80 | }, 81 | }) 82 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactPlugin from 'eslint-plugin-react' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { 9 | // Adjust these ignores to match your component project structure 10 | ignores: [ 11 | 'node_modules', 12 | 'dist', 13 | 'build', 14 | 'demo', 15 | '.rollup.cache', 16 | 'custom-component-library', 17 | 'build_package', 18 | ], 19 | }, 20 | { 21 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 22 | files: ['**/*.{ts,tsx}'], 23 | languageOptions: { 24 | ecmaVersion: 2020, 25 | globals: { 26 | ...globals.browser, 27 | React: 'readonly', 28 | }, 29 | }, 30 | plugins: { 31 | react: reactPlugin, 32 | 'react-hooks': reactHooks, 33 | }, 34 | rules: { 35 | ...reactPlugin.configs.recommended.rules, 36 | ...reactHooks.configs.recommended.rules, 37 | 'react/prop-types': 'off', 38 | // Keep React in JSX scope for React 16 compatibility 39 | 'react/react-in-jsx-scope': 'error', 40 | }, 41 | settings: { 42 | react: { 43 | version: 'detect', 44 | }, 45 | }, 46 | }, 47 | // Add JavaScript files support 48 | { 49 | extends: [js.configs.recommended], 50 | files: ['**/*.{js,jsx}'], 51 | languageOptions: { 52 | ecmaVersion: 2020, 53 | globals: { 54 | ...globals.browser, 55 | React: 'readonly', 56 | }, 57 | }, 58 | plugins: { 59 | react: reactPlugin, 60 | 'react-hooks': reactHooks, 61 | }, 62 | rules: { 63 | ...reactPlugin.configs.recommended.rules, 64 | ...reactHooks.configs.recommended.rules, 65 | 'react/prop-types': 'off', 66 | // Keep React in JSX scope for React 16 compatibility 67 | 'react/react-in-jsx-scope': 'error', 68 | }, 69 | settings: { 70 | react: { 71 | version: 'detect', 72 | }, 73 | }, 74 | } 75 | ) 76 | -------------------------------------------------------------------------------- /image/admonition_npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/admonition_npm.png -------------------------------------------------------------------------------- /image/custom_component_levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/custom_component_levels.png -------------------------------------------------------------------------------- /image/custom_text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/custom_text.png -------------------------------------------------------------------------------- /image/enum_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/enum_example.png -------------------------------------------------------------------------------- /image/key_select.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/key_select.png -------------------------------------------------------------------------------- /image/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/logo192.png -------------------------------------------------------------------------------- /image/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/screenshot.png -------------------------------------------------------------------------------- /image/text-editor-comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/text-editor-comparison.png -------------------------------------------------------------------------------- /image/theme_edit_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/theme_edit_after.png -------------------------------------------------------------------------------- /image/theme_edit_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CarlosNZ/json-edit-react/564ff1487f11ec46afa4af9c12aa6200247803c8/image/theme_edit_before.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-edit-react", 3 | "version": "1.27.0", 4 | "description": "React component for editing or viewing JSON/object data", 5 | "main": "build/index.cjs.js", 6 | "module": "build/index.esm.js", 7 | "types": "build/index.d.ts", 8 | "sideEffects": false, 9 | "exports": { 10 | ".": { 11 | "types": "./build/index.d.ts", 12 | "import": "./build/index.esm.js", 13 | "require": "./build/index.cjs.js", 14 | "default": "./build/index.esm.js" 15 | } 16 | }, 17 | "files": [ 18 | "build/**/*" 19 | ], 20 | "repository": "https://github.com/CarlosNZ/json-edit-react.git", 21 | "author": "Carl Smith <5456533+CarlosNZ@users.noreply.github.com>", 22 | "license": "MIT", 23 | "homepage": "https://carlosnz.github.io/json-edit-react", 24 | "scripts": { 25 | "setup": "yarn install && cd demo && yarn install", 26 | "test": "jest", 27 | "dev": "cd demo && yarn && yarn dev", 28 | "demo": "cd demo && yarn && yarn start", 29 | "demo:package": "yarn build-package && cd demo && yarn && yarn start:package", 30 | "prebuild": "yarn lint", 31 | "build": "rollup -c && rm -R build/dts", 32 | "postbuild": "node ./scripts/cleanBuildTypes.cjs", 33 | "build-package": "yarn build && yarn pack --filename package.tgz && tar -xzf package.tgz && mv package build_package && rm package.tgz", 34 | "lint": "eslint", 35 | "prepareReadme": "python3 scripts/build_npm_readme.py", 36 | "prepublishOnly": "yarn build && yarn prepareReadme && python3 scripts/use_npm_readme.py prepare", 37 | "postpublish": "python3 scripts/use_npm_readme.py restore && node scripts/installLatestPackage.mjs pause &", 38 | "compile": "tsc --noEmit && ts-prune", 39 | "release": "yarn publish", 40 | "release-demo": "cd demo && yarn deploy" 41 | }, 42 | "peerDependencies": { 43 | "react": ">=16.0.0" 44 | }, 45 | "dependencies": { 46 | "object-property-assigner": "^1.3.5", 47 | "object-property-extractor": "^1.0.13" 48 | }, 49 | "devDependencies": { 50 | "@eslint/js": "^9.24.0", 51 | "@rollup/plugin-node-resolve": "^16.0.0", 52 | "@rollup/plugin-terser": "^0.4.4", 53 | "@rollup/plugin-typescript": "^11.1.6", 54 | "@types/jest": "^29.5.14", 55 | "@types/node": "^20.11.17", 56 | "@types/react": ">=16.0.0", 57 | "eslint": "^9.24.0", 58 | "eslint-plugin-react": "^7.37.5", 59 | "eslint-plugin-react-hooks": "^5.2.0", 60 | "fs-extra": "^11.2.0", 61 | "globals": "^16.0.0", 62 | "jest": "^29.7.0", 63 | "react": "^19.1.0", 64 | "react-dom": "^19.1.0", 65 | "rollup": "^4.10.0", 66 | "rollup-plugin-bundle-size": "^1.0.3", 67 | "rollup-plugin-delete": "^3.0.1", 68 | "rollup-plugin-dts": "^6.1.0", 69 | "rollup-plugin-peer-deps-external": "^2.2.4", 70 | "rollup-plugin-sizes": "^1.0.6", 71 | "rollup-plugin-styles": "^4.0.0", 72 | "ts-jest": "^29.2.5", 73 | "ts-node": "^10.9.2", 74 | "ts-prune": "^0.10.3", 75 | "tslib": "^2.6.2", 76 | "typescript": "^5.3.3", 77 | "typescript-eslint": "^8.29.1" 78 | }, 79 | "keywords": [ 80 | "react", 81 | "component", 82 | "components", 83 | "interactive", 84 | "interactive-json", 85 | "json", 86 | "json-component", 87 | "json-display", 88 | "json-tree", 89 | "json-viewer", 90 | "json-inspector", 91 | "json-schema", 92 | "json-editor", 93 | "editor", 94 | "data-viewer", 95 | "form-builder", 96 | "drag-and-drop", 97 | "customizable", 98 | "typescript", 99 | "react-component", 100 | "react-json", 101 | "theme", 102 | "tree-view", 103 | "treeview", 104 | "react-json-view" 105 | ] 106 | } 107 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript' 2 | import dts from 'rollup-plugin-dts' 3 | import peerDepsExternal from 'rollup-plugin-peer-deps-external' 4 | import styles from 'rollup-plugin-styles' 5 | import terser from '@rollup/plugin-terser' 6 | import del from 'rollup-plugin-delete' 7 | import bundleSize from 'rollup-plugin-bundle-size' 8 | import sizes from 'rollup-plugin-sizes' 9 | 10 | export default [ 11 | // Main Package 12 | { 13 | input: 'src/index.ts', 14 | output: [ 15 | { 16 | file: 'build/index.cjs.js', 17 | format: 'cjs', 18 | }, 19 | { 20 | file: 'build/index.esm.js', 21 | format: 'esm', 22 | }, 23 | ], 24 | plugins: [ 25 | del({ targets: 'build/*' }), 26 | styles({ minimize: true }), 27 | peerDepsExternal({ includeDependencies: true }), 28 | typescript({ 29 | module: 'ESNext', 30 | target: 'es6', 31 | declaration: true, 32 | declarationDir: 'build/dts', 33 | }), 34 | terser(), 35 | bundleSize(), 36 | sizes(), 37 | ], 38 | external: ['object-property-assigner', 'object-property-extractor'], 39 | }, 40 | // Types 41 | { 42 | input: 'build/dts/index.d.ts', 43 | output: [{ file: 'build/index.d.ts', format: 'es' }], 44 | external: [/\.css$/], 45 | plugins: [dts()], 46 | }, 47 | ] 48 | -------------------------------------------------------------------------------- /scripts/build_npm_readme.py: -------------------------------------------------------------------------------- 1 | """ 2 | Github README to NPM README transformation script 3 | 4 | This script takes the README_npm.md file and replaces the content blocks (marked 5 | by {{ }}) with the corresponding blocks from the main README.md file, which is 6 | for Github. This is so we can re-use the introductory content in both READMEs 7 | without duplicating it. 8 | 9 | It also converts Github-style admonition blocks to HTML that mimics Github 10 | styling and converts internal anchor links to full GitHub repository URLs. 11 | """ 12 | 13 | import re 14 | 15 | def extract_block_from_source(source_content, block_name): 16 | """ 17 | Extract a block from the source content based on the block name. 18 | 19 | Args: 20 | source_content (str): The entire content of the source markdown file 21 | block_name (str): The name of the block to extract 22 | 23 | Returns: 24 | str: The extracted block content, or an empty string if not found 25 | """ 26 | # Create a regex pattern to find the block between comments 27 | pattern = rf'(.*?)' 28 | 29 | # Use re.DOTALL to match across multiple lines 30 | match = re.search(pattern, source_content, re.DOTALL) 31 | 32 | return match.group(1).strip() if match else f"[Block {block_name} not found]" 33 | 34 | def convert_github_admonition(text): 35 | """ 36 | Convert Github-style admonition blocks to HTML that mimics Github styling. 37 | 38 | Args: 39 | text (str): The input text potentially containing Github admonition blocks 40 | 41 | Returns: 42 | str: Converted text with Github admonitions replaced by HTML 43 | """ 44 | # Emoji and color mapping for different admonition types 45 | admonition_styles = { 46 | 'IMPORTANT': {'emoji': '🚨', 'color': '#d63384'}, 47 | 'NOTE': {'emoji': '📝', 'color': '#0075ff'}, 48 | 'WARNING': {'emoji': '⚠️', 'color': '#bf8700'}, 49 | 'TIP': {'emoji': '💡', 'color': '#3aa76d'} 50 | } 51 | 52 | # Regex to match Github-style admonition blocks 53 | pattern = r'> \[!(.*?)\]\n((?:> .*\n)*)' 54 | 55 | def replace_admonition(match): 56 | admonition_type = match.group(1) 57 | content = match.group(2) 58 | 59 | # Remove the '> ' prefix from each line while preserving original formatting 60 | content_lines = [line[2:] for line in content.split('\n') if line.startswith('> ')] 61 | 62 | # Use default styles for unknown types 63 | style = admonition_styles.get(admonition_type, 64 | {'emoji': '❗', 'color': '#0075ff'}) 65 | 66 | # Create an HTML block that looks similar to Github's admonition 67 | content = '\n'.join(content_lines) 68 | return f'''
69 |

70 | {style['emoji']} {admonition_type}: 71 |

72 | 73 | {content} 74 |
\n''' 75 | 76 | return re.sub(pattern, replace_admonition, text, flags=re.MULTILINE) 77 | 78 | def convert_internal_links(text, base_url="https://github.com/CarlosNZ/json-edit-react"): 79 | """ 80 | Convert internal Markdown anchor links to full GitHub documentation links. 81 | 82 | Args: 83 | text (str): The input text containing internal links 84 | base_url (str): The base URL for the GitHub repository 85 | 86 | Returns: 87 | str: Text with converted links 88 | """ 89 | # Regex to match internal Markdown links: [text](#anchor) 90 | # But avoid matching links that already have a full URL or are not anchors 91 | pattern = r'\[([^\]]+)\]\(#([^)]+)\)' 92 | 93 | def replace_link(match): 94 | link_text = match.group(1) 95 | anchor = match.group(2) 96 | 97 | # Create the full GitHub URL with the anchor 98 | return f'[{link_text}]({base_url}#{anchor})' 99 | 100 | return re.sub(pattern, replace_link, text) 101 | 102 | def replace_blocks(content_file, source_file, output_file=None, base_url="https://github.com/CarlosNZ/json-edit-react"): 103 | """ 104 | Replace blocks in the content file with corresponding blocks from the source file, 105 | and convert internal links to full GitHub links. 106 | 107 | Args: 108 | content_file (str): Path to the content markdown file 109 | source_file (str): Path to the source markdown file 110 | output_file (str, optional): Path to save the modified content. 111 | If None, returns the modified content. 112 | base_url (str): The base URL for the GitHub repository 113 | 114 | Returns: 115 | str: Modified content if no output file is specified 116 | """ 117 | # Read source and content files 118 | with open(source_file, 'r', encoding='utf-8') as f: 119 | source_content = f.read() 120 | 121 | with open(content_file, 'r', encoding='utf-8') as f: 122 | content = f.read() 123 | 124 | # Find block markers to replace, excluding those inside comments 125 | def replace_non_comment_blocks(match): 126 | # Check if the match is inside a comment 127 | block = match.group(1) 128 | full_match = match.group(0) 129 | preceding_content = content[:match.start()] 130 | 131 | # Count the number of comment start and end tags before this match 132 | comment_starts = len(re.findall(r'', preceding_content)) 134 | 135 | # If inside a comment (more starts than ends), return the original match 136 | if comment_starts > comment_ends: 137 | return full_match 138 | 139 | # Otherwise, replace the block, removing the {{ }} 140 | return extract_block_from_source(source_content, block) 141 | 142 | # Use regex with a callback to replace blocks 143 | modified_content = re.sub(r'{{(.*?)}}', replace_non_comment_blocks, content) 144 | 145 | # Convert Github admonition blocks 146 | modified_content = convert_github_admonition(modified_content) 147 | 148 | # Convert internal links to full GitHub links 149 | modified_content = convert_internal_links(modified_content, base_url) 150 | 151 | # If output file is specified, write to file 152 | if output_file: 153 | with open(output_file, 'w', encoding='utf-8') as f: 154 | f.write(modified_content) 155 | print(f"Modified content written to {output_file}") 156 | return None 157 | 158 | return modified_content 159 | 160 | # Example usage 161 | def main(): 162 | replace_blocks( 163 | content_file='.README_npm.md', 164 | source_file='README.md', 165 | output_file='.README_npm_output.md', 166 | base_url="https://github.com/CarlosNZ/json-edit-react" 167 | ) 168 | 169 | if __name__ == '__main__': 170 | main() -------------------------------------------------------------------------------- /scripts/cleanBuildTypes.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * There is a problem with the export of the Typescript declarations file: the 3 | * current version of Typescript exports types with the "type" prefix, e.g. 4 | * 5 | * {export type Type1, type Type2, normalFunction...} 6 | * 7 | * However, older versions of Typescript don't understand this syntax, and throw 8 | * an error when using this package. Current workaround is to run this script 9 | * immediately after build (postbuild script in package.json), which replaces 10 | * the "type" exports with "old" style ones, e.g: 11 | * 12 | * {export Type1, Type2, normalFunction...} 13 | * 14 | * This will then work on both new and old Typescript environments. 15 | * 16 | * There has to a better way to handle this, but I haven't found it. If you 17 | * know, please let me know: https://github.com/CarlosNZ/json-edit-react 18 | */ 19 | 20 | const fs = require('fs-extra') 21 | 22 | const exportTextMatch = /^export type { .+ };$/m 23 | 24 | const cleanBuildTypes = async () => { 25 | const data = await fs.readFile('build/index.d.ts', 'utf8') 26 | const exportText = exportTextMatch.exec(data)[0] 27 | const cleanedText = '\n// Types\n' + exportText.replace('export type ', 'export ') 28 | const newData = data.replace(exportText, cleanedText) 29 | console.log('Cleaning up type declarations...') 30 | 31 | await fs.writeFile('build/index.d.ts', newData) 32 | } 33 | 34 | cleanBuildTypes() 35 | -------------------------------------------------------------------------------- /scripts/installLatestPackage.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { execSync } from 'child_process' 4 | 5 | const foldersToInstall = ['demo', 'custom-component-library'] 6 | const packageName = 'json-edit-react' 7 | 8 | /** 9 | * Installs the most recent version of a package (stable or beta) 10 | * @param {string} packageName - The name of the package to check 11 | */ 12 | async function installLatestPackage() { 13 | console.log('Installing most recent version of the published package in apps...') 14 | try { 15 | // Get all package info in a single call 16 | const packageData = JSON.parse(execSync(`npm view ${packageName} --json`).toString()) 17 | 18 | // Get stable version from dist-tags (always exists) 19 | const stableVersion = packageData['dist-tags'].latest 20 | const stableDate = new Date(packageData.time[stableVersion]) 21 | 22 | console.log(`Latest stable: ${stableVersion} (${stableDate.toISOString()})`) 23 | 24 | // Check if beta tag exists 25 | if (!packageData['dist-tags'].beta) { 26 | console.log('No beta version available. Installing stable version') 27 | installPackage(packageName, stableVersion, foldersToInstall) 28 | return 29 | } 30 | 31 | // Get beta version and timestamp 32 | const betaVersion = packageData['dist-tags'].beta 33 | const betaDate = new Date(packageData.time[betaVersion]) 34 | 35 | console.log(`Latest beta: ${betaVersion} (${betaDate.toISOString()})`) 36 | 37 | // Compare dates and install the most recent version 38 | if (betaDate > stableDate) { 39 | installPackage(packageName, betaVersion, foldersToInstall) 40 | } else { 41 | installPackage(packageName, stableVersion, foldersToInstall) 42 | } 43 | } catch (error) { 44 | console.error('Error checking or installing package:', error.message) 45 | } 46 | } 47 | 48 | const installPackage = (packageName, version, installFolders) => { 49 | for (const folder of installFolders) { 50 | console.log(`Installing version ${version} in ${folder}`) 51 | execSync(`cd ${folder} && yarn add ${packageName}@${version}`, { stdio: 'inherit' }) 52 | } 53 | } 54 | 55 | const pause = process.argv[2] === 'pause' 56 | 57 | if (pause) { 58 | console.log('Pausing installation to wait for newly published package...') 59 | setTimeout(() => installLatestPackage(), 10_000) 60 | } else installLatestPackage(packageName) 61 | -------------------------------------------------------------------------------- /scripts/use_npm_readme.py: -------------------------------------------------------------------------------- 1 | """ 2 | use-npm-readme.py 3 | 4 | This script handles the README file swapping process for npm publishing. 5 | 6 | After the README_npm_output.md file is generated by the conversion script, this script is run by the `prepublish` hook, which (temporarily) replaces the main README with the npm version. After publishing, the original README is restored. 7 | """ 8 | 9 | import os 10 | import shutil 11 | import sys 12 | import argparse 13 | 14 | 15 | def prepare_for_publish(): 16 | """ 17 | Prepare README for npm publishing by replacing it with the npm-compatible version. 18 | """ 19 | # Backup the original README if it hasn't been backed up already 20 | if os.path.exists('README.md') and not os.path.exists('.original-readme.md'): 21 | shutil.copy2('README.md', '.original-readme.md') 22 | print("Original README backed up as .original-readme.md") 23 | 24 | # Use the npm version as the main README 25 | if os.path.exists('.README_npm_output.md'): 26 | shutil.copy2('.README_npm_output.md', 'README.md') 27 | print("NPM README is now in place for publishing") 28 | else: 29 | print("Error: .README_npm_output.md not found. Run the conversion script first.") 30 | sys.exit(1) 31 | 32 | 33 | def restore_after_publish(): 34 | """ 35 | Restore the original README after npm publishing completes. 36 | """ 37 | # Restore the original README 38 | if os.path.exists('.original-readme.md'): 39 | shutil.copy2('.original-readme.md', 'README.md') 40 | os.remove('.original-readme.md') 41 | print("Original README restored") 42 | else: 43 | print("Warning: No backed-up README found to restore") 44 | 45 | # Remove the temporary npm README 46 | if os.path.exists('.README_npm_output.md'): 47 | os.remove('.README_npm_output.md') 48 | print("Temporary npm README removed") 49 | 50 | 51 | def main(): 52 | parser = argparse.ArgumentParser( 53 | description='Manage README files for npm publishing process' 54 | ) 55 | parser.add_argument( 56 | 'action', 57 | choices=['prepare', 'restore'], 58 | help='Action to perform: "prepare" before publishing or "restore" after' 59 | ) 60 | 61 | args = parser.parse_args() 62 | 63 | if args.action == 'prepare': 64 | prepare_for_publish() 65 | elif args.action == 'restore': 66 | restore_after_publish() 67 | 68 | 69 | if __name__ == "__main__": 70 | main() -------------------------------------------------------------------------------- /src/AutogrowTextArea.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * This uses a cheeky hack to make the text-area input resize automatically 3 | * based on the content. It seemed necessary, as the text inputs (String or raw 4 | * JSON) could reasonably be anything from a single character to several hundred 5 | * lines. 6 | * 7 | * See https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas for 8 | * the basic idea of how it works. 9 | */ 10 | 11 | import React from 'react' 12 | 13 | interface TextAreaProps { 14 | className: string 15 | name: string 16 | value: string 17 | setValue: React.Dispatch> 18 | handleKeyPress: (e: React.KeyboardEvent) => void 19 | styles: React.CSSProperties 20 | textAreaRef?: React.MutableRefObject 21 | } 22 | 23 | export const AutogrowTextArea: React.FC = ({ 24 | className, 25 | name, 26 | value, 27 | setValue, 28 | handleKeyPress, 29 | styles, 30 | textAreaRef, 31 | }) => { 32 | // Adding extra (hidden) char when adding new lines to input prevents 33 | // mis-alignment between real value and dummy value 34 | if (typeof value !== 'string') return null 35 | const dummyValue = value.slice(-1) === '\n' ? value + '.' : value 36 | 37 | return ( 38 |
39 |