import { computed, ref } from 'vue' import type { Ref } from 'vue' import type { Field } from '@/types/field' import type { FormPage } from '@/types/form-page' import type { FormStyleSettings } from '@/types/form-style' import type { PageNavigationSettings, ProgressIndicatorSettings } from '@/stores/useMultiPageStore' const DEFAULT_MAX_DEPTH = 40 const FIELD_EDIT_BURST_MS = 400 const SUBMIT_BUTTON_EDIT_BURST_MS = 400 const STYLE_SETTINGS_EDIT_BURST_MS = 400 const PROGRESS_INDICATOR_EDIT_BURST_MS = 400 const PAGE_NAVIGATION_EDIT_BURST_MS = 400 const PAGE_LABEL_EDIT_BURST_MS = 400 export type { ProgressIndicatorSettings } export interface FormBuilderHistorySnapshot { fields: Field[] counterFields: number submitButtonSettings: { label: string position: 'default' | 'left' | 'center' | 'right' } selectedFieldIndex: number | null isSubmitButtonSelected: boolean activeTab: string styleSettings: FormStyleSettings pages: FormPage[] currentPageId: string progressIndicator: ProgressIndicatorSettings pageNavigationSettings: Record } export interface CreateFormBuilderHistoryOptions { maxDepth?: number fields: Ref counterFields: Ref submitButtonSettings: Ref<{ label: string position: 'default' | 'left' | 'center' | 'right' }> selectedField: Ref isSubmitButtonSelected: Ref activeTab: Ref styleSettings: Ref pages: Ref currentPageId: Ref progressIndicator: Ref pageNavigationSettings: Ref> markDirty: () => void } // JSON clone avoids DataCloneError from Vue/Pinia proxies (structuredClone fails on reactive trees). function cloneSerializableJson(value: T): T { return JSON.parse(JSON.stringify(value)) as T } function cloneFields(fields: Field[]): Field[] { return cloneSerializableJson(fields) } /** * Session undo/redo for form builder canvas state (fields, styles, multi-page, submit button). * Uses snapshot stacks; coalesces rapid field/style option edits into one undo step per burst. */ export function createFormBuilderHistory(options: CreateFormBuilderHistoryOptions) { const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH const past = ref([]) const future = ref([]) const isApplyingHistory = ref(false) const suppressRecordingDepth = ref(0) let fieldEditBurstTimer: ReturnType | null = null let fieldEditBurstActive = false let submitButtonEditBurstTimer: ReturnType | null = null let submitButtonEditBurstActive = false let styleSettingsEditBurstTimer: ReturnType | null = null let styleSettingsEditBurstActive = false let progressIndicatorEditBurstTimer: ReturnType | null = null let progressIndicatorEditBurstActive = false let pageNavigationEditBurstTimer: ReturnType | null = null let pageNavigationEditBurstActive = false let pageLabelEditBurstTimer: ReturnType | null = null let pageLabelEditBurstActive = false const canRecord = (): boolean => { return !isApplyingHistory.value && suppressRecordingDepth.value === 0 } const captureSnapshot = (): FormBuilderHistorySnapshot => { return { fields: cloneFields(options.fields.value), counterFields: options.counterFields.value, submitButtonSettings: cloneSerializableJson(options.submitButtonSettings.value), selectedFieldIndex: options.selectedField.value?.fieldIndex ?? null, isSubmitButtonSelected: options.isSubmitButtonSelected.value, activeTab: options.activeTab.value, styleSettings: cloneSerializableJson(options.styleSettings.value), pages: cloneSerializableJson(options.pages.value), currentPageId: options.currentPageId.value, progressIndicator: cloneSerializableJson(options.progressIndicator.value), pageNavigationSettings: cloneSerializableJson(options.pageNavigationSettings.value), } } const pushSnapshot = (): void => { if (!canRecord()) { return } const snapshot = captureSnapshot() past.value.push(snapshot) if (past.value.length > maxDepth) { past.value.shift() } future.value = [] } const applySnapshot = (snapshot: FormBuilderHistorySnapshot): void => { isApplyingHistory.value = true options.fields.value = cloneFields(snapshot.fields) options.counterFields.value = snapshot.counterFields options.submitButtonSettings.value = { ...snapshot.submitButtonSettings } const selectedIndex = snapshot.selectedFieldIndex options.selectedField.value = selectedIndex === null ? null : (options.fields.value.find((field) => field.fieldIndex === selectedIndex) ?? null) options.isSubmitButtonSelected.value = snapshot.isSubmitButtonSelected options.activeTab.value = snapshot.activeTab options.styleSettings.value = cloneSerializableJson(snapshot.styleSettings) options.pages.value = cloneSerializableJson(snapshot.pages) const restoredPageId = snapshot.currentPageId options.currentPageId.value = snapshot.pages.some((page) => page.id === restoredPageId) ? restoredPageId : (snapshot.pages[0]?.id ?? restoredPageId) options.progressIndicator.value = cloneSerializableJson(snapshot.progressIndicator) options.pageNavigationSettings.value = cloneSerializableJson(snapshot.pageNavigationSettings) options.markDirty() isApplyingHistory.value = false } const undo = (): void => { if (past.value.length === 0 || !canRecord()) { return } const current = captureSnapshot() const previous = past.value.pop() if (!previous) { return } future.value.push(current) applySnapshot(previous) } const redo = (): void => { if (future.value.length === 0 || !canRecord()) { return } const current = captureSnapshot() const next = future.value.pop() if (!next) { return } past.value.push(current) applySnapshot(next) } const clearHistory = (): void => { past.value = [] future.value = [] fieldEditBurstActive = false submitButtonEditBurstActive = false styleSettingsEditBurstActive = false progressIndicatorEditBurstActive = false pageNavigationEditBurstActive = false pageLabelEditBurstActive = false if (fieldEditBurstTimer !== null) { clearTimeout(fieldEditBurstTimer) fieldEditBurstTimer = null } if (submitButtonEditBurstTimer !== null) { clearTimeout(submitButtonEditBurstTimer) submitButtonEditBurstTimer = null } if (styleSettingsEditBurstTimer !== null) { clearTimeout(styleSettingsEditBurstTimer) styleSettingsEditBurstTimer = null } if (progressIndicatorEditBurstTimer !== null) { clearTimeout(progressIndicatorEditBurstTimer) progressIndicatorEditBurstTimer = null } if (pageNavigationEditBurstTimer !== null) { clearTimeout(pageNavigationEditBurstTimer) pageNavigationEditBurstTimer = null } if (pageLabelEditBurstTimer !== null) { clearTimeout(pageLabelEditBurstTimer) pageLabelEditBurstTimer = null } } const captureBeforeFieldOptionsEditBurst = (): void => { if (isApplyingHistory.value || suppressRecordingDepth.value > 0) { return } if (!fieldEditBurstActive) { pushSnapshot() fieldEditBurstActive = true } if (fieldEditBurstTimer !== null) { clearTimeout(fieldEditBurstTimer) } fieldEditBurstTimer = setTimeout(() => { fieldEditBurstActive = false fieldEditBurstTimer = null }, FIELD_EDIT_BURST_MS) } const captureBeforeSubmitButtonEditBurst = (): void => { if (isApplyingHistory.value || suppressRecordingDepth.value > 0) { return } if (!submitButtonEditBurstActive) { pushSnapshot() submitButtonEditBurstActive = true } if (submitButtonEditBurstTimer !== null) { clearTimeout(submitButtonEditBurstTimer) } submitButtonEditBurstTimer = setTimeout(() => { submitButtonEditBurstActive = false submitButtonEditBurstTimer = null }, SUBMIT_BUTTON_EDIT_BURST_MS) } const captureBeforeStyleSettingsEditBurst = (): void => { if (isApplyingHistory.value || suppressRecordingDepth.value > 0) { return } if (!styleSettingsEditBurstActive) { pushSnapshot() styleSettingsEditBurstActive = true } if (styleSettingsEditBurstTimer !== null) { clearTimeout(styleSettingsEditBurstTimer) } styleSettingsEditBurstTimer = setTimeout(() => { styleSettingsEditBurstActive = false styleSettingsEditBurstTimer = null }, STYLE_SETTINGS_EDIT_BURST_MS) } const captureBeforeProgressIndicatorEditBurst = (): void => { if (isApplyingHistory.value || suppressRecordingDepth.value > 0) { return } if (!progressIndicatorEditBurstActive) { pushSnapshot() progressIndicatorEditBurstActive = true } if (progressIndicatorEditBurstTimer !== null) { clearTimeout(progressIndicatorEditBurstTimer) } progressIndicatorEditBurstTimer = setTimeout(() => { progressIndicatorEditBurstActive = false progressIndicatorEditBurstTimer = null }, PROGRESS_INDICATOR_EDIT_BURST_MS) } const captureBeforePageNavigationSettingsEditBurst = (): void => { if (isApplyingHistory.value || suppressRecordingDepth.value > 0) { return } if (!pageNavigationEditBurstActive) { pushSnapshot() pageNavigationEditBurstActive = true } if (pageNavigationEditBurstTimer !== null) { clearTimeout(pageNavigationEditBurstTimer) } pageNavigationEditBurstTimer = setTimeout(() => { pageNavigationEditBurstActive = false pageNavigationEditBurstTimer = null }, PAGE_NAVIGATION_EDIT_BURST_MS) } const captureBeforePageLabelEditBurst = (): void => { if (isApplyingHistory.value || suppressRecordingDepth.value > 0) { return } if (!pageLabelEditBurstActive) { pushSnapshot() pageLabelEditBurstActive = true } if (pageLabelEditBurstTimer !== null) { clearTimeout(pageLabelEditBurstTimer) } pageLabelEditBurstTimer = setTimeout(() => { pageLabelEditBurstActive = false pageLabelEditBurstTimer = null }, PAGE_LABEL_EDIT_BURST_MS) } const runWithoutRecording = (callback: () => void): void => { suppressRecordingDepth.value++ try { callback() } finally { suppressRecordingDepth.value-- } } const canUndo = computed(() => past.value.length > 0) const canRedo = computed(() => future.value.length > 0) return { pushSnapshot, undo, redo, clearHistory, captureBeforeFieldOptionsEditBurst, captureBeforeSubmitButtonEditBurst, captureBeforeStyleSettingsEditBurst, captureBeforeProgressIndicatorEditBurst, captureBeforePageNavigationSettingsEditBurst, captureBeforePageLabelEditBurst, runWithoutRecording, canUndo, canRedo, isApplyingHistory, } } export type FormBuilderHistoryController = ReturnType