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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Features include:

- edit individual values, or whole objects as JSON text
- fine-grained control over which elements can be edited, deleted, or added to
- customisable UI, through simple, pre-defined [themes](#themes--styles), or specific CSS overrides
- customisable UI, through simple, pre-defined [themes](#themes--styles), specific CSS overrides for UI components, or by targeting CSS classes
- self-contained — rendered with plain HTML/CSS, so no dependance on external UI libraries
- provide your own [custom component](#custom-nodes) to integrate specialised UI for certain data.

Expand Down Expand Up @@ -73,6 +73,7 @@ It's pretty self explanatory (click the "edit" icon to edit, etc.), but there ar
- It's the opposite when editing a full object/array node (which you do by clicking "edit" on an object or array value) — `Enter` for new line, and `Cmd/Ctrl/Shift-Enter` for submit
- `Escape` to cancel editing
- When clicking the "clipboard" icon, holding down `Cmd/Ctrl` will copy the *path* to the selected node rather than its value
- When opening/closing a node, hold down "Alt/Option" to open/close *all* child nodes at once

## Props overview

Expand Down Expand Up @@ -467,6 +468,8 @@ This component is heavily inspired by [react-json-view](https://github.com/mac-s

## Changelog

- **1.5.0**:
- Open/close all descendant nodes by holding "Alt"/"Option" while opening/closing a node
- **1.4.0**:
- [Style functions](#themes--styles) for context-dependent styling
- Handle "loose" ([JSON5](https://json5.org/)) JSON text input(e.g. non-quoted keys, trailing commas, etc.)
Expand Down
4 changes: 2 additions & 2 deletions demo/src/JsonEditImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
FilterFunction,
LinkCustomComponent,
LinkCustomNodeDefinition,
// } from './json-edit-react/src'
} from 'json-edit-react'
} from './json-edit-react/src'
// } from 'json-edit-react'
// } from './package'

export {
Expand Down
2 changes: 2 additions & 0 deletions demo/src/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const data: Record<string, DemoData> = {
'"Escape" to cancel': '👍',
'To start a new line': 'Shift/Ctrl/Cmd-Enter (or just "Enter" when editing JSON nodes)',
'When copying to clipboard': 'Hold down "Ctrl/Cmd" to copy path instead of data',
'When opening/closing a node':
'Hold down "Alt/Option" to open/close ALL child nodes at once',
},
},
customNodeDefinitions: [dateNodeDefinition],
Expand Down
41 changes: 41 additions & 0 deletions src/CollapseProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { createContext, useContext, useState } from 'react'
import { type CollectionKey } from './types'

interface CollapseAllState {
path: CollectionKey[]
open: boolean
}
interface CollapseContext {
collapseState: CollapseAllState | null
setCollapseState: React.Dispatch<React.SetStateAction<CollapseAllState | null>>
doesPathMatch: (path: CollectionKey[]) => boolean
}
const initialContext: CollapseContext = {
collapseState: null,
setCollapseState: () => {},
doesPathMatch: () => false,
}

const CollapseProviderContext = createContext(initialContext)

export const CollapseProvider = ({ children }: { children: React.ReactNode }) => {
const [collapseState, setCollapseState] = useState<CollapseAllState | null>(null)

const doesPathMatch = (path: CollectionKey[]) => {
if (collapseState === null) return false

for (const [index, value] of collapseState.path.entries()) {
if (value !== path[index]) return false
}

return true
}

return (
<CollapseProviderContext.Provider value={{ collapseState, setCollapseState, doesPathMatch }}>
{children}
</CollapseProviderContext.Provider>
)
}

export const useCollapseAll = () => useContext(CollapseProviderContext)
18 changes: 16 additions & 2 deletions src/CollectionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Icon } from './Icons'
import './style.css'
import { AutogrowTextArea } from './AutogrowTextArea'
import { useTheme } from './theme'
import { useCollapseAll } from './CollapseProvider'

export const isCollection = (value: unknown) => value !== null && typeof value === 'object'

Expand All @@ -19,6 +20,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
...props
}) => {
const { getStyles } = useTheme()
const { collapseState, setCollapseState, doesPathMatch } = useCollapseAll()
const {
onEdit,
onAdd,
Expand Down Expand Up @@ -62,6 +64,13 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
setCollapsed(collapseFilter(nodeData))
}, [collapseFilter])

useEffect(() => {
if (collapseState !== null && doesPathMatch(path)) {
hasBeenOpened.current = true
setCollapsed(collapseState.open)
}
}, [collapseState])

const collectionType = Array.isArray(data) ? 'array' : 'object'
const brackets =
collectionType === 'array' ? { open: '[', close: ']' } : { open: '{', close: '}' }
Expand All @@ -75,7 +84,12 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
else if (e.key === 'Escape') handleCancel()
}

const handleCollapse = () => {
const handleCollapse = (e: React.MouseEvent) => {
if (e.getModifierState('Alt')) {
hasBeenOpened.current = true
setCollapseState({ open: !collapsed, path })
return
}
if (!isEditing) {
setIsAnimating(true)
hasBeenOpened.current = true
Expand Down Expand Up @@ -274,7 +288,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
>
<div className="jer-collection-header-row" style={{ position: 'relative' }}>
<div className="jer-collection-name">
<div className="jer-collapse-icon" onClick={handleCollapse}>
<div className="jer-collapse-icon" onClick={(e) => handleCollapse(e)}>
<Icon name="chevron" rotate={collapsed} nodeData={nodeData} />
</div>
{!isEditingKey && (
Expand Down
7 changes: 6 additions & 1 deletion src/JsonEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type NodeData,
} from './types'
import { useTheme, ThemeProvider } from './theme'
import { CollapseProvider, useCollapseAll } from './CollapseProvider'
import { getTranslateFunction } from './localisation'
import './style.css'
import { ValueNodeWrapper } from './ValueNodeWrapper'
Expand Down Expand Up @@ -45,6 +46,7 @@ const Editor: React.FC<JsonEditorProps> = ({
customNodeDefinitions = [],
}) => {
const { getStyles, setTheme, setIcons } = useTheme()
const { setCollapseState } = useCollapseAll()
const collapseFilter = useCallback(getFilterFunction(collapse), [collapse])
const translate = useCallback(getTranslateFunction(translations, customText), [
translations,
Expand All @@ -59,6 +61,7 @@ const Editor: React.FC<JsonEditorProps> = ({
}, [theme, icons])

useEffect(() => {
setCollapseState(null)
setData(srcData)
}, [srcData])

Expand Down Expand Up @@ -178,7 +181,9 @@ const Editor: React.FC<JsonEditorProps> = ({

const JsonEditor: React.FC<JsonEditorProps> = (props) => (
<ThemeProvider>
<Editor {...props} />
<CollapseProvider>
<Editor {...props} />
</CollapseProvider>
</ThemeProvider>
)

Expand Down