Much simpler undo/redo handling, bugs fixed and less code.
This commit is contained in:
@@ -1,198 +0,0 @@
|
||||
// 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 }]))
|
||||
}
|
||||
|
||||
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
|
||||
return `${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 }
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
// Pinia plugin to ensure calendar store keeps Map for events after undo/redo snapshots
|
||||
export function calendarUndoNormalize({ store }) {
|
||||
if (store.$id !== 'calendar') return
|
||||
|
||||
function fixEvents() {
|
||||
const evs = store.events
|
||||
if (evs instanceof Map) return
|
||||
// If serialized form { __map: true, data: [...] }
|
||||
if (evs && evs.__map && Array.isArray(evs.data)) {
|
||||
store.events = new Map(evs.data)
|
||||
return
|
||||
}
|
||||
// If an array of [k,v]
|
||||
if (Array.isArray(evs) && evs.every((x) => Array.isArray(x) && x.length === 2)) {
|
||||
store.events = new Map(evs)
|
||||
return
|
||||
}
|
||||
// If plain object, convert own enumerable props
|
||||
if (evs && typeof evs === 'object') {
|
||||
store.events = new Map(Object.entries(evs))
|
||||
}
|
||||
}
|
||||
|
||||
// Patch undo/redo if present (after pinia-undo is installed)
|
||||
const patchFns = ['undo', 'redo']
|
||||
for (const fn of patchFns) {
|
||||
if (typeof store[fn] === 'function') {
|
||||
const original = store[fn].bind(store)
|
||||
store[fn] = (...args) => {
|
||||
console.log(`[calendar history] ${fn} invoked`)
|
||||
const beforeType = store.events && store.events.constructor && store.events.constructor.name
|
||||
const out = original(...args)
|
||||
const afterRawType =
|
||||
store.events && store.events.constructor && store.events.constructor.name
|
||||
fixEvents()
|
||||
const finalType = store.events && store.events.constructor && store.events.constructor.name
|
||||
let size = null
|
||||
try {
|
||||
if (store.events instanceof Map) size = store.events.size
|
||||
else if (Array.isArray(store.events)) size = store.events.length
|
||||
} catch {}
|
||||
console.log(
|
||||
`[calendar history] ${fn} types: before=${beforeType} afterRaw=${afterRawType} final=${finalType} size=${size}`,
|
||||
)
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also watch all mutations (includes direct assigns and action commits)
|
||||
store.$subscribe(() => {
|
||||
fixEvents()
|
||||
})
|
||||
|
||||
// Initial sanity
|
||||
fixEvents()
|
||||
}
|
||||
110
src/plugins/history.js
Normal file
110
src/plugins/history.js
Normal file
@@ -0,0 +1,110 @@
|
||||
// Minimal undo/redo with explicit snapshot triggers.
|
||||
// API: store.$history = { push(), undo(), redo(), clear(), canUndo, canRedo, _baselineIfNeeded(), _beginDrag(), _endDrag() }
|
||||
|
||||
function cloneEvent(ev) {
|
||||
if (!ev || typeof ev !== 'object') return ev
|
||||
const c = { ...ev }
|
||||
if (c.recur && typeof c.recur === 'object') {
|
||||
c.recur = {
|
||||
...c.recur,
|
||||
weekdays: Array.isArray(c.recur.weekdays) ? [...c.recur.weekdays] : c.recur.weekdays,
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
function cloneState(store) {
|
||||
const events = new Map()
|
||||
for (const [k, ev] of store.events) events.set(k, cloneEvent(ev))
|
||||
return {
|
||||
today: store.today,
|
||||
weekend: [...store.weekend],
|
||||
config: JSON.parse(JSON.stringify(store.config)),
|
||||
events,
|
||||
}
|
||||
}
|
||||
function restoreState(store, snap) {
|
||||
store.today = snap.today
|
||||
store.weekend = [...snap.weekend]
|
||||
store.config = JSON.parse(JSON.stringify(snap.config))
|
||||
const events = new Map()
|
||||
for (const [k, ev] of snap.events) events.set(k, cloneEvent(ev))
|
||||
store.events = events
|
||||
}
|
||||
function same(a, b) {
|
||||
if (!a || !b) return false
|
||||
if (a.today !== b.today) return false
|
||||
if (JSON.stringify(a.config) !== JSON.stringify(b.config)) return false
|
||||
if (JSON.stringify(a.weekend) !== JSON.stringify(b.weekend)) return false
|
||||
if (a.events.size !== b.events.size) return false
|
||||
for (const [k, ev] of a.events) {
|
||||
const other = b.events.get(k)
|
||||
if (!other || JSON.stringify(ev) !== JSON.stringify(other)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function history({ store }) {
|
||||
if (store.$id !== 'calendar') return
|
||||
|
||||
const undoStack = []
|
||||
const redoStack = []
|
||||
const maxHistory = 50
|
||||
|
||||
function push() {
|
||||
const snap = cloneState(store)
|
||||
const last = undoStack[undoStack.length - 1]
|
||||
if (last && same(snap, last)) return
|
||||
undoStack.push(snap)
|
||||
redoStack.length = 0
|
||||
if (undoStack.length > maxHistory) undoStack.shift()
|
||||
updateIndicators()
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (!undoStack.length) return
|
||||
redoStack.push(cloneState(store))
|
||||
restoreState(store, undoStack.pop())
|
||||
updateIndicators()
|
||||
}
|
||||
|
||||
function redo() {
|
||||
if (!redoStack.length) return
|
||||
undoStack.push(cloneState(store))
|
||||
restoreState(store, redoStack.pop())
|
||||
updateIndicators()
|
||||
}
|
||||
|
||||
function clear() {
|
||||
undoStack.length = redoStack.length = 0
|
||||
updateIndicators()
|
||||
}
|
||||
|
||||
function updateIndicators() {
|
||||
store.historyCanUndo = undoStack.length > 0
|
||||
store.historyCanRedo = redoStack.length > 0
|
||||
store.historyTick = (store.historyTick + 1) % 1000000
|
||||
}
|
||||
|
||||
// No initial snapshot: caller decides when to baseline.
|
||||
|
||||
store.$history = {
|
||||
undo,
|
||||
redo,
|
||||
clear,
|
||||
push,
|
||||
get canUndo() {
|
||||
return undoStack.length > 0
|
||||
},
|
||||
get canRedo() {
|
||||
return redoStack.length > 0
|
||||
},
|
||||
// Drag lifecycle helpers used by EventOverlay.vue
|
||||
_baselineIfNeeded(force = false) {
|
||||
// Force: always push (resize, explicit dialogs). Non-force: rely on duplicate detection.
|
||||
if (force) return push()
|
||||
push()
|
||||
},
|
||||
_beginDrag() {},
|
||||
_endDrag() {},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user