import React from "react"
import ContentEditable from "react-contenteditable"
import * as Chakras from "@chakra-ui/react"
import {
Box,
Center,
Flex,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
Spinner,
Switch,
Textarea,
} from "@chakra-ui/react"
import { Commander } from "../Commander/Commander"
// import { Magic } from "../Commander/Magic"
import { Icon } from "../Icon/Icon"
import { MagicInput } from "../MagicInput/MagicInput"
import { Safe } from "../Safety/Safe"
import { SettingsMenu } from "./SettingsMenu"
import { useThingtime } from "./useThingtime"
import { useThings } from "~/hooks/useThings"
import { getThing } from "~/smarts"
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 { append } = useThings()
const { thingtime, setThingtime, getThingtime, loading, events } =
useThingtime()
const [uuid, setUuid] = React.useState(undefined)
const [root, setRoot] = React.useState(props?.notRoot ? false : true)
const [circular, setCircular] = React.useState(props?.circular)
const thingtimeRef = React.useRef()
const [showFullPathContext, setShowFullPathContext] = React.useState(false)
const editValueRef = React.useRef({})
const depth = React.useMemo(() => {
return typeof props?.depth === "number" ? props?.depth : 0
}, [props?.depth])
const render = React.useMemo(() => {
return !props?.edit || props?.render || false
}, [props?.render, props?.edit])
const width = React.useMemo(() => {
if (props?.width) {
return props?.width
}
if (props?.w) {
return props?.w
}
if (render) {
return "auto"
}
return "100%"
}, [props?.width, props?.w, render])
const chakraChild = React.useMemo(() => {
if (!props?.edit && props?.chakraChild) {
return true
}
return false
}, [props?.edit, props?.chakraChild])
const pl = React.useMemo(() => {
if (!props.edit && chakraChild) {
return [0]
}
return props?.pl || [4, 6]
}, [props?.pl, props?.edit, chakraChild])
const pr = React.useMemo(() => {
return props?.pr || (depth === 0 ? [4, 6] : 0)
}, [props?.pr, depth])
const multiplyPl = React.useCallback(
(num) => {
return pl.map((p) => p * num)
},
[pl]
)
const propsRef = React.useRef(props)
React.useEffect(() => {
propsRef.current = props
}, [props])
// 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
}
}
React.useEffect(() => {
createDependancies()
}, [])
const thing = React.useMemo(() => {
return props.thing
}, [props.thing, uuid, childrenRef.current])
const chakra = React.useMemo(() => {
return !props?.edit && typeof thing?.chakra === "string" && thing?.chakra
}, [thing?.chakra, props?.edit])
const path = React.useMemo(() => {
return props?.path?.key || props?.path || ""
}, [props?.path])
const fullPath = React.useMemo(() => {
const ret = props?.fullPath || props?.path?.key || props?.path
// store this thing in the global db
try {
window.meta.things[ret] = props?.thing
} catch {
// nothing
}
return ret
}, [props?.fullPath, props?.path, props?.thing])
const parentPath = React.useMemo(() => {
const parentPath = fullPath?.split(".")?.slice(0, -1)?.join(".")
if (!parentPath) {
return "thingtime"
}
return parentPath
}, [fullPath])
const parent = React.useMemo(() => {
return getThingtime(parentPath)
}, [parentPath, getThingtime])
React.useEffect(() => {
console.log("thingtime changed in path", props?.fullPath)
createDependancies()
}, [thingtime, props?.fullPath, childrenRef])
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])
const mode = React.useMemo(() => {
return "view"
}, [])
const validKeyTypes = React.useMemo(() => {
return ["object", "array"]
}, [])
const keys = React.useMemo(() => {
if (validKeyTypes?.includes(typeof thing)) {
try {
const keysRet = Object.keys(thing)
return keysRet
} catch {
// nothing
}
} else {
return []
}
}, [thing, thingDep, validKeyTypes])
const type = React.useMemo(() => {
if (thing === null) {
return "undefined"
}
return typeof thing
}, [thing])
const typeIcon = React.useMemo(() => {
const size = 7
if (thing instanceof Array) {
return
} else if (type === "object") {
return
} else if (type === "string") {
return
} else if (type === "number") {
return
} else if (type === "boolean") {
return
} else if (type === "undefined") {
return
} else {
return
}
}, [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])
const renderableValue = React.useMemo(() => {
if (chakraChild) {
return null
}
if (type === "string") {
return
} else if (type === "number") {
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) {
// console.error(
// "Caught error making renderableValue of thing",
// err,
// thing
// )
return (
setCircular(false)}>
Click to Expand
)
}
} else if (type === "undefined") {
return "Imagine.."
} else {
return "Something.."
}
}, [thing, thingDep, type, chakraChild, keys])
const renderChakra = React.useMemo(() => {
if (!props?.edit && chakra && render) {
return true
}
return false
}, [chakra, props?.edit, render])
const keysToUse = React.useMemo(() => {
if (renderChakra) {
return ["children"]
}
return keys
}, [keys, renderChakra])
// const keysToUse = flattenedKeys
const AtomicWrapper = React.useCallback((args) => {
return (
{args?.children}
)
}, [])
const thingtimeChildren = React.useMemo(() => {
let inner = Imagine..
if (keys?.length && !circular) {
inner = (
<>
{keysToUse?.length &&
keysToUse.map((key, idx) => {
if (!key?.human) {
key = {
human: key,
key: key,
}
}
const nextThing = thing[key?.key]
const nextSeen = [...seen]
if (typeof nextThing === "object") {
nextSeen.push(nextThing)
}
return (
)
})}
>
)
}
if (type === "object" && !circular) {
if (chakra) {
const ChakraComponent = Chakras[chakra]
console.log("Thingtime is chakra", fullPath, chakra)
const rawChildren = thing?.rawChildren
try {
if (ChakraComponent) {
console.log(
"Thingtime found ChakraComponent",
fullPath,
ChakraComponent
)
console.log("Thingtime found thing?.props", fullPath, thing?.props)
const ret = (
{rawChildren}
{inner}
)
return ret
}
} catch {
// chakra error
}
}
// TODO: Is it safe to spread props
// because having props as a dependency will cause a re-render every time
if (props?.chakraChild) {
return inner
}
return (
{inner}
)
}
}, [
AtomicWrapper,
keysToUse,
circular,
seen,
type,
props?.chakraChild,
props?.path,
props?.edit,
chakra,
fullPath,
render,
depth,
thing,
thingDep,
valuePl,
pl,
keys,
])
const updateValue = React.useCallback(
(args) => {
const { value } = args
setThingtime(fullPath, value)
},
[fullPath, setThingtime]
)
const resetValue = React.useCallback(() => {
updateValue({ value: null })
}, [updateValue])
const onChangeType = React.useCallback(
(args) => {
const { type, wrap } = args
const typeValue =
typeof type?.value === "function" ? type?.value() : type?.value
if (type) {
const wrapTarget = type?.wrap
if (wrap && wrapTarget) {
const newValue = append({
thing: typeValue[wrapTarget],
value: thing,
})
typeValue[wrapTarget] = newValue
if (typeValue) {
updateValue({ value: typeValue })
}
} else {
updateValue({ value: typeValue })
}
}
},
[updateValue, thing, append, fullPath]
)
const onWrapType = React.useCallback(
(type) => {
// nothing
},
[updateValue]
)
const deleteValue = React.useCallback(() => {
// use parent path to clone parent object but without this key
const clone = { ...parent }
delete clone[path]
setThingtime(parentPath, clone)
}, [path, parent, parentPath, setThingtime])
const atomicValue = React.useMemo(() => {
if (props?.edit) {
if (type === "boolean") {
return (
{
e?.preventDefault?.()
e?.stopPropagation?.()
// cancel bubble
e?.nativeEvent?.stopImmediatePropagation?.()
setTimeout(() => {
updateValue({ value: !thing })
}, 1)
}}
>
)
}
if (type === "number") {
const numberPxLength = thing?.toString()?.length * 13 + 30
return (
{
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}
>
)
}
if (type === "string") {
return (
{/* {
const innerText = value?.target?.innerText
if (typeof innerText === "string") {
const time = Date.now()
editValueRef.current[time] = innerText
updateValue({ value: innerText })
}
}}
> */}
)
}
if (type === "undefined") {
return (
{/* TODO: Implement UI-less commander */}
)
}
}
return (
{renderableValue}
)
}, [
renderableValue,
pl,
type,
fullPath,
uuid,
AtomicWrapper,
props?.edit,
thing,
thingDep,
updateValue,
])
const contextMenu = (
Settings
)
const [showContextMenu, setShowContextMenu] = React.useState(false)
const humanPath = React.useMemo(() => {
if (typeof props?.path === "string") {
return props?.path
}
return props?.path?.human || ""
}, [props?.path])
const renderedPath = React.useMemo(() => {
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]
}
return humanPath
}, [humanPath, props?.edit])
const updatePath = React.useCallback(
(args) => {
if (typeof args?.value === "string") {
try {
const parentKeys = Object.keys(parent)
// create new object with new key order
const newObject = {}
parentKeys.forEach((key) => {
if (key === path) {
newObject[args.value] = parent[key]
return
}
newObject[key] = parent[key]
})
// set new object
setThingtime(parentPath, newObject)
// focus next input
const focusableNodeList = thingtimeRef?.current?.querySelectorAll?.(
".magic-input-focusable"
)
// convert focusable to array
const focusable = Array.from(focusableNodeList)
const pathMagicInputFocusable = pathRef?.current?.querySelector?.(
".magic-input-focusable"
)
const nearestMagicInput = focusable?.find((input) => {
if (input !== pathMagicInputFocusable) {
return true
}
return false
})
if (nearestMagicInput && nearestMagicInput?.focus) {
nearestMagicInput.focus()
}
} catch (err) {
console.error(
"Thingtime:657 Something went wrong reassigning path",
err
)
}
}
},
[parent, path, parentPath, setThingtime]
)
const pathRef = React.useRef(null)
const pathDom = React.useMemo(() => {
if (chakraChild) {
return <>>
}
if (renderedPath) {
return (
<>
>
)
}
}, [renderedPath, pl, chakraChild, props?.edit, 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(
(args) => {
const { type } = args
const newChild =
typeof type?.value === "function" ? type?.value() : type?.value || 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 [showContextIcon, setShowContextIcon] = React.useState(false)
const [showNewContextIcon, setShowNewContextIcon] = React.useState(false)
// should be absolute last
React.useEffect(() => {
try {
window.meta.things[uuid] = {
thing: props?.thing,
props,
state: {
chakra,
chakraChild,
circular,
depth,
fullPath,
parent,
parentPath,
path,
},
}
} catch {
// nothing
}
}, [
thing,
props,
uuid,
chakra,
chakraChild,
circular,
depth,
fullPath,
parent,
parentPath,
path,
])
if (chakra || chakraChild) {
return thingtimeChildren
}
return (
{/* {uuid?.current} */}
{!chakraChild && !chakra && (
setShowContextIcon(true)}
onMouseLeave={() => setShowContextIcon(false)}
>
{pathDom}
{props?.edit && (
{typeIcon}
)}
{pathDom && (
)}
)}
{/* {showContextMenu && contextMenu} */}
{!loading && !thingtimeChildren && atomicValue && (
{atomicValue}
)}
{!loading && thingtimeChildren && (
{thingtimeChildren}
{!render && type === "object" && (
{
setShowFullPathContext(true)
setShowNewContextIcon(true)
}}
onMouseLeave={() => {
setShowFullPathContext(false)
setShowNewContextIcon(false)
}}
paddingY={2}
>
{fullPath}
{
if (e?.key === "Enter") {
addNewChild()
}
}}
tabIndex={0}
size={10}
name="seedling"
>
{
e?.preventDefault()
e?.stopPropagation()
e?.nativeEvent?.stopImmediatePropagation()
}}
>
{/*
*/}
)}
)}
)
}