// Custom lightweight undo/redo specifically for calendar store with Map support // Adds store.$history = { undo(), redo(), canUndo, canRedo, clear(), pushManual() } // Wraps action calls to create history entries only for meaningful mutations. function deepCloneCalendarState(raw) { // We only need to snapshot keys we care about; omit volatile fields const { today, events, config, weekend } = raw return { today, weekend: Array.isArray(weekend) ? [...weekend] : weekend, config: JSON.parse(JSON.stringify(config)), events: new Map([...events].map(([k, v]) => [k, { ...v }])), } } function restoreCalendarState(store, snap) { store.today = snap.today store.weekend = Array.isArray(snap.weekend) ? [...snap.weekend] : snap.weekend store.config = JSON.parse(JSON.stringify(snap.config)) store.events = new Map([...snap.events].map(([k, v]) => [k, { ...v }])) store.eventsMutation = (store.eventsMutation + 1) % 1_000_000_000 } export function calendarHistory({ store }) { if (store.$id !== 'calendar') return const max = 100 // history depth limit const history = [] // past states let pointer = -1 // index of current state in history let isRestoring = false let lastSerialized = null // Compound editing session (e.g. event dialog) flags let compoundActive = false let compoundBaseSig = null let compoundChanged = false function serializeForComparison() { const evCount = store.events instanceof Map ? store.events.size : 0 const em = store.eventsMutation || 0 return `${em}|${evCount}|${store.today}|${JSON.stringify(store.config)}` } function pushSnapshot() { if (isRestoring) return const sig = serializeForComparison() if (sig === lastSerialized) return // Drop any redo branch if (pointer < history.length - 1) history.splice(pointer + 1) history.push(deepCloneCalendarState(store)) if (history.length > max) history.shift() pointer = history.length - 1 lastSerialized = sig bumpIndicators() // console.log('[history] pushed', pointer, sig) } function bumpIndicators() { if (typeof store.historyTick === 'number') { store.historyTick = (store.historyTick + 1) % 1_000_000_000 } if (typeof store.historyCanUndo === 'boolean') { store.historyCanUndo = pointer > 0 } if (typeof store.historyCanRedo === 'boolean') { store.historyCanRedo = pointer >= 0 && pointer < history.length - 1 } } function markPotentialChange() { if (isRestoring) return if (compoundActive) { const sig = serializeForComparison() if (sig !== compoundBaseSig) compoundChanged = true return } pushSnapshot() } function beginCompound() { if (compoundActive) return compoundActive = true compoundBaseSig = serializeForComparison() compoundChanged = false } function endCompound() { if (!compoundActive) return const finalSig = serializeForComparison() const changed = compoundChanged || finalSig !== compoundBaseSig compoundActive = false compoundBaseSig = null if (changed) pushSnapshot() else bumpIndicators() // session ended without change – still refresh flags } function undo() { // Ensure any active compound changes are finalized before moving back if (compoundActive) endCompound() else { // If current state differs from last snapshot, push it so redo can restore it const curSig = serializeForComparison() if (curSig !== lastSerialized) pushSnapshot() } if (pointer <= 0) return pointer-- isRestoring = true try { restoreCalendarState(store, history[pointer]) lastSerialized = serializeForComparison() } finally { isRestoring = false } bumpIndicators() } function redo() { if (compoundActive) endCompound() else { const curSig = serializeForComparison() if (curSig !== lastSerialized) pushSnapshot() } if (pointer >= history.length - 1) return pointer++ isRestoring = true try { restoreCalendarState(store, history[pointer]) lastSerialized = serializeForComparison() } finally { isRestoring = false } bumpIndicators() } function clear() { history.length = 0 pointer = -1 lastSerialized = null bumpIndicators() } // Wrap selected mutating actions to push snapshot AFTER they run if state changed. const actionNames = [ 'createEvent', 'deleteEvent', 'deleteFirstOccurrence', 'deleteSingleOccurrence', 'deleteFromOccurrence', 'setEventRange', 'splitMoveVirtualOccurrence', 'splitRepeatSeries', '_terminateRepeatSeriesAtIndex', 'toggleHolidays', 'initializeHolidays', ] for (const name of actionNames) { if (typeof store[name] === 'function') { const original = store[name].bind(store) store[name] = (...args) => { const beforeSig = serializeForComparison() const result = original(...args) const afterSig = serializeForComparison() if (afterSig !== beforeSig) markPotentialChange() return result } } } // Capture direct property edits (e.g., deep field edits signaled via touchEvents()) store.$subscribe((mutation, _state) => { if (mutation.storeId !== 'calendar') return markPotentialChange() }) // Initial snapshot after hydration (next microtask to let persistence load) Promise.resolve().then(() => pushSnapshot()) store.$history = { undo, redo, clear, pushManual: pushSnapshot, beginCompound, endCompound, flush() { pushSnapshot() }, get canUndo() { return pointer > 0 }, get canRedo() { return pointer >= 0 && pointer < history.length - 1 }, get compoundActive() { return compoundActive }, _debug() { return { pointer, length: history.length } }, } }