import type { EntityID } from '@wovin/core/applog' import type { Accessor, Setter } from 'solid-js' import { Logger } from 'besonders-logger' import { useContext } from 'solid-js' import { BlockContext } from '../components/BlockTree' import { useCurrentThread, useRawThread } from '../ui/reactive' import { focusBlockAsInput } from '../ui/utils-ui' import { indentBlk, outdentBlk } from './block-utils' import { reorderRelation } from './relation-utils' import { useBlk } from './VMs/BlockVM' import { useRel } from './VMs/RelationVM' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) export function useBlockContext() { return useContext(BlockContext) } export function createTreeItemKeydownHandler( blockID: EntityID, relationID: EntityID, isExpandedSignal: Accessor, setExpandedSignal: Setter | ((newVal: boolean) => any[]), ag: string, ): (evt: React.KeyboardEvent) => boolean { VERBOSE('Creating Keydown for', { blockID, relationID }) const currentDS = useCurrentThread() const rawDS = useRawThread() const blockContext = useBlockContext() if (blockContext.id !== blockID) { WARN(`block context != args`, blockID, blockContext) } if (blockContext.relationToParent !== relationID) { WARN(`rel context != args`, relationID, blockContext) } const blockVM = useBlk(blockID) return function treeItemKeydownHandler(evt: React.KeyboardEvent) { if (!['Escape', 'Tab', 'Backspace', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(evt.key)) { // ! beware to add your key here :P return } if (blockContext.id !== blockID) { WARN(`block context != args`, blockID, blockContext) } if (blockContext.relationToParent !== relationID) { WARN(`rel context != args`, relationID, blockContext) } const relToParentVM = useRel(relationID, rawDS) const parentID = relToParentVM?.childOf const parentVM = useBlk(parentID, rawDS) const siblingAboveID = relToParentVM?.after // const siblingAboveVM = siblingAbove ? BlockVM.get(siblingAbove, rawDS) : null const siblingAboveVM = useBlk(siblingAboveID, rawDS) const siblingRelations = parentVM?.kidRelations ?? [] const siblingAboveRelID = (siblingRelations?.find(rel => rel.block === siblingAboveID))?.en const siblingAboveRelVM = useRel(siblingAboveRelID, rawDS) const isSiblingAboveExpanded = siblingAboveRelVM?.isExpanded const grandParentRelID = blockContext.parentContext?.relationToParent ?? null // const grandParentVM = grandParentID ? BlockVM.get(grandParentID, rawDS) : null const grandParentRelVM = useRel(grandParentRelID, rawDS) const grandParentID = grandParentRelVM?.childOf const grandParentVM = useBlk(grandParentID, rawDS) // const focus = useFocus() const { target, currentTarget: { innerText } } = evt const isExpanded = relToParentVM?.isExpanded // !!(target.parentElement.attributes['aria-expanded']?.nodeValue === 'true') // ? is there a more elegant way to get this? const myTopKidID = blockVM.kidRelations[0]?.block const siblingBelowID = (siblingRelations?.find(rel => rel.after === blockID))?.block // const mySiblingIndex = siblingRelations.findIndex(rel => rel.block === blockID) // const isBottomSibling = !!(!siblingRelations.length || mySiblingIndex == siblingRelations.length - 1) const siblingAboveRelations = siblingAboveVM?.kidRelations ?? [] const siblingAbovesLastKid = siblingAboveRelations.length ? siblingAboveRelations[siblingAboveRelations.length - 1].block : null // TODO -> BlockVM const parentSiblingRelations = grandParentVM?.kidRelations ?? [] const parentSiblingIndex = parentSiblingRelations.findIndex(rel => rel.block === parentID) const belowMyParent = parentSiblingRelations[parentSiblingIndex + 1]?.block const parentsBottomKid = parentSiblingRelations.length ? parentSiblingRelations[parentSiblingRelations.length - 1].block : null const belowMe = isExpandedSignal() ? myTopKidID ?? siblingBelowID ?? belowMyParent : siblingBelowID ?? belowMyParent // const siblingAboveHTML = target.parentElement.previousElementSibling // HACK well... its better than jquery right? // const isSiblingAboveExpanded = !!(siblingAboveHTML?.attributes['aria-expanded']?.nodeValue === 'true') // ? is there a more elegant way to get this? const aboveMe = isSiblingAboveExpanded ? (siblingAbovesLastKid ?? relToParentVM?.after ?? relToParentVM?.childOf) : (relToParentVM?.after ?? relToParentVM?.childOf) // HACK: Rework for tiptap editor // const selection = getSelectionInContentEditable(target) // const pos = getCursorPositionInContentEditable(target) // const beforePos = innerText.slice(0, pos) // const afterPos = innerText.slice(pos) const isAtStart = false // !!(pos === 0) const isAtEnd = false // !!(pos === innerText.length && !selection.length) VERBOSE('[EditableTreeItem] onKeyDown', { evt, relationToParent: relToParentVM, // pos, innerText, isAtEnd, // selection, blockContext, blockVM, isSiblingAboveExpanded, siblingAbovesLastKid, }) const isContentEmpty = innerText.trim().length === 0 // often is `\n` when empty // if (evt.key === 'Escape') { // evt.preventDefault() // const prev = '//TODO undo' // evt.currentTarget.innerText = prev // return DEBUG('escape TODO - revert changes', { evt }) // } else if (evt.key === 'Tab') { // if (innerText !== blockVM.content) { // HACK broken with tiptap - still relevant? (quick test said: no, debounce still works) // VERBOSE('[EditableTreeItem] onBlur via tab', { evt, innerText }) // blockVM.content = innerText // } if (relToParentVM) { if (evt.shiftKey) { outdentBlk(rawDS, relToParentVM, grandParentID) return evt.preventDefault() // only preventDefault if necessary - tab key can sometimes be useful for other things } else if (siblingAboveRelVM) { if (!isSiblingAboveExpanded) { siblingAboveRelVM.isExpanded = true } indentBlk(rawDS, relToParentVM) return evt.preventDefault() } } evt.shiftKey ? DEBUG('rev tab - outdent', { relationToParent: relToParentVM, relationsOfParent: siblingRelations }) : DEBUG('tab - indent', { relationToParent: relToParentVM, relationsOfParent: siblingRelations }) // } else if (evt.key === 'Backspace') { -- moved to useBlockKeyhandlers // DEBUG(`Backspace (shift=${evt.shiftKey}, ctrl=${evt.ctrlKey}, start=${isAtStart}, empty=${isContentEmpty})`) // if ((evt.ctrlKey && evt.shiftKey) || (isAtStart && isContentEmpty)) { // removeBlockRelAndMaybeDelete(currentDS, blockID, relToParentVM?.en) // removeBlockFromRelChain(currentDS, blockID, relToParentVM?.en) // evt.preventDefault() // if (relToParentVM) { // focusBlockAsInput({ id: relToParentVM.after ?? relToParentVM.childOf, end: true }) // } // } /* else if (isAtStart && !isContentEmpty) { // evt.preventDefault() // if (relToParentVM) { // const aboveMe = siblingAboveID ?? parentID // const aboveMeVM = siblingAboveVM ?? parentVM // const mergedContent = `${aboveMeVM.content}${afterPos}` // const originalLengthAbove = untracked(() => aboveMeVM.content.length) // focusBlockAsInput({ id: aboveMe, /* end: true * / pos: originalLengthAbove }) // queueMicrotask(() => { // aboveMeVM.persistContent(mergedContent) // DEBUG('merging into aboveMe', { aboveMe, afterPos, aboveMeVM, mergedContent }) // removeBlockRelAndMaybeDelete(currentDS, blockID, relToParentVM.en) // }) // } // // TODO add old content up to end of upper block // } */ // return } else { VERBOSE('RELOFPARENT', { relationsOfParent: siblingRelations, belowMe, aboveMe, belowMyParent, siblingAbovesLastKid, }) switch (evt.key) { case 'ArrowUp': if (evt.ctrlKey) { reorderRelation(relToParentVM, { up: 1 }, siblingRelations, ag) focusBlockAsInput({ id: blockID, end: true }) return evt.preventDefault() } else { DEBUG('ArrowUp', { relationToParent: relToParentVM, relationsOfParent: siblingRelations }) if (relToParentVM) { // (so upArrow works if not expanded - leftArrow works when expanded) focusBlockAsInput({ id: aboveMe, /* relToParentVM.after ?? relToParentVM.childOf */ end: true }) return evt.preventDefault() } break } case 'ArrowDown': if (evt.ctrlKey) { reorderRelation(relToParentVM, { down: 1 }, siblingRelations, ag) focusBlockAsInput({ id: blockID, end: true }) return evt.preventDefault() } else { DEBUG('ArrowDown', { relationToParent: relToParentVM, relationsOfParent: siblingRelations }) if (belowMe) { focusBlockAsInput({ id: belowMe, select: true }) return evt.preventDefault() } break } case 'ArrowLeft': if (evt.ctrlKey && evt.altKey) { setExpandedSignal(false) // ? consider if cursor is on a kid with no kids - collapse and focus on parent? return evt.preventDefault() } if (relToParentVM && isAtStart) { focusBlockAsInput({ id: aboveMe, end: true }) return evt.preventDefault() } break case 'ArrowRight': if (evt.ctrlKey && evt.altKey) { setExpandedSignal(true) return evt.preventDefault() } if (belowMe && isAtEnd) { focusBlockAsInput({ id: belowMe, end: false, start: true }) return evt.preventDefault() } break default: break } } } }