diff --git a/core/src/comps/KeyValues.tsx b/core/src/comps/KeyValues.tsx index 47bcc19c..2f05c7b1 100644 --- a/core/src/comps/KeyValues.tsx +++ b/core/src/comps/KeyValues.tsx @@ -1,4 +1,4 @@ -import { Fragment, useRef } from 'react'; +import { Fragment, useRef, useState } from 'react'; import { useStore } from '../store'; import { useExpandsStore } from '../store/Expands'; import { useShowToolsDispatch } from '../store/ShowTools'; @@ -11,6 +11,8 @@ import { useHighlight } from '../utils/useHighlight'; import { type SectionElementResult } from '../store/Section'; import { Copied } from '../comps/Copied'; import { useIdCompat } from '../comps/useIdCompat'; +import { ValueExtraComp } from '../section/ValueExtra'; +import { KeyValueItemContext } from '../editor/store'; interface KeyValuesProps extends SectionElementResult { expandKey?: string; @@ -114,12 +116,17 @@ export const KeyValuesItem = (props: KeyValuesProps) => { onMouseEnter: () => dispatch({ [subkeyid]: true }), onMouseLeave: () => dispatch({ [subkeyid]: false }), }; + + const [editable, setEditable] = useState(false) return ( - - - - - + + + + + + + + ); }; diff --git a/core/src/comps/NestedOpen.tsx b/core/src/comps/NestedOpen.tsx index ae0f866b..e8ca99d5 100644 --- a/core/src/comps/NestedOpen.tsx +++ b/core/src/comps/NestedOpen.tsx @@ -60,7 +60,7 @@ export const NestedOpen = (props: NestedOpenProps) => { - + ); diff --git a/core/src/editor/CountInfoExtra.tsx b/core/src/editor/CountInfoExtra.tsx new file mode 100644 index 00000000..9c7d4f5d --- /dev/null +++ b/core/src/editor/CountInfoExtra.tsx @@ -0,0 +1,50 @@ +import { type SectionElementProps } from '../store/Section'; +import { useStore } from './store'; +import { AddIcon } from './icon/add'; +import { DeleteIcon } from './icon/delete'; +import { useShowToolsStore } from '../store/ShowTools'; + +export const CountInfoExtraRender: SectionElementProps['render'] = ( + { children, ...reset }, + { value, parentValue, keyName, expandKey }, +) => { + const showTools = useShowToolsStore(); + const isShowTools = expandKey && showTools[expandKey]; + if (!isShowTools) return null; + + // const { onEdit } = useStore() + + const click = async (event: React.MouseEvent) => { + event.stopPropagation(); + // const keyOrValue = 'AddKeyOrValue'; + // const isArray = Array.isArray(value); + // const isAdd = isArray ? true : !(keyOrValue in value); + // const result = isArray ? [...value, keyOrValue] : { ...value, [keyOrValue]: undefined }; + // if (onAdd && setValue) { + // const maybeAdd = await onAdd(keyOrValue, result as T, props.value, isAdd); + // if (maybeAdd) { + // setValue!(result as T); + // } + // } + }; + const deleteHandle = async (event: React.MouseEvent) => { + event.stopPropagation(); + // if (onDelete && (keyName || typeof keyName === 'number') && parentValue) { + // const maybeDelete = await onDelete(keyName, value, parentValue as T, { namespace }); + // if (maybeDelete && setParentValue) { + // if (Array.isArray(parentValue)) { + // parentValue.splice(keyName as number, 1); + // setParentValue([...parentValue] as T); + // } else if (keyName in parentValue) { + // delete (parentValue as Record)[keyName as string]; + // setParentValue({ ...parentValue } as T); + // } + // } + // } + }; + return <> + + {parentValue && } + +}; + diff --git a/core/src/editor/ValueExtra.tsx b/core/src/editor/ValueExtra.tsx new file mode 100644 index 00000000..2af4efbc --- /dev/null +++ b/core/src/editor/ValueExtra.tsx @@ -0,0 +1,40 @@ +import { type SectionElementProps } from '../store/Section'; +import { useKeyValueItem, useStore } from './store'; +import { EditIcon } from './icon/edit'; +import { DeleteIcon } from './icon/delete'; +import { useShowToolsStore } from '../store/ShowTools'; + +export const ValueExtraRender: SectionElementProps['render'] = ( + { children, ...reset }, + { value, parentValue, keyName, expandKey }, +) => { + const { onEdit } = useStore(); + const { setEditable } = useKeyValueItem() + + const showTools = useShowToolsStore(); + const isShowTools = expandKey && showTools[expandKey]; + if (!isShowTools) return null; + + const click = (evn: React.MouseEvent) => { + evn.stopPropagation(); + setEditable?.(true); + }; + + const deleteHandle = async (evn: React.MouseEvent) => { + evn.stopPropagation(); + // if (data && keyName && keyName in data && setValue && onDelete) { + // const maybeDelete = await onDelete(keyName, value as T, parentValue as T, { namespace }); + // if (maybeDelete) { + // delete (data as Record)[keyName as string]; + // setValue({ ...data } as T); + // } + // } + }; + return <> + {/* {visible && editableValue && onEdit && } + {visible && editableValue && onDelete && } */} + + + ; +}; + diff --git a/core/src/editor/icon/add.tsx b/core/src/editor/icon/add.tsx new file mode 100644 index 00000000..8a852fbb --- /dev/null +++ b/core/src/editor/icon/add.tsx @@ -0,0 +1,19 @@ +import { CSSProperties } from 'react'; + +export interface AddIconProps extends React.SVGAttributes {} +export const AddIcon = (props: AddIconProps) => { + const { style } = props + const defaultStyle: CSSProperties = { + verticalAlign: 'middle', + display: 'inline-block', + cursor: 'pointer', + marginLeft: 5, + height: '1em', + width: '1em', + } + return ( + + + + ); +} diff --git a/core/src/editor/icon/delete.tsx b/core/src/editor/icon/delete.tsx new file mode 100644 index 00000000..340e54e8 --- /dev/null +++ b/core/src/editor/icon/delete.tsx @@ -0,0 +1,19 @@ +import { CSSProperties } from 'react'; + +export interface DeleteIconProps extends React.SVGAttributes {} +export const DeleteIcon = (props: DeleteIconProps) => { + const { style } = props + const defaultStyle: CSSProperties = { + verticalAlign: 'middle', + display: 'inline-block', + cursor: 'pointer', + marginLeft: 5, + height: '1em', + width: '1em', + } + return ( + + + + ); +} diff --git a/core/src/editor/icon/edit.tsx b/core/src/editor/icon/edit.tsx new file mode 100644 index 00000000..f5ff572c --- /dev/null +++ b/core/src/editor/icon/edit.tsx @@ -0,0 +1,24 @@ +import { CSSProperties } from 'react'; + +export interface EditIconProps extends React.SVGAttributes {} +export const EditIcon = (props: EditIconProps) => { + const { style } = props + const defaultStyle: CSSProperties = { + verticalAlign: 'middle', + display: 'inline-block', + cursor: 'pointer', + marginLeft: 5, + height: '1em', + width: '1em', + } + return ( + + + + ) +} \ No newline at end of file diff --git a/core/src/editor/index.tsx b/core/src/editor/index.tsx index 5aaf8a98..8d5f96e4 100644 --- a/core/src/editor/index.tsx +++ b/core/src/editor/index.tsx @@ -1,7 +1,9 @@ -import { forwardRef } from 'react'; +import { forwardRef, useRef } from 'react'; import JsonView, { type JsonViewProps } from '../'; import { KeyNameRender } from './KeyName'; -import { Context, Dispatch, useStoreReducer } from './store'; +import { CountInfoExtraRender } from './CountInfoExtra'; +import { Context, Dispatch, useKeyValueItem, useStore, useStoreReducer } from './store'; +import { ValueExtraRender } from './ValueExtra'; export interface JsonViewEditorProps extends Omit, 'shortenTextAfterLength'> { /** @@ -25,8 +27,20 @@ const JsonViewEditor = forwardRef>(( return ( - + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } + {editable && } {children} @@ -35,3 +49,27 @@ const JsonViewEditor = forwardRef>(( }); export default JsonViewEditor; + +const EditableSpan = ({ ...props }: any) => { + const ref = useRef(null) + const { editable, setEditable } = useKeyValueItem() + const { onEdit } = useStore() + if (editable) { + ref.current?.setAttribute("contentEditable", "true") + ref.current?.focus() + } + + const onKeyDown = (evn: React.KeyboardEvent) => { + if (evn.key === 'Enter') { + ref.current?.setAttribute('contentEditable', 'false'); + setEditable(false) + } + }; + + const onBlur = (evn: React.FocusEvent) => { + ref.current?.setAttribute('contentEditable', 'false'); + setEditable(false) + onEdit && onEdit({ value: evn.target.textContent, oldValue: props.children, keyName: "keyName" }); + }; + return {props.children} +} diff --git a/core/src/editor/store.tsx b/core/src/editor/store.tsx index ef3f65ad..2fcd6754 100644 --- a/core/src/editor/store.tsx +++ b/core/src/editor/store.tsx @@ -17,6 +17,7 @@ type Dispatch = React.Dispatch; const initialState: InitialState = {}; export const Context = createContext(initialState); +export const KeyValueItemContext = createContext<{ editable: boolean, setEditable: (b: boolean) => void }>({ editable: false, setEditable: () => { } }); const reducer = (state: InitialState, action: InitialState) => ({ ...state, @@ -30,6 +31,10 @@ export const useStore = () => { return useContext(Context); }; +export const useKeyValueItem = () => { + return useContext(KeyValueItemContext); +}; + export function useStoreReducer(initialState: InitialState) { return useReducer(reducer, initialState); } diff --git a/core/src/index.tsx b/core/src/index.tsx index 7122e4df..98bc02d0 100644 --- a/core/src/index.tsx +++ b/core/src/index.tsx @@ -32,6 +32,7 @@ import { CountInfoExtra } from './section/CountInfoExtra'; import { Ellipsis } from './section/Ellipsis'; import { KeyName } from './section/KeyName'; import { Row } from './section/Row'; +import { ValueExtra } from './section/ValueExtra'; export * from './store'; export * from './store/Expands'; @@ -107,6 +108,7 @@ type JsonViewComponent = React.FC>> & { CountInfo: typeof CountInfo; CountInfoExtra: typeof CountInfoExtra; KeyName: typeof KeyName; + ValueExtra: typeof ValueExtra; Row: typeof Row; }; @@ -191,6 +193,7 @@ JsonView.Copied = Copied; JsonView.CountInfo = CountInfo; JsonView.CountInfoExtra = CountInfoExtra; JsonView.KeyName = KeyName; +JsonView.ValueExtra = ValueExtra; JsonView.Row = Row; JsonView.displayName = 'JVR.JsonView'; diff --git a/core/src/section/CountInfoExtra.tsx b/core/src/section/CountInfoExtra.tsx index dbd73988..98f45450 100644 --- a/core/src/section/CountInfoExtra.tsx +++ b/core/src/section/CountInfoExtra.tsx @@ -12,20 +12,22 @@ CountInfoExtra.displayName = 'JVR.CountInfoExtra'; export interface CountInfoExtraCompsProps { value?: T; + parentValue?: T; keyName: string | number; + expandKey?: string; } export const CountInfoExtraComps = ( props: SectionElementProps & CountInfoExtraCompsProps, ) => { - const { value = {}, keyName, ...other } = props; + const { value = {}, parentValue, keyName, expandKey, ...other } = props; const { CountInfoExtra: Comp = {} } = useSectionStore(); const { as, render, ...reset } = Comp; if (!render && !reset.children) return null; const Elm = as || 'span'; const isRender = render && typeof render === 'function'; const elmProps = { ...reset, ...other }; - const child = isRender && render(elmProps as React.HTMLAttributes, { value, keyName }); + const child = isRender && render(elmProps as React.HTMLAttributes, { value, keyName, parentValue, expandKey }); if (child) return child; return ; }; diff --git a/core/src/section/ValueExtra.tsx b/core/src/section/ValueExtra.tsx new file mode 100644 index 00000000..7ba4e122 --- /dev/null +++ b/core/src/section/ValueExtra.tsx @@ -0,0 +1,40 @@ +import { type TagType } from '../store/Types'; +import { type SectionElement, useSectionStore } from '../store/Section'; +import { useSectionRender } from '../utils/useRender'; +import { type SectionElementResult } from '../store/Section'; + +export const ValueExtra = (props: SectionElement) => { + const { ValueExtra: Comp = {} } = useSectionStore(); + useSectionRender(Comp, props, 'ValueExtra'); + return null; +}; + +ValueExtra.displayName = 'JVR.ValueExtra'; + +export interface ValueExtraCompProps extends React.HTMLAttributes, SectionElementResult { +} + +export const ValueExtraComp = (props: React.PropsWithChildren>) => { + const { children, value, parentValue, keyName, keys, expandKey, ...other } = props; + const { ValueExtra: Comp = {} } = useSectionStore(); + const { as, render, children: _, ...reset } = Comp; + const Elm = as || 'div'; + const child = + render && + typeof render === 'function' && + render({ ...other, ...reset, children }, { value, keyName, parentValue, keys, expandKey }); + if (child) return child; + return ( + + {children} + + ); +}; + +ValueExtraComp.displayName = 'JVR.ValueExtraComp'; + + + + + + diff --git a/core/src/store/Section.tsx b/core/src/store/Section.tsx index 0ea0d58a..f81e1255 100644 --- a/core/src/store/Section.tsx +++ b/core/src/store/Section.tsx @@ -5,6 +5,7 @@ export interface SectionElementResult { value?: T; parentValue?: T; keyName?: K; + expandKey?: string; /** Index of the parent `keyName` */ keys?: K[]; } @@ -23,6 +24,7 @@ type InitialState = { Ellipsis?: SectionElement; Row?: SectionElement; KeyName?: SectionElement; + ValueExtra?: SectionElement; }; type Dispatch = React.Dispatch>; @@ -71,6 +73,13 @@ const initialState: InitialState = { as: 'span', className: 'w-rjv-object-key', }, + ValueExtra: { + as: 'span', + className: 'w-rjv-object-extra', + style: { + paddingLeft: 8, + }, + }, }; const Context = createContext>(initialState); diff --git a/www/src/App.tsx b/www/src/App.tsx index d876d27f..3a37fe01 100644 --- a/www/src/App.tsx +++ b/www/src/App.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import styled, { css } from 'styled-components'; import { Example } from './example/default'; +import { ExampleEditor } from './example/editor'; const ExampleWrapper = styled.div` max-width: 630px; @@ -33,12 +34,12 @@ export default function App() { - {/* */} + {tabs === 'preview' && } - {/* {tabs === 'editor' && } */} + {tabs === 'editor' && } ); } diff --git a/www/src/example/default.tsx b/www/src/example/default.tsx index b8a1145a..d5dcb373 100644 --- a/www/src/example/default.tsx +++ b/www/src/example/default.tsx @@ -67,7 +67,7 @@ export const example = { myMap, }; -const Label = styled.label` +export const Label = styled.label` margin-top: 0.83rem; display: block; span { @@ -75,12 +75,12 @@ const Label = styled.label` } `; -const Options = styled.div` +export const Options = styled.div` display: grid; grid-template-columns: 50% 60%; `; -const initialState: Partial< +export const initialState: Partial< JsonViewProps & { quote: string; theme: keyof typeof themesData; diff --git a/www/src/example/editor.tsx b/www/src/example/editor.tsx index 54829a5c..c47e6396 100644 --- a/www/src/example/editor.tsx +++ b/www/src/example/editor.tsx @@ -1,54 +1,43 @@ -import React, { useReducer } from 'react'; -// import { styled } from 'styled-components'; -import JsonViewEditor, { JsonViewEditorProps } from '@uiw/react-json-view/editor'; -import { themesData } from './default'; - -export const example = { - string: 'Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet', - // integer: 42, - // float: 114.514, - // bigint: BigInt(10086), - // nan: NaN, - // null: null, - // undefined, - // boolean: true, - // timer: 0, - array: [19, 100.86, 'test', NaN, Infinity], - object2: { - 'first-child': true, - 'second-child': false, - 'last-child': null, - }, -}; -const initialState: Partial< - JsonViewEditorProps & { - quote: string; - theme: keyof typeof themesData; - } -> = { - displayObjectSize: true, - displayDataTypes: true, - enableClipboard: true, - highlightUpdates: true, - objectSortKeys: false, - indentWidth: 15, - collapsed: 2, - quote: '"', - shortenTextAfterLength: 50, - theme: 'nord', -}; +import { useReducer } from 'react'; +import JsonViewEditor from '@uiw/react-json-view/editor'; +import { example, initialState, Label, Options, themesData } from './default'; const reducer = (state: typeof initialState, action: typeof initialState) => ({ ...state, ...action }); -export default function Demo() { - const [state] = useReducer(reducer, initialState); +export function ExampleEditor() { + const [state, dispatch] = useReducer(reducer, initialState); + const themeKeys = Object.keys(themesData) as Array; + return ( - { - console.log(':value:', value); - }} - style={{ ...themesData[state.theme!], padding: 6, borderRadius: 6 }} - /> + <> + { + console.log(':value:', value); + }} + style={{ ...themesData[state.theme!], padding: 6, borderRadius: 6 }} + displayObjectSize={state.displayObjectSize} + displayDataTypes={state.displayDataTypes} + enableClipboard={state.enableClipboard} + highlightUpdates={state.highlightUpdates} + indentWidth={state.indentWidth} + shortenTextAfterLength={state.shortenTextAfterLength} + collapsed={state.collapsed} + objectSortKeys={state.objectSortKeys} > + + + + + + + ); }