import React from "react" import ClickAwayListener from "react-click-away-listener" import { Center, Flex, Input } from "@chakra-ui/react" import Fuse from "fuse.js" import { Rainbow } from "../Rainbow/Rainbow" import { Thingtime } from "../Thingtime/Thingtime" import { useThingtime } from "../Thingtime/useThingtime" import { sanitise } from "~/functions/sanitise" import { getParentPath } from "~/smarts" export const Commander = (props) => { const { thingtime, setThingtime, getThingtime, thingtimeRef, paths } = useThingtime() const commanderId = React.useMemo(() => { return props?.id || "global" }, [props?.id]) const inputRef = React.useRef() const global = props?.global const commanderSettings = React.useMemo(() => { return thingtime?.settings?.commander?.[commanderId] || {} }, [ thingtime?.settings?.commander, thingtime?.settings?.commander?.[commanderId], commanderId, ]) const [inputValue, setInputValue] = React.useState("") const [virtualValue, setVirtualValue] = React.useState("") const [hoveredSuggestion, setHoveredSuggestion] = React.useState() const [active, setActive] = React.useState(false) const [contextPath, setContextPath] = React.useState() const mode = React.useMemo(() => { return props?.mode || "value" }, [props?.mode]) const [showContext, setShowContextState] = React.useState(false) const mobileVW = React.useMemo(() => { return "calc(100vw - 55px)" }, []) const rainbowRepeats = 2 const setShowContext = React.useCallback( (value, from?: string) => { setShowContextState(value) }, [setShowContextState] ) // const [suggestions, setSuggestions] = React.useState([]) const contextValue = React.useMemo(() => { // TODO: Figure out why this is running on every click const ret = getThingtime(contextPath) return ret }, [contextPath, getThingtime]) const commanderActive = React.useMemo(() => { return thingtime?.settings?.commander?.[commanderId]?.commanderActive }, [commanderSettings, commanderId]) // commanderActive useEffect React.useEffect(() => { if (commanderActive) { inputRef?.current?.focus?.() } else { document.activeElement.blur() if ( thingtimeRef?.current?.settings?.commander?.[commanderId] ?.clearCommanderOnToggle ) { setInputValue("") setHoveredSuggestion(null) } if ( thingtimeRef?.current?.settings?.commander?.[commanderId]?.commander?.[ commanderId ]?.clearCommanderContextOnToggle ) { setShowContext(false, "commanderActive useEffect") } if (contextPath !== undefined && !inputValue) { setContextPath(undefined) } if (showContext !== false) { setShowContext(false) } } }, [ commanderActive, thingtimeRef, setShowContext, commanderId, inputValue, contextPath, showContext, ]) const onInputChange = React.useCallback((e) => { setInputValue(e.target.value) setHoveredSuggestion(null) }, []) const validSetters = React.useMemo(() => { return ["=", " is ", " IS ", " Is ", " iS "] }, []) const command = React.useMemo(() => { // const sanitizedCommand = sanitise(value) // const sanitizedCommand = inputValue const sanitizedCommand = virtualValue const validSetter = validSetters?.find((setter) => { if (sanitizedCommand?.includes(setter)) { return setter } return false }) if (typeof validSetter === "string") { const indexOfSplitter = sanitizedCommand?.indexOf(validSetter) const [pathRaw, valRaw] = [ sanitizedCommand?.slice(0, indexOfSplitter), sanitizedCommand?.slice(indexOfSplitter + validSetter?.length), ] return [pathRaw?.trim(), valRaw?.trim()] } return [sanitizedCommand] }, [ // inputValue, virtualValue, validSetters, ]) const commandPath = React.useMemo(() => { return command?.[0] // return sanitise(command?.[0]) }, [command]) const commandValue = React.useMemo(() => { return command?.[1] }, [command]) const validQuotations = React.useMemo(() => { return ['"', "'"] }, []) const escapedCommandValue = React.useMemo(() => { // replace quotations with escaped quoations except for first and last quotation const startingQuotation = commandValue?.[0] const endingQuotation = commandValue?.[commandValue?.length - 1] const isQuoted = validQuotations?.includes(startingQuotation) && validQuotations?.includes(endingQuotation) const restOfCommandValue = isQuoted ? commandValue?.slice(1, commandValue?.length - 1) : commandValue const escaped = restOfCommandValue ?.replace(/"/g, '\\"') ?.replace(/'/g, "\\'") const ret = `"${escaped}"` return ret }, [commandValue, validQuotations]) const commandIsAction = React.useMemo(() => { return commandPath && commandValue }, [commandPath, commandValue]) const suggestions = React.useMemo(() => { try { const fuse = new Fuse(paths) const results = fuse.search(inputValue) const mappedResults = results?.map((result) => { return result?.item }) return mappedResults } catch (err) { console.error("fuse error", err) } }, [inputValue, paths]) const showSuggestions = React.useMemo(() => { return ( inputValue?.length && suggestions?.length && commanderActive && thingtime?.settings?.commander?.[commanderId]?.hideSuggestionsOnToggle ) }, [ inputValue, suggestions, commanderActive, commanderId, thingtime?.settings?.commander, commanderSettings, ]) const selectSuggestion = React.useCallback( (suggestionIdx) => { const suggestion = suggestions?.[suggestionIdx] setInputValue(suggestion) setHoveredSuggestion(null) setContextPath(suggestion) setShowContext(true, "Select suggestion") }, [setInputValue, setContextPath, setShowContext, suggestions] ) const commandContainsPath = React.useMemo(() => { const commandIncludesSuggestion = suggestions?.find((suggestion) => { return commandPath?.includes(suggestion) }) // return false return commandIncludesSuggestion }, [commandPath, suggestions]) const openCommander = React.useCallback(() => { setThingtime(`settings.commander.${commanderId}.commanderActive`, true) }, [setThingtime, commanderId]) const closeCommander = React.useCallback( (e?: any) => { if (!e?.defaultPrevented) { if (thingtime?.settings?.commander?.[commanderId]?.commanderActive) { setThingtime( `settings.commander.${commanderId}.commanderActive`, false ) } } }, [ setThingtime, commanderId, commanderSettings, thingtime?.settings?.commander, ] ) const toggleCommander = React.useCallback(() => { if (thingtime?.settings?.commander?.[commanderId]?.commanderActive) { closeCommander() } else { openCommander() } }, [ thingtime?.settings?.commander, commanderSettings, commanderId, closeCommander, openCommander, ]) const executeCommand = React.useCallback(() => { // if selection is active then select it const curSuggestionIdx = hoveredSuggestion if (curSuggestionIdx !== null) { selectSuggestion(curSuggestionIdx) } if (commanderActive) { try { if (commandIsAction) { // nothing const prevVal = getThingtime(commandPath) const parentPath = getParentPath(commandPath) || "thingtime" try { // first try to execute literal javscript const fn = `() => { return ${commandValue} }` const evalFn = eval(fn) const realVal = evalFn() setThingtime(commandPath, realVal) } catch (err) { console.log( "Caught error after trying to execute literal javascript", err ) // likely literaly javascript wasn't valid try { const fn = `() => { return ${escapedCommandValue} }` const evalFn = eval(fn) const realVal = evalFn() const prevVal = getThingtime(commandPath) const parentPath = getParentPath(commandPath) setThingtime(commandPath, realVal) } catch { // something very bad went wrong console.log( "Caught error after trying to execute escaped literal javascript", err ) } } // if (!prevVal) { setContextPath(commandPath) setShowContext(true, "commandIsAction check") // } } // if (commandContainsPath) else { // const prevValue = getThingtime(commandPath) // const newValue = setThingtime(commandPath, prevValue) console.log("Setting context path", commandPath) setContextPath(commandPath) setShowContext(true, "commandContainsPath check") } } catch (err) { console.error("Caught error on commander onEnter", err) } } }, [ hoveredSuggestion, selectSuggestion, commanderActive, commandIsAction, commandPath, commandValue, escapedCommandValue, getThingtime, setThingtime, setContextPath, setShowContext, ]) const allCommanderKeyListener = React.useCallback( (e: any) => { console.log("commander key listener e?.code", e?.code) thingtimeRef.current = thingtime if (e?.metaKey && e?.code === "KeyP") { e.preventDefault() e.stopPropagation() toggleCommander() } // if key escape close all modals else if (e?.code === "Escape") { closeCommander() } // only run these if commander active if (commanderActive) { // if arrow keys then move selection if (e?.code === "ArrowUp") { // move selection up const curSuggestionIdx = typeof hoveredSuggestion === "number" ? hoveredSuggestion : suggestions?.length const newSuggestionIdx = curSuggestionIdx - 1 if (newSuggestionIdx >= 0) { setHoveredSuggestion(newSuggestionIdx) } else { setHoveredSuggestion(suggestions?.length - 1) } } else if (e?.code === "ArrowDown") { // move selection down const curSuggestionIdx = typeof hoveredSuggestion === "number" ? hoveredSuggestion : -1 const newSuggestionIdx = curSuggestionIdx + 1 if (newSuggestionIdx < suggestions?.length) { setHoveredSuggestion(newSuggestionIdx) } else { setHoveredSuggestion(0) } } else if (e?.code === "Enter") { executeCommand() } } }, [ closeCommander, toggleCommander, hoveredSuggestion, suggestions, thingtime, thingtimeRef, commanderActive, executeCommand, ] ) React.useEffect(() => { window.addEventListener("keydown", allCommanderKeyListener) return () => { window.removeEventListener("keydown", allCommanderKeyListener) } }, [allCommanderKeyListener]) React.useEffect(() => { if (typeof hoveredSuggestion === "number") { setVirtualValue(suggestions?.[hoveredSuggestion]) } else { setVirtualValue(inputValue) } }, [hoveredSuggestion, inputValue, suggestions]) React.useEffect(() => { setVirtualValue(inputValue) }, [inputValue]) const InputJSX = React.useMemo(() => { return ( ) }, [inputRef, inputValue, onInputChange, openCommander]) const MainInput = React.useMemo(() => { return (
{props?.rainbow && ( )} {InputJSX}
) }, [InputJSX, commanderActive, rainbowRepeats, props?.rainbow, mobileVW]) return ( setHoveredSuggestion(null)} paddingY={3} > {suggestions?.map((suggestion, i) => { return ( selectSuggestion(i)} onMouseEnter={() => setHoveredSuggestion(i)} paddingX={4} > {suggestion} ) })} {showContext && ( )}
{props?.rainbow && ( {MainInput} )} {!props?.rainbow && MainInput}
) }