import type { EntityID } from '@wovin/core/applog' import type { Component, JSX } from 'solid-js' import type { SortableEvent } from 'solid-sortablejs' import type { FakeBlock } from './BlockTree' import { Logger } from 'besonders-logger' import classNames from 'classnames' import { createSignal, untrack } from 'solid-js' import Sortable from 'solid-sortablejs' import { handleBlockDrag } from '../data/block-ui-helpers' import { useRel } from '../data/VMs/RelationVM' import { useCurrentThread } from '../ui/reactive' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars export interface SortableBlockItem { id: string fake?: FakeBlock relationID?: EntityID blockID?: EntityID // smartList?: SmartList kidBlockIDs?: readonly EntityID[] } // Root-level blocks need to be sortables to be able to be dragged, but cannot be drag targets, so we disable them while something is being dragged export const [draggingBlock, setDraggingBlock] = createSignal(null) export const SortableBlocks: Component<{ blockID?: EntityID // specialRoot?: SpecialRoot // kidRelationIDs: Array | null // blockIDs?: Array | null disabled?: boolean class?: string items: readonly SortableBlockItem[] children: (item: SortableBlockItem) => JSX.Element }> = (props) => { DEBUG(` created`, untrack(() => ({ ...props }))) const currentDS = useCurrentThread() const handleDrag = (event: SortableEvent & { originalEvent: DragEvent }) => { const { item } = event const getRel = (relID: EntityID) => useRel(relID) const itemID = item.dataset.id let sourceRelation = null let blockID = null if (itemID.startsWith('FakeDragRelation')) { blockID = props.blockID WARN(`[SortableBlocks] fake item blockID?`, item.dataset.blockID, { item, event }) } else { sourceRelation = getRel(item.dataset.id) if (!sourceRelation) throw ERROR(`[SortableBlocks] item has no relID`, { item, event }) blockID = sourceRelation.block } const newParentID = event.to.dataset.block // const afterRelID = event.newIndex === 0 ? null : event.to.dataset.relations.split(',')[event.newIndex - 1] const kidRelationsOfTargetParent = event.to.dataset.relations.split(',') const newIndex = event.newIndex const selfIndex = sourceRelation && kidRelationsOfTargetParent.indexOf(sourceRelation.en) DEBUG(`[handleDrag]`, { kidRelationsOfTargetParent, newIndex, selfIndex, newParentID, sourceRelation, event }) if (selfIndex > -1) { // we might be dragging into the same parent as we also are now, but the index from sortablejs already took our removal into account kidRelationsOfTargetParent.splice(selfIndex, 1) } const newAfterID = newIndex === 0 ? null : getRel(kidRelationsOfTargetParent[newIndex - 1]).block if (newAfterID === blockID) { throw ERROR(`[handleBlockDrag] newAfterID === blockID`) } if (newParentID === blockID) { throw ERROR(`[handleBlockDrag] newParentID === blockID`) // TODO: this can happen if we drop the block back in place } DEBUG({ event }) if (event.originalEvent.ctrlKey) sourceRelation = null if ( !sourceRelation || newParentID !== sourceRelation.childOf || newAfterID !== sourceRelation.after ) { DEBUG(`Calling handleBlockDrag`, { newAfterID, newIndex, kidRelationsOfTargetParent, event }) handleBlockDrag( currentDS, blockID, sourceRelation?.en, newParentID, newAfterID, ) } } // ℹ OPTION DOCS: https://github.com/SortableJS/Sortable#options return ( class={classNames(props.class, 'flex flex-col gap-2')} items={props.items} idField='id' // setItems={handleSort} group='blocks' handle='.drag-handle' disabled={props.disabled} swapThreshold={0.5} delay={200 /* if drag is shorter than this, ignore it */} delayOnTouchOnly={true} touchStartThreshold={10 /* non-intuitive option - see docs */} onChoose={(event) => { setDraggingBlock(props.blockID) VERBOSE(`[Sortable#${props.blockID}] onChoose`, event) return false }} onUnchoose={(event) => { setDraggingBlock(null) VERBOSE(`[Sortable#${props.blockID}] onUnchoose`, event) }} onStart={event => VERBOSE(`[Sortable#${props.blockID}] onStart`, event)} onEnd={(event) => { VERBOSE(`[Sortable#${props.blockID}] onEnd`, event) handleDrag(event) }} setItems={() => {}} setData={(dataTransfer, element) => { VERBOSE('dataTransfer', { dataTransfer, element }) dataTransfer.setData('text/plain', '') dataTransfer.setData('text/html', '') }} data-block={props.blockID} data-relations={props.items.filter(item => item.relationID).map(({ relationID }) => relationID).join(',')} > {props.children} ) }