656 lines
17 KiB
TypeScript
Raw Normal View History

import React from "react"
import ContentEditable from "react-contenteditable"
import {
Box,
Center,
Flex,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Spinner,
Switch,
Textarea,
} from "@chakra-ui/react"
2023-06-28 08:25:17 +00:00
import { Commander } from "../Commander/Commander"
2023-07-21 01:13:02 +00:00
import { Icon } from "../Icon/Icon"
import { Safe } from "../Safety/Safe"
import { useThingtime } from "./useThingtime"
export const Thingtime = (props) => {
// TODO: Add a circular reference seen prop check
// and add button to expand circular reference
// up to 1 level deep
const { thingtime, setThingtime, loading } = useThingtime()
2023-07-21 01:13:02 +00:00
const [uuid, setUuid] = React.useState(undefined)
2023-06-30 01:46:42 +00:00
const [root, setRoot] = React.useState(props?.notRoot ? false : true)
2023-07-21 01:13:02 +00:00
const [circular, setCircular] = React.useState(props?.circular)
const contentEditableRef = React.useRef(null)
const editValueRef = React.useRef({})
const depth = React.useMemo(() => {
return props?.depth || 1
}, [props?.depth])
const pl = React.useMemo(() => {
return props?.pl || [4, 6]
}, [props?.pl])
const pr = React.useMemo(() => {
return props?.pr || (depth === 1 ? [4, 6] : 0)
}, [props?.pr, depth])
const multiplyPl = React.useCallback(
(num) => {
return pl.map((p) => p * num)
},
[pl]
)
// will only run on the client
React.useEffect(() => {
setUuid(Math.random().toString(36).substring(7))
}, [])
const childrenRef = React.useRef([])
const [thingDep, setThingDep] = React.useState(childrenRef.current)
const createDependancies = () => {
// push all children into childrenRef.current
try {
window.meta.db["createDependancies"] =
window.meta.db["createDependancies"] || 0
window.meta.db["createDependancies"]++
} catch {}
try {
const values = Object.values(props?.thing)
// if childrenRef.current does not shallow equal values then replace with array of values
const valuesNotEqual =
values?.length !== childrenRef.current?.length ||
!values?.every?.((value, idx) => {
return childrenRef.current[idx] === value
})
if (valuesNotEqual) {
childrenRef.current = values
setThingDep(childrenRef.current)
}
} catch {
// nothing
}
}
2023-06-30 01:46:42 +00:00
React.useEffect(() => {
createDependancies()
}, [])
2023-06-28 08:25:17 +00:00
const thing = React.useMemo(() => {
return props.thing
}, [props.thing, childrenRef.current])
React.useEffect(() => {
console.log("thingtime changed in path", props?.fullPath)
createDependancies()
}, [thingtime, props?.fullPath, childrenRef])
2023-06-28 08:25:17 +00:00
const fullPath = React.useMemo(() => {
return props?.fullPath || props?.path
}, [props?.fullPath, props?.path])
2023-07-15 08:08:36 +00:00
const seen = React.useMemo(() => {
if (props?.seen instanceof Array) {
if (props?.seen?.includes(thing)) {
return props?.seen
} else if (typeof thing === "object") {
return [...props.seen, thing]
}
return props?.seen || []
}
if (typeof thing === "object") {
return [thing]
}
return []
}, [props?.seen, thing])
2023-06-29 11:06:58 +00:00
const mode = React.useMemo(() => {
return "view"
2023-06-29 11:06:58 +00:00
}, [])
const validKeyTypes = React.useMemo(() => {
return ["object", "array"]
2023-06-29 11:06:58 +00:00
}, [])
const keys = React.useMemo(() => {
if (validKeyTypes?.includes(typeof thing)) {
try {
const keysRet = Object.keys(thing)
return keysRet
} catch {
// nothing
}
2023-06-29 11:06:58 +00:00
} else {
return []
}
}, [thing, thingDep, validKeyTypes])
2023-06-29 11:06:58 +00:00
const type = React.useMemo(() => {
if (thing === null) {
return "undefined"
}
2023-06-29 11:06:58 +00:00
return typeof thing
}, [thing])
2023-07-21 13:33:47 +00:00
const typeIcon = React.useMemo(() => {
const size = 7
if (thing instanceof Array) {
return <Icon name="array" size={size}></Icon>
} else if (type === "object") {
return <Icon name="object" size={size}></Icon>
} else if (type === "string") {
return <Icon name="string" size={size}></Icon>
} else if (type === "number") {
return <Icon name="number" size={size}></Icon>
} else if (type === "boolean") {
return <Icon name="boolean" size={size}></Icon>
} else if (type === "undefined") {
return <Icon name="undefined" size={size}></Icon>
2023-07-21 13:33:47 +00:00
} else {
return <Icon name="box" size={size}></Icon>
}
}, [type, thing])
const valuePl = React.useMemo(() => {
if (typeof props?.valuePl === "number") {
return props?.valuePl
}
return props?.path ? [4, 6] : [0, 0]
}, [props?.valuePl, props?.path])
2023-06-29 11:06:58 +00:00
const renderableValue = React.useMemo(() => {
if (type === "string") {
const trimmed = thing.trim()
if (!trimmed) {
2023-07-15 08:08:36 +00:00
return ""
}
return trimmed
} else if (type === "number") {
2023-06-29 11:06:58 +00:00
return thing
} else if (type === "boolean") {
return thing ? "true" : "false"
} else if (type === "object") {
if (thing === null) {
return "null"
}
if (!keys?.length) {
return "Something!"
}
try {
return JSON.stringify(thing, null, 2)
} catch (err) {
2023-07-15 08:08:36 +00:00
// console.error(
// "Caught error making renderableValue of thing",
// err,
// thing
// )
return (
<Box cursor="pointer" onClick={() => setCircular(false)}>
Click to Expand
</Box>
)
}
} else if (type === "undefined") {
return "undefined"
2023-06-29 11:06:58 +00:00
} else {
return "Something!"
2023-06-29 11:06:58 +00:00
}
}, [thing, thingDep, type, keys])
2023-06-29 11:06:58 +00:00
2023-07-21 01:13:02 +00:00
const keysToUse = React.useMemo(() => {
return keys
}, [keys])
// const keysToUse = flattenedKeys
2023-06-29 11:06:58 +00:00
2023-07-21 01:13:02 +00:00
const template1Modes = React.useMemo(() => {
return ["view", "edit"]
}, [])
2023-06-29 11:06:58 +00:00
2023-07-21 13:33:47 +00:00
const thingtimeChildren = React.useMemo(() => {
2023-07-21 01:13:02 +00:00
if (template1Modes?.includes(mode)) {
if (keys?.length && !circular) {
const ret = (
2023-07-21 01:13:02 +00:00
<Safe {...props}>
<Flex
className="nested-things"
2023-07-21 01:13:02 +00:00
position="relative"
flexDirection="column"
// w={'500px'}
// w={['200px', '500px']}
maxWidth="100%"
paddingLeft={valuePl}
paddingY={props?.path ? 3 : 0}
>
{keysToUse?.length &&
keysToUse.map((key, idx) => {
if (!key?.human) {
key = {
human: key,
key: key,
}
}
2023-06-29 11:06:58 +00:00
2023-07-21 01:13:02 +00:00
const nextThing = thing[key?.key]
const nextSeen = [...seen]
2023-06-29 11:06:58 +00:00
2023-07-21 01:13:02 +00:00
if (typeof nextThing === "object") {
nextSeen.push(nextThing)
}
2023-07-21 01:13:02 +00:00
return (
<Thingtime
key={idx}
seen={nextSeen}
2023-07-21 13:33:47 +00:00
edit={props?.edit}
2023-07-21 01:13:02 +00:00
circular={seen?.includes?.(nextThing)}
depth={depth + 1}
parent={thing}
notRoot
2023-07-21 13:33:47 +00:00
fullPath={fullPath + "." + key?.key}
2023-07-21 01:13:02 +00:00
path={key}
thing={nextThing}
// thing={{ infinite: { yes: true } }}
valuePl={pl}
></Thingtime>
)
})}
</Flex>
</Safe>
)
return ret
2023-07-21 01:13:02 +00:00
}
}
}, [
keysToUse,
mode,
circular,
seen,
type,
fullPath,
2023-07-21 01:13:02 +00:00
depth,
thing,
thingDep,
2023-07-21 01:13:02 +00:00
props,
valuePl,
pl,
keys,
template1Modes,
])
const AtomicWrapper = React.useCallback((props) => {
return (
<Flex
position="relative"
flexDirection="row"
flexShrink={1}
width="100%"
paddingLeft={props?.pl || props?.paddingLeft}
fontSize="20px"
border="none"
// whiteSpace="pre-line"
whiteSpace="pre-wrap"
wordBreak={props?.wordBreak || "break-word"}
outline="none"
paddingY={2}
// dangerouslySetInnerHTML={{ __html: renderableValue }}
>
{props?.children}
</Flex>
)
}, [])
2023-07-21 13:33:47 +00:00
const [contentEditableThing, setContentEditableThing] = React.useState(thing)
const updateContentEditableThing = React.useCallback((value) => {
// replace all new line occurences in value with <div><br></div>
// extract all series of new lines
const newlines = value?.split?.(/[^\n]/)?.filter((v) => v !== "")
let newValue = value
// replace all new lines groups with <div><br></div>
newlines?.forEach?.((newline) => {
const baseLength = "\n"?.length
const newlineClone = newline
const newlineClonePart1 = newlineClone?.replace(
"\n\n\n",
"<div><br /></div>"
)
const newlineClonePart2 = newlineClonePart1?.replace(
/\n\n/g,
"<div><br /></div>"
)
const newlineClonePart3 = newlineClonePart2?.replace(/\n/g, "<br />")
newValue = newValue?.replace(newline, newlineClonePart3)
})
setContentEditableThing(newValue)
}, [])
React.useEffect(() => {
const entries = Object.entries(editValueRef.current)
const propsThingInEntries = entries?.find?.(
(entry) => entry[1] === props?.thing
)
if (!propsThingInEntries) {
updateContentEditableThing(props?.thing)
// setContentEditableThing(props?.thing)
} else {
const [time, value] = propsThingInEntries
if (time && value) {
delete editValueRef.current[time]
}
}
}, [props?.thing, updateContentEditableThing])
2023-07-21 13:33:47 +00:00
const updateValue = React.useCallback(
(args) => {
const { value } = args
setThingtime(fullPath, value)
2023-07-21 13:33:47 +00:00
},
[fullPath, setThingtime]
2023-07-21 13:33:47 +00:00
)
const atomicValue = React.useMemo(() => {
if (props?.edit) {
if (type === "boolean") {
return (
<AtomicWrapper paddingLeft={pl} className="boolean-atomic-wrapper">
2023-07-21 13:33:47 +00:00
<Box
onClick={(e) => {
e?.preventDefault?.()
e?.stopPropagation?.()
// cancel bubble
e?.nativeEvent?.stopImmediatePropagation?.()
setTimeout(() => {
updateValue({ value: !thing })
}, 1)
}}
>
<Switch isChecked={thing}></Switch>
2023-07-21 13:33:47 +00:00
</Box>
</AtomicWrapper>
)
}
if (type === "number") {
const numberPxLength = thing?.toString()?.length * 13 + 30
return (
<AtomicWrapper paddingLeft={pl} className="number-atomic-wrapper">
<Flex>
<NumberInput
alignItems="center"
justifyContent="center"
onChange={(value) => {
setTimeout(() => {
try {
const number = Number(value)
console.log("typeof number", typeof number)
updateValue({ value: number })
} catch {
// something went wrong casting to number
}
}, 1)
}}
value={thing}
>
<NumberInputField width={numberPxLength + "px"} />
<NumberInputStepper transform="scale(0.9)">
<NumberIncrementStepper
// transform="scale(0.7)"
/>
<NumberDecrementStepper
// transform="scale(0.7)"
/>
</NumberInputStepper>
</NumberInput>
</Flex>
2023-07-21 13:33:47 +00:00
</AtomicWrapper>
)
}
if (type === "string" && typeof contentEditableThing === "string") {
return (
<AtomicWrapper paddingLeft={pl} className="string-atomic-wrapper">
<Box
ref={contentEditableRef}
width="100%"
border="none"
outline="none"
contentEditable={true}
dangerouslySetInnerHTML={{ __html: contentEditableThing }}
onInput={(value) => {
const innerText = value?.target?.innerText
if (typeof innerText === "string") {
const time = Date.now()
editValueRef.current[time] = innerText
updateValue({ value: innerText })
}
}}
></Box>
</AtomicWrapper>
)
}
if (type === "undefined") {
return (
<AtomicWrapper>
{/* TODO: Implement UI-less commander */}
{/* <Commander></Commander> */}
</AtomicWrapper>
)
}
2023-06-29 11:06:58 +00:00
}
return (
<AtomicWrapper paddingLeft={pl} className="default-atomic-wrapper">
{renderableValue}
</AtomicWrapper>
)
}, [
contentEditableThing,
renderableValue,
pl,
type,
AtomicWrapper,
props?.edit,
thing,
thingDep,
updateValue,
])
2023-06-29 11:06:58 +00:00
const contextMenu = (
<Flex
position="absolute"
top={0}
right={0}
paddingRight={4}
userSelect="none"
>
2023-06-29 11:06:58 +00:00
Settings
</Flex>
)
const [showContextMenu, setShowContextMenu] = React.useState(false)
const humanPath = React.useMemo(() => {
if (typeof props?.path === "string") {
return props?.path
}
return props?.path?.human || ""
}, [props?.path])
2023-07-10 00:49:30 +00:00
const renderedPath = React.useMemo(() => {
2023-07-21 13:33:47 +00:00
if (props?.edit) {
return humanPath
}
if (humanPath?.includes?.("hidden")) {
return null
}
if (humanPath?.includes?.("unique")) {
// take only path from before the string unique
return humanPath.split?.("unique")?.[0]
}
2023-07-10 00:49:30 +00:00
return humanPath
2023-07-21 13:33:47 +00:00
}, [humanPath, props?.edit])
2023-07-10 00:49:30 +00:00
2023-07-21 13:33:47 +00:00
const pathDom = React.useMemo(() => {
if (renderedPath) {
return (
<Flex
maxWidth="100%"
paddingLeft={props?.pathPl || pl}
fontSize="12px"
wordBreak="break-all"
>
{renderedPath}
</Flex>
)
}
2023-07-10 00:49:30 +00:00
}, [renderedPath, pl, props?.pathPl])
const handleMouseEvent = React.useCallback(
(e) => {
const target = e?.target
// extract uuid from className
const className = target?.className
if (className?.includes(uuid?.current)) {
setShowContextMenu(e?.type === "mouseenter")
}
},
[uuid]
)
const addNewChild = React.useCallback(() => {
const newChild = null
if (thing instanceof Array) {
// add new child to array
const newValue = [...thing, newChild]
setThingtime(fullPath, newValue)
return
}
const newChildBasePath = "New Value"
// find increment that thing doesn't already have New Value N+1
let increment = 0
let newPath = newChildBasePath
while (Object.hasOwnProperty.call(thing, newPath) && increment <= 999) {
increment++
newPath = newChildBasePath + " " + increment
}
const newChildPath = newPath
const newChildFullPath = fullPath + "." + newChildPath
// create new child on thing using setThingtime
setThingtime(newChildFullPath, newChild)
}, [fullPath, setThingtime, thing])
const addChildUi = React.useMemo(() => {
if (type === "object" && props?.edit) {
return (
<Flex
width="100%"
paddingLeft={multiplyPl(2)}
cursor="pointer"
onClick={addNewChild}
paddingY={2}
>
<Icon size={12} name="seedling"></Icon>
{/* <Icon size={7} name="plus"></Icon>
<Icon size={7} name="plus"></Icon> */}
</Flex>
)
}
}, [props?.edit, type, multiplyPl, addNewChild])
2023-07-21 13:33:47 +00:00
const [showContextIcon, setShowContextIcon] = React.useState(false)
return (
<Safe {...props} depth={depth} uuid={uuid}>
<Flex
position="relative"
flexDirection="column"
// width="500px"
width={props?.width || props?.w || "100%"}
maxWidth="100%"
paddingRight={pr}
onMouseEnter={handleMouseEvent}
onMouseLeave={handleMouseEvent}
// minW={depth === 1 ? '120px' : null}
paddingY={3}
2023-07-15 08:08:36 +00:00
{...(props.chakras || {})}
className={`thing uuid-${uuid}`}
data-path={props?.path}
>
{/* {uuid?.current} */}
2023-07-21 01:13:02 +00:00
<Flex position="relative" flexDirection="row">
2023-07-21 13:33:47 +00:00
<Flex
alignItems="center"
flexDirection="row"
marginRight="auto"
onMouseEnter={() => setShowContextIcon(true)}
onMouseLeave={() => setShowContextIcon(false)}
>
<Flex>{pathDom}</Flex>
{props?.edit && (
<Box
// marginTop={-3}
marginTop={-1}
2023-07-21 13:33:47 +00:00
paddingLeft={1}
opacity={0.5}
cursor="pointer"
>
{typeIcon}
</Box>
)}
{pathDom && (
<Flex
paddingLeft={1}
opacity={showContextIcon ? 1 : 0}
cursor="pointer"
transition="all 0.2s ease-in-out"
>
<Icon name="magic" size={10}></Icon>
</Flex>
)}
2023-07-21 01:13:02 +00:00
</Flex>
</Flex>
{/* {showContextMenu && contextMenu} */}
{!loading && !thingtimeChildren && atomicValue && (
<Box className="atomicValue">{atomicValue}</Box>
)}
{!loading && thingtimeChildren && (
<Box className="thingtimeChildren">{thingtimeChildren}</Box>
)}
{addChildUi}
</Flex>
</Safe>
2023-06-29 11:06:58 +00:00
)
}