diff --git a/examples/basic/pages/index.tsx b/examples/basic/pages/index.tsx index df23b88d..2502573b 100644 --- a/examples/basic/pages/index.tsx +++ b/examples/basic/pages/index.tsx @@ -11,7 +11,22 @@ function aPlusB (a: number, b: number) { return a + b } +const loopObject = { + foo: 1, + goo: 'string' +} as Record + +loopObject.self = loopObject + +const loopArray = [ + loopObject +] + +loopArray[1] = loopArray + const example = { + loopObject, + loopArray, string: 'this is a string', integer: 42, array: [19, 19, 810, 'test', NaN], diff --git a/src/components/DataKeyPair.tsx b/src/components/DataKeyPair.tsx index b86b22fe..ebf3c5a9 100644 --- a/src/components/DataKeyPair.tsx +++ b/src/components/DataKeyPair.tsx @@ -13,6 +13,7 @@ import { useTextColor } from '../hooks/useColor' import { useJsonViewerStore } from '../stores/JsonViewerStore' import { useTypeComponents } from '../stores/typeRegistry' import type { DataItemProps } from '../type' +import { isCycleReference } from '../utils' import { DataBox } from './mui/DataBox' export type DataKeyPairProps = { @@ -35,8 +36,14 @@ export const DataKeyPair: React.FC = (props) => { return hoverPath && path.every((value, index) => value === hoverPath[index]) }, [hoverPath, path]) const setHover = useJsonViewerStore(store => store.setHover) + const root = useJsonViewerStore(store => store.value) + const isTrap = useMemo(() => isCycleReference(root, path, value), [path, root, value]) + const defaultCollapsed = useJsonViewerStore(store => store.defaultCollapsed) + // do not inspect when it is a cycle reference, otherwise there will have a loop const [inspect, setInspect] = useState( - !useJsonViewerStore(store => store.defaultCollapsed) + isTrap + ? false + : !defaultCollapsed ) const [editing, setEditing] = useState(false) const onChange = useJsonViewerStore(store => store.onChange) @@ -46,7 +53,7 @@ export const DataKeyPair: React.FC = (props) => { const { Component, PreComponent, PostComponent, Editor } = useTypeComponents( value) const rootName = useJsonViewerStore(store => store.rootName) - const isRoot = useJsonViewerStore(store => store.value) === value + const isRoot = root === value const isNumberKey = Number.isInteger(Number(key)) const displayKey = isRoot ? rootName : key const downstreamProps: DataItemProps = useMemo(() => ({ @@ -169,11 +176,12 @@ export const DataKeyPair: React.FC = (props) => { {PreComponent && } {(isHover && expandable && inspect) && actionIcons} - {editing - ? (Editor && ) - : Component - ? - : ) + : (Component) + ? + : {JSON.stringify(value)} } {PostComponent && } diff --git a/src/components/DataTypes/Array.tsx b/src/components/DataTypes/Array.tsx deleted file mode 100644 index 7b0aa9b7..00000000 --- a/src/components/DataTypes/Array.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { Box } from '@mui/material' -import React, { useMemo } from 'react' - -import { useTextColor } from '../../hooks/useColor' -import { useJsonViewerStore } from '../../stores/JsonViewerStore' -import type { DataItemProps } from '../../type' -import { DataKeyPair } from '../DataKeyPair' - -const arrayLb = '[' -const arrayRb = ']' - -export const PreArrayType: React.FC> = (props) => { - const metadataColor = useJsonViewerStore(store => store.colorNamespace.base04) - const sizeOfValue = useMemo( - () => props.inspect ? `${Object.keys(props.value).length} Items` : '', - [props.inspect, props.value] - ) - return ( - - {arrayLb} - - {sizeOfValue} - - - ) -} - -export const PostArrayType: React.FC> = (props) => { - const metadataColor = useJsonViewerStore(store => store.colorNamespace.base04) - const sizeOfValue = useMemo( - () => !props.inspect ? `${Object.keys(props.value).length} Items` : '', - [props.inspect, props.value] - ) - return ( - - {arrayRb} - - {sizeOfValue} - - - ) -} - -export const ArrayType: React.FC> = (props) => { - const keyColor = useTextColor() - const groupArraysAfterLength = useJsonViewerStore(store => store.groupArraysAfterLength) - const elements = useMemo(() => { - if (props.value.length <= groupArraysAfterLength) { - return props.value.map((value, index) => { - const path = [...props.path, index] - return ( - - ) - }) - } - const value = props.value.reduce((array, value, index) => { - const target = Math.floor(index / groupArraysAfterLength) - if (array[target]) { - array[target].push(value) - } else { - array[target] = [value] - } - return array - }, []) - - return value.map((list, index) => { - const path = [...props.path] - return ( - - ) - }) - }, [props.path, props.value, groupArraysAfterLength]) - return ( - - { - props.inspect - ? elements - : ( - - ... - - ) - } - - ) -} diff --git a/src/components/DataTypes/Object.tsx b/src/components/DataTypes/Object.tsx index a2b1905a..4610732a 100644 --- a/src/components/DataTypes/Object.tsx +++ b/src/components/DataTypes/Object.tsx @@ -4,7 +4,9 @@ import React, { useMemo } from 'react' import { useTextColor } from '../../hooks/useColor' import { useJsonViewerStore } from '../../stores/JsonViewerStore' import type { DataItemProps } from '../../type' +import { isCycleReference } from '../../utils' import { DataKeyPair } from '../DataKeyPair' +import { CircularArrowsIcon } from '../icons/CircularArrowsIcon' const objectLb = '{' const arrayLb = '[' @@ -13,11 +15,16 @@ const arrayRb = ']' export const PreObjectType: React.FC> = (props) => { const metadataColor = useJsonViewerStore(store => store.colorNamespace.base04) + const textColor = useTextColor() const isArray = useMemo(() => Array.isArray(props.value), [props.value]) const sizeOfValue = useMemo( () => props.inspect ? `${Object.keys(props.value).length} Items` : '', [props.inspect, props.value] ) + const rootValue = useJsonViewerStore(store => store.value) + const isTrap = useMemo( + () => isCycleReference(rootValue, props.path, props.value), + [props.path, props.value, rootValue]) return ( > = (props) => { > {sizeOfValue} + {isTrap + ? + : null} ) } @@ -66,14 +77,48 @@ export const PostObjectType: React.FC> = (props) => { export const ObjectType: React.FC> = (props) => { const keyColor = useTextColor() - const elements = useMemo(() => ( - Object.entries(props.value).map(([key, value]) => { - const path = [...props.path, key] - return ( - - ) - }) - ), [props.path, props.value]) + const groupArraysAfterLength = useJsonViewerStore( + store => store.groupArraysAfterLength) + const rootValue = useJsonViewerStore(store => store.value) + const isTrap = useMemo( + () => isCycleReference(rootValue, props.path, props.value), + [props.path, props.value, rootValue] + ) + const elements = useMemo(() => { + if (Array.isArray(props.value)) { + if (props.value.length <= groupArraysAfterLength) { + return props.value.map((value, index) => { + const path = [...props.path, index] + return ( + + ) + }) + } + const value = props.value.reduce((array, value, index) => { + const target = Math.floor(index / groupArraysAfterLength) + if (array[target]) { + array[target].push(value) + } else { + array[target] = [value] + } + return array + }, []) + + return value.map((list, index) => { + const path = [...props.path] + return ( + + ) + }) + } else { + return Object.entries(props.value).map(([key, value]) => { + const path = [...props.path, key] + return ( + + ) + }) + } + }, [props.value, props.path, groupArraysAfterLength]) return ( > = (props) => { { props.inspect ? elements - : ( - - ... - - ) + : !isTrap + ? ( + + ... + + ) + : null } ) diff --git a/src/components/icons/CircularArrowsIcon.tsx b/src/components/icons/CircularArrowsIcon.tsx new file mode 100644 index 00000000..680a0520 --- /dev/null +++ b/src/components/icons/CircularArrowsIcon.tsx @@ -0,0 +1,10 @@ +import { SvgIcon, SvgIconProps } from '@mui/material' +import type React from 'react' + +export const CircularArrowsIcon: React.FC = (props) => { + return ( + + + + ) +} diff --git a/src/stores/typeRegistry.tsx b/src/stores/typeRegistry.tsx index 6a30b68f..59809527 100644 --- a/src/stores/typeRegistry.tsx +++ b/src/stores/typeRegistry.tsx @@ -2,11 +2,6 @@ import { Box } from '@mui/material' import { DevelopmentError } from '@textea/dev-kit/utils' import React, { useMemo } from 'react' -import { - ArrayType, - PostArrayType, - PreArrayType -} from '../components/DataTypes/Array' import { createEasyType } from '../components/DataTypes/createEasyType' import { FunctionType, PostFunctionType, @@ -205,15 +200,6 @@ registerType( } ) -registerType( - { - is: (value): value is unknown[] => Array.isArray(value), - Component: ArrayType, - PreComponent: PreArrayType, - PostComponent: PostArrayType - } -) - // fallback for all data like 'object' registerType( { diff --git a/src/utils/index.ts b/src/utils/index.ts index 91685717..fd218c68 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -15,3 +15,31 @@ export const applyValue = (obj: any, path: (string | number)[], value: any) => { } return obj } + +export const isCycleReference = (root: any, path: (string | number)[], value: unknown): boolean => { + if (root === null || value === null) { + return false + } + if (typeof root !== 'object') { + return false + } + if (typeof value !== 'object') { + return false + } + if (Object.is(root, value) && path.length !== 0) { + return true + } + const arr = [...path] + let currentRoot = root + while (currentRoot !== value || arr.length !== 0) { + if (typeof currentRoot !== 'object' || currentRoot === null) { + return false + } + const target = arr.shift()! + if (Object.is(currentRoot, value)) { + return true + } + currentRoot = currentRoot[target] + } + return false +}