From 8b9140ae6c43e7fc90b6d8c12a6df1a9436cdf4f Mon Sep 17 00:00:00 2001 From: tolvitty Date: Fri, 13 Feb 2026 15:43:19 +0100 Subject: [PATCH] feat: add drag & drop field reordering to form builder Co-Authored-By: Claude Opus 4.6 --- src/ui/form-builder.ts | 59 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/ui/form-builder.ts b/src/ui/form-builder.ts index ca85ac0..151596d 100644 --- a/src/ui/form-builder.ts +++ b/src/ui/form-builder.ts @@ -26,6 +26,7 @@ export class FormBuilderModal extends Modal { private onSave: (form: FormDefinition) => void; private history: FormDefinition[] = []; private historyIndex = 0; + private dragSourceIndex: number | null = null; constructor( app: App, @@ -221,10 +222,17 @@ export class FormBuilderModal extends Modal { for (let i = 0; i < this.draft.fields.length; i++) { const field = this.draft.fields[i]; const fieldEl = container.createDiv({ cls: 'ff-builder-field' }); + fieldEl.setAttribute('draggable', 'true'); + fieldEl.dataset.index = String(i); // Field header with number, label preview, and action buttons const headerEl = fieldEl.createDiv({ cls: 'ff-builder-field-header' }); + headerEl.createSpan({ + cls: 'ff-drag-handle', + text: '\u2807', + }); + headerEl.createSpan({ cls: 'ff-builder-field-num', text: `#${i + 1}`, @@ -283,6 +291,57 @@ export class FormBuilderModal extends Modal { this.render(); }); + // --- Drag & Drop --- + fieldEl.addEventListener('dragstart', (e: DragEvent) => { + this.dragSourceIndex = i; + fieldEl.addClass('ff-dragging'); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + } + }); + + fieldEl.addEventListener('dragend', () => { + this.dragSourceIndex = null; + fieldEl.removeClass('ff-dragging'); + }); + + fieldEl.addEventListener('dragover', (e: DragEvent) => { + e.preventDefault(); + if (this.dragSourceIndex === null || this.dragSourceIndex === i) return; + if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; + + const rect = fieldEl.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + fieldEl.removeClass('ff-drop-above', 'ff-drop-below'); + if (e.clientY < midY) { + fieldEl.addClass('ff-drop-above'); + } else { + fieldEl.addClass('ff-drop-below'); + } + }); + + fieldEl.addEventListener('dragleave', () => { + fieldEl.removeClass('ff-drop-above', 'ff-drop-below'); + }); + + fieldEl.addEventListener('drop', (e: DragEvent) => { + e.preventDefault(); + fieldEl.removeClass('ff-drop-above', 'ff-drop-below'); + if (this.dragSourceIndex === null || this.dragSourceIndex === i) return; + + const rect = fieldEl.getBoundingClientRect(); + const midY = rect.top + rect.height / 2; + const insertBefore = e.clientY < midY; + + this.pushSnapshot(); + const [moved] = this.draft.fields.splice(this.dragSourceIndex, 1); + let targetIndex = insertBefore ? i : i + 1; + if (this.dragSourceIndex < i) targetIndex--; + this.draft.fields.splice(targetIndex, 0, moved); + this.dragSourceIndex = null; + this.render(); + }); + // Field body — settings const bodyEl = fieldEl.createDiv({ cls: 'ff-builder-field-body' });