diff --git a/src/ui/form-builder.ts b/src/ui/form-builder.ts index c20847f..ca85ac0 100644 --- a/src/ui/form-builder.ts +++ b/src/ui/form-builder.ts @@ -24,6 +24,8 @@ const FIELD_TYPES: FieldType[] = [ export class FormBuilderModal extends Modal { private draft: FormDefinition; private onSave: (form: FormDefinition) => void; + private history: FormDefinition[] = []; + private historyIndex = 0; constructor( app: App, @@ -33,6 +35,8 @@ export class FormBuilderModal extends Modal { super(app); this.draft = structuredClone(form); this.onSave = onSave; + this.history = [structuredClone(this.draft)]; + this.historyIndex = 0; } onOpen(): void { @@ -43,11 +47,48 @@ export class FormBuilderModal extends Modal { this.contentEl.empty(); } + private pushSnapshot(): void { + // Truncate any forward history + this.history = this.history.slice(0, this.historyIndex + 1); + this.history.push(structuredClone(this.draft)); + // Cap at 30 entries + if (this.history.length > 30) { + this.history.shift(); + } else { + this.historyIndex++; + } + } + + private undo(): void { + if (this.historyIndex <= 0) return; + this.historyIndex--; + this.draft = structuredClone(this.history[this.historyIndex]); + this.render(); + } + + private redo(): void { + if (this.historyIndex >= this.history.length - 1) return; + this.historyIndex++; + this.draft = structuredClone(this.history[this.historyIndex]); + this.render(); + } + private render(): void { const { contentEl } = this; contentEl.empty(); contentEl.addClass('ff-builder-modal'); + // Undo/Redo keyboard shortcuts + contentEl.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) { + e.preventDefault(); + this.undo(); + } else if (e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey) { + e.preventDefault(); + this.redo(); + } + }); + // --- General settings --- contentEl.createEl('h2', { text: 'Form Settings' }); @@ -65,6 +106,7 @@ export class FormBuilderModal extends Modal { ['create', 'update'], this.draft.mode, (v) => { + this.pushSnapshot(); this.draft.mode = v as 'create' | 'update'; this.render(); // Re-render to show mode-specific settings }, @@ -126,6 +168,7 @@ export class FormBuilderModal extends Modal { type: 'text', required: false, }; + this.pushSnapshot(); this.draft.fields.push(newField); this.render(); }); @@ -133,12 +176,34 @@ export class FormBuilderModal extends Modal { // --- Footer --- const footer = contentEl.createDiv({ cls: 'ff-builder-footer' }); - const cancelBtn = footer.createEl('button', { text: 'Cancel' }); + // Left side: undo/redo + const undoRedoEl = footer.createDiv({ cls: 'ff-builder-undo-redo' }); + + const undoBtn = undoRedoEl.createEl('button', { + cls: 'ff-builder-action-btn', + text: '\u21A9', + }); + undoBtn.setAttribute('aria-label', 'Undo (Ctrl+Z)'); + if (this.historyIndex <= 0) undoBtn.setAttribute('disabled', ''); + undoBtn.addEventListener('click', () => this.undo()); + + const redoBtn = undoRedoEl.createEl('button', { + cls: 'ff-builder-action-btn', + text: '\u21AA', + }); + redoBtn.setAttribute('aria-label', 'Redo (Ctrl+Shift+Z)'); + if (this.historyIndex >= this.history.length - 1) redoBtn.setAttribute('disabled', ''); + redoBtn.addEventListener('click', () => this.redo()); + + // Right side: cancel/save + const footerRight = footer.createDiv({ cls: 'ff-builder-footer-right' }); + + const cancelBtn = footerRight.createEl('button', { text: 'Cancel' }); cancelBtn.addEventListener('click', () => { this.close(); }); - const saveBtn = footer.createEl('button', { + const saveBtn = footerRight.createEl('button', { text: 'Save', cls: 'mod-cta', }); @@ -180,6 +245,7 @@ export class FormBuilderModal extends Modal { }); upBtn.setAttribute('aria-label', 'Move up'); upBtn.addEventListener('click', () => { + this.pushSnapshot(); [this.draft.fields[i - 1], this.draft.fields[i]] = [ this.draft.fields[i], this.draft.fields[i - 1], @@ -196,6 +262,7 @@ export class FormBuilderModal extends Modal { }); downBtn.setAttribute('aria-label', 'Move down'); downBtn.addEventListener('click', () => { + this.pushSnapshot(); [this.draft.fields[i], this.draft.fields[i + 1]] = [ this.draft.fields[i + 1], this.draft.fields[i], @@ -211,6 +278,7 @@ export class FormBuilderModal extends Modal { }); deleteBtn.setAttribute('aria-label', 'Delete field'); deleteBtn.addEventListener('click', () => { + this.pushSnapshot(); this.draft.fields.splice(i, 1); this.render(); }); @@ -235,6 +303,7 @@ export class FormBuilderModal extends Modal { FIELD_TYPES, field.type, (v) => { + this.pushSnapshot(); field.type = v as FieldType; this.render(); }, @@ -331,7 +400,10 @@ export class FormBuilderModal extends Modal { type: 'text', value: value, }); - input.addEventListener('change', () => onChange(input.value)); + input.addEventListener('change', () => { + this.pushSnapshot(); + onChange(input.value); + }); } private addTextareaSetting( @@ -347,7 +419,10 @@ export class FormBuilderModal extends Modal { }); textarea.value = value; textarea.rows = 4; - textarea.addEventListener('change', () => onChange(textarea.value)); + textarea.addEventListener('change', () => { + this.pushSnapshot(); + onChange(textarea.value); + }); } private addDropdownSetting( @@ -364,7 +439,10 @@ export class FormBuilderModal extends Modal { select.createEl('option', { value: opt, text: opt }); } select.value = value; - select.addEventListener('change', () => onChange(select.value)); + select.addEventListener('change', () => { + this.pushSnapshot(); + onChange(select.value); + }); } private addToggleSetting( @@ -378,6 +456,7 @@ export class FormBuilderModal extends Modal { const toggle = row.createDiv({ cls: 'checkbox-container' }); if (value) toggle.addClass('is-enabled'); toggle.addEventListener('click', () => { + this.pushSnapshot(); const enabled = !toggle.hasClass('is-enabled'); toggle.toggleClass('is-enabled', enabled); onChange(enabled);