diff --git a/README.md b/README.md index d731d49b..511a1fa1 100644 --- a/README.md +++ b/README.md @@ -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! @@ -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)', } ``` @@ -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) diff --git a/src/ValueNodeWrapper.tsx b/src/ValueNodeWrapper.tsx index d8cf9408..9ead0355 100644 --- a/src/ValueNodeWrapper.tsx +++ b/src/ValueNodeWrapper.tsx @@ -223,6 +223,7 @@ export const ValueNodeWrapper: React.FC = (props) => { parentData, setValue: updateValue, isEditing, + canEdit, setIsEditing: canEdit ? () => setCurrentlyEditingElement(path) : () => {}, handleEdit, handleCancel, diff --git a/src/ValueNodes.tsx b/src/ValueNodes.tsx index a02c0495..12f4cbec 100644 --- a/src/ValueNodes.tsx +++ b/src/ValueNodes.tsx @@ -1,23 +1,90 @@ -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) => void + translate: TranslateFunction +} +export const StringDisplay: React.FC = ({ + 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 ( +
{ + if (e.getModifierState('Control') || e.getModifierState('Meta')) handleMaybeEdit() + }} + className="jer-value-string" + style={styles} + > + {quoteChar} + {!requiresTruncation ? ( + `${value}${quoteChar}` + ) : isExpanded ? ( + <> + + {value} + {quoteChar} + + setIsExpanded(false)}> + {' '} + {translate('SHOW_LESS', nodeData)} + + + ) : ( + <> + {value.slice(0, stringTruncate - 2).trimEnd()} + setIsExpanded(true)}> + ... + + {quoteChar} + + )} +
+ ) +} + export const StringValue: React.FC = ({ value, setValue, isEditing, path, - setIsEditing, handleEdit, - stringTruncate, - showStringQuotes, nodeData, handleKeyboard, keyboardCommon, + ...props }) => { const { getStyles } = useTheme() @@ -25,8 +92,6 @@ export const StringValue: React.FC = ({ const pathString = toPathString(path) - const quoteChar = showStringQuotes ? '"' : '' - return isEditing ? ( = ({ styles={getStyles('input', nodeData)} /> ) : ( -
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} -
+ ) } diff --git a/src/customComponents/ActiveHyperlinks.tsx b/src/customComponents/ActiveHyperlinks.tsx index 6a207af1..d0a0a8b9 100644 --- a/src/customComponents/ActiveHyperlinks.tsx +++ b/src/customComponents/ActiveHyperlinks.tsx @@ -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> = ({ - 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 (
setIsEditing(true)} @@ -34,7 +33,13 @@ export const LinkCustomComponent: React.FC - "{truncate(value as string, stringTruncateLength)}" +
) @@ -44,7 +49,7 @@ export const LinkCustomComponent: React.FC typeof value === 'string' && /^https?:\/\/.+\..+$/.test(value), - element: LinkCustomComponent, // the component defined above + element: LinkCustomComponent as React.FC, // the component defined above showOnView: true, showOnEdit: false, } diff --git a/src/helpers.ts b/src/helpers.ts index f83fa59e..4985c3a1 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -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 */ diff --git a/src/index.ts b/src/index.ts index 3b718028..aa786db3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { diff --git a/src/localisation.ts b/src/localisation.ts index 091b9965..210db099 100644 --- a/src/localisation.ts +++ b/src/localisation.ts @@ -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 diff --git a/src/style.css b/src/style.css index 532d5de5..5909cde6 100644 --- a/src/style.css +++ b/src/style.css @@ -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; } diff --git a/src/types.ts b/src/types.ts index 9529bf0f..76bd08fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -298,6 +298,7 @@ export interface CustomNodeDefinition, U = Record> @@ -314,6 +315,7 @@ export interface CustomButtonDefinition { export interface InputProps { value: unknown setValue: (value: ValueData) => void + canEdit: boolean isEditing: boolean setIsEditing: React.Dispatch> handleEdit: () => void