diff --git a/README.md b/README.md index 79d5cc88..c8db97e0 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ see [src/type.ts](src/type.ts) ### Basic Example -```tsx +```jsx import { JsonViewer } from '@textea/json-viewer' const object = { @@ -57,8 +57,8 @@ const Component = () => ### Customizable data type -```tsx -import { JsonViewer, createDataType } from '@textea/json-viewer' +```jsx +import { JsonViewer, defineDataType } from '@textea/json-viewer' const object = { // what if I want to inspect a image? @@ -75,10 +75,10 @@ const Component = () => ( Component: (props) => {props.value} }, // or - createDataType( - (value) => typeof value === 'string' && value.startsWith('https://i.imgur.com'), - (props) => {props.value} - ) + defineDataType({ + is: (value) => typeof value === 'string' && value.startsWith('https://i.imgur.com'), + Component: (props) => {props.value} + }) ]} /> ) diff --git a/docs/pages/full/index.tsx b/docs/pages/full/index.tsx index b6fe4248..04b529be 100644 --- a/docs/pages/full/index.tsx +++ b/docs/pages/full/index.tsx @@ -20,7 +20,7 @@ import type { } from '@textea/json-viewer' import { applyValue, - createDataType, + defineDataType, JsonViewer, stringType } from '@textea/json-viewer' @@ -44,8 +44,8 @@ const aPlusBConst = function (a: number, b: number) { } const loopObject = { - foo: 1, - goo: 'string' + foo: 42, + goo: 'Lorem Ipsum' } as Record loopObject.self = loopObject @@ -67,30 +67,35 @@ const set = new Set([1, 2, 3]) const superLongString = '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' const example = { + avatar, + string: 'Lorem ipsum dolor sit amet', + integer: 42, + float: 114.514, + bigint: 123456789087654321n, + undefined, + timer: 0, + date: new Date('Tue Sep 13 2022 14:07:44 GMT-0500 (Central Daylight Time)'), + link: 'http://example.com', + emptyArray: [], + array: [19, 19, 810, 'test', NaN], + emptyObject: {}, + object: { + foo: true, + bar: false, + last: null + }, + emptyMap: new Map(), + map, + emptySet: new Set(), + set, loopObject, loopArray, longArray, - string: 'this is a string', - integer: 42, - array: [19, 19, 810, 'test', NaN], - emptyArray: [], nestedArray: [ [1, 2], [3, 4] ], - map, - emptyMap: new Map(), - set, - emptySet: new Set(), - float: 114.514, - undefined, superLongString, - object: { - 'first-child': true, - 'second-child': false, - 'last-child': null - }, - emptyObject: {}, function: aPlusB, constFunction: aPlusBConst, anonymousFunction: function (a: number, b: number) { @@ -101,12 +106,7 @@ const example = { console.log(arg1, arg2) return '123' }, - string_number: '1234', - timer: 0, - link: 'http://example.com', - avatar, - date: new Date('Tue Sep 13 2022 14:07:44 GMT-0500 (Central Daylight Time)'), - bigint: 123456789087654321n + string_number: '1234' } const KeyRenderer: JsonViewerKeyRenderer = ({ path }) => { @@ -116,8 +116,8 @@ const KeyRenderer: JsonViewerKeyRenderer = ({ path }) => { } KeyRenderer.when = (props) => props.value === 114.514 -const imageDataType = createDataType( - (value) => { +const imageDataType = defineDataType({ + is: (value) => { if (typeof value === 'string') { try { const url = new URL(value) @@ -128,17 +128,18 @@ const imageDataType = createDataType( } return false }, - (props) => { + Component: (props) => { return ( {props.value} ) } -) +}) const LinkIcon = (props: SvgIconProps) => ( // @@ -163,7 +164,7 @@ const linkType: DataType = { textDecoration: 'underline' }} > - + Open diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index 9a5d96cb..b4af3e31 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -43,6 +43,8 @@ const IconBox: FC = (props) => ( export const DataKeyPair: FC = (props) => { const { value, prevValue, path, nestedIndex } = props + const { Component, PreComponent, PostComponent, Editor, serialize, deserialize } = useTypeComponents(value, path) + const propsEditable = props.editable ?? undefined const storeEditable = useJsonViewerStore(store => store.editable) const editable = useMemo(() => { @@ -58,7 +60,7 @@ export const DataKeyPair: FC = (props) => { } return storeEditable }, [path, propsEditable, storeEditable, value]) - const [tempValue, setTempValue] = useState(typeof value === 'function' ? () => value : value) + const [tempValue, setTempValue] = useState('') const depth = path.length const key = path[depth - 1] const hoverPath = useJsonViewerStore(store => store.hoverPath) @@ -75,7 +77,6 @@ export const DataKeyPair: FC = (props) => { const keyColor = useTextColor() const numberKeyColor = useJsonViewerStore(store => store.colorspace.base0C) const highlightColor = useJsonViewerStore(store => store.colorspace.base0A) - const { Component, PreComponent, PostComponent, Editor } = useTypeComponents(value, path) const quotesOnKeys = useJsonViewerStore(store => store.quotesOnKeys) const rootName = useJsonViewerStore(store => store.rootName) const isRoot = root === value @@ -134,7 +135,7 @@ export const DataKeyPair: FC = (props) => { }, [highlightColor, isHighlight, prevValue, value]) const actionIcons = useMemo(() => { - if (editing) { + if (editing && deserialize) { return ( <> @@ -143,7 +144,7 @@ export const DataKeyPair: FC = (props) => { onClick={() => { // abort editing setEditing(false) - setTempValue(value) + setTempValue('') }} /> @@ -153,7 +154,12 @@ export const DataKeyPair: FC = (props) => { onClick={() => { // finish editing, save data setEditing(false) - onChange(path, value, tempValue) + try { + const newValue = deserialize(tempValue) + onChange(path, value, newValue) + } catch (e) { + // do nothing when deserialize failed + } }} /> @@ -182,14 +188,13 @@ export const DataKeyPair: FC = (props) => { } )} - {/* todo: support edit object */} - {(Editor && editable) && + {(Editor && editable && serialize && deserialize) && ( { event.preventDefault() + setTempValue(serialize(value)) setEditing(true) - setTempValue(value) }} > @@ -200,6 +205,8 @@ export const DataKeyPair: FC = (props) => { }, [ Editor, + serialize, + deserialize, copied, copy, editable, diff --git a/src/components/DataTypes/Boolean.tsx b/src/components/DataTypes/Boolean.tsx index e43b3a09..3e69a450 100644 --- a/src/components/DataTypes/Boolean.tsx +++ b/src/components/DataTypes/Boolean.tsx @@ -1,15 +1,15 @@ -import type { DataType } from '../../type' import { createEasyType } from './createEasyType' -export const booleanType: DataType = { +export const booleanType = createEasyType({ is: (value) => typeof value === 'boolean', - ...createEasyType( - 'bool', - ({ value }) => <>{value ? 'true' : 'false'}, - { - colorKey: 'base0E', - fromString: value => Boolean(value) - } - ) -} + type: 'bool', + colorKey: 'base0E', + serialize: value => value.toString(), + deserialize: value => { + if (value === 'true') return true + if (value === 'false') return false + throw new Error('Invalid boolean value') + }, + Renderer: ({ value }) => <>{value ? 'true' : 'false'} +}) diff --git a/src/components/DataTypes/Date.tsx b/src/components/DataTypes/Date.tsx index a57dbb3e..1c185799 100644 --- a/src/components/DataTypes/Date.tsx +++ b/src/components/DataTypes/Date.tsx @@ -1,5 +1,4 @@ -import type { DataType } from '../../type' import { createEasyType } from './createEasyType' const displayOptions: Intl.DateTimeFormatOptions = { @@ -11,13 +10,9 @@ const displayOptions: Intl.DateTimeFormatOptions = { minute: '2-digit' } -export const dateType: DataType = { +export const dateType = createEasyType({ is: (value) => value instanceof Date, - ...createEasyType( - 'date', - ({ value }) => <>{value.toLocaleTimeString('en-us', displayOptions)}, - { - colorKey: 'base0D' - } - ) -} + type: 'date', + colorKey: 'base0D', + Renderer: ({ value }) => <>{value.toLocaleTimeString('en-us', displayOptions)} +}) diff --git a/src/components/DataTypes/Null.tsx b/src/components/DataTypes/Null.tsx index bf162b53..ed40b037 100644 --- a/src/components/DataTypes/Null.tsx +++ b/src/components/DataTypes/Null.tsx @@ -1,32 +1,27 @@ import { Box } from '@mui/material' import { useJsonViewerStore } from '../../stores/JsonViewerStore' -import type { DataType } from '../../type' import { createEasyType } from './createEasyType' -export const nullType: DataType = { +export const nullType = createEasyType({ is: (value) => value === null, - ...createEasyType( - 'null', - () => { - const backgroundColor = useJsonViewerStore(store => store.colorspace.base02) - return ( - - NULL - - ) - }, - { - colorKey: 'base08', - displayTypeLabel: false - } - ) -} + type: 'null', + colorKey: 'base08', + displayTypeLabel: false, + Renderer: () => { + const backgroundColor = useJsonViewerStore(store => store.colorspace.base02) + return ( + + NULL + + ) + } +}) diff --git a/src/components/DataTypes/Number.tsx b/src/components/DataTypes/Number.tsx index 96cc4923..6101ceb2 100644 --- a/src/components/DataTypes/Number.tsx +++ b/src/components/DataTypes/Number.tsx @@ -1,69 +1,60 @@ import { Box } from '@mui/material' import { useJsonViewerStore } from '../../stores/JsonViewerStore' -import type { DataType } from '../../type' import { createEasyType } from './createEasyType' const isInt = (n: number) => n % 1 === 0 -export const nanType: DataType = { +export const nanType = createEasyType({ is: (value) => typeof value === 'number' && isNaN(value), - ...createEasyType( - 'NaN', - () => { - const backgroundColor = useJsonViewerStore(store => store.colorspace.base02) - return ( - - NaN - - ) - }, - { - colorKey: 'base08', - displayTypeLabel: false - } - ) -} + type: 'NaN', + colorKey: 'base08', + displayTypeLabel: false, + serialize: () => 'NaN', + // allow deserialize the value back to number + deserialize: (value) => parseFloat(value), + Renderer: () => { + const backgroundColor = useJsonViewerStore(store => store.colorspace.base02) + return ( + + NaN + + ) + } +}) -export const floatType: DataType = { - is: (value) => typeof value === 'number' && !isInt(value), - ...createEasyType( - 'float', - ({ value }) => <>{value}, - { - colorKey: 'base0B', - fromString: value => parseFloat(value) - } - ) -} +export const floatType = createEasyType({ + is: (value) => typeof value === 'number' && !isInt(value) && !isNaN(value), + type: 'float', + colorKey: 'base0B', + serialize: value => value.toString(), + deserialize: value => parseFloat(value), + Renderer: ({ value }) => <>{value} +}) -export const intType: DataType = { +export const intType = createEasyType({ is: (value) => typeof value === 'number' && isInt(value), - ...createEasyType( - 'int', - ({ value }) => <>{value}, - { - colorKey: 'base0F', - fromString: value => parseInt(value) - } - ) -} + type: 'int', + colorKey: 'base0F', + serialize: value => value.toString(), + // allow deserialize the value to float + deserialize: value => parseFloat(value), + Renderer: ({ value }) => <>{value} +}) -export const bigIntType: DataType = { +export const bigIntType = createEasyType({ is: (value) => typeof value === 'bigint', - ...createEasyType( - 'bigint', - ({ value }) => <>{`${value}n`}, - { - colorKey: 'base0F', - fromString: value => BigInt(value.replace(/\D/g, '')) - } - ) -} + type: 'bigint', + colorKey: 'base0F', + serialize: value => value.toString(), + deserialize: value => BigInt(value.replace(/\D/g, '')), + Renderer: ({ value }) => <>{`${value}n`} +}) diff --git a/src/components/DataTypes/String.tsx b/src/components/DataTypes/String.tsx index 52c063e0..d9fb3974 100644 --- a/src/components/DataTypes/String.tsx +++ b/src/components/DataTypes/String.tsx @@ -2,43 +2,39 @@ import { Box } from '@mui/material' import { useState } from 'react' import { useJsonViewerStore } from '../../stores/JsonViewerStore' -import type { DataType } from '../../type' import { createEasyType } from './createEasyType' -export const stringType: DataType = { +export const stringType = createEasyType({ is: (value) => typeof value === 'string', - ...createEasyType( - 'string', - (props) => { - const [showRest, setShowRest] = useState(false) - const collapseStringsAfterLength = useJsonViewerStore(store => store.collapseStringsAfterLength) - const value = showRest - ? props.value - : props.value.slice(0, collapseStringsAfterLength) - const hasRest = props.value.length > collapseStringsAfterLength - return ( - { - if (hasRest) { - setShowRest(value => !value) - } - }} - > - " - {value} - {hasRest && !showRest && ()} - " - - ) - }, - { - colorKey: 'base09', - fromString: value => value - } - ) -} + type: 'string', + colorKey: 'base09', + serialize: value => value, + deserialize: value => value, + Renderer: (props) => { + const [showRest, setShowRest] = useState(false) + const collapseStringsAfterLength = useJsonViewerStore(store => store.collapseStringsAfterLength) + const value = showRest + ? props.value + : props.value.slice(0, collapseStringsAfterLength) + const hasRest = props.value.length > collapseStringsAfterLength + return ( + { + if (hasRest) { + setShowRest(value => !value) + } + }} + > + " + {value} + {hasRest && !showRest && ()} + " + + ) + } +}) diff --git a/src/components/DataTypes/Undefined.tsx b/src/components/DataTypes/Undefined.tsx index 9b15b52b..0d7d2bee 100644 --- a/src/components/DataTypes/Undefined.tsx +++ b/src/components/DataTypes/Undefined.tsx @@ -1,31 +1,26 @@ import { Box } from '@mui/material' import { useJsonViewerStore } from '../../stores/JsonViewerStore' -import type { DataType } from '../../type' import { createEasyType } from './createEasyType' -export const undefinedType: DataType = { +export const undefinedType = createEasyType({ is: (value) => value === undefined, - ...createEasyType( - 'undefined', - () => { - const backgroundColor = useJsonViewerStore(store => store.colorspace.base02) - return ( - - undefined - - ) - }, - { - colorKey: 'base05', - displayTypeLabel: false - } - ) -} + type: 'undefined', + colorKey: 'base05', + displayTypeLabel: false, + Renderer: () => { + const backgroundColor = useJsonViewerStore(store => store.colorspace.base02) + return ( + + undefined + + ) + } +}) diff --git a/src/components/DataTypes/createEasyType.tsx b/src/components/DataTypes/createEasyType.tsx index 5d45b96e..56f9d465 100644 --- a/src/components/DataTypes/createEasyType.tsx +++ b/src/components/DataTypes/createEasyType.tsx @@ -8,18 +8,22 @@ import type { DataItemProps, DataType, EditorProps } from '../../type' import { DataTypeLabel } from '../DataTypeLabel' import { DataBox } from '../mui/DataBox' -export function createEasyType ( - type: string, - renderValue: ComponentType, 'value'>>, - config: { - colorKey: keyof Colorspace - fromString?: (value: string) => Value - displayTypeLabel?: boolean - } -): Omit, 'is'> { - const { fromString, colorKey, displayTypeLabel = true } = config - - const Render = memo(renderValue) +type EasyTypeConfig = Pick, 'is' | 'serialize' | 'deserialize'> & { + type: string + colorKey: keyof Colorspace + displayTypeLabel?: boolean + Renderer: ComponentType, 'value'>> +} +export function createEasyType ({ + is, + serialize, + deserialize, + type, + colorKey, + displayTypeLabel = true, + Renderer +}: EasyTypeConfig): DataType { + const Render = memo(Renderer) const EasyType: FC> = (props) => { const storeDisplayDataTypes = useJsonViewerStore(store => store.displayDataTypes) const color = useJsonViewerStore(store => store.colorspace[colorKey]) @@ -36,13 +40,14 @@ export function createEasyType ( } EasyType.displayName = `easy-${type}-type` - if (!fromString) { + if (!serialize || !deserialize) { return { + is, Component: EasyType } } - const EasyTypeEditor: FC> = ({ value, setValue }) => { + const EasyTypeEditor: FC> = ({ value, setValue }) => { const color = useJsonViewerStore(store => store.colorspace[colorKey]) return ( ( onChange={ useCallback>( (event) => { - const value = fromString(event.target.value) - setValue(value) + setValue(event.target.value) }, [setValue] ) } @@ -73,6 +77,9 @@ export function createEasyType ( EasyTypeEditor.displayName = `easy-${type}-type-editor` return { + is, + serialize, + deserialize, Component: EasyType, Editor: EasyTypeEditor } diff --git a/src/index.tsx b/src/index.tsx index 78a856fb..d633272f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -173,4 +173,4 @@ export const JsonViewer = function JsonViewer (props: JsonViewerProps(selector: (state: TypeRe } function matchTypeComponents ( - value: Value, path: Path, registry: TypeRegistryState['registry']): DataType { + value: Value, + path: Path, + registry: TypeRegistryState['registry'] +): DataType { let potential: DataType | undefined for (const T of registry) { if (T.is(value, path)) { diff --git a/src/type.ts b/src/type.ts index 7137f1c6..2a53d634 100644 --- a/src/type.ts +++ b/src/type.ts @@ -52,8 +52,18 @@ export type DataType = { * Whether the value belongs to the data type */ is: (value: unknown, path: Path) => boolean + /** + * transform the value to a string for editing + */ + serialize?: (value: ValueType) => string + /** + * parse the string back to a value + * throw an error if the input is invalid + * and the editor will ignore the change + */ + deserialize?: (value: string) => ValueType Component: ComponentType> - Editor?: ComponentType> + Editor?: ComponentType> PreComponent?: ComponentType> PostComponent?: ComponentType> } diff --git a/src/utils/index.ts b/src/utils/index.ts index c709f216..ba603906 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,7 @@ import copyToClipboard from 'copy-to-clipboard' import type { ComponentType } from 'react' -import type { DataItemProps, EditorProps, Path } from '../type' +import type { DataItemProps, DataType, EditorProps, Path } from '../type' // reference: https://github.com/immerjs/immer/blob/main/src/utils/common.ts const objectCtorString = Object.prototype.constructor.toString() @@ -63,6 +63,9 @@ export function applyValue (input: any, path: (string | number)[], value: any) { return input } +/** + * @deprecated use `defineDataType` instead + */ // case 1: you only render with a single component export function createDataType ( is: (value: unknown, path: Path) => boolean, @@ -108,7 +111,6 @@ export function createDataType ( PreComponent: ComponentType> PostComponent: ComponentType> } - export function createDataType ( is: (value: unknown, path: Path) => boolean, Component: ComponentType>, @@ -116,8 +118,47 @@ export function createDataType ( PreComponent?: ComponentType> | undefined, PostComponent?: ComponentType> | undefined ): any { + if (process.env.NODE_ENV !== 'production') { + console.warn('createDataType is deprecated, please use `defineDataType` instead') + } + return { + is, + Component, + Editor, + PreComponent, + PostComponent + } +} + +export function defineDataType ({ + is, + serialize, + deserialize, + Component, + Editor, + PreComponent, + PostComponent +}: { + is: (value: unknown, path: Path) => boolean + /** + * transform the value to a string for editing + */ + serialize?: (value: ValueType) => string + /** + * parse the string back to a value + * throw an error if the input is invalid + * and the editor will ignore the change + */ + deserialize?: (value: string) => ValueType + Component: ComponentType> + Editor?: ComponentType> + PreComponent?: ComponentType> + PostComponent?: ComponentType> +}): DataType { return { is, + serialize, + deserialize, Component, Editor, PreComponent,