calendar/src/plugins/calendarHistory.js
2025-08-24 21:59:56 -06:00

201 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 }
},
}
}