Created
December 3, 2020 04:51
-
-
Save g4rcez/848dc093be5c2ddfb09b13ac8652e7e6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { assocPath } from "ramda"; | |
import { useClassNames } from "hulks"; | |
import React, { | |
Fragment, | |
useCallback, | |
useEffect, | |
useMemo, | |
useState, | |
} from "react"; | |
import { Arrow } from "./arrow"; | |
import "./App.css"; | |
import "./index.css"; | |
const prototype = {}.toString; | |
const getType = (obj: any): string => | |
prototype | |
.call(obj) | |
.match(/\s([a-zA-Z]+)/)![1] | |
.toLowerCase(); | |
enum Type { | |
Obj = "object", | |
Num = "number", | |
Date = "date", | |
Regex = "regexp", | |
Null = "null", | |
Undefined = "undefined", | |
Str = "string", | |
} | |
const conversionMap = { | |
[Type.Obj]: (value: string) => JSON.parse(value), | |
[Type.Str]: (value: string) => | |
value === "undefined" || value === undefined ? undefined : value, | |
[Type.Num]: (value: string, isInt: boolean) => | |
isInt ? Number.parseInt(value) : Number.parseFloat(value), | |
[Type.Null]: (value: string) => | |
value === "null" || value === undefined ? undefined : value, | |
[Type.Undefined]: (value: string) => | |
value === "undefined" || value === undefined ? undefined : value, | |
[Type.Regex]: (value: string) => new RegExp(value), | |
[Type.Date]: (value: string) => new Date(value), | |
}; | |
const InlineValues = [ | |
Type.Null, | |
Type.Num, | |
Type.Str, | |
Type.Undefined, | |
Type.Date, | |
Type.Regex, | |
]; | |
let obj = [ | |
{ | |
a: 1, | |
b: 2, | |
c: [ | |
1, | |
2, | |
3.12, | |
4, | |
5, | |
null, | |
undefined, | |
new Date(), | |
{ a: 1, b: [1, 2, { a: [{ a: 1 }] }] }, | |
], | |
}, | |
]; | |
type ArrayParserProps<T = any> = { | |
data: T[]; | |
source: T; | |
type: Type; | |
path: string[]; | |
}; | |
type CommonProps<T = any> = { | |
type: Type; | |
source: T; | |
data: T; | |
index: number; | |
path: string[]; | |
arrayItem: boolean; | |
}; | |
type CounterProps<T> = CommonProps<T> & { | |
containerClassName?: string; | |
children: React.ReactNode; | |
onChange?: (a: any) => any; | |
}; | |
function useTracePath( | |
path: string[], | |
index?: number, | |
isArray?: boolean | |
): (string | number)[] { | |
const tracePath = useMemo(() => { | |
if (index !== undefined && isArray) { | |
return [...path, index]; | |
} | |
return [...path]; | |
}, [path, index, isArray]); | |
return tracePath; | |
} | |
function ValueContainer<T>({ onChange, ...props }: CounterProps<T>) { | |
const path = useTracePath(props.path, props.index, props.arrayItem); | |
const isInline = useMemo(() => InlineValues.includes(props.type), [ | |
props.type, | |
]); | |
const [editMode, setEditMode] = useState(false); | |
const [newValue, setNewValue] = useState(props.data); | |
useEffect(() => { | |
setNewValue(props.data); | |
}, [props.data]); | |
const toggle = useCallback(() => { | |
setEditMode((p) => !p); | |
setNewValue(props.data); | |
}, [props.data]); | |
const containerClassName = useClassNames( | |
[props.containerClassName, props.arrayItem], | |
{ "flex-col": isInline }, | |
"inline-block relative text-left tabular-nums align-start tabular-nums justify-start", | |
props.containerClassName | |
); | |
const textAreaClassName = useClassNames( | |
[isInline], | |
{ | |
"resize-none": !isInline, | |
}, | |
"absolute top-0 jv-editor-textarea" | |
); | |
const onBlur = useCallback( | |
(e: React.FocusEvent<HTMLTextAreaElement>) => { | |
const value = e.target.value; | |
const convertedValue = conversionMap[props.type]( | |
value, | |
Number.isInteger(value) | |
); | |
setNewValue(convertedValue); | |
setEditMode(false); | |
const newObj = assocPath(path, convertedValue, props.source); | |
onChange?.(newObj); | |
}, | |
[props.type, props.source, path, onChange] | |
); | |
return ( | |
<div className={containerClassName}> | |
{props.arrayItem && <span className="jv-array-count">{props.index}</span>} | |
{!editMode && ( | |
<span onClick={toggle} role="button"> | |
{props.children} | |
</span> | |
)} | |
{editMode && ( | |
<textarea | |
autoFocus | |
onBlur={onBlur} | |
className={textAreaClassName} | |
rows={1} | |
defaultValue={`${newValue}`} | |
/> | |
)} | |
</div> | |
); | |
} | |
function Numbers(props: CommonProps<number>) { | |
const isInt = useMemo(() => Number.isInteger(props.data), [props.data]); | |
if (isInt) { | |
return ( | |
<ValueContainer {...props}> | |
<span className="jv-label-int">int</span> | |
{props.data} | |
</ValueContainer> | |
); | |
} | |
return ( | |
<ValueContainer {...props}> | |
<span className="jv-label-float">float</span> | |
{props.data} | |
</ValueContainer> | |
); | |
} | |
function Strings(props: CommonProps<string>) { | |
return ( | |
<ValueContainer {...props}> | |
<span className="jv-label-string">string</span> | |
{props.data} | |
</ValueContainer> | |
); | |
} | |
function Dates(props: CommonProps<Date> & { dateFormat: (d: Date) => Date }) { | |
return ( | |
<ValueContainer {...props}> | |
<span className="jv-label-date">string</span> | |
<div className="inline-block jv-value-date"> | |
{props.dateFormat(props.data)} | |
</div> | |
</ValueContainer> | |
); | |
} | |
function Null(props: CommonProps<number>) { | |
return ( | |
<ValueContainer {...props}> | |
<div className="inline-block jv-null-value">null</div> | |
</ValueContainer> | |
); | |
} | |
function Undefined(props: CommonProps<number>) { | |
return ( | |
<ValueContainer {...props}> | |
<div className="inline-block jv-undefined-value">undefined</div> | |
</ValueContainer> | |
); | |
} | |
function ArrayParser<T>(props: ArrayParserProps<T>) { | |
const [view, setView] = useState(true); | |
const toggle = useCallback(() => setView((p) => !p), []); | |
const className = useMemo( | |
() => | |
view ? "flex order-2 flex flex-col" : "flex order-2 flex flex-col hidden", | |
[view] | |
); | |
return ( | |
<div className="jv-array-items flex flex-col"> | |
<div className="order-1 flex"> | |
<span className="jv-square-brackets">{"["}</span> | |
<button onClick={toggle} className="bg-transparent ml-4 outline-none"> | |
<Arrow | |
className={ | |
view ? "jv-collapse-icon" : "jv-collapse-icon jv-collapse-icon-up" | |
} | |
/> | |
</button> | |
</div> | |
<div hidden={view} className={className}> | |
{props.data.map((x, i) => { | |
const path = props.path ?? [i]; | |
const pathKey = path.join("-"); | |
return ( | |
<div | |
className="jv-array-item flex-col" | |
key={`${pathKey}-jv-array-item-${i}`} | |
> | |
<JsonEditor | |
{...props} | |
source={props.source} | |
type={props.type} | |
arrayItem | |
path={path} | |
index={i} | |
data={x} | |
/> | |
</div> | |
); | |
})} | |
</div> | |
<div className="flex order-3"> | |
<span className="jv-square-brackets">{"]"}</span> | |
</div> | |
</div> | |
); | |
} | |
type MainProps<T> = CommonProps<T> & { | |
dateFormat?: Props["dateFormat"]; | |
onChange?: Props["onChange"]; | |
}; | |
function JsonEditor<T>(props: MainProps<T>) { | |
const path = useMemo(() => props.path ?? [], [props.path]); | |
const type: Type = useMemo(() => getType(props.data) as never, [props.data]); | |
const minorProps: any = useMemo( | |
() => ({ | |
onChange: props.onChange, | |
dateFormat: props.dateFormat, | |
arrayItem: props.arrayItem, | |
source: props.source, | |
type: type, | |
path: props.path, | |
index: props.index, | |
data: props.data, | |
}), | |
[ | |
props.onChange, | |
props.data, | |
props.index, | |
props.dateFormat, | |
props.path, | |
type, | |
props.source, | |
props.arrayItem, | |
] | |
); | |
if (Array.isArray(props.data)) { | |
return <ArrayParser {...(minorProps as any)} />; | |
} | |
if (type === Type.Obj) { | |
return ( | |
<Fragment> | |
<span className="jv-curly-brackets">{"{"}</span> | |
{Object.entries(props.data).map(([key, val], i) => { | |
const pathKey = (props.path ?? []).join("-"); | |
return ( | |
<div key={`${pathKey}-object-${i}`} className="jv-object-code"> | |
<span className="jv-object-key">{key}</span> | |
<div className="jv-object-code"> | |
<JsonEditor | |
{...minorProps} | |
arrayItem={false} | |
index={i} | |
path={path.concat(key)} | |
data={val} | |
/> | |
</div> | |
</div> | |
); | |
})} | |
<span className="jv-curly-brackets">{"}"}</span> | |
</Fragment> | |
); | |
} | |
if (type === Type.Num) { | |
return <Numbers {...minorProps} />; | |
} | |
if (type === Type.Str) { | |
return <Strings {...minorProps} />; | |
} | |
if (type === Type.Undefined) { | |
return <Undefined {...minorProps} />; | |
} | |
if (type === Type.Null) { | |
return <Null {...minorProps} />; | |
} | |
if (type === Type.Regex) { | |
return <Numbers {...minorProps} />; | |
} | |
if (type === Type.Date) { | |
return <Dates {...minorProps} />; | |
} | |
return <div className="block">Confused {props.data}</div>; | |
} | |
type Props<T = any> = { | |
data: T; | |
} & Partial<{ | |
onChange: (newData: T) => void; | |
dateFormat: (date: Date) => string; | |
}>; | |
export const JsonEditorTabajara = <T,>({ | |
dateFormat, | |
onChange, | |
...props | |
}: Props<T>) => { | |
const dateFormatter = useCallback( | |
(date: Date) => { | |
if (dateFormat) return dateFormat(date); | |
return date.toISOString(); | |
}, | |
[dateFormat] | |
); | |
const changeJson = useCallback( | |
(date: T) => { | |
if (onChange) return onChange(date); | |
return date; | |
}, | |
[onChange] | |
); | |
return ( | |
<JsonEditor | |
onChange={changeJson} | |
path={undefined as any} | |
type={undefined as any} | |
arrayItem={undefined as any} | |
index={undefined as any} | |
source={props.data} | |
data={props.data} | |
dateFormat={dateFormatter} | |
/> | |
); | |
}; | |
const App = () => { | |
const [main, setMain] = useState(obj); | |
return ( | |
<div style={{ padding: "10px" }}> | |
<JsonEditorTabajara | |
onChange={(e) => { | |
setMain(e); | |
console.log(e); | |
}} | |
data={main} | |
/> | |
</div> | |
); | |
}; | |
export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment