From 7256f53e0cb47f3d1c5b97c2a023bc8696e0bf00 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 3 Jun 2026 00:27:12 +0200 Subject: [PATCH] words: wire HTML5 drag-reorder for blocks via the gutter grip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gutter grip rendered (cursor: grab) but the drag-to-move functionality was lost when the soma-DragDrop migration was abandoned — clicking it only opened the action menu (Move up / Move down). Restore the reorder as native HTML5 drag-drop: - words-block-drag-type.ts: private MIME `application/x-words-block` the grip writes (its top-level index) and the controller reads, so block reorders are told apart from native text drags. - words-block-gutter.svelte: the grip's DropdownMenu.Trigger is now `draggable`; `ondragstart` stamps the block index on the transfer. The menu still opens on click (the dropdown opens on click, not pointerdown — no collision with the drag gesture). - words-block-drag.svelte: the drop sink. Listens on the contenteditable for dragover/drop, paints a horizontal indicator at the nearest inter-block seam, and repositions via the engine's `moveBlockTo` (target is post-removal: `to = from < n ? n - 1 : n`). Bails unless the drag carries our private type, leaving native text drag untouched. A document-capture `dragend` clears the indicator wherever the drag ends. - words.svelte: mount the controller next to the gutter. - words.css: `[data-words-block-drop-line]` accent indicator. Reorder is top-level only (the gutter is top-level only), mirroring the gutter's `[data-words-node='block']` block detection exactly. Verified in-browser: grip is draggable=true; dragstart writes index; dragover paints the indicator + preventDefaults; drop reorders correctly downward (0→end) and upward (14→1) with no block lost; native text drags are not hijacked; no console errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/words/words-block-drag-type.ts | 8 ++ .../components/words/words-block-drag.svelte | 118 ++++++++++++++++++ .../words/words-block-gutter.svelte | 12 ++ src/uix/eidos/components/words/words.css | 15 +++ src/uix/eidos/components/words/words.svelte | 2 + 5 files changed, 155 insertions(+) create mode 100644 src/uix/eidos/components/words/words-block-drag-type.ts create mode 100644 src/uix/eidos/components/words/words-block-drag.svelte diff --git a/src/uix/eidos/components/words/words-block-drag-type.ts b/src/uix/eidos/components/words/words-block-drag-type.ts new file mode 100644 index 000000000..c678c368d --- /dev/null +++ b/src/uix/eidos/components/words/words-block-drag-type.ts @@ -0,0 +1,8 @@ +/** + * Private clipboard MIME the block grip writes on `dragstart` (carrying the + * dragged block's top-level index as its data). The drag-reorder controller + * reads it on `dragover` / `drop` to tell a block reorder apart from a native + * text drag — every drop handler bails unless this type is present, so + * selecting text and dragging it keeps the browser's default behaviour. + */ +export const WORDS_BLOCK_DRAG_TYPE = 'application/x-words-block'; diff --git a/src/uix/eidos/components/words/words-block-drag.svelte b/src/uix/eidos/components/words/words-block-drag.svelte new file mode 100644 index 000000000..baa91b867 --- /dev/null +++ b/src/uix/eidos/components/words/words-block-drag.svelte @@ -0,0 +1,118 @@ + + +{#if line} +
+{/if} diff --git a/src/uix/eidos/components/words/words-block-gutter.svelte b/src/uix/eidos/components/words/words-block-gutter.svelte index 43bdda88d..59d8dd2b8 100644 --- a/src/uix/eidos/components/words/words-block-gutter.svelte +++ b/src/uix/eidos/components/words/words-block-gutter.svelte @@ -29,6 +29,7 @@ } from '$soma/components/words'; import { pickImageFile } from './words-image-file'; import { WORDS_MENU_LANGS as L } from './langs-menu'; + import { WORDS_BLOCK_DRAG_TYPE } from './words-block-drag-type'; let { api, @@ -200,6 +201,15 @@ // the next selectionchange). api.selectAtomicBlock(index, [index]); } + // Start an HTML5 drag carrying this block's top-level index. The drop + // controller (`words-block-drag.svelte`) reads it on drop and repositions + // the block. The grip stays a click target too (the menu opens on click, + // the drag on a press-and-move) — they don't collide. + function onGripDragStart(event: DragEvent) { + if (index < 0 || !event.dataTransfer) return; + event.dataTransfer.setData(WORDS_BLOCK_DRAG_TYPE, String(index)); + event.dataTransfer.effectAllowed = 'move'; + } function moveUp() { if (index > 0) api.applyCommand({ type: 'moveBlock', blockIndex: index, direction: 'up' }); } @@ -326,6 +336,8 @@ rounded="md" aria-label="Block actions" data-words-block-handle + draggable={true} + ondragstart={onGripDragStart} > {#snippet icon()}