Implement inertial scrolling (momentum)
This commit is contained in:
parent
10c9cedc8e
commit
b07c0808ab
@ -57,7 +57,7 @@ watch(
|
||||
() => [calendarStore.selectedDate, calendarStore.rangeStartDate],
|
||||
() => {
|
||||
if (calendarStore.selectedDate || calendarStore.rangeStartDate) {
|
||||
scheduleRebuild('selection-change')
|
||||
scheduleDataRebuild('selection-change')
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' },
|
||||
@ -115,28 +115,30 @@ const contentHeight = computed(() => {
|
||||
|
||||
const visibleWeeks = ref([])
|
||||
let lastScrollRange = { startVW: null, endVW: null }
|
||||
|
||||
let rebuildTimer = null
|
||||
let lastReason = null
|
||||
const REBUILD_DEBOUNCE_MS = 40
|
||||
|
||||
function scheduleRebuild(reason) {
|
||||
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)
|
||||
let windowTimer = null
|
||||
let dataTimer = null
|
||||
const WINDOW_DEBOUNCE_MS = 30
|
||||
const DATA_DEBOUNCE_MS = 40
|
||||
function scheduleWindowUpdate(reason) {
|
||||
if (windowTimer) return
|
||||
windowTimer = setTimeout(() => {
|
||||
windowTimer = null
|
||||
const fn = () => updateVisibleWeeks(reason)
|
||||
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 80 })
|
||||
else requestAnimationFrame(fn)
|
||||
}, WINDOW_DEBOUNCE_MS)
|
||||
}
|
||||
function scheduleDataRebuild(reason) {
|
||||
if (dataTimer) return
|
||||
dataTimer = setTimeout(() => {
|
||||
dataTimer = null
|
||||
const fn = () => rebuildVisibleWeeks(reason)
|
||||
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 120 })
|
||||
else requestAnimationFrame(fn)
|
||||
}, DATA_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const scrollManager = createScrollManager({ viewport, scheduleRebuild })
|
||||
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
|
||||
|
||||
const { scrollTop, setScrollTop, onScroll } = scrollManager
|
||||
|
||||
@ -174,6 +176,44 @@ const selectedDateRange = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
function updateVisibleWeeks(reason) {
|
||||
const buffer = 10
|
||||
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
||||
const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value)
|
||||
const endIdx = Math.ceil(
|
||||
(currentScrollTop + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value,
|
||||
)
|
||||
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
|
||||
const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value)
|
||||
if (
|
||||
lastScrollRange.startVW === startVW &&
|
||||
lastScrollRange.endVW === endVW &&
|
||||
visibleWeeks.value.length
|
||||
)
|
||||
return
|
||||
if (visibleWeeks.value.length) {
|
||||
while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW)
|
||||
visibleWeeks.value.shift()
|
||||
while (
|
||||
visibleWeeks.value.length &&
|
||||
visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek > endVW
|
||||
)
|
||||
visibleWeeks.value.pop()
|
||||
let needFirst = visibleWeeks.value[0]?.virtualWeek
|
||||
if (visibleWeeks.value.length === 0) needFirst = endVW + 1
|
||||
for (let vw = (needFirst ?? startVW) - 1; vw >= startVW; vw--)
|
||||
visibleWeeks.value.unshift(createWeek(vw))
|
||||
let needLast = visibleWeeks.value[visibleWeeks.value.length - 1]?.virtualWeek
|
||||
for (let vw = (needLast ?? startVW - 1) + 1; vw <= endVW; vw++)
|
||||
visibleWeeks.value.push(createWeek(vw))
|
||||
lastScrollRange = { startVW, endVW }
|
||||
return
|
||||
}
|
||||
const weeks = []
|
||||
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
|
||||
visibleWeeks.value = weeks
|
||||
lastScrollRange = { startVW, endVW }
|
||||
}
|
||||
function rebuildVisibleWeeks(reason) {
|
||||
const buffer = 10
|
||||
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
||||
@ -183,28 +223,16 @@ function rebuildVisibleWeeks(reason) {
|
||||
)
|
||||
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
|
||||
const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value)
|
||||
console.debug('[CalendarView] rebuildVisibleWeeks', {
|
||||
reason,
|
||||
currentScrollTop,
|
||||
startIdx,
|
||||
endIdx,
|
||||
startVW,
|
||||
endVW,
|
||||
rowHeight: rowHeight.value,
|
||||
})
|
||||
|
||||
if (
|
||||
reason === 'scroll' &&
|
||||
lastScrollRange.startVW === startVW &&
|
||||
lastScrollRange.endVW === endVW &&
|
||||
visibleWeeks.value.length
|
||||
) {
|
||||
return
|
||||
}
|
||||
const weeks = []
|
||||
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
|
||||
visibleWeeks.value = weeks
|
||||
lastScrollRange = { startVW, endVW }
|
||||
console.debug('[CalendarView] rebuildVisibleWeeks', {
|
||||
reason,
|
||||
startVW,
|
||||
endVW,
|
||||
count: weeks.length,
|
||||
})
|
||||
}
|
||||
|
||||
function computeRowHeight() {
|
||||
@ -235,6 +263,7 @@ function measureFromProbe() {
|
||||
rowHeight.value = newH
|
||||
const newScrollTop = (topVirtualWeek - minVirtualWeek.value) * newH
|
||||
setScrollTop(newScrollTop, 'row-height-change')
|
||||
scheduleDataRebuild('row-height-change')
|
||||
}
|
||||
}
|
||||
|
||||
@ -562,7 +591,7 @@ onMounted(() => {
|
||||
}, 60000)
|
||||
|
||||
// Initial build after mount & measurement
|
||||
scheduleRebuild('init')
|
||||
scheduleDataRebuild('init')
|
||||
|
||||
if (window.ResizeObserver && rowProbe.value) {
|
||||
rowProbeObserver = new ResizeObserver(() => {
|
||||
@ -646,7 +675,7 @@ watch(
|
||||
const newTopWeekIndex = getWeekIndex(currentTopDate)
|
||||
const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
|
||||
setScrollTop(newScroll, 'first-day-change')
|
||||
scheduleRebuild('first-day-change')
|
||||
scheduleDataRebuild('first-day-change')
|
||||
})
|
||||
},
|
||||
)
|
||||
@ -655,7 +684,7 @@ watch(
|
||||
watch(
|
||||
() => calendarStore.events,
|
||||
() => {
|
||||
scheduleRebuild('events')
|
||||
scheduleDataRebuild('events')
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
@ -664,7 +693,7 @@ watch(
|
||||
window.addEventListener('resize', () => {
|
||||
if (viewport.value) viewportHeight.value = viewport.value.clientHeight
|
||||
measureFromProbe()
|
||||
scheduleRebuild('resize')
|
||||
scheduleWindowUpdate('resize')
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -1,5 +1,185 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
function createMomentumDrag({
|
||||
viewport,
|
||||
viewportHeight,
|
||||
contentHeight,
|
||||
setScrollTop,
|
||||
speed,
|
||||
reasonDragPointer,
|
||||
reasonDragTouch,
|
||||
reasonMomentum,
|
||||
allowTouch,
|
||||
hitTest,
|
||||
}) {
|
||||
let dragging = false
|
||||
let startY = 0
|
||||
let startScroll = 0
|
||||
let velocity = 0
|
||||
let samples = [] // { timestamp, position }
|
||||
let momentumActive = false
|
||||
let momentumFrame = null
|
||||
let dragAccumY = 0 // used when pointer lock active
|
||||
let usingPointerLock = false
|
||||
const frictionPerMs = 0.0018
|
||||
const MIN_V = 0.03
|
||||
const VELOCITY_MS = 50
|
||||
|
||||
function cancelMomentum() {
|
||||
if (!momentumActive) return
|
||||
momentumActive = false
|
||||
if (momentumFrame) cancelAnimationFrame(momentumFrame)
|
||||
momentumFrame = null
|
||||
}
|
||||
function startMomentum() {
|
||||
if (Math.abs(velocity) < MIN_V) return
|
||||
cancelMomentum()
|
||||
momentumActive = true
|
||||
let lastTs = performance.now()
|
||||
const step = () => {
|
||||
if (!momentumActive) return
|
||||
const now = performance.now()
|
||||
const dt = now - lastTs
|
||||
lastTs = now
|
||||
if (dt <= 0) {
|
||||
momentumFrame = requestAnimationFrame(step)
|
||||
return
|
||||
}
|
||||
const decay = Math.exp(-frictionPerMs * dt)
|
||||
velocity *= decay
|
||||
const delta = velocity * dt
|
||||
if (viewport.value) {
|
||||
let cur = viewport.value.scrollTop
|
||||
let target = cur + delta
|
||||
const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
||||
if (target < 0) {
|
||||
target = 0
|
||||
velocity = 0
|
||||
} else if (target > maxScroll) {
|
||||
target = maxScroll
|
||||
velocity = 0
|
||||
}
|
||||
setScrollTop(target, reasonMomentum)
|
||||
}
|
||||
if (Math.abs(velocity) < MIN_V * 0.6) {
|
||||
momentumActive = false
|
||||
return
|
||||
}
|
||||
momentumFrame = requestAnimationFrame(step)
|
||||
}
|
||||
momentumFrame = requestAnimationFrame(step)
|
||||
}
|
||||
function applyDragByDelta(deltaY, reason) {
|
||||
const newScrollTop = startScroll - deltaY * speed
|
||||
const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
||||
const clamped = Math.max(0, Math.min(newScrollTop, maxScroll))
|
||||
setScrollTop(clamped, reason)
|
||||
}
|
||||
function applyDragPosition(clientY, reason) {
|
||||
const deltaY = clientY - startY
|
||||
applyDragByDelta(deltaY, reason)
|
||||
}
|
||||
function endDrag() {
|
||||
dragging = false
|
||||
window.removeEventListener('pointermove', onPointerMove, true)
|
||||
window.removeEventListener('pointerup', onPointerUp, true)
|
||||
window.removeEventListener('pointercancel', onPointerUp, true)
|
||||
if (allowTouch) {
|
||||
window.removeEventListener('touchmove', onTouchMove)
|
||||
window.removeEventListener('touchend', onTouchEnd)
|
||||
window.removeEventListener('touchcancel', onTouchEnd)
|
||||
}
|
||||
document.removeEventListener('pointerlockchange', onPointerLockChange, true)
|
||||
if (usingPointerLock && document.pointerLockElement === viewport.value) {
|
||||
try {
|
||||
document.exitPointerLock()
|
||||
} catch {}
|
||||
}
|
||||
usingPointerLock = false
|
||||
if (samples.length) {
|
||||
const first = samples[0]
|
||||
const now = performance.now()
|
||||
const last = samples[samples.length - 1]
|
||||
const dy = last.position - first.position
|
||||
velocity = (-dy * speed) / (now - first.timestamp)
|
||||
} else velocity = 0
|
||||
console.log(velocity, samples)
|
||||
samples = []
|
||||
startMomentum()
|
||||
}
|
||||
function onPointerMove(e) {
|
||||
if (!dragging) return
|
||||
if (document.pointerLockElement === viewport.value) {
|
||||
// Use movementY deltas under pointer lock
|
||||
const now = performance.now()
|
||||
dragAccumY += e.movementY
|
||||
while (samples[0]?.timestamp < now - VELOCITY_MS) samples.shift()
|
||||
samples.push({ timestamp: now, position: dragAccumY })
|
||||
applyDragByDelta(dragAccumY, reasonDragPointer)
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
function onPointerUp() {
|
||||
if (dragging) endDrag()
|
||||
}
|
||||
function onTouchMove(e) {
|
||||
if (!dragging) return
|
||||
const t = e.touches[0]
|
||||
const now = performance.now()
|
||||
while (samples[0]?.timestamp < now - VELOCITY_MS) samples.shift()
|
||||
samples.push({ timestamp: now, position: t.clientY })
|
||||
applyDragPosition(t.clientY, reasonDragTouch)
|
||||
e.preventDefault()
|
||||
}
|
||||
function onTouchEnd() {
|
||||
if (dragging) endDrag()
|
||||
}
|
||||
function handlePointerDown(e) {
|
||||
if (e.button !== undefined && e.button !== 0) return
|
||||
if (hitTest && !hitTest(e)) return
|
||||
e.preventDefault()
|
||||
cancelMomentum()
|
||||
dragging = true
|
||||
startY = e.clientY
|
||||
startScroll = viewport.value?.scrollTop || 0
|
||||
velocity = 0
|
||||
dragAccumY = 0
|
||||
samples = [{ timestamp: performance.now(), position: e.clientY }]
|
||||
window.addEventListener('pointermove', onPointerMove, true)
|
||||
window.addEventListener('pointerup', onPointerUp, true)
|
||||
window.addEventListener('pointercancel', onPointerUp, true)
|
||||
document.addEventListener('pointerlockchange', onPointerLockChange, true)
|
||||
viewport.value.requestPointerLock()
|
||||
}
|
||||
function handleTouchStart(e) {
|
||||
if (!allowTouch) return
|
||||
if (e.touches.length !== 1) return
|
||||
if (hitTest && !hitTest(e.touches[0])) return
|
||||
cancelMomentum()
|
||||
dragging = true
|
||||
const t = e.touches[0]
|
||||
startY = t.clientY
|
||||
startScroll = viewport.value?.scrollTop || 0
|
||||
velocity = 0
|
||||
dragAccumY = 0
|
||||
samples = [{ timestamp: performance.now(), position: t.clientY }]
|
||||
window.addEventListener('touchmove', onTouchMove, { passive: false })
|
||||
window.addEventListener('touchend', onTouchEnd, { passive: false })
|
||||
window.addEventListener('touchcancel', onTouchEnd, { passive: false })
|
||||
e.preventDefault()
|
||||
}
|
||||
function onPointerLockChange() {
|
||||
const lockedEl = document.pointerLockElement
|
||||
if (dragging && lockedEl === viewport.value) {
|
||||
usingPointerLock = true
|
||||
return
|
||||
}
|
||||
if (dragging && usingPointerLock && lockedEl !== viewport.value) endDrag()
|
||||
if (!dragging) usingPointerLock = false
|
||||
}
|
||||
return { handlePointerDown, handleTouchStart, cancelMomentum }
|
||||
}
|
||||
|
||||
export function createScrollManager({ viewport, scheduleRebuild }) {
|
||||
const scrollTop = ref(0)
|
||||
let lastProgrammatic = null
|
||||
@ -79,75 +259,47 @@ export function createWeekColumnScrollManager({
|
||||
setScrollTop,
|
||||
}) {
|
||||
const isWeekColDragging = ref(false)
|
||||
let weekColDragStartScroll = 0
|
||||
let weekColAccum = 0
|
||||
let weekColPointerLocked = false
|
||||
let weekColLastY = 0
|
||||
|
||||
function getWeekLabelRect() {
|
||||
const headerYear = document.querySelector('.calendar-header .year-label')
|
||||
if (headerYear) return headerYear.getBoundingClientRect()
|
||||
const weekLabel = viewport.value?.querySelector('.week-row .week-label')
|
||||
return weekLabel ? weekLabel.getBoundingClientRect() : null
|
||||
}
|
||||
|
||||
function handleWeekColMouseDown(e) {
|
||||
if (e.button !== 0) return
|
||||
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return
|
||||
if (!viewport.value) return
|
||||
const drag = createMomentumDrag({
|
||||
viewport,
|
||||
viewportHeight,
|
||||
contentHeight,
|
||||
setScrollTop,
|
||||
speed: 1,
|
||||
reasonDragPointer: 'week-col-drag',
|
||||
reasonDragTouch: 'week-col-drag',
|
||||
reasonMomentum: 'week-col-momentum',
|
||||
allowTouch: false,
|
||||
hitTest: (e) => {
|
||||
const rect = getWeekLabelRect()
|
||||
if (!rect) return
|
||||
if (e.clientX < rect.left || e.clientX > rect.right) return
|
||||
if (!rect) return false
|
||||
const x = e.clientX ?? e.pageX
|
||||
return x >= rect.left && x <= rect.right
|
||||
},
|
||||
})
|
||||
function handleWeekColMouseDown(e) {
|
||||
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return
|
||||
isWeekColDragging.value = true
|
||||
weekColDragStartScroll = viewport.value.scrollTop
|
||||
weekColAccum = 0
|
||||
weekColLastY = e.clientY
|
||||
if (viewport.value.requestPointerLock) viewport.value.requestPointerLock()
|
||||
window.addEventListener('mousemove', handleWeekColMouseMove, { passive: false })
|
||||
window.addEventListener('mouseup', handleWeekColMouseUp, { passive: false })
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
function handleWeekColMouseMove(e) {
|
||||
if (!isWeekColDragging.value || !viewport.value) return
|
||||
let dy
|
||||
if (weekColPointerLocked) {
|
||||
dy = e.movementY
|
||||
} else {
|
||||
dy = e.clientY - weekColLastY
|
||||
weekColLastY = e.clientY
|
||||
}
|
||||
weekColAccum += dy
|
||||
let desired = weekColDragStartScroll - weekColAccum
|
||||
if (desired < 0) desired = 0
|
||||
const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
||||
if (desired > maxScroll) desired = maxScroll
|
||||
setScrollTop(desired, 'week-col-drag')
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
function handleWeekColMouseUp(e) {
|
||||
if (!isWeekColDragging.value) return
|
||||
drag.handlePointerDown(e)
|
||||
const end = () => {
|
||||
isWeekColDragging.value = false
|
||||
window.removeEventListener('mousemove', handleWeekColMouseMove)
|
||||
window.removeEventListener('mouseup', handleWeekColMouseUp)
|
||||
if (weekColPointerLocked && document.exitPointerLock) document.exitPointerLock()
|
||||
e.preventDefault()
|
||||
window.removeEventListener('pointerup', end, true)
|
||||
window.removeEventListener('pointercancel', end, true)
|
||||
}
|
||||
window.addEventListener('pointerup', end, true)
|
||||
window.addEventListener('pointercancel', end, true)
|
||||
}
|
||||
|
||||
function handlePointerLockChange() {
|
||||
weekColPointerLocked = document.pointerLockElement === viewport.value
|
||||
if (!weekColPointerLocked && isWeekColDragging.value) {
|
||||
handleWeekColMouseUp(new MouseEvent('mouseup'))
|
||||
if (document.pointerLockElement !== viewport.value) {
|
||||
isWeekColDragging.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isWeekColDragging,
|
||||
handleWeekColMouseDown,
|
||||
handlePointerLockChange,
|
||||
}
|
||||
return { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange }
|
||||
}
|
||||
|
||||
export function createMonthScrollManager({
|
||||
@ -156,79 +308,32 @@ export function createMonthScrollManager({
|
||||
contentHeight,
|
||||
setScrollTop,
|
||||
}) {
|
||||
let dragging = false
|
||||
let startY = 0
|
||||
let startScroll = 0
|
||||
const SPEED = 10
|
||||
|
||||
function applyDrag(clientY, reason) {
|
||||
const deltaY = clientY - startY
|
||||
const newScrollTop = startScroll - deltaY * SPEED
|
||||
const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
||||
const clamped = Math.max(0, Math.min(newScrollTop, maxScroll))
|
||||
setScrollTop(clamped, reason)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
const drag = createMomentumDrag({
|
||||
viewport,
|
||||
viewportHeight,
|
||||
contentHeight,
|
||||
setScrollTop,
|
||||
speed: 10,
|
||||
reasonDragPointer: 'month-scroll-drag',
|
||||
reasonDragTouch: 'month-scroll-touch',
|
||||
reasonMomentum: 'month-scroll-momentum',
|
||||
allowTouch: true,
|
||||
hitTest: null,
|
||||
})
|
||||
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)
|
||||
drag.handlePointerDown(e)
|
||||
}
|
||||
|
||||
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()
|
||||
drag.handleTouchStart(e)
|
||||
}
|
||||
|
||||
function handleMonthScrollWheel(e) {
|
||||
drag.cancelMomentum()
|
||||
const currentScroll = viewport.value?.scrollTop || 0
|
||||
const newScrollTop = currentScroll + e.deltaY * SPEED
|
||||
const newScrollTop = currentScroll + e.deltaY * 10
|
||||
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 }
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user