Much simpler undo/redo handling, bugs fixed and less code.
This commit is contained in:
parent
45939939f2
commit
57aefc5b4c
@ -22,10 +22,18 @@ import { shallowRef } from 'vue'
|
|||||||
const eventDialogRef = shallowRef(null)
|
const eventDialogRef = shallowRef(null)
|
||||||
function openCreateEventDialog(eventData) {
|
function openCreateEventDialog(eventData) {
|
||||||
if (!eventDialogRef.value) return
|
if (!eventDialogRef.value) return
|
||||||
|
// Capture baseline before dialog opens (new event creation flow)
|
||||||
|
try {
|
||||||
|
calendarStore.$history?._baselineIfNeeded?.(true)
|
||||||
|
} catch {}
|
||||||
const selectionData = { startDate: eventData.startDate, dayCount: eventData.dayCount }
|
const selectionData = { startDate: eventData.startDate, dayCount: eventData.dayCount }
|
||||||
setTimeout(() => eventDialogRef.value?.openCreateDialog(selectionData), 30)
|
setTimeout(() => eventDialogRef.value?.openCreateDialog(selectionData), 30)
|
||||||
}
|
}
|
||||||
function openEditEventDialog(eventClickPayload) {
|
function openEditEventDialog(eventClickPayload) {
|
||||||
|
// Capture baseline before editing existing event
|
||||||
|
try {
|
||||||
|
calendarStore.$history?._baselineIfNeeded?.(true)
|
||||||
|
} catch {}
|
||||||
eventDialogRef.value?.openEditDialog(eventClickPayload)
|
eventDialogRef.value?.openEditDialog(eventClickPayload)
|
||||||
}
|
}
|
||||||
const viewport = ref(null)
|
const viewport = ref(null)
|
||||||
|
@ -22,6 +22,8 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['clear-selection'])
|
const emit = defineEmits(['clear-selection'])
|
||||||
|
|
||||||
const calendarStore = useCalendarStore()
|
const calendarStore = useCalendarStore()
|
||||||
|
// Track baseline signature when dialog opens to decide if we need an undo snapshot on close
|
||||||
|
let dialogBaselineSig = null
|
||||||
|
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
// Anchoring: element of the DayCell representing the event's start date.
|
// Anchoring: element of the DayCell representing the event's start date.
|
||||||
@ -208,7 +210,8 @@ function resolveAnchorFromDate(dateStr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreateDialog(selectionData = null) {
|
function openCreateDialog(selectionData = null) {
|
||||||
calendarStore.$history?.beginCompound()
|
// Pre-change snapshot (before creating stub event)
|
||||||
|
calendarStore.$history?.push?.()
|
||||||
if (unsavedCreateId.value && !eventSaved.value) {
|
if (unsavedCreateId.value && !eventSaved.value) {
|
||||||
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
||||||
calendarStore.deleteEvent(unsavedCreateId.value)
|
calendarStore.deleteEvent(unsavedCreateId.value)
|
||||||
@ -272,6 +275,7 @@ function openCreateDialog(selectionData = null) {
|
|||||||
// anchor to the starting day cell
|
// anchor to the starting day cell
|
||||||
anchorElement.value = resolveAnchorFromDate(start)
|
anchorElement.value = resolveAnchorFromDate(start)
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
|
// (Pre snapshot already taken before stub creation)
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (titleInput.value) {
|
if (titleInput.value) {
|
||||||
@ -284,7 +288,6 @@ function openCreateDialog(selectionData = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openEditDialog(payload) {
|
function openEditDialog(payload) {
|
||||||
calendarStore.$history?.beginCompound()
|
|
||||||
if (
|
if (
|
||||||
dialogMode.value === 'create' &&
|
dialogMode.value === 'create' &&
|
||||||
unsavedCreateId.value &&
|
unsavedCreateId.value &&
|
||||||
@ -348,6 +351,8 @@ function openEditDialog(payload) {
|
|||||||
// anchor to base event start date
|
// anchor to base event start date
|
||||||
anchorElement.value = resolveAnchorFromDate(event.startDate)
|
anchorElement.value = resolveAnchorFromDate(event.startDate)
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
|
// Pre-change snapshot (only once when dialog opens)
|
||||||
|
calendarStore.$history?.push?.()
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (titleInput.value) {
|
if (titleInput.value) {
|
||||||
@ -360,7 +365,6 @@ function openEditDialog(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
calendarStore.$history?.endCompound()
|
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -392,13 +396,11 @@ function saveEvent() {
|
|||||||
unsavedCreateId.value = null
|
unsavedCreateId.value = null
|
||||||
}
|
}
|
||||||
if (dialogMode.value === 'create') emit('clear-selection')
|
if (dialogMode.value === 'create') emit('clear-selection')
|
||||||
calendarStore.$history?.endCompound()
|
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEventAll() {
|
function deleteEventAll() {
|
||||||
if (editingEventId.value) calendarStore.deleteEvent(editingEventId.value)
|
if (editingEventId.value) calendarStore.deleteEvent(editingEventId.value)
|
||||||
calendarStore.$history?.endCompound()
|
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,14 +410,12 @@ function deleteEventOne() {
|
|||||||
} else if (isRepeatingBaseEdit.value && editingEventId.value) {
|
} else if (isRepeatingBaseEdit.value && editingEventId.value) {
|
||||||
calendarStore.deleteFirstOccurrence(editingEventId.value)
|
calendarStore.deleteFirstOccurrence(editingEventId.value)
|
||||||
}
|
}
|
||||||
calendarStore.$history?.endCompound()
|
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteEventFrom() {
|
function deleteEventFrom() {
|
||||||
if (!occurrenceContext.value) return
|
if (!occurrenceContext.value) return
|
||||||
calendarStore.deleteFromOccurrence(occurrenceContext.value)
|
calendarStore.deleteFromOccurrence(occurrenceContext.value)
|
||||||
calendarStore.$history?.endCompound()
|
|
||||||
closeDialog()
|
closeDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,8 +431,6 @@ watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
|
|||||||
})
|
})
|
||||||
watch(showDialog, (val, oldVal) => {
|
watch(showDialog, (val, oldVal) => {
|
||||||
if (oldVal && !val) {
|
if (oldVal && !val) {
|
||||||
// Closed (cancel, escape, outside click) -> end compound session
|
|
||||||
calendarStore.$history?.endCompound()
|
|
||||||
if (dialogMode.value === 'create' && unsavedCreateId.value && !eventSaved.value) {
|
if (dialogMode.value === 'create' && unsavedCreateId.value && !eventSaved.value) {
|
||||||
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
if (calendarStore.events?.has(unsavedCreateId.value)) {
|
||||||
calendarStore.deleteEvent(unsavedCreateId.value)
|
calendarStore.deleteEvent(unsavedCreateId.value)
|
||||||
|
@ -314,7 +314,46 @@ function startLocalDrag(init, evt) {
|
|||||||
realizedId: null,
|
realizedId: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
store.$history?.beginCompound()
|
// If history is empty (no baseline), create a baseline snapshot BEFORE any movement mutations
|
||||||
|
try {
|
||||||
|
const isResize = init.mode === 'resize-left' || init.mode === 'resize-right'
|
||||||
|
// Move: only baseline if history empty. Resize: force baseline (so undo returns to pre-resize) but only once.
|
||||||
|
store.$history?._baselineIfNeeded?.(isResize)
|
||||||
|
const evs = []
|
||||||
|
if (store.events instanceof Map) {
|
||||||
|
for (const [id, ev] of store.events) {
|
||||||
|
evs.push({
|
||||||
|
id,
|
||||||
|
start: ev.startDate,
|
||||||
|
days: ev.days,
|
||||||
|
title: ev.title,
|
||||||
|
color: ev.colorId,
|
||||||
|
recur: ev.recur
|
||||||
|
? {
|
||||||
|
f: ev.recur.freq,
|
||||||
|
i: ev.recur.interval,
|
||||||
|
c: ev.recur.count,
|
||||||
|
w: Array.isArray(ev.recur.weekdays) ? ev.recur.weekdays.join('') : null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.debug(
|
||||||
|
isResize ? '[history] pre-resize baseline snapshot' : '[history] pre-drag baseline snapshot',
|
||||||
|
{
|
||||||
|
mode: init.mode,
|
||||||
|
events: evs,
|
||||||
|
weekend: store.weekend,
|
||||||
|
firstDay: store.config?.first_day,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Enter drag suppression (prevent intermediate pushes)
|
||||||
|
try {
|
||||||
|
store.$history?._beginDrag?.()
|
||||||
|
} catch {}
|
||||||
|
|
||||||
if (evt.currentTarget && evt.pointerId !== undefined) {
|
if (evt.currentTarget && evt.pointerId !== undefined) {
|
||||||
try {
|
try {
|
||||||
@ -453,7 +492,10 @@ function onDragPointerUp(e) {
|
|||||||
justDragged.value = false
|
justDragged.value = false
|
||||||
}, 120)
|
}, 120)
|
||||||
}
|
}
|
||||||
store.$history?.endCompound()
|
// End drag suppression regardless; no post snapshot (pre-only model)
|
||||||
|
try {
|
||||||
|
store.$history?._endDrag?.()
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const min = (a, b) => (a < b ? a : b)
|
const min = (a, b) => (a < b ? a : b)
|
||||||
|
@ -95,6 +95,10 @@ function toggleVisibility() {
|
|||||||
// Settings dialog integration
|
// Settings dialog integration
|
||||||
const settingsDialog = ref(null)
|
const settingsDialog = ref(null)
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
|
// Capture baseline before opening settings
|
||||||
|
try {
|
||||||
|
calendarStore.$history?._baselineIfNeeded?.(true)
|
||||||
|
} catch {}
|
||||||
settingsDialog.value?.open()
|
settingsDialog.value?.open()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import './assets/calendar.css'
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
import { calendarHistory } from '@/plugins/calendarHistory'
|
import { history } from '@/plugins/history'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ const app = createApp(App)
|
|||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
// Order: persistence first so snapshots recorded by undo reflect already-hydrated state
|
// Order: persistence first so snapshots recorded by undo reflect already-hydrated state
|
||||||
pinia.use(piniaPluginPersistedstate)
|
pinia.use(piniaPluginPersistedstate)
|
||||||
pinia.use(calendarHistory)
|
pinia.use(history)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
@ -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() {},
|
||||||
|
}
|
||||||
|
}
|
@ -410,6 +410,8 @@ export const useCalendarStore = defineStore('calendar', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.notifyEventsChanged()
|
this.notifyEventsChanged()
|
||||||
|
// NOTE: Do NOT push a history snapshot here; wait until drag pointer up.
|
||||||
|
// Mid-drag snapshots create extra undo points. Final snapshot occurs in EventOverlay on pointerup.
|
||||||
return newId
|
return newId
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user