diff --git a/README.md b/README.md index fa67138c..ae45c03f 100644 --- a/README.md +++ b/README.md @@ -279,118 +279,148 @@ const editor = new JSONEditor({ To adjust the text color of keys or values, the color of the classes `.jse-key` and `.jse-value` can be overwritten. - - `onRenderValue(props: RenderValueProps) : RenderValueComponentDescription[]` +- `onRenderValue(props: RenderValueProps) : RenderValueComponentDescription[]` - _EXPERIMENTAL! This API will most likely change in future versions._ + Customize rendering of the values. By default, `renderValue` is used, which renders a value as an editable div and depending on the value can also render a boolean toggle, a color picker, and a timestamp tag. Multiple components can be rendered alongside each other, like the boolean toggle and color picker being rendered left from the editable div. Built in value renderer components: `EditableValue`, `ReadonlyValue`, `BooleanToggle`, `ColorPicker`, `TimestampTag`, `EnumValue`. - Customize rendering of the values. By default, `renderValue` is used, which renders a value as an editable div and depending on the value can also render a boolean toggle, a color picker, and a timestamp tag. Multiple components can be rendered alongside each other, like the boolean toggle and color picker being rendered left from the editable div. Built in value renderer components: `EditableValue`, `ReadonlyValue`, `BooleanToggle`, `ColorPicker`, `TimestampTag`, `EnumValue`. + For JSON Schema enums, there is a ready-made value renderer `renderJSONSchemaEnum` which renders enums using the `EnumValue` component. This can be used like: - For JSON Schema enums, there is a value renderer `renderJSONSchemaEnum` which renders enums using the `EnumValue` component. This can be used like: + ```js + import { renderJSONSchemaEnum, renderValue } from 'svelte-jsoneditor' + + function onRenderValue(props) { + // use the enum renderer, and fallback on the default renderer + return renderJSONSchemaEnum(props, schema, schemaDefinitions) || renderValue(props) + } + ``` + + The callback `onRenderValue` must return an array with one or multiple renderers. Each renderer can be either a Svelte component or a Svelte action: + + ```ts + interface SvelteComponentRenderer { + component: typeof SvelteComponent + props: Record + } + + interface SvelteActionRenderer { + action: Action // Svelte Action + props: Record + } + ``` - ```js - import { renderJSONSchemaEnum, renderValue } from 'svelte-jsoneditor' + The `SvelteComponentRenderer` interface can be used to provide Svelte components like the `EnumValue` component mentioned above. The `SvelteActionRenderer` expects a [Svelte Action](https://svelte.dev/docs/svelte-action) as `action` property. Since this interface is a plain JavaScript interface, this allows to create custom components in a vanilla JS environment. Basically it is a function that gets a DOM node passed, and needs to return an object with `update` and `destroy` functions: - function onRenderValue(props) { - // use the enum renderer, and fallback on the default renderer - return renderJSONSchemaEnum(props, schema, schemaDefinitions) || renderValue(props) + ```js + const myRendererAction = { + action: (node) => { + // attach something to the HTML DOM node + return { + update: (node) => { + // update the DOM + }, + destroy: () => { + // cleanup the DOM + } + } + } + } + ``` + +- `onRenderMenu(items: MenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean }) : MenuItem[] | undefined`. + Callback which can be used to make changes to the menu items. New items can + be added, or existing items can be removed or reorganized. When the function + returns `undefined`, the original `items` will be applied. Using the context values `mode` and `modal`, different actions can be taken depending on the mode of the editor and whether the editor is rendered inside a modal or not. + + A menu item `MenuItem` can be one of the following types: + + - Button: + + ```ts + interface MenuButton { + type: 'button' + onClick: () => void + icon?: IconDefinition + text?: string + title?: string + className?: string + disabled?: boolean } ``` - - `onRenderMenu(items: MenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean }) : MenuItem[] | undefined`. - Callback which can be used to make changes to the menu items. New items can - be added, or existing items can be removed or reorganized. When the function - returns `undefined`, the original `items` will be applied. Using the context values `mode` and `modal`, different actions can be taken depending on the mode of the editor and whether the editor is rendered inside a modal or not. + - Separator (gray vertical line between a group of items): - A menu item `MenuItem` can be one of the following types: + ```ts + interface MenuSeparator { + type: 'separator' + } + ``` - - Button: + - Space (fills up empty space): - ```ts - interface MenuButton { - type: 'button' - onClick: () => void - icon?: IconDefinition - text?: string - title?: string - className?: string - disabled?: boolean - } - ``` + ```ts + interface MenuSpace { + type: 'space' + } + ``` - - Separator (gray vertical line between a group of items): +- `onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined`. + Callback which can be used to make changes to the context menu items. New items can + be added, or existing items can be removed or reorganized. When the function + returns `undefined`, the original `items` will be applied. Using the context values `mode`, `modal` and `selection`, different actions can be taken depending on the mode of the editor, whether the editor is rendered inside a modal or not and the path of selection. - ```ts - interface MenuSeparator { - type: 'separator' - } - ``` + A menu item `ContextMenuItem` can be one of the following types: - - Space (fills up empty space): + - Button: - ```ts - interface MenuSpace { - type: 'space' - } - ``` - - - `onRenderContextMenu(items: ContextMenuItem[], context: { mode: 'tree' | 'text' | 'table', modal: boolean, selection: JSONEditorSelection | null }) : ContextMenuItem[] | undefined`. - Callback which can be used to make changes to the context menu items. New items can - be added, or existing items can be removed or reorganized. When the function - returns `undefined`, the original `items` will be applied. Using the context values `mode`, `modal` and `selection`, different actions can be taken depending on the mode of the editor, whether the editor is rendered inside a modal or not and the path of selection. - - A menu item `ContextMenuItem` can be one of the following types: - - - Button: - - ```ts - interface MenuButton { - type: 'button' - onClick: () => void - icon?: IconDefinition - text?: string - title?: string - className?: string - disabled?: boolean - } - ``` + ```ts + interface MenuButton { + type: 'button' + onClick: () => void + icon?: IconDefinition + text?: string + title?: string + className?: string + disabled?: boolean + } + ``` - - Dropdown button: + - Dropdown button: - ```ts - interface MenuDropDownButton { - type: 'dropdown-button' - main: MenuButton - width?: string - items: MenuButton[] - } - ``` + ```ts + interface MenuDropDownButton { + type: 'dropdown-button' + main: MenuButton + width?: string + items: MenuButton[] + } + ``` - - Separator (gray line between a group of items): + - Separator (gray line between a group of items): - ```ts - interface MenuSeparator { - type: 'separator' - } - ``` + ```ts + interface MenuSeparator { + type: 'separator' + } + ``` - - Menu row and column: + - Menu row and column: - ```ts - interface MenuLabel { - type: 'label' - text: string - } + ```ts + interface MenuLabel { + type: 'label' + text: string + } - interface ContextMenuColumn { - type: 'column' - items: Array - } + interface ContextMenuColumn { + type: 'column' + items: Array + } - interface ContextMenuRow { - type: 'row' - items: Array - } - ``` + interface ContextMenuRow { + type: 'row' + items: Array + } + ``` - `onSelect: (selection: JSONEditorSelection | null) => void` diff --git a/src/lib/components/__snapshots__/JSONEditor.test.ts.snap b/src/lib/components/__snapshots__/JSONEditor.test.ts.snap index bed4450f..c5ec1c7c 100644 --- a/src/lib/components/__snapshots__/JSONEditor.test.ts.snap +++ b/src/lib/components/__snapshots__/JSONEditor.test.ts.snap @@ -300,6 +300,7 @@ exports[`JSONEditor > render table mode 1`] = ` + @@ -320,6 +321,7 @@ exports[`JSONEditor > render table mode 1`] = ` + @@ -352,6 +354,7 @@ exports[`JSONEditor > render table mode 1`] = ` + @@ -372,6 +375,7 @@ exports[`JSONEditor > render table mode 1`] = ` + @@ -404,6 +408,7 @@ exports[`JSONEditor > render table mode 1`] = ` + @@ -424,6 +429,7 @@ exports[`JSONEditor > render table mode 1`] = ` + @@ -1649,6 +1655,7 @@ exports[`JSONEditor > render tree mode 1`] = ` + @@ -1825,6 +1832,7 @@ exports[`JSONEditor > render tree mode 1`] = ` + @@ -1888,6 +1896,7 @@ exports[`JSONEditor > render tree mode 1`] = ` + @@ -2064,6 +2073,7 @@ exports[`JSONEditor > render tree mode 1`] = ` + diff --git a/src/lib/components/modes/tablemode/JSONValue.svelte b/src/lib/components/modes/tablemode/JSONValue.svelte index 7beeeb50..b24404c1 100644 --- a/src/lib/components/modes/tablemode/JSONValue.svelte +++ b/src/lib/components/modes/tablemode/JSONValue.svelte @@ -7,6 +7,7 @@ JSONSelection, SearchResultItem } from '$lib/types' + import { isSvelteActionRenderer } from '$lib/typeguards.js' import type { JSONPatchDocument, JSONPath } from 'immutable-json-patch' import { isEditingSelection, isValueSelection } from '$lib/logic/selection.js' import { createNestedValueOperations } from '$lib/logic/operations.js' @@ -47,7 +48,20 @@ {#each renderers as renderer} - {#key renderer.component} - - {/key} + {#if isSvelteActionRenderer(renderer)} + {@const action = renderer.action} + {#key renderer.action} +
+ {/key} + {:else} + {#key renderer.component} + + {/key} + {/if} {/each} diff --git a/src/lib/components/modes/treemode/JSONValue.svelte b/src/lib/components/modes/treemode/JSONValue.svelte index a8abd9db..e0bffb4b 100644 --- a/src/lib/components/modes/treemode/JSONValue.svelte +++ b/src/lib/components/modes/treemode/JSONValue.svelte @@ -4,6 +4,7 @@ import type { JSONEditorContext, JSONSelection, SearchResultItem } from '$lib/types.js' import type { JSONPath } from 'immutable-json-patch' import { isEditingSelection, isValueSelection } from '$lib/logic/selection.js' + import { isSvelteActionRenderer } from '$lib/typeguards.js' export let path: JSONPath export let value: unknown @@ -34,7 +35,20 @@ {#each renderers as renderer} - {#key renderer.component} - - {/key} + {#if isSvelteActionRenderer(renderer)} + {@const action = renderer.action} + {#key renderer.action} +
+ {/key} + {:else} + {#key renderer.component} + + {/key} + {/if} {/each} diff --git a/src/lib/typeguards.ts b/src/lib/typeguards.ts index f3dd139c..c82a3905 100644 --- a/src/lib/typeguards.ts +++ b/src/lib/typeguards.ts @@ -9,7 +9,9 @@ import type { MenuSeparator, MenuSpace, ValidationError, - NestedValidationError + NestedValidationError, + SvelteActionRenderer, + SvelteComponentRenderer } from './types.js' import { isObject } from '$lib/utils/typeUtils.js' @@ -88,3 +90,11 @@ export function isValidationError(value: unknown): value is ValidationError { export function isNestedValidationError(value: unknown): value is NestedValidationError { return isObject(value) && isValidationError(value) && typeof value.isChildError === 'boolean' } + +export function isSvelteComponentRenderer(value: unknown): value is SvelteComponentRenderer { + return isObject(value) && 'component' in value && isObject(value.props) +} + +export function isSvelteActionRenderer(value: unknown): value is SvelteActionRenderer { + return isObject(value) && typeof value.action === 'function' && isObject(value.props) +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 40b9c6a1..c22075b8 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,6 +1,7 @@ import type { JSONPatchDocument, JSONPath, JSONPointer } from 'immutable-json-patch' import type { SvelteComponent } from 'svelte' import type { IconDefinition } from '@fortawesome/free-solid-svg-icons' +import type { Action } from 'svelte/action' export type TextContent = { text: string } @@ -541,11 +542,18 @@ export interface DraggingState { didMoveItems: boolean } -export interface RenderValueComponentDescription { +export type RenderValueComponentDescription = SvelteComponentRenderer | SvelteActionRenderer + +export interface SvelteComponentRenderer { component: typeof SvelteComponent props: Record } +export interface SvelteActionRenderer { + action: Action> + props: Record +} + export interface TransformModalOptions { id?: string rootPath?: JSONPath diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7cfa81a1..1522e5eb 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -33,7 +33,7 @@ Custom JSON Parser (Lossless JSON)
  • - Custom value renderer (password, enum) + Custom value renderer (password, enum, action)
  • JSON Schema validation diff --git a/src/routes/components/EvaluatorAction.ts b/src/routes/components/EvaluatorAction.ts new file mode 100644 index 00000000..73d6fc11 --- /dev/null +++ b/src/routes/components/EvaluatorAction.ts @@ -0,0 +1,58 @@ +import { createValueSelection, type OnJSONSelect } from 'svelte-jsoneditor' +import type { Action } from 'svelte/action' +import { type JSONPath } from 'immutable-json-patch' + +export interface EvaluatorActionProps { + value: unknown + path: JSONPath + readOnly: boolean + onSelect: OnJSONSelect +} + +export const EvaluatorAction: Action> = ( + node, + initialProps +) => { + let props = toEvaluatorProps(initialProps) + + function updateResult() { + node.innerText = evaluate(String(props.value)) + } + + function handleValueDoubleClick(event: MouseEvent) { + if (!props.readOnly) { + event.preventDefault() + event.stopPropagation() + + // open in edit mode + props.onSelect(createValueSelection(props.path, true)) + } + } + + node.addEventListener('dblclick', handleValueDoubleClick) + updateResult() + + return { + update: (updatedProps) => { + props = toEvaluatorProps(updatedProps) + updateResult() + }, + destroy: () => { + node.removeEventListener('dblclick', handleValueDoubleClick) + } + } +} + +function evaluate(expr: string) { + const result = expr + .split('+') + .map((value) => parseFloat(value.trim())) + .reduce((a, b) => a + b) + + return `The result of "${expr}" is "${result}" (double-click to edit)` +} + +function toEvaluatorProps(props: Record): EvaluatorActionProps { + // you can add validations and typeguards here if needed + return props as unknown as EvaluatorActionProps +} diff --git a/src/routes/examples/custom_value_renderer/+page.svelte b/src/routes/examples/custom_value_renderer/+page.svelte index f7c9b41e..dd0e7e43 100644 --- a/src/routes/examples/custom_value_renderer/+page.svelte +++ b/src/routes/examples/custom_value_renderer/+page.svelte @@ -1,13 +1,21 @@ - - Custom value renderer (password, enum) | svelte-jsoneditor + Custom value renderer (password, enum, action) | svelte-jsoneditor -

    Custom value renderer (password, enum)

    +

    Custom value renderer (password, enum, action)

    - Provide a custom onRenderValue method, which hides the value of all fields with the name - "password", and creates an enum for the fields with name "gender". -

    -

    - EXPERIMENTAL! This API will most likely change in future versions. + Provide a custom onRenderValue method, which demonstrates three things:

    +
      +
    1. + It hides the value of all fields with the name "password" using a Svelte password component ReadonlyPassword +
    2. +
    3. + It creates an enum component for the fields with name "gender" using a Svelte component EnumValue. +
    4. +
    5. + The creates a custom component for the field named "evaluate" using a Svelte Action, which + evaluates the value as an expression containing an addition of two or more values. This solution + can be used when using svelte-jsoneditor in a Vanilla JS environment. +
    6. +