201 lines
5.8 KiB
JavaScript
201 lines
5.8 KiB
JavaScript
// 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 }
|
||
},
|
||
}
|
||
}
|