Skip to content

Instantly share code, notes, and snippets.

@g4rcez
Created December 3, 2020 04:51
Show Gist options
  • Save g4rcez/848dc093be5c2ddfb09b13ac8652e7e6 to your computer and use it in GitHub Desktop.
Save g4rcez/848dc093be5c2ddfb09b13ac8652e7e6 to your computer and use it in GitHub Desktop.
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