From d794758b54606762c79c4342bfec06547087d00a Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 22 Aug 2025 21:13:21 -0600 Subject: [PATCH] Event moving fixed again, splitting correctly for recurrent events. --- src/components/EventOverlay.vue | 28 ++++++++-- src/stores/CalendarStore.js | 92 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/src/components/EventOverlay.vue b/src/components/EventOverlay.vue index 3f0d918..a87ffb2 100644 --- a/src/components/EventOverlay.vue +++ b/src/components/EventOverlay.vue @@ -161,6 +161,25 @@ function startLocalDrag(init, evt) { window.addEventListener('pointercancel', onDragPointerUp, { passive: false }) } +// Determine date under pointer: traverse DOM to find day cell carrying data-date attribute +function getDateUnderPointer(x, y, el) { + let cur = el + while (cur) { + if (cur.dataset && cur.dataset.date) { + return { date: cur.dataset.date } + } + cur = cur.parentElement + } + // Fallback: elementFromPoint scan + const probe = document.elementFromPoint(x, y) + let p = probe + while (p) { + if (p.dataset && p.dataset.date) return { date: p.dataset.date } + p = p.parentElement + } + return null +} + function onDragPointerMove(e) { const st = dragState.value if (!st) return @@ -234,9 +253,12 @@ function normalizeDateOrder(aStr, bStr) { } function applyRangeDuringDrag(st, startDate, endDate) { - // If dragging a virtual occurrence, map to base move without changing recurrence pattern mid-series. - // We disallow resizing individual virtual occurrences; treat as move of whole series anchor. - if (st.isVirtual && st.mode !== 'move') return + if (st.isVirtual) { + if (st.mode !== 'move') return // no resize for virtual occurrence + // Split-move: occurrence being dragged treated as first of new series + store.splitMoveVirtualOccurrence(st.id, st.startDate, startDate, endDate) + return + } store.setEventRange(st.id, startDate, endDate, { mode: st.mode }) } diff --git a/src/stores/CalendarStore.js b/src/stores/CalendarStore.js index 6fe5b87..e0f3a84 100644 --- a/src/stores/CalendarStore.js +++ b/src/stores/CalendarStore.js @@ -586,6 +586,98 @@ export const useCalendarStore = defineStore('calendar', { // no expansion }, + // Split a repeating series at a specific occurrence date and move that occurrence (and future) to new range + splitMoveVirtualOccurrence(baseId, occurrenceDateStr, newStartStr, newEndStr) { + const base = this._findEventInAnyList(baseId) + if (!base || !base.isRepeating) return + const originalCountRaw = base.repeatCount + const spanDays = Math.max( + 0, + Math.round( + (fromLocalString(base.endDate) - fromLocalString(base.startDate)) / (24 * 60 * 60 * 1000), + ), + ) + const occurrenceDate = fromLocalString(occurrenceDateStr) + const baseStart = fromLocalString(base.startDate) + if (occurrenceDate <= baseStart) { + // Moving the base itself: just move entire series + this.setEventRange(baseId, newStartStr, newEndStr, { mode: 'move' }) + return + } + let keptOccurrences = 0 // number of occurrences BEFORE the moved one + if (base.repeat === 'weeks') { + const interval = base.repeatInterval || 1 + const pattern = base.repeatWeekdays || [] + if (!pattern.some(Boolean)) return + const WEEK_MS = 7 * 86400000 + const blockStartBase = new Date(baseStart) + blockStartBase.setDate(blockStartBase.getDate() - blockStartBase.getDay()) + function isAligned(d) { + const blk = new Date(d) + blk.setDate(d.getDate() - d.getDay()) + const diff = Math.floor((blk - blockStartBase) / WEEK_MS) + return diff % interval === 0 + } + const cursor = new Date(baseStart) + while (cursor < occurrenceDate) { + if (pattern[cursor.getDay()] && isAligned(cursor)) keptOccurrences++ + cursor.setDate(cursor.getDate() + 1) + } + } else if (base.repeat === 'months') { + const diffMonths = + (occurrenceDate.getFullYear() - baseStart.getFullYear()) * 12 + + (occurrenceDate.getMonth() - baseStart.getMonth()) + const interval = base.repeatInterval || 1 + if (diffMonths <= 0 || diffMonths % interval !== 0) return // invalid occurrence + keptOccurrences = diffMonths // base is occurrence 0; we keep all before diffMonths + } else { + // Unsupported repeat type + return + } + // Truncate original series to keptOccurrences + this._terminateRepeatSeriesAtIndex(baseId, keptOccurrences) + // Compute remaining occurrences count + let remainingCount = 'unlimited' + if (originalCountRaw !== 'unlimited') { + const total = parseInt(originalCountRaw, 10) + if (!isNaN(total)) { + const rem = total - keptOccurrences + if (rem <= 0) return + remainingCount = String(rem) + } + } + // Determine repeat-specific adjustments + let repeatWeekdays = base.repeatWeekdays + if (base.repeat === 'weeks' && Array.isArray(base.repeatWeekdays)) { + // Rotate pattern so that the moved occurrence weekday stays active relative to new anchor + const origWeekday = occurrenceDate.getDay() + const newWeekday = fromLocalString(newStartStr).getDay() + const shift = newWeekday - origWeekday + if (shift !== 0) { + const rotated = [false, false, false, false, false, false, false] + for (let i = 0; i < 7; i++) { + if (base.repeatWeekdays[i]) { + let ni = (i + shift) % 7 + if (ni < 0) ni += 7 + rotated[ni] = true + } + } + repeatWeekdays = rotated + } + } + // Create continuation series starting at newStartStr + this.createEvent({ + title: base.title, + startDate: newStartStr, + endDate: newEndStr, + colorId: base.colorId, + repeat: base.repeat, + repeatInterval: base.repeatInterval, + repeatCount: remainingCount, + repeatWeekdays, + }) + }, + // Split a repeating series at a given occurrence index; returns new series id splitRepeatSeries(baseId, occurrenceIndex, newStartStr, newEndStr, _grabbedWeekday) { const base = this._findEventInAnyList(baseId)