feat: add snapshot-based undo/redo to form builder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1ed9845af3
commit
03c471a60b
1 changed files with 84 additions and 5 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue