Skip to content

Commit 5f45ab2

Browse files
authored
#57 Long string fixes (#60)
* Better handling of component height with lots of lines * Improvements to resizing and edit controls * Rename Collapse Provider * Finish rename, improve isEditing logic * Organise derived values better * Make key editing part of exclusive edit * Key edit for ValueNodes
1 parent 8e6ec26 commit 5f45ab2

File tree

9 files changed

+192
-127
lines changed

9 files changed

+192
-127
lines changed

src/AutogrowTextArea.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const AutogrowTextArea: React.FC<TextAreaProps> = ({
3636
return (
3737
<div style={{ display: 'grid' }}>
3838
<textarea
39-
id={name}
39+
id={`${name}_textarea`}
4040
style={{
4141
height: 'auto',
4242
gridArea: '1 / 1 / 2 / 2',
@@ -47,7 +47,7 @@ export const AutogrowTextArea: React.FC<TextAreaProps> = ({
4747
}}
4848
rows={1}
4949
className={className}
50-
name={name}
50+
name={`${name}_textarea`}
5151
value={value}
5252
onChange={(e) => setValue(e.target.value)}
5353
autoFocus

src/CollapseProvider.tsx

Lines changed: 0 additions & 41 deletions
This file was deleted.

src/CollectionNode.tsx

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { filterNode, isCollection } from './filterHelpers'
1414
import './style.css'
1515
import { AutogrowTextArea } from './AutogrowTextArea'
1616
import { useTheme } from './theme'
17-
import { useCollapseAll } from './CollapseProvider'
17+
import { useTreeState } from './TreeStateProvider'
18+
import { toPathString } from './ValueNodes'
1819

1920
export const CollectionNode: React.FC<CollectionNodeProps> = ({
2021
data,
@@ -24,7 +25,14 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
2425
...props
2526
}) => {
2627
const { getStyles } = useTheme()
27-
const { collapseState, setCollapseState, doesPathMatch } = useCollapseAll()
28+
const {
29+
collapseState,
30+
setCollapseState,
31+
doesPathMatch,
32+
currentlyEditingElement,
33+
setCurrentlyEditingElement,
34+
areChildrenBeingEdited,
35+
} = useTreeState()
2836
const {
2937
onEdit,
3038
onAdd,
@@ -43,8 +51,6 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
4351
translate,
4452
customNodeDefinitions,
4553
} = props
46-
const [isEditing, setIsEditing] = useState(false)
47-
const [isEditingKey, setIsEditingKey] = useState(false)
4854
const [stringifiedValue, setStringifiedValue] = useState(JSON.stringify(data, null, 2))
4955
const [error, setError] = useState<string | null>(null)
5056

@@ -54,6 +60,8 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
5460
const nodeData = { ...incomingNodeData, collapsed }
5561
const { path, key: name, size } = nodeData
5662

63+
const pathString = toPathString(path)
64+
5765
// This allows us to not render the children on load if they're hidden (which
5866
// gives a big performance improvement with large data sets), but still keep
5967
// the animation transition when opening and closing the accordion
@@ -129,7 +137,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
129137
setCollapseState({ open: !collapsed, path })
130138
return
131139
}
132-
if (!isEditing) {
140+
if (!(currentlyEditingElement && currentlyEditingElement.includes(pathString))) {
133141
setIsAnimating(true)
134142
hasBeenOpened.current = true
135143
setCollapsed(!collapsed)
@@ -146,7 +154,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
146154
const handleEdit = () => {
147155
try {
148156
const value = JSON5.parse(stringifiedValue)
149-
setIsEditing(false)
157+
setCurrentlyEditingElement(null)
150158
setError(null)
151159
if (JSON.stringify(value) === JSON.stringify(data)) return
152160
onEdit(value, path).then((error) => {
@@ -160,7 +168,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
160168
}
161169

162170
const handleEditKey = (newKey: string) => {
163-
setIsEditingKey(false)
171+
setCurrentlyEditingElement(null)
164172
if (name === newKey) return
165173
if (!parentData) return
166174
const parentPath = path.slice(0, -1)
@@ -204,30 +212,39 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
204212
: undefined
205213

206214
const handleCancel = () => {
207-
setIsEditing(false)
208-
setIsEditingKey(false)
215+
setCurrentlyEditingElement(null)
209216
setError(null)
210217
setStringifiedValue(JSON.stringify(data, null, 2))
211218
}
212219

220+
// DERIVED VALUES (this makes the render logic easier to understand)
221+
const isEditing = currentlyEditingElement === pathString
222+
const isEditingKey = currentlyEditingElement === `key_${pathString}`
213223
const isArray = typeof path.slice(-1)[0] === 'number'
214224
const showLabel = showArrayIndices || !isArray
215225
const showCount = showCollectionCount === 'when-closed' ? collapsed : showCollectionCount
226+
const showEditButtons = !isEditing && showEditTools
227+
const showKey = showLabel && !hideKey && name !== '' && name !== undefined
228+
const showCustomNodeContents =
229+
CustomNode && ((isEditing && showOnEdit) || (!isEditing && showOnView))
230+
const sortKeys = keySort && collectionType === 'object'
216231

217232
const keyValueArray = Object.entries(data).map(([key, value]) => [
218233
collectionType === 'array' ? Number(key) : key,
219234
value,
220235
])
221236

222-
if (keySort && collectionType === 'object') {
237+
if (sortKeys) {
223238
keyValueArray.sort(
224239
typeof keySort === 'function' ? (a: string[], b) => keySort(a[0], b[0] as string) : undefined
225240
)
226241
}
227242

228243
// A crude measure to estimate the approximate height of the block, for
229-
// setting the max-height in the collapsible interior
230-
const numOfLines = JSON.stringify(data, null, 2).split('\n').length
244+
// setting the max-height in the collapsible interior.
245+
// The Regexp replacement is to parse escaped line breaks *within* the JSON
246+
// into *actual* line breaks before splitting
247+
const numOfLines = JSON.stringify(data, null, 2).replace(/\\n/g, '\n').split('\n').length
231248

232249
const CollectionChildren = !hasBeenOpened.current ? null : !isEditing ? (
233250
keyValueArray.map(([key, value], index) => {
@@ -274,7 +291,7 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
274291
<div>
275292
<AutogrowTextArea
276293
className="jer-collection-text-area"
277-
name={path.join('.')}
294+
name={pathString}
278295
value={stringifiedValue}
279296
setValue={setStringifiedValue}
280297
isEditing={isEditing}
@@ -306,24 +323,23 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
306323
handleCancel,
307324
handleKeyPress,
308325
isEditing,
309-
setIsEditing,
326+
setIsEditing: () => setCurrentlyEditingElement(pathString),
310327
getStyles,
311328
}
312329

313-
const CollectionContents =
314-
CustomNode && ((isEditing && showOnEdit) || (!isEditing && showOnView)) ? (
315-
<CustomNode customNodeProps={customNodeProps} {...customNodeAllProps}>
316-
{CollectionChildren}
317-
</CustomNode>
318-
) : (
319-
CollectionChildren
320-
)
330+
const CollectionContents = showCustomNodeContents ? (
331+
<CustomNode customNodeProps={customNodeProps} {...customNodeAllProps}>
332+
{CollectionChildren}
333+
</CustomNode>
334+
) : (
335+
CollectionChildren
336+
)
321337

322338
const KeyDisplay = isEditingKey ? (
323339
<input
324340
className="jer-collection-name"
325341
type="text"
326-
name={path.join('.')}
342+
name={pathString}
327343
defaultValue={name}
328344
autoFocus
329345
onFocus={(e) => e.target.select()}
@@ -333,19 +349,19 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
333349
) : (
334350
<span
335351
style={getStyles('property', nodeData)}
336-
onDoubleClick={() => canEditKey && setIsEditingKey(true)}
352+
onDoubleClick={() => canEditKey && setCurrentlyEditingElement(`key_${pathString}`)}
337353
>
338-
{showLabel && !hideKey && name !== '' && name !== undefined ? `${name}:` : null}
354+
{showKey ? `${name}:` : null}
339355
</span>
340356
)
341357

342-
const EditButtonDisplay = !isEditing && showEditTools && (
358+
const EditButtonDisplay = showEditButtons && (
343359
<EditButtons
344360
startEdit={
345361
canEdit
346362
? () => {
347363
hasBeenOpened.current = true
348-
setIsEditing(true)
364+
setCurrentlyEditingElement(pathString)
349365
setCollapsed(false)
350366
}
351367
: undefined
@@ -412,7 +428,15 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
412428
<div
413429
className={'jer-collection-inner'}
414430
style={{
415-
maxHeight: isCollapsed ? 0 : !isEditing ? `${numOfLines * 3}em` : undefined,
431+
// Don't limit the height when collection or any of its children are
432+
// being edited, so it won't overlap lower elements if the editing
433+
// input gets too large. This won't cause problems, as it can't be
434+
// collapsed while being edited anyway.
435+
maxHeight: isCollapsed
436+
? 0
437+
: !areChildrenBeingEdited(pathString)
438+
? `${numOfLines * 3}em`
439+
: undefined,
416440
overflowY: isCollapsed || isAnimating ? 'hidden' : 'visible',
417441
// Need to use max-height for animation to work, unfortunately
418442
// "height: auto" doesn't 😔
@@ -437,11 +461,11 @@ export const CollectionNode: React.FC<CollectionNodeProps> = ({
437461
</div>
438462
)
439463

440-
if (CustomWrapper) {
441-
return (
442-
<CustomWrapper customNodeProps={wrapperProps} {...customNodeAllProps}>
443-
{CollectionNodeComponent}
444-
</CustomWrapper>
445-
)
446-
} else return CollectionNodeComponent
464+
return CustomWrapper ? (
465+
<CustomWrapper customNodeProps={wrapperProps} {...customNodeAllProps}>
466+
{CollectionNodeComponent}
467+
</CustomWrapper>
468+
) : (
469+
CollectionNodeComponent
470+
)
447471
}

src/JsonEditor.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
type SearchFilterFunction,
1313
} from './types'
1414
import { useTheme, ThemeProvider } from './theme'
15-
import { CollapseProvider, useCollapseAll } from './CollapseProvider'
15+
import { TreeStateProvider, useTreeState } from './TreeStateProvider'
1616
import { getTranslateFunction } from './localisation'
1717
import './style.css'
1818
import { ValueNodeWrapper } from './ValueNodeWrapper'
@@ -54,7 +54,7 @@ const Editor: React.FC<JsonEditorProps> = ({
5454
customNodeDefinitions = [],
5555
}) => {
5656
const { getStyles, setTheme, setIcons } = useTheme()
57-
const { setCollapseState } = useCollapseAll()
57+
const { setCollapseState } = useTreeState()
5858
const collapseFilter = useCallback(getFilterFunction(collapse), [collapse])
5959
const translate = useCallback(getTranslateFunction(translations, customText), [
6060
translations,
@@ -212,9 +212,9 @@ const Editor: React.FC<JsonEditorProps> = ({
212212

213213
const JsonEditor: React.FC<JsonEditorProps> = (props) => (
214214
<ThemeProvider>
215-
<CollapseProvider>
215+
<TreeStateProvider>
216216
<Editor {...props} />
217-
</CollapseProvider>
217+
</TreeStateProvider>
218218
</ThemeProvider>
219219
)
220220

src/TreeStateProvider.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Captures the collapse state and the editing state of the entire tree, as
3+
* nodes sometimes need to know the state of other (sibling or child) nodes
4+
*/
5+
6+
import React, { createContext, useContext, useState } from 'react'
7+
import { type CollectionKey } from './types'
8+
9+
interface CollapseAllState {
10+
path: CollectionKey[]
11+
open: boolean
12+
}
13+
interface TreeStateContext {
14+
collapseState: CollapseAllState | null
15+
setCollapseState: React.Dispatch<React.SetStateAction<CollapseAllState | null>>
16+
doesPathMatch: (path: CollectionKey[]) => boolean
17+
currentlyEditingElement: string | null
18+
setCurrentlyEditingElement: React.Dispatch<React.SetStateAction<string | null>>
19+
areChildrenBeingEdited: (pathString: string) => boolean
20+
}
21+
const initialContext: TreeStateContext = {
22+
collapseState: null,
23+
setCollapseState: () => {},
24+
doesPathMatch: () => false,
25+
currentlyEditingElement: null,
26+
setCurrentlyEditingElement: () => {},
27+
areChildrenBeingEdited: () => false,
28+
}
29+
30+
const TreeStateProviderContext = createContext(initialContext)
31+
32+
export const TreeStateProvider = ({ children }: { children: React.ReactNode }) => {
33+
const [collapseState, setCollapseState] = useState<CollapseAllState | null>(null)
34+
const [currentlyEditingElement, setCurrentlyEditingElement] = useState<string | null>(null)
35+
36+
const doesPathMatch = (path: CollectionKey[]) => {
37+
if (collapseState === null) return false
38+
39+
for (const [index, value] of collapseState.path.entries()) {
40+
if (value !== path[index]) return false
41+
}
42+
43+
return true
44+
}
45+
46+
const areChildrenBeingEdited = (pathString: string) =>
47+
currentlyEditingElement !== null && currentlyEditingElement.includes(pathString)
48+
49+
return (
50+
<TreeStateProviderContext.Provider
51+
value={{
52+
// Collapse
53+
collapseState,
54+
setCollapseState,
55+
doesPathMatch,
56+
// Editing
57+
currentlyEditingElement,
58+
setCurrentlyEditingElement,
59+
areChildrenBeingEdited,
60+
}}
61+
>
62+
{children}
63+
</TreeStateProviderContext.Provider>
64+
)
65+
}
66+
67+
export const useTreeState = () => useContext(TreeStateProviderContext)

0 commit comments

Comments
 (0)