Skip to content

Commit ab727a5

Browse files
authored
#172 Expand truncated string (#174)
1 parent 5bba75e commit ab727a5

File tree

9 files changed

+115
-45
lines changed

9 files changed

+115
-45
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ It's pretty self explanatory (click the "edit" icon to edit, etc.), but there ar
116116
- For Boolean inputs, space bar will toggle the value
117117
- Easily move to the next/previous node (for editing) using the `Tab`/`Shift-Tab` key
118118
- Drag and drop items to change the structure or modify display order
119+
- 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)
119120
- JSON text input can accept "looser" input, if an additional JSON parsing method is provided (e.g. [JSON5](https://json5.org/)). See `jsonParse` prop.
120121

121122
[Have a play with the Demo app](https://carlosnz.github.io/json-edit-react/) to get a feel for it!
@@ -639,6 +640,7 @@ Localise your implementation by passing in a `translations` object to replace th
639640
ERROR_ADD: 'Adding node unsuccessful',
640641
DEFAULT_STRING: 'New data!',
641642
DEFAULT_NEW_KEY: 'key',
643+
SHOW_LESS: '(Show less)',
642644
}
643645
644646
```
@@ -809,9 +811,9 @@ A few helper functions, components and types that might be useful in your own im
809811
- `themes`: an object containing all the built-in theme definitions
810812
- `LinkCustomComponent`: the component used to render [hyperlinks](#active-hyperlinks)
811813
- `LinkCustomNodeDefinition`: custom node definition for [hyperlinks](#active-hyperlinks)
814+
- `StringDisplay`: main component used to display a string value, re-used in the above "Link" Custom Component
812815
- `IconAdd`, `IconEdit`, `IconDelete`, `IconCopy`, `IconOk`, `IconCancel`, `IconChevron`: all the built-in [icon](#icons) components
813816
- `matchNode`, `matchNodeKey`: helpers for defining custom [Search](#searchfiltering) functions
814-
- `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)
815817
- `extract`: function to extract a deeply nested object value from a string path. See [here](https://github.com/CarlosNZ/object-property-extractor)
816818
- `assign`: function to set a deep object value from a string path. See [here](https://github.com/CarlosNZ/object-property-assigner)
817819

src/ValueNodeWrapper.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ export const ValueNodeWrapper: React.FC<ValueNodeProps> = (props) => {
223223
parentData,
224224
setValue: updateValue,
225225
isEditing,
226+
canEdit,
226227
setIsEditing: canEdit ? () => setCurrentlyEditingElement(path) : () => {},
227228
handleEdit,
228229
handleCancel,

src/ValueNodes.tsx

+79-21
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,97 @@
1-
import React, { useEffect, useRef } from 'react'
1+
import React, { useEffect, useRef, useState } from 'react'
22
import { AutogrowTextArea } from './AutogrowTextArea'
3-
import { insertCharInTextArea, toPathString, truncate } from './helpers'
3+
import { insertCharInTextArea, toPathString } from './helpers'
44
import { useTheme } from './contexts'
5-
import { type InputProps } from './types'
5+
import { type NodeData, type InputProps } from './types'
6+
import { type TranslateFunction } from './localisation'
67

78
export const INVALID_FUNCTION_STRING = '**INVALID_FUNCTION**'
89

10+
interface StringDisplayProps {
11+
nodeData: NodeData
12+
styles: React.CSSProperties
13+
pathString: string
14+
showStringQuotes?: boolean
15+
stringTruncate?: number
16+
canEdit: boolean
17+
setIsEditing: (value: React.SetStateAction<boolean>) => void
18+
translate: TranslateFunction
19+
}
20+
export const StringDisplay: React.FC<StringDisplayProps> = ({
21+
nodeData,
22+
showStringQuotes = true,
23+
stringTruncate = 200,
24+
pathString,
25+
canEdit,
26+
setIsEditing,
27+
styles,
28+
translate,
29+
}) => {
30+
const value = nodeData.value as string
31+
const [isExpanded, setIsExpanded] = useState(false)
32+
33+
const quoteChar = showStringQuotes ? '"' : ''
34+
35+
const requiresTruncation = value.length > stringTruncate
36+
37+
const handleMaybeEdit = () => {
38+
canEdit ? setIsEditing(true) : setIsExpanded(!isExpanded)
39+
}
40+
41+
return (
42+
<div
43+
id={`${pathString}_display`}
44+
onDoubleClick={handleMaybeEdit}
45+
onClick={(e) => {
46+
if (e.getModifierState('Control') || e.getModifierState('Meta')) handleMaybeEdit()
47+
}}
48+
className="jer-value-string"
49+
style={styles}
50+
>
51+
{quoteChar}
52+
{!requiresTruncation ? (
53+
`${value}${quoteChar}`
54+
) : isExpanded ? (
55+
<>
56+
<span>
57+
{value}
58+
{quoteChar}
59+
</span>
60+
<span className="jer-string-expansion jer-show-less" onClick={() => setIsExpanded(false)}>
61+
{' '}
62+
{translate('SHOW_LESS', nodeData)}
63+
</span>
64+
</>
65+
) : (
66+
<>
67+
<span>{value.slice(0, stringTruncate - 2).trimEnd()}</span>
68+
<span className="jer-string-expansion jer-ellipsis" onClick={() => setIsExpanded(true)}>
69+
...
70+
</span>
71+
{quoteChar}
72+
</>
73+
)}
74+
</div>
75+
)
76+
}
77+
978
export const StringValue: React.FC<InputProps & { value: string }> = ({
1079
value,
1180
setValue,
1281
isEditing,
1382
path,
14-
setIsEditing,
1583
handleEdit,
16-
stringTruncate,
17-
showStringQuotes,
1884
nodeData,
1985
handleKeyboard,
2086
keyboardCommon,
87+
...props
2188
}) => {
2289
const { getStyles } = useTheme()
2390

2491
const textAreaRef = useRef<HTMLTextAreaElement>(null)
2592

2693
const pathString = toPathString(path)
2794

28-
const quoteChar = showStringQuotes ? '"' : ''
29-
3095
return isEditing ? (
3196
<AutogrowTextArea
3297
className="jer-input-text"
@@ -54,19 +119,12 @@ export const StringValue: React.FC<InputProps & { value: string }> = ({
54119
styles={getStyles('input', nodeData)}
55120
/>
56121
) : (
57-
<div
58-
id={`${pathString}_display`}
59-
onDoubleClick={() => setIsEditing(true)}
60-
onClick={(e) => {
61-
if (e.getModifierState('Control') || e.getModifierState('Meta')) setIsEditing(true)
62-
}}
63-
className="jer-value-string"
64-
style={getStyles('string', nodeData)}
65-
>
66-
{quoteChar}
67-
{truncate(value, stringTruncate)}
68-
{quoteChar}
69-
</div>
122+
<StringDisplay
123+
nodeData={nodeData}
124+
pathString={pathString}
125+
styles={getStyles('string', nodeData)}
126+
{...props}
127+
/>
70128
)
71129
}
72130

src/customComponents/ActiveHyperlinks.tsx

+17-12
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,17 @@
77
*/
88

99
import React from 'react'
10-
import { truncate } from '../../src/helpers'
11-
import { type CustomNodeProps, type CustomNodeDefinition } from '../types'
10+
import { StringDisplay } from '../../src/ValueNodes'
11+
import { toPathString } from '../helpers'
12+
import { type CustomNodeProps, type CustomNodeDefinition, type ValueNodeProps } from '../types'
13+
import { useCommon } from '../hooks'
1214

13-
export const LinkCustomComponent: React.FC<CustomNodeProps<{ stringTruncate?: number }>> = ({
14-
value,
15-
setIsEditing,
16-
getStyles,
17-
customNodeProps,
18-
nodeData,
19-
}) => {
20-
const stringTruncateLength = customNodeProps?.stringTruncate ?? 100
15+
export const LinkCustomComponent: React.FC<
16+
CustomNodeProps<{ stringTruncate?: number }> & ValueNodeProps
17+
> = (props) => {
18+
const { value, setIsEditing, getStyles, nodeData } = props
2119
const styles = getStyles('string', nodeData)
20+
const { canEdit } = useCommon({ props })
2221
return (
2322
<div
2423
onDoubleClick={() => setIsEditing(true)}
@@ -34,7 +33,13 @@ export const LinkCustomComponent: React.FC<CustomNodeProps<{ stringTruncate?: nu
3433
rel="noreferrer"
3534
style={{ color: styles.color ?? undefined }}
3635
>
37-
&quot;{truncate(value as string, stringTruncateLength)}&quot;
36+
<StringDisplay
37+
{...props}
38+
nodeData={nodeData}
39+
pathString={toPathString(nodeData.path)}
40+
styles={styles}
41+
canEdit={canEdit}
42+
/>
3843
</a>
3944
</div>
4045
)
@@ -44,7 +49,7 @@ export const LinkCustomComponent: React.FC<CustomNodeProps<{ stringTruncate?: nu
4449
export const LinkCustomNodeDefinition: CustomNodeDefinition = {
4550
// Condition is a regex to match url strings
4651
condition: ({ value }) => typeof value === 'string' && /^https?:\/\/.+\..+$/.test(value),
47-
element: LinkCustomComponent, // the component defined above
52+
element: LinkCustomComponent as React.FC<CustomNodeProps>, // the component defined above
4853
showOnView: true,
4954
showOnEdit: false,
5055
}

src/helpers.ts

-10
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,6 @@ export const matchNodeKey: SearchFilterFunction = ({ key, path }, searchText = '
110110
return false
111111
}
112112

113-
/**
114-
* Truncates a string to a specified length, appends `...` if truncated
115-
*/
116-
export const truncate = (string: string, length = 200) =>
117-
typeof string === 'string'
118-
? string.length < length
119-
? string
120-
: `${string.slice(0, length - 2).trim()}...`
121-
: string
122-
123113
/**
124114
* Converts a part expressed as an array of properties to a single string
125115
*/

src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
export { JsonEditor } from './JsonEditor'
22
export { defaultTheme } from './contexts/ThemeProvider'
33
export { IconAdd, IconEdit, IconDelete, IconCopy, IconOk, IconCancel, IconChevron } from './Icons'
4+
export { StringDisplay } from './ValueNodes'
45
export { LinkCustomComponent, LinkCustomNodeDefinition } from './customComponents'
5-
export { matchNode, matchNodeKey, isCollection, truncate } from './helpers'
6+
export { matchNode, matchNodeKey, isCollection } from './helpers'
67
export { default as assign } from 'object-property-assigner'
78
export { default as extract } from 'object-property-extractor'
89
export {

src/localisation.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const localisedStrings = {
1111
ERROR_ADD: 'Adding node unsuccessful',
1212
DEFAULT_STRING: 'New data!',
1313
DEFAULT_NEW_KEY: 'key',
14+
SHOW_LESS: '(Show less)',
1415
}
1516

1617
export type LocalisedStrings = typeof localisedStrings

src/style.css

+10
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,16 @@ select:focus + .focus {
225225
overflow-wrap: anywhere;
226226
}
227227

228+
.jer-string-expansion {
229+
cursor: pointer;
230+
opacity: 0.6;
231+
filter: saturate(50%);
232+
}
233+
234+
.jer-show-less {
235+
font-size: 80%;
236+
}
237+
228238
.jer-hyperlink {
229239
text-decoration: underline;
230240
}

src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export interface CustomNodeDefinition<T = Record<string, unknown>, U = Record<st
298298
showOnEdit?: boolean // default false
299299
showOnView?: boolean // default true
300300
showEditTools?: boolean // default true
301+
301302
// For collection nodes only:
302303
showCollectionWrapper?: boolean // default true
303304
wrapperElement?: React.FC<CustomNodeProps<U>>
@@ -314,6 +315,7 @@ export interface CustomButtonDefinition {
314315
export interface InputProps {
315316
value: unknown
316317
setValue: (value: ValueData) => void
318+
canEdit: boolean
317319
isEditing: boolean
318320
setIsEditing: React.Dispatch<React.SetStateAction<boolean>>
319321
handleEdit: () => void

0 commit comments

Comments
 (0)