diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts index 7179f961c..b797d71fd 100644 --- a/packages/vue/src/index.ts +++ b/packages/vue/src/index.ts @@ -46,6 +46,13 @@ export type { MenuActionNode, MenuEntry, MenuSeparatorNode } from '#vue/editor/m /** Miscellaneous editor-shell helpers. */ export { useViewportKind } from '#vue/editor/viewport-kind/use' export { useLayerDrag } from '#vue/primitives/LayerTree/useLayerDrag' +export { useFlatReorderDrag } from '#vue/shared/drag/useFlatReorderDrag' +export type { + FlatReorderAxis, + FlatReorderInstruction, + FlatReorderItem, + UseFlatReorderDragOptions +} from '#vue/shared/drag/useFlatReorderDrag' export { useInlineRename } from '#vue/editor/inline-rename/use' export { useToolbarState } from '#vue/primitives/Toolbar/useToolbarState' export { useNodeFontStatus } from '#vue/shared/font-status/use' diff --git a/packages/vue/src/shared/drag/useFlatReorderDrag.ts b/packages/vue/src/shared/drag/useFlatReorderDrag.ts new file mode 100644 index 000000000..5e128a58e --- /dev/null +++ b/packages/vue/src/shared/drag/useFlatReorderDrag.ts @@ -0,0 +1,167 @@ +import { + attachInstruction, + extractInstruction, + type Instruction +} from '@atlaskit/pragmatic-drag-and-drop-hitbox/list-item' +import { getReorderDestinationIndex } from '@atlaskit/pragmatic-drag-and-drop-hitbox/util/get-reorder-destination-index' +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine' +import { + draggable, + dropTargetForElements, + monitorForElements +} from '@atlaskit/pragmatic-drag-and-drop/element/adapter' +import { onScopeDispose, ref } from 'vue' + +export type FlatReorderAxis = 'vertical' | 'horizontal' +export type FlatReorderInstruction = Extract< + Instruction, + { operation: 'reorder-before' | 'reorder-after' } +> + +export interface FlatReorderItem { + id: string +} + +export interface UseFlatReorderDragOptions { + items: () => readonly TItem[] + onMove: (sourceId: string, targetIndex: number) => void + axis?: FlatReorderAxis + getId?: (item: TItem) => string +} + +interface RegisteredItem { + element: HTMLElement + cleanup: () => void +} + +function isFlatReorderInstruction( + instruction: Instruction | null +): instruction is FlatReorderInstruction { + return ( + !!instruction && + !instruction.blocked && + (instruction.operation === 'reorder-before' || instruction.operation === 'reorder-after') + ) +} + +function edgeForInstruction(instruction: FlatReorderInstruction, axis: FlatReorderAxis) { + if (axis === 'vertical') { + return instruction.operation === 'reorder-before' ? 'top' : 'bottom' + } + return instruction.operation === 'reorder-before' ? 'left' : 'right' +} + +export function useFlatReorderDrag({ + items, + onMove, + axis = 'vertical', + getId = (item) => item.id +}: UseFlatReorderDragOptions) { + const draggingId = ref(null) + const instruction = ref(null) + const instructionTargetId = ref(null) + const registered = new Map() + + function clearInstruction() { + instruction.value = null + instructionTargetId.value = null + } + + function cleanupItem(id: string) { + registered.get(id)?.cleanup() + registered.delete(id) + } + + function setupItem(element: HTMLElement | null, item: () => FlatReorderItem) { + const { id } = item() + const current = registered.get(id) + if (current?.element === element) return + + cleanupItem(id) + if (!element) return + + const cleanup = combine( + draggable({ + element, + getInitialData: () => ({ id }), + onDragStart: () => { + draggingId.value = id + }, + onDrop: () => { + draggingId.value = null + } + }), + dropTargetForElements({ + element, + getData: ({ input, element: target }) => + attachInstruction( + { id }, + { + input, + element: target, + axis, + operations: { + 'reorder-before': 'available', + 'reorder-after': 'available' + } + } + ), + canDrop: ({ source }) => source.data.id !== id, + onDrag: ({ self }) => { + const nextInstruction = extractInstruction(self.data) + if (!isFlatReorderInstruction(nextInstruction)) { + clearInstruction() + return + } + instruction.value = nextInstruction + instructionTargetId.value = id + }, + onDragLeave: clearInstruction, + onDrop: clearInstruction, + getIsSticky: () => true + }) + ) + + registered.set(id, { element, cleanup }) + } + + const cleanupMonitor = monitorForElements({ + onDrop: ({ source, location }) => { + const target = location.current.dropTargets.at(0) + if (!target) return + + const sourceId = typeof source.data.id === 'string' ? source.data.id : null + const targetId = typeof target.data.id === 'string' ? target.data.id : null + if (!sourceId || !targetId || sourceId === targetId) return + + const dropInstruction = extractInstruction(target.data) + if (!isFlatReorderInstruction(dropInstruction)) return + + const currentItems = items() + const startIndex = currentItems.findIndex((item) => getId(item) === sourceId) + const indexOfTarget = currentItems.findIndex((item) => getId(item) === targetId) + const targetIndex = getReorderDestinationIndex({ + startIndex, + indexOfTarget, + axis, + closestEdgeOfTarget: edgeForInstruction(dropInstruction, axis) + }) + + if (targetIndex !== startIndex) onMove(sourceId, targetIndex) + draggingId.value = null + clearInstruction() + } + }) + + onScopeDispose(() => { + cleanupMonitor() + for (const id of registered.keys()) cleanupItem(id) + }) + + return { + draggingId, + instruction, + instructionTargetId, + setupItem + } +} diff --git a/src/components/PagesPanel.vue b/src/components/PagesPanel.vue index 565bb60fa..2007c13b6 100644 --- a/src/components/PagesPanel.vue +++ b/src/components/PagesPanel.vue @@ -1,5 +1,5 @@ @@ -118,19 +94,13 @@ function onDrop(
@@ -172,7 +142,7 @@ function onDrop( {{ pg.name }}