Skip to content

Commit a63846d

Browse files
committed
feat: json keyname editor.
1 parent 7c38a5a commit a63846d

File tree

10 files changed

+221
-112
lines changed

10 files changed

+221
-112
lines changed

core/editor.d.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,37 @@
22

33
declare module '@uiw/react-json-view/editor' {
44
import { JsonViewProps } from '@uiw/react-json-view';
5-
import type { CountInfoExtraProps } from '@uiw/react-json-view/cjs/editor/countInfoExtra';
5+
// import type { CountInfoExtraProps } from '@uiw/react-json-view/cjs/editor/countInfoExtra';
66
type Option = {
7-
value: string;
8-
prevValue: string;
9-
keyName: string | number;
7+
value: string;
8+
prevValue: string;
9+
keyName: string | number;
1010
};
1111

1212
export interface JsonViewEditorProps<T extends object> extends JsonViewProps<T> {
1313
/** Callback when value edit functionality */
1414
onEdit?: (option: {
15-
value: unknown;
16-
oldValue: unknown;
17-
keyName?: string | number;
18-
parentName?: string | number;
19-
type?: 'value' | 'key';
15+
value: unknown;
16+
oldValue: unknown;
17+
keyName?: string | number;
18+
parentName?: string | number;
19+
type?: 'value' | 'key';
2020
}) => void;
21-
/**
22-
* When a callback function is passed in, add functionality is enabled. The callback is invoked before additions are completed.
23-
* @returns {boolean} Returning false from onAdd will prevent the change from being made.
24-
*/
25-
onAdd?: CountInfoExtraProps<T>['onAdd'];
26-
/**
27-
* When a callback function is passed in, delete functionality is enabled. The callback is invoked before deletions are completed.
28-
* @returns Returning false from onDelete will prevent the change from being made.
29-
*/
30-
onDelete?: CountInfoExtraProps<T>['onDelete'];
21+
// /**
22+
// * When a callback function is passed in, add functionality is enabled. The callback is invoked before additions are completed.
23+
// * @returns {boolean} Returning false from onAdd will prevent the change from being made.
24+
// */
25+
// onAdd?: CountInfoExtraProps<T>['onAdd'];
26+
// /**
27+
// * When a callback function is passed in, delete functionality is enabled. The callback is invoked before deletions are completed.
28+
// * @returns Returning false from onDelete will prevent the change from being made.
29+
// */
30+
// onDelete?: CountInfoExtraProps<T>['onDelete'];
3131
/** Whether enable edit feature. @default true */
3232
editable?: boolean;
3333
}
34-
const JsonViewEditor: import("react").ForwardRefExoticComponent<Omit<JsonViewEditorProps<object>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
34+
const JsonViewEditor: import('react').ForwardRefExoticComponent<
35+
Omit<JsonViewEditorProps<object>, 'ref'> & import('react').RefAttributes<HTMLDivElement>
36+
>;
3537
export default JsonViewEditor;
3638
}

core/package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,8 @@
114114
"tree-view",
115115
"treeview"
116116
],
117-
"dependencies": {
118-
"@babel/runtime": "^7.22.6"
119-
},
120117
"devDependencies": {
118+
"@babel/runtime": "^7.22.6",
121119
"@testing-library/react": "^14.0.0",
122120
"@types/react-test-renderer": "^18.0.0",
123121
"react": "^18.2.0",

core/src/comps/KeyValues.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ KeyValues.displayName = 'JVR.KeyValues';
5858

5959
interface KayNameProps<T extends object> extends Omit<KeyValuesProps<T>, 'level'> {}
6060
export const KayName = <T extends object>(props: KayNameProps<T>) => {
61-
const { keyName, value } = props;
61+
const { keyName, parentValue, value } = props;
6262
const { highlightUpdates } = useStore();
6363
const isNumber = typeof keyName === 'number';
6464
const highlightContainer = useRef<HTMLSpanElement>(null);
@@ -67,7 +67,7 @@ export const KayName = <T extends object>(props: KayNameProps<T>) => {
6767
<Fragment>
6868
<span ref={highlightContainer}>
6969
<Quote isNumber={isNumber} data-placement="left" />
70-
<KeyNameComp keyName={keyName!} value={value}>
70+
<KeyNameComp keyName={keyName!} value={value} parentValue={parentValue}>
7171
{keyName}
7272
</KeyNameComp>
7373
<Quote isNumber={isNumber} data-placement="right" />
@@ -102,7 +102,7 @@ export const KeyValuesItem = <T extends object>(props: KeyValuesProps<T>) => {
102102
};
103103
return (
104104
<div className="w-rjv-line" {...reset}>
105-
<KayName keyName={keyName} value={value} />
105+
<KayName keyName={keyName} value={value} parentValue={parentValue} />
106106
<Value keyName={keyName!} value={value} expandKey={subkeyid} />
107107
</div>
108108
);

core/src/editor/KeyName.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { FC, useRef, useState } from 'react';
2+
import { SectionElementProps } from '../store/Section';
3+
import { useStore } from './store';
4+
5+
export const KeyNameRender: SectionElementProps['render'] = (
6+
{ children, ...reset },
7+
{ value, parentValue, keyName },
8+
) => {
9+
if (typeof children === 'number') {
10+
return <span {...reset}>{children}</span>;
11+
}
12+
return (
13+
<Child {...reset} value={value} parentValue={parentValue} keyName={keyName}>
14+
{children}
15+
</Child>
16+
);
17+
};
18+
19+
interface ChildProps extends React.HTMLAttributes<HTMLSpanElement> {
20+
value: unknown;
21+
parentValue?: unknown;
22+
keyName: string | number;
23+
}
24+
25+
const Child: FC<ChildProps> = (props) => {
26+
const { value, parentValue, keyName, ...reset } = props;
27+
const $dom = useRef<HTMLElement>(null);
28+
const [currentValue, setCurrentValue] = useState(props.children);
29+
const { onEdit } = useStore();
30+
31+
const onKeyDown = (evn: React.KeyboardEvent<HTMLSpanElement>) => {
32+
if (evn.key === 'Enter') {
33+
$dom.current?.setAttribute('contentEditable', 'false');
34+
}
35+
};
36+
37+
const onClick = (evn: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
38+
evn.stopPropagation();
39+
$dom.current?.setAttribute('contentEditable', 'true');
40+
$dom.current?.focus();
41+
};
42+
43+
const onBlur = (evn: React.FocusEvent<HTMLSpanElement, Element>) => {
44+
$dom.current?.setAttribute('contentEditable', 'false');
45+
const callback = onEdit && onEdit({ value: evn.target.textContent, oldValue: value, keyName });
46+
if (callback) {
47+
setCurrentValue(evn.target.textContent);
48+
}
49+
};
50+
51+
const spanProps: React.HTMLAttributes<HTMLSpanElement> = {
52+
...reset,
53+
onKeyDown,
54+
onClick,
55+
onBlur,
56+
spellCheck: false,
57+
contentEditable: 'false',
58+
suppressContentEditableWarning: true,
59+
children: currentValue,
60+
};
61+
62+
return <span {...spanProps} ref={$dom} />;
63+
};

core/src/editor/index.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { forwardRef } from 'react';
2+
import JsonView, { type JsonViewProps } from '../';
3+
import { KeyNameRender } from './KeyName';
4+
import { Context, Dispatch, useStoreReducer } from './store';
5+
6+
export interface JsonViewEditorProps<T extends object> extends Omit<JsonViewProps<T>, 'shortenTextAfterLength'> {
7+
/**
8+
* When a callback function is passed in, edit functionality is enabled. The callback is invoked before edits are completed.
9+
* @returns {boolean} Returning false from onEdit will prevent the change from being made.
10+
*/
11+
onEdit?: (option: {
12+
value: unknown;
13+
oldValue: unknown;
14+
keyName?: string | number;
15+
parentName?: string | number;
16+
type?: 'value' | 'key';
17+
}) => boolean;
18+
/** Whether enable edit feature. @default true */
19+
editable?: boolean;
20+
}
21+
22+
const JsonViewEditor = forwardRef<HTMLDivElement, JsonViewEditorProps<object>>((props, ref) => {
23+
const { children, onEdit, editable = true, ...reset } = props;
24+
const [state, dispatch] = useStoreReducer({ onEdit });
25+
return (
26+
<Context.Provider value={state}>
27+
<Dispatch.Provider value={dispatch}>
28+
<JsonView {...reset} shortenTextAfterLength={0} ref={ref}>
29+
{editable && <JsonView.KeyName render={KeyNameRender} />}
30+
{children}
31+
</JsonView>
32+
</Dispatch.Provider>
33+
</Context.Provider>
34+
);
35+
});
36+
37+
export default JsonViewEditor;

core/src/editor/store.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { FC, PropsWithChildren, createContext, useContext, useReducer } from 'react';
2+
3+
type InitialState = {
4+
/**
5+
* When a callback function is passed in, edit functionality is enabled. The callback is invoked before edits are completed.
6+
* @returns {boolean} Returning false from onEdit will prevent the change from being made.
7+
*/
8+
onEdit?: (option: {
9+
value: unknown;
10+
oldValue: unknown;
11+
keyName?: string | number;
12+
parentName?: string | number;
13+
type?: 'value' | 'key';
14+
}) => boolean;
15+
};
16+
type Dispatch = React.Dispatch<InitialState>;
17+
18+
const initialState: InitialState = {};
19+
export const Context = createContext<InitialState>(initialState);
20+
21+
const reducer = (state: InitialState, action: InitialState) => ({
22+
...state,
23+
...action,
24+
});
25+
26+
export const Dispatch = createContext<Dispatch>(() => {});
27+
Dispatch.displayName = 'JVR.Editor.Dispatch';
28+
29+
export const useStore = () => {
30+
return useContext(Context);
31+
};
32+
33+
export function useStoreReducer(initialState: InitialState) {
34+
return useReducer(reducer, initialState);
35+
}

core/src/section/KeyName.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ KeyName.displayName = 'JVR.KeyName';
1414
type KeyNameCompProps = {
1515
keyName: string | number;
1616
value?: unknown;
17+
parentValue?: unknown;
1718
};
1819

1920
export const KeyNameComp: FC<PropsWithChildren<KeyNameCompProps>> = (props) => {
20-
const { children, value, keyName } = props;
21+
const { children, value, parentValue, keyName } = props;
2122
const isNumber = typeof children === 'number';
2223
const style: React.CSSProperties = {
2324
color: isNumber ? 'var(--w-rjv-key-number, #268bd2)' : 'var(--w-rjv-key-string, #002b36)',
@@ -26,7 +27,8 @@ export const KeyNameComp: FC<PropsWithChildren<KeyNameCompProps>> = (props) => {
2627
const { as, render, ...reset } = Comp;
2728
reset.style = { ...reset.style, ...style };
2829
const Elm = as || 'span';
29-
const child = render && typeof render === 'function' && render({ ...reset, children }, { value, keyName });
30+
const child =
31+
render && typeof render === 'function' && render({ ...reset, children }, { value, parentValue, keyName });
3032
if (child) return child;
3133
return <Elm {...reset}>{children}</Elm>;
3234
};

core/src/store/Section.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { type TagType } from './Types';
33

44
export type SectionElementProps<T extends TagType = 'span'> = {
55
as?: T;
6-
render?: (props: SectionElement<T>, result: { value: unknown; keyName: string | number }) => React.ReactNode;
6+
render?: (
7+
props: SectionElement<T>,
8+
result: { value: unknown; parentValue?: unknown; keyName: string | number },
9+
) => React.ReactNode;
710
};
811

912
export type SectionElement<T extends TagType = 'span'> = SectionElementProps<T> & ComponentPropsWithoutRef<T>;

www/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState } from 'react';
22
import styled, { css } from 'styled-components';
33
import { Example } from './example/default';
4-
// import { ExampleEditor } from './example/editor';
4+
// import ExampleEditor from './example/editor';
55

66
const ExampleWrapper = styled.div`
77
max-width: 630px;

0 commit comments

Comments
 (0)