diff --git a/src/components/CalendarView.vue b/src/components/CalendarView.vue index 6bbe1c3..ca8031c 100644 --- a/src/components/CalendarView.vue +++ b/src/components/CalendarView.vue @@ -115,20 +115,25 @@ const contentHeight = computed(() => { const visibleWeeks = ref([]) let lastScrollRange = { startVW: null, endVW: null } -let pendingRebuild = false + +let rebuildTimer = null +let lastReason = null +const REBUILD_DEBOUNCE_MS = 40 function scheduleRebuild(reason) { - if (pendingRebuild) return - pendingRebuild = true - const cb = () => { - pendingRebuild = false - rebuildVisibleWeeks(reason) - } - if ('requestIdleCallback' in window) { - requestIdleCallback(cb, { timeout: 120 }) - } else { - requestAnimationFrame(cb) - } + lastReason = lastReason ? lastReason + ',' + reason : reason + if (rebuildTimer) return + rebuildTimer = setTimeout(() => { + const r = lastReason + rebuildTimer = null + lastReason = null + const fn = () => rebuildVisibleWeeks(r) + if ('requestIdleCallback' in window) { + requestIdleCallback(fn, { timeout: 120 }) + } else { + requestAnimationFrame(fn) + } + }, REBUILD_DEBOUNCE_MS) } const scrollManager = createScrollManager({ viewport, scheduleRebuild }) @@ -152,7 +157,7 @@ const monthScrollManager = createMonthScrollManager({ setScrollTop, }) -const { handleMonthScrollMouseDown, handleMonthScrollTouchStart, handleMonthScrollWheel } = +const { handleMonthScrollPointerDown, handleMonthScrollTouchStart, handleMonthScrollWheel } = monthScrollManager const initialScrollTop = computed(() => { @@ -178,7 +183,7 @@ function rebuildVisibleWeeks(reason) { ) const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value) const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value) - console.log('[CalendarView] rebuildVisibleWeeks', { + console.debug('[CalendarView] rebuildVisibleWeeks', { reason, currentScrollTop, startIdx, @@ -229,13 +234,6 @@ function measureFromProbe() { const topVirtualWeek = Math.floor(scrollTop.value / oldH) + minVirtualWeek.value rowHeight.value = newH const newScrollTop = (topVirtualWeek - minVirtualWeek.value) * newH - console.log('[CalendarView] measureFromProbe rowHeight change', { - oldH, - newH, - topVirtualWeek, - oldScrollTop: scrollTop.value, - newScrollTop, - }) setScrollTop(newScrollTop, 'row-height-change') } } @@ -712,8 +710,8 @@ window.addEventListener('resize', () => { height: `calc(var(--row-h) * ${monthWeek.monthLabel?.weeksSpan || 1})`, top: (monthWeek.top || 0) + 'px', }" - @mousedown="handleMonthScrollMouseDown" - @touchstart="handleMonthScrollTouchStart" + @pointerdown="handleMonthScrollPointerDown" + @touchstart.prevent="handleMonthScrollTouchStart" @wheel="handleMonthScrollWheel" > {{ @@ -805,6 +803,7 @@ header h1 { overflow: hidden; cursor: ns-resize; user-select: none; + touch-action: none; } .month-label > span { diff --git a/src/plugins/scrollManager.js b/src/plugins/scrollManager.js index 2b0f3c4..9b9db23 100644 --- a/src/plugins/scrollManager.js +++ b/src/plugins/scrollManager.js @@ -156,79 +156,79 @@ export function createMonthScrollManager({ contentHeight, setScrollTop, }) { - let isMonthScrolling = false - let monthScrollStartY = 0 - let monthScrollStartScroll = 0 + let dragging = false + let startY = 0 + let startScroll = 0 + const SPEED = 10 - const handleMonthScrollMouseDown = (e) => { - if (e.button !== 0) return - isMonthScrolling = true - monthScrollStartY = e.clientY - monthScrollStartScroll = viewport.value?.scrollTop || 0 - - const handleMouseMove = (e) => { - if (!isMonthScrolling) return - const deltaY = e.clientY - monthScrollStartY - const newScrollTop = monthScrollStartScroll - deltaY * 10 - const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value) - const clampedScroll = Math.max(0, Math.min(newScrollTop, maxScroll)) - - setScrollTop(clampedScroll, 'month-scroll-drag') - e.preventDefault() - } - - const handleMouseUp = () => { - isMonthScrolling = false - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } - - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - e.preventDefault() - } - - const handleMonthScrollTouchStart = (e) => { - if (e.touches.length !== 1) return - isMonthScrolling = true - monthScrollStartY = e.touches[0].clientY - monthScrollStartScroll = viewport.value?.scrollTop || 0 - - const handleTouchMove = (e) => { - if (!isMonthScrolling || e.touches.length !== 1) return - const deltaY = e.touches[0].clientY - monthScrollStartY - const newScrollTop = monthScrollStartScroll - deltaY * 10 - const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value) - const clampedScroll = Math.max(0, Math.min(newScrollTop, maxScroll)) - - setScrollTop(clampedScroll, 'month-scroll-touch') - e.preventDefault() - } - - const handleTouchEnd = () => { - isMonthScrolling = false - document.removeEventListener('touchmove', handleTouchMove) - document.removeEventListener('touchend', handleTouchEnd) - } - - document.addEventListener('touchmove', handleTouchMove, { passive: false }) - document.addEventListener('touchend', handleTouchEnd) - e.preventDefault() - } - - const handleMonthScrollWheel = (e) => { - const currentScroll = viewport.value?.scrollTop || 0 - const newScrollTop = currentScroll + e.deltaY * 10 + function applyDrag(clientY, reason) { + const deltaY = clientY - startY + const newScrollTop = startScroll - deltaY * SPEED const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value) - const clampedScroll = Math.max(0, Math.min(newScrollTop, maxScroll)) + const clamped = Math.max(0, Math.min(newScrollTop, maxScroll)) + setScrollTop(clamped, reason) + } - setScrollTop(clampedScroll, 'month-scroll-wheel') + function endDrag() { + dragging = false + window.removeEventListener('pointermove', onPointerMove, true) + window.removeEventListener('pointerup', onPointerUp, true) + window.removeEventListener('pointercancel', onPointerUp, true) + window.removeEventListener('touchmove', onTouchMove) + window.removeEventListener('touchend', onTouchEnd) + window.removeEventListener('touchcancel', onTouchEnd) + } + + function onPointerMove(e) { + if (!dragging) return + applyDrag(e.clientY, 'month-scroll-drag') + e.preventDefault() + } + function onPointerUp() { + if (dragging) endDrag() + } + + function onTouchMove(e) { + if (!dragging) return + const t = e.touches[0] + applyDrag(t.clientY, 'month-scroll-touch') + e.preventDefault() + } + function onTouchEnd() { + if (dragging) endDrag() + } + + function handleMonthScrollPointerDown(e) { + if (e.button !== undefined && e.button !== 0) return + e.preventDefault() + dragging = true + startY = e.clientY + startScroll = viewport.value?.scrollTop || 0 + window.addEventListener('pointermove', onPointerMove, true) + window.addEventListener('pointerup', onPointerUp, true) + window.addEventListener('pointercancel', onPointerUp, true) + } + + function handleMonthScrollTouchStart(e) { + if (e.touches.length !== 1) return + dragging = true + const t = e.touches[0] + startY = t.clientY + startScroll = viewport.value?.scrollTop || 0 + window.addEventListener('touchmove', onTouchMove, { passive: false }) + window.addEventListener('touchend', onTouchEnd, { passive: false }) + window.addEventListener('touchcancel', onTouchEnd, { passive: false }) e.preventDefault() } - return { - handleMonthScrollMouseDown, - handleMonthScrollTouchStart, - handleMonthScrollWheel, + function handleMonthScrollWheel(e) { + const currentScroll = viewport.value?.scrollTop || 0 + const newScrollTop = currentScroll + e.deltaY * SPEED + const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value) + const clamped = Math.max(0, Math.min(newScrollTop, maxScroll)) + setScrollTop(clamped, 'month-scroll-wheel') + e.preventDefault() } + + return { handleMonthScrollPointerDown, handleMonthScrollTouchStart, handleMonthScrollWheel } }