Skip to content

#187 Handle Date objects #188

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1149,11 +1149,13 @@ A few helper functions, components and types that might be useful in your own im
- `LinkCustomComponent`: the component used to render [hyperlinks](#active-hyperlinks)
- `LinkCustomNodeDefinition`: custom node definition for [hyperlinks](#active-hyperlinks)
- `StringDisplay`: main component used to display a string value, re-used in the above "Link" Custom Component
- `StringEdit`: component used when editing a string value, can be useful for custom components
- `IconAdd`, `IconEdit`, `IconDelete`, `IconCopy`, `IconOk`, `IconCancel`, `IconChevron`: all the built-in [icon](#icons) components
- `matchNode`, `matchNodeKey`: helpers for defining custom [Search](#searchfiltering) functions
- `extract`: function to extract a deeply nested object value from a string path. See [here](https://github.com/CarlosNZ/object-property-extractor)
- `assign`: function to set a deep object value from a string path. See [here](https://github.com/CarlosNZ/object-property-assigner)
- `isCollection`: simple utility that returns `true` if input is a "Collection" (i.e. an Object or Array)
- `toPathString`: transforms a path array to a string representation, e.g. `["data", 0, "property1", "name"] => "data.0.property1.name"`
- `defaultTheme`, `githubDarkTheme`, `monoDarkTheme`, `monoLightTheme`, `candyWrapperTheme`, `psychedelicTheme`: all built-in themes
- `standardDataTypes`: array containing all standard data types: `[ 'string','number', 'boolean', 'null', 'object', 'array' ]`

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

## Changelog

- **1.25.2**: Don't treat Date objects as collections, so they can be handled by custom components (#187)[https://github.com/CarlosNZ/json-edit-react/issues/187]
- **1.25.1**: Small bug fix for incorrect resetting of cancelled edits (#184)[https://github.com/CarlosNZ/json-edit-react/issues/184]
- **1.25.0**:
- Implement [External control](#external-control) via event callbacks and triggers ([#138](https://github.com/CarlosNZ/json-edit-react/issues/138), [#145](https://github.com/CarlosNZ/json-edit-react/issues/145))
Expand Down
1 change: 1 addition & 0 deletions demo/src/demoData/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const data: Record<string, object> = {
boolean: true,
nothing: null,
enum: 'Option B',
// Date: new Date(),
Usage: [
'Edit a value by clicking the "edit" icon, or double-clicking the value.',
'You can change the type of any value',
Expand Down
36 changes: 35 additions & 1 deletion demo/src/demoData/dataDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
LinkCustomNodeDefinition,
assign,
matchNode,
StringDisplay,
StringEdit,
toPathString,
} from '../_imports'
import {
DefaultValueFunction,
Expand Down Expand Up @@ -95,7 +98,38 @@ export const demoDataDefinitions: Record<string, DemoData> = {
rootName: 'data',
collapse: 2,
data: data.intro,
customNodeDefinitions: [dateNodeDefinition],
customNodeDefinitions: [
dateNodeDefinition,
// {
// condition: (nodeData) => nodeData.value instanceof Date,
// element: (props) => {
// const { nodeData, isEditing, setValue, getStyles, canEdit, value, handleEdit } = props
// return isEditing ? (
// <StringEdit
// styles={getStyles('input', nodeData)}
// pathString={toPathString(nodeData.path)}
// {...props}
// value={value instanceof Date ? value.toISOString() : (value as string)}
// setValue={setValue as React.Dispatch<React.SetStateAction<string>>}
// handleEdit={() => {
// const newDate = new Date(value as string)
// handleEdit(newDate as any)
// }}
// />
// ) : (
// <StringDisplay
// {...props}
// styles={getStyles('string', nodeData)}
// canEdit={canEdit}
// pathString={toPathString(nodeData.path)}
// value={nodeData.value.toLocaleString()}
// />
// )
// },
// showEditTools: true,
// showOnEdit: true,
// },
],
// restrictEdit: ({ key }) => key === 'number',
customTextEditorAvailable: true,
restrictTypeSelection: ({ key }) => {
Expand Down
1 change: 0 additions & 1 deletion src/AutogrowTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ interface TextAreaProps {
name: string
value: string
setValue: React.Dispatch<React.SetStateAction<string>>
isEditing: boolean
handleKeyPress: (e: React.KeyboardEvent) => void
styles: React.CSSProperties
textAreaRef?: React.MutableRefObject<HTMLTextAreaElement | null>
Expand Down
3 changes: 2 additions & 1 deletion src/CollectionNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,6 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
name={pathString}
value={stringifiedValue}
setValue={setStringifiedValue}
isEditing={isEditing}
handleKeyPress={handleKeyPressEdit}
styles={getStyles('input', nodeData)}
/>
Expand Down Expand Up @@ -382,6 +381,8 @@ export const CollectionNode: React.FC<CollectionNodeProps> = (props) => {
setIsEditing: () => setCurrentlyEditingElement(path),
getStyles,
canDragOnto: canEdit,
canEdit,
keyboardCommon: {},
}

const CollectionContents = showCustomNodeContents ? (
Expand Down
37 changes: 21 additions & 16 deletions src/ValueNodeWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,25 +202,28 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
})
}

const handleEdit = () => {
const handleEdit = (inputValue?: unknown) => {
setCurrentlyEditingElement(null)
setPreviousValue(null)
let newValue: JsonData
switch (dataType) {
case 'object':
newValue = { [translate('DEFAULT_NEW_KEY', nodeData)]: value }
break
case 'array':
newValue = value ?? []
break
case 'number': {
const n = Number(value)
if (isNaN(n)) newValue = 0
else newValue = n
break
if (inputValue !== undefined) newValue = inputValue as JsonData
else {
switch (dataType) {
case 'object':
newValue = { [translate('DEFAULT_NEW_KEY', nodeData)]: value }
break
case 'array':
newValue = value ?? []
break
case 'number': {
const n = Number(value)
if (isNaN(n)) newValue = 0
else newValue = n
break
}
default:
newValue = value
}
default:
newValue = value
}
onEdit(newValue, path).then((error) => {
if (error) onError({ code: 'UPDATE_ERROR', message: error }, newValue)
Expand All @@ -247,7 +250,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
const { isEditingKey, canEditKey } = derivedValues
const showErrorString = !isEditing && error
const showTypeSelector = isEditing && allowedDataTypes.length > 1
const showEditButtons = dataType !== 'invalid' && !error && showEditTools
const showEditButtons = (dataType !== 'invalid' || CustomNode) && !error && showEditTools
const showKey = showLabel && !hideKey
const showCustomNode = CustomNode && ((isEditing && showOnEdit) || (!isEditing && showOnView))

Expand Down Expand Up @@ -320,6 +323,8 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
getStyles={getStyles}
originalNode={passOriginalNode ? getInputComponent(data, inputProps) : undefined}
originalNodeKey={passOriginalNode ? <KeyDisplay {...keyDisplayProps} /> : undefined}
canEdit={canEdit}
keyboardCommon={inputProps.keyboardCommon}
/>
) : (
// Need to re-fetch data type to make sure it's one of the "core" ones
Expand Down
102 changes: 66 additions & 36 deletions src/ValueNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import React, { useEffect, useRef, useState } from 'react'
import { AutogrowTextArea } from './AutogrowTextArea'
import { insertCharInTextArea, toPathString } from './helpers'
import { useTheme } from './contexts'
import { type NodeData, type InputProps, type EnumDefinition } from './types'
import {
type NodeData,
type InputProps,
type EnumDefinition,
type KeyboardControlsFull,
} from './types'
import { type TranslateFunction } from './localisation'

export const INVALID_FUNCTION_STRING = '**INVALID_FUNCTION**'
Expand All @@ -16,6 +21,9 @@ interface StringDisplayProps {
canEdit: boolean
setIsEditing: (value: React.SetStateAction<boolean>) => void
translate: TranslateFunction
// Can override nodeDate.value if we need to modify it for specific display
// purposes
value?: string
}
export const StringDisplay: React.FC<StringDisplayProps> = ({
nodeData,
Expand All @@ -26,8 +34,9 @@ export const StringDisplay: React.FC<StringDisplayProps> = ({
setIsEditing,
styles,
translate,
value: displayValue,
}) => {
const value = nodeData.value as string
const value = displayValue ?? (nodeData.value as string)
const [isExpanded, setIsExpanded] = useState(false)

const quoteChar = showStringQuotes ? '"' : ''
Expand Down Expand Up @@ -75,24 +84,69 @@ export const StringDisplay: React.FC<StringDisplayProps> = ({
)
}

export const StringValue: React.FC<InputProps & { value: string; enumType?: EnumDefinition }> = ({
interface StringEditProps {
styles: React.CSSProperties
pathString: string
value: string
setValue: React.Dispatch<React.SetStateAction<string>>
handleEdit: () => void
handleKeyboard: (
e: React.KeyboardEvent,
eventMap: Partial<Record<keyof KeyboardControlsFull, () => void>>
) => void
keyboardCommon: Partial<Record<keyof KeyboardControlsFull, () => void>>
}
export const StringEdit: React.FC<StringEditProps> = ({
styles,
pathString,
value,
setValue,
isEditing,
path,
handleEdit,
nodeData,
handleKeyboard,
keyboardCommon,
}) => {
const textAreaRef = useRef<HTMLTextAreaElement>(null)

return (
<AutogrowTextArea
className="jer-input-text"
textAreaRef={textAreaRef}
name={pathString}
value={value}
setValue={setValue}
handleKeyPress={(e) => {
handleKeyboard(e, {
stringConfirm: handleEdit,
stringLineBreak: () => {
// Simulates standard text-area line break behaviour. Only
// required when control key is not "standard" text-area
// behaviour ("Shift-Enter" or "Enter")
const newValue = insertCharInTextArea(
textAreaRef as React.MutableRefObject<HTMLTextAreaElement>,
'\n'
)
setValue(newValue)
},
...keyboardCommon,
})
}}
styles={styles}
/>
)
}

export const StringValue: React.FC<InputProps & { value: string; enumType?: EnumDefinition }> = ({
isEditing,
path,
enumType,
...props
}) => {
const { getStyles } = useTheme()

const textAreaRef = useRef<HTMLTextAreaElement>(null)

const pathString = toPathString(path)

const { value, setValue, nodeData, handleEdit, handleKeyboard, keyboardCommon } = props

if (isEditing && enumType) {
return (
<div className="jer-select jer-select-enums">
Expand Down Expand Up @@ -123,38 +177,14 @@ export const StringValue: React.FC<InputProps & { value: string; enumType?: Enum
}

return isEditing ? (
<AutogrowTextArea
className="jer-input-text"
textAreaRef={textAreaRef}
name={pathString}
value={value}
setValue={setValue as React.Dispatch<React.SetStateAction<string>>}
isEditing={isEditing}
handleKeyPress={(e) => {
handleKeyboard(e, {
stringConfirm: handleEdit,
stringLineBreak: () => {
// Simulates standard text-area line break behaviour. Only
// required when control key is not "standard" text-area
// behaviour ("Shift-Enter" or "Enter")
const newValue = insertCharInTextArea(
textAreaRef as React.MutableRefObject<HTMLTextAreaElement>,
'\n'
)
setValue(newValue)
},
...keyboardCommon,
})
}}
<StringEdit
styles={getStyles('input', nodeData)}
/>
) : (
<StringDisplay
nodeData={nodeData}
pathString={pathString}
styles={getStyles('string', nodeData)}
{...props}
setValue={props.setValue as React.Dispatch<React.SetStateAction<string>>}
/>
) : (
<StringDisplay pathString={pathString} styles={getStyles('string', nodeData)} {...props} />
)
}

Expand Down
5 changes: 1 addition & 4 deletions src/customComponents/ActiveHyperlinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import React from 'react'
import { StringDisplay } from '../../src/ValueNodes'
import { toPathString } from '../helpers'
import { type CustomNodeProps, type CustomNodeDefinition, type ValueNodeProps } from '../types'
import { useCommon } from '../hooks'

export const LinkCustomComponent: React.FC<
CustomNodeProps<{ stringTruncate?: number }> & ValueNodeProps
> = (props) => {
const { value, setIsEditing, getStyles, nodeData } = props
const styles = getStyles('string', nodeData)
const { canEdit } = useCommon({ props })
return (
<div
onDoubleClick={() => setIsEditing(true)}
Expand All @@ -35,10 +33,9 @@ export const LinkCustomComponent: React.FC<
>
<StringDisplay
{...props}
nodeData={nodeData}
pathString={toPathString(nodeData.path)}
styles={styles}
canEdit={canEdit}
value={nodeData.value as string}
/>
</a>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from './types'

export const isCollection = (value: unknown): value is Record<string, unknown> | unknown[] =>
value !== null && typeof value === 'object'
value !== null && typeof value === 'object' && !(value instanceof Date)

/**
* FILTERING
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export { JsonEditor } from './JsonEditor'
export { defaultTheme } from './contexts/ThemeProvider'
export { IconAdd, IconEdit, IconDelete, IconCopy, IconOk, IconCancel, IconChevron } from './Icons'
export { StringDisplay } from './ValueNodes'
export { StringDisplay, StringEdit } from './ValueNodes'
export { LinkCustomComponent, LinkCustomNodeDefinition } from './customComponents'
export { matchNode, matchNodeKey, isCollection } from './helpers'
export { matchNode, matchNodeKey, isCollection, toPathString } from './helpers'
export { default as assign } from 'object-property-assigner'
export { default as extract } from 'object-property-extractor'
export {
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ export interface CustomNodeProps<T = Record<string, unknown>> extends BaseNodePr
customNodeProps?: T
parentData: CollectionData | null
setValue: (value: ValueData) => void
handleEdit: () => void
handleEdit: (value?: unknown) => void
handleCancel: () => void
handleKeyPress: (e: React.KeyboardEvent) => void
isEditing: boolean
Expand All @@ -319,6 +319,8 @@ export interface CustomNodeProps<T = Record<string, unknown>> extends BaseNodePr
children?: JSX.Element | JSX.Element[] | null
originalNode?: JSX.Element
originalNodeKey?: JSX.Element
canEdit: boolean
keyboardCommon: Partial<Record<keyof KeyboardControlsFull, () => void>>
}

export interface CustomNodeDefinition<T = Record<string, unknown>, U = Record<string, unknown>> {
Expand Down