feat: add drag & drop field reordering to form builder

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-13 15:43:19 +01:00
parent 03c471a60b
commit 8b9140ae6c

View file

@ -26,6 +26,7 @@ export class FormBuilderModal extends Modal {
private onSave: (form: FormDefinition) => void; private onSave: (form: FormDefinition) => void;
private history: FormDefinition[] = []; private history: FormDefinition[] = [];
private historyIndex = 0; private historyIndex = 0;
private dragSourceIndex: number | null = null;
constructor( constructor(
app: App, app: App,
@ -221,10 +222,17 @@ export class FormBuilderModal extends Modal {
for (let i = 0; i < this.draft.fields.length; i++) { for (let i = 0; i < this.draft.fields.length; i++) {
const field = this.draft.fields[i]; const field = this.draft.fields[i];
const fieldEl = container.createDiv({ cls: 'ff-builder-field' }); 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 // Field header with number, label preview, and action buttons
const headerEl = fieldEl.createDiv({ cls: 'ff-builder-field-header' }); const headerEl = fieldEl.createDiv({ cls: 'ff-builder-field-header' });
headerEl.createSpan({
cls: 'ff-drag-handle',
text: '\u2807',
});
headerEl.createSpan({ headerEl.createSpan({
cls: 'ff-builder-field-num', cls: 'ff-builder-field-num',
text: `#${i + 1}`, text: `#${i + 1}`,
@ -283,6 +291,57 @@ export class FormBuilderModal extends Modal {
this.render(); 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 // Field body — settings
const bodyEl = fieldEl.createDiv({ cls: 'ff-builder-field-body' }); const bodyEl = fieldEl.createDiv({ cls: 'ff-builder-field-body' });