Skip to content

#172 Expand truncated string #174

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 9 commits into from
Feb 19, 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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ It's pretty self explanatory (click the "edit" icon to edit, etc.), but there ar
- For Boolean inputs, space bar will toggle the value
- Easily move to the next/previous node (for editing) using the `Tab`/`Shift-Tab` key
- Drag and drop items to change the structure or modify display order
- When editing is not permitted, double-clicking a string value will expand the text to the full value if it is truncated due to length (there is also a clickable "..." for long strings)
- JSON text input can accept "looser" input, if an additional JSON parsing method is provided (e.g. [JSON5](https://json5.org/)). See `jsonParse` prop.

[Have a play with the Demo app](https://carlosnz.github.io/json-edit-react/) to get a feel for it!
Expand Down Expand Up @@ -639,6 +640,7 @@ Localise your implementation by passing in a `translations` object to replace th
ERROR_ADD: 'Adding node unsuccessful',
DEFAULT_STRING: 'New data!',
DEFAULT_NEW_KEY: 'key',
SHOW_LESS: '(Show less)',
}

```
Expand Down Expand Up @@ -809,9 +811,9 @@ A few helper functions, components and types that might be useful in your own im
- `themes`: an object containing all the built-in theme definitions
- `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
- `IconAdd`, `IconEdit`, `IconDelete`, `IconCopy`, `IconOk`, `IconCancel`, `IconChevron`: all the built-in [icon](#icons) components
- `matchNode`, `matchNodeKey`: helpers for defining custom [Search](#searchfiltering) functions
- `truncate`: function to truncate a string to a specified length. See [here](https://github.com/CarlosNZ/json-edit-react/blob/d5fdbdfed6da7152f5802c67fbb3577810d13adc/src/ValueNodes.tsx#L9-L13)
- `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)

Expand Down
1 change: 1 addition & 0 deletions src/ValueNodeWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
parentData,
setValue: updateValue,
isEditing,
canEdit,
setIsEditing: canEdit ? () => setCurrentlyEditingElement(path) : () => {},
handleEdit,
handleCancel,
Expand Down
100 changes: 79 additions & 21 deletions src/ValueNodes.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,97 @@
import React, { useEffect, useRef } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { AutogrowTextArea } from './AutogrowTextArea'
import { insertCharInTextArea, toPathString, truncate } from './helpers'
import { insertCharInTextArea, toPathString } from './helpers'
import { useTheme } from './contexts'
import { type InputProps } from './types'
import { type NodeData, type InputProps } from './types'
import { type TranslateFunction } from './localisation'

export const INVALID_FUNCTION_STRING = '**INVALID_FUNCTION**'

interface StringDisplayProps {
nodeData: NodeData
styles: React.CSSProperties
pathString: string
showStringQuotes?: boolean
stringTruncate?: number
canEdit: boolean
setIsEditing: (value: React.SetStateAction<boolean>) => void
translate: TranslateFunction
}
export const StringDisplay: React.FC<StringDisplayProps> = ({
nodeData,
showStringQuotes = true,
stringTruncate = 200,
pathString,
canEdit,
setIsEditing,
styles,
translate,
}) => {
const value = nodeData.value as string
const [isExpanded, setIsExpanded] = useState(false)

const quoteChar = showStringQuotes ? '"' : ''

const requiresTruncation = value.length > stringTruncate

const handleMaybeEdit = () => {
canEdit ? setIsEditing(true) : setIsExpanded(!isExpanded)
}

return (
<div
id={`${pathString}_display`}
onDoubleClick={handleMaybeEdit}
onClick={(e) => {
if (e.getModifierState('Control') || e.getModifierState('Meta')) handleMaybeEdit()
}}
className="jer-value-string"
style={styles}
>
{quoteChar}
{!requiresTruncation ? (
`${value}${quoteChar}`
) : isExpanded ? (
<>
<span>
{value}
{quoteChar}
</span>
<span className="jer-string-expansion jer-show-less" onClick={() => setIsExpanded(false)}>
{' '}
{translate('SHOW_LESS', nodeData)}
</span>
</>
) : (
<>
<span>{value.slice(0, stringTruncate - 2).trimEnd()}</span>
<span className="jer-string-expansion jer-ellipsis" onClick={() => setIsExpanded(true)}>
...
</span>
{quoteChar}
</>
)}
</div>
)
}

export const StringValue: React.FC<InputProps & { value: string }> = ({
value,
setValue,
isEditing,
path,
setIsEditing,
handleEdit,
stringTruncate,
showStringQuotes,
nodeData,
handleKeyboard,
keyboardCommon,
...props
}) => {
const { getStyles } = useTheme()

const textAreaRef = useRef<HTMLTextAreaElement>(null)

const pathString = toPathString(path)

const quoteChar = showStringQuotes ? '"' : ''

return isEditing ? (
<AutogrowTextArea
className="jer-input-text"
Expand Down Expand Up @@ -54,19 +119,12 @@ export const StringValue: React.FC<InputProps & { value: string }> = ({
styles={getStyles('input', nodeData)}
/>
) : (
<div
id={`${pathString}_display`}
onDoubleClick={() => setIsEditing(true)}
onClick={(e) => {
if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true)
}}
className="jer-value-string"
style={getStyles('string', nodeData)}
>
{quoteChar}
{truncate(value, stringTruncate)}
{quoteChar}
</div>
<StringDisplay
nodeData={nodeData}
pathString={pathString}
styles={getStyles('string', nodeData)}
{...props}
/>
)
}

Expand Down
29 changes: 17 additions & 12 deletions src/customComponents/ActiveHyperlinks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,17 @@
*/

import React from 'react'
import { truncate } from '../../src/helpers'
import { type CustomNodeProps, type CustomNodeDefinition } from '../types'
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 }>> = ({
value,
setIsEditing,
getStyles,
customNodeProps,
nodeData,
}) => {
const stringTruncateLength = customNodeProps?.stringTruncate ?? 100
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 @@ -34,7 +33,13 @@ export const LinkCustomComponent: React.FC<CustomNodeProps<{ stringTruncate?: nu
rel="noreferrer"
style={{ color: styles.color ?? undefined }}
>
&quot;{truncate(value as string, stringTruncateLength)}&quot;
<StringDisplay
{...props}
nodeData={nodeData}
pathString={toPathString(nodeData.path)}
styles={styles}
canEdit={canEdit}
/>
</a>
</div>
)
Expand All @@ -44,7 +49,7 @@ export const LinkCustomComponent: React.FC<CustomNodeProps<{ stringTruncate?: nu
export const LinkCustomNodeDefinition: CustomNodeDefinition = {
// Condition is a regex to match url strings
condition: ({ value }) => typeof value === 'string' && /^https?:\/\/.+\..+$/.test(value),
element: LinkCustomComponent, // the component defined above
element: LinkCustomComponent as React.FC<CustomNodeProps>, // the component defined above
showOnView: true,
showOnEdit: false,
}
10 changes: 0 additions & 10 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,6 @@ export const matchNodeKey: SearchFilterFunction = ({ key, path }, searchText = '
return false
}

/**
* Truncates a string to a specified length, appends `...` if truncated
*/
export const truncate = (string: string, length = 200) =>
typeof string === 'string'
? string.length < length
? string
: `${string.slice(0, length - 2).trim()}...`
: string

/**
* Converts a part expressed as an array of properties to a single string
*/
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +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 { LinkCustomComponent, LinkCustomNodeDefinition } from './customComponents'
export { matchNode, matchNodeKey, isCollection, truncate } from './helpers'
export { matchNode, matchNodeKey, isCollection } from './helpers'
export { default as assign } from 'object-property-assigner'
export { default as extract } from 'object-property-extractor'
export {
Expand Down
1 change: 1 addition & 0 deletions src/localisation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const localisedStrings = {
ERROR_ADD: 'Adding node unsuccessful',
DEFAULT_STRING: 'New data!',
DEFAULT_NEW_KEY: 'key',
SHOW_LESS: '(Show less)',
}

export type LocalisedStrings = typeof localisedStrings
Expand Down
10 changes: 10 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,16 @@ select:focus + .focus {
overflow-wrap: anywhere;
}

.jer-string-expansion {
cursor: pointer;
opacity: 0.6;
filter: saturate(50%);
}

.jer-show-less {
font-size: 80%;
}

.jer-hyperlink {
text-decoration: underline;
}
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export interface CustomNodeDefinition<T = Record<string, unknown>, U = Record<st
showOnEdit?: boolean // default false
showOnView?: boolean // default true
showEditTools?: boolean // default true

// For collection nodes only:
showCollectionWrapper?: boolean // default true
wrapperElement?: React.FC<CustomNodeProps<U>>
Expand All @@ -314,6 +315,7 @@ export interface CustomButtonDefinition {
export interface InputProps {
value: unknown
setValue: (value: ValueData) => void
canEdit: boolean
isEditing: boolean
setIsEditing: React.Dispatch<React.SetStateAction<boolean>>
handleEdit: () => void
Expand Down