Implement inertial scrolling (momentum)
This commit is contained in:
parent
10c9cedc8e
commit
b07c0808ab
@ -57,7 +57,7 @@ watch(
|
|||||||
() => [calendarStore.selectedDate, calendarStore.rangeStartDate],
|
() => [calendarStore.selectedDate, calendarStore.rangeStartDate],
|
||||||
() => {
|
() => {
|
||||||
if (calendarStore.selectedDate || calendarStore.rangeStartDate) {
|
if (calendarStore.selectedDate || calendarStore.rangeStartDate) {
|
||||||
scheduleRebuild('selection-change')
|
scheduleDataRebuild('selection-change')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ flush: 'sync' },
|
{ flush: 'sync' },
|
||||||
@ -115,28 +115,30 @@ const contentHeight = computed(() => {
|
|||||||
|
|
||||||
const visibleWeeks = ref([])
|
const visibleWeeks = ref([])
|
||||||
let lastScrollRange = { startVW: null, endVW: null }
|
let lastScrollRange = { startVW: null, endVW: null }
|
||||||
|
let windowTimer = null
|
||||||
let rebuildTimer = null
|
let dataTimer = null
|
||||||
let lastReason = null
|
const WINDOW_DEBOUNCE_MS = 30
|
||||||
const REBUILD_DEBOUNCE_MS = 40
|
const DATA_DEBOUNCE_MS = 40
|
||||||
|
function scheduleWindowUpdate(reason) {
|
||||||
function scheduleRebuild(reason) {
|
if (windowTimer) return
|
||||||
lastReason = lastReason ? lastReason + ',' + reason : reason
|
windowTimer = setTimeout(() => {
|
||||||
if (rebuildTimer) return
|
windowTimer = null
|
||||||
rebuildTimer = setTimeout(() => {
|
const fn = () => updateVisibleWeeks(reason)
|
||||||
const r = lastReason
|
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 80 })
|
||||||
rebuildTimer = null
|
else requestAnimationFrame(fn)
|
||||||
lastReason = null
|
}, WINDOW_DEBOUNCE_MS)
|
||||||
const fn = () => rebuildVisibleWeeks(r)
|
}
|
||||||
if ('requestIdleCallback' in window) {
|
function scheduleDataRebuild(reason) {
|
||||||
requestIdleCallback(fn, { timeout: 120 })
|
if (dataTimer) return
|
||||||
} else {
|
dataTimer = setTimeout(() => {
|
||||||
requestAnimationFrame(fn)
|
dataTimer = null
|
||||||
}
|
const fn = () => rebuildVisibleWeeks(reason)
|
||||||
}, REBUILD_DEBOUNCE_MS)
|
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
|
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) {
|
function rebuildVisibleWeeks(reason) {
|
||||||
const buffer = 10
|
const buffer = 10
|
||||||
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
||||||
@ -183,28 +223,16 @@ function rebuildVisibleWeeks(reason) {
|
|||||||
)
|
)
|
||||||
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
|
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
|
||||||
const endVW = Math.min(maxVirtualWeek.value, endIdx + 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 = []
|
const weeks = []
|
||||||
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
|
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
|
||||||
visibleWeeks.value = weeks
|
visibleWeeks.value = weeks
|
||||||
lastScrollRange = { startVW, endVW }
|
lastScrollRange = { startVW, endVW }
|
||||||
|
console.debug('[CalendarView] rebuildVisibleWeeks', {
|
||||||
|
reason,
|
||||||
|
startVW,
|
||||||
|
endVW,
|
||||||
|
count: weeks.length,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeRowHeight() {
|
function computeRowHeight() {
|
||||||
@ -235,6 +263,7 @@ function measureFromProbe() {
|
|||||||
rowHeight.value = newH
|
rowHeight.value = newH
|
||||||
const newScrollTop = (topVirtualWeek - minVirtualWeek.value) * newH
|
const newScrollTop = (topVirtualWeek - minVirtualWeek.value) * newH
|
||||||
setScrollTop(newScrollTop, 'row-height-change')
|
setScrollTop(newScrollTop, 'row-height-change')
|
||||||
|
scheduleDataRebuild('row-height-change')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,7 +591,7 @@ onMounted(() => {
|
|||||||
}, 60000)
|
}, 60000)
|
||||||
|
|
||||||
// Initial build after mount & measurement
|
// Initial build after mount & measurement
|
||||||
scheduleRebuild('init')
|
scheduleDataRebuild('init')
|
||||||
|
|
||||||
if (window.ResizeObserver && rowProbe.value) {
|
if (window.ResizeObserver && rowProbe.value) {
|
||||||
rowProbeObserver = new ResizeObserver(() => {
|
rowProbeObserver = new ResizeObserver(() => {
|
||||||
@ -646,7 +675,7 @@ watch(
|
|||||||
const newTopWeekIndex = getWeekIndex(currentTopDate)
|
const newTopWeekIndex = getWeekIndex(currentTopDate)
|
||||||
const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
|
const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
|
||||||
setScrollTop(newScroll, 'first-day-change')
|
setScrollTop(newScroll, 'first-day-change')
|
||||||
scheduleRebuild('first-day-change')
|
scheduleDataRebuild('first-day-change')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -655,7 +684,7 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => calendarStore.events,
|
() => calendarStore.events,
|
||||||
() => {
|
() => {
|
||||||
scheduleRebuild('events')
|
scheduleDataRebuild('events')
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
@ -664,7 +693,7 @@ watch(
|
|||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
if (viewport.value) viewportHeight.value = viewport.value.clientHeight
|
if (viewport.value) viewportHeight.value = viewport.value.clientHeight
|
||||||
measureFromProbe()
|
measureFromProbe()
|
||||||
scheduleRebuild('resize')
|
scheduleWindowUpdate('resize')
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,5 +1,185 @@
|
|||||||
import { ref } from 'vue'
|
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 }) {
|
export function createScrollManager({ viewport, scheduleRebuild }) {
|
||||||
const scrollTop = ref(0)
|
const scrollTop = ref(0)
|
||||||
let lastProgrammatic = null
|
let lastProgrammatic = null
|
||||||
@ -79,75 +259,47 @@ export function createWeekColumnScrollManager({
|
|||||||
setScrollTop,
|
setScrollTop,
|
||||||
}) {
|
}) {
|
||||||
const isWeekColDragging = ref(false)
|
const isWeekColDragging = ref(false)
|
||||||
let weekColDragStartScroll = 0
|
|
||||||
let weekColAccum = 0
|
|
||||||
let weekColPointerLocked = false
|
|
||||||
let weekColLastY = 0
|
|
||||||
|
|
||||||
function getWeekLabelRect() {
|
function getWeekLabelRect() {
|
||||||
const headerYear = document.querySelector('.calendar-header .year-label')
|
const headerYear = document.querySelector('.calendar-header .year-label')
|
||||||
if (headerYear) return headerYear.getBoundingClientRect()
|
if (headerYear) return headerYear.getBoundingClientRect()
|
||||||
const weekLabel = viewport.value?.querySelector('.week-row .week-label')
|
const weekLabel = viewport.value?.querySelector('.week-row .week-label')
|
||||||
return weekLabel ? weekLabel.getBoundingClientRect() : null
|
return weekLabel ? weekLabel.getBoundingClientRect() : null
|
||||||
}
|
}
|
||||||
|
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 false
|
||||||
|
const x = e.clientX ?? e.pageX
|
||||||
|
return x >= rect.left && x <= rect.right
|
||||||
|
},
|
||||||
|
})
|
||||||
function handleWeekColMouseDown(e) {
|
function handleWeekColMouseDown(e) {
|
||||||
if (e.button !== 0) return
|
|
||||||
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return
|
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey) return
|
||||||
if (!viewport.value) return
|
|
||||||
const rect = getWeekLabelRect()
|
|
||||||
if (!rect) return
|
|
||||||
if (e.clientX < rect.left || e.clientX > rect.right) return
|
|
||||||
isWeekColDragging.value = true
|
isWeekColDragging.value = true
|
||||||
weekColDragStartScroll = viewport.value.scrollTop
|
drag.handlePointerDown(e)
|
||||||
weekColAccum = 0
|
const end = () => {
|
||||||
weekColLastY = e.clientY
|
isWeekColDragging.value = false
|
||||||
if (viewport.value.requestPointerLock) viewport.value.requestPointerLock()
|
window.removeEventListener('pointerup', end, true)
|
||||||
window.addEventListener('mousemove', handleWeekColMouseMove, { passive: false })
|
window.removeEventListener('pointercancel', end, true)
|
||||||
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
|
window.addEventListener('pointerup', end, true)
|
||||||
let desired = weekColDragStartScroll - weekColAccum
|
window.addEventListener('pointercancel', end, true)
|
||||||
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
|
|
||||||
isWeekColDragging.value = false
|
|
||||||
window.removeEventListener('mousemove', handleWeekColMouseMove)
|
|
||||||
window.removeEventListener('mouseup', handleWeekColMouseUp)
|
|
||||||
if (weekColPointerLocked && document.exitPointerLock) document.exitPointerLock()
|
|
||||||
e.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handlePointerLockChange() {
|
function handlePointerLockChange() {
|
||||||
weekColPointerLocked = document.pointerLockElement === viewport.value
|
if (document.pointerLockElement !== viewport.value) {
|
||||||
if (!weekColPointerLocked && isWeekColDragging.value) {
|
isWeekColDragging.value = false
|
||||||
handleWeekColMouseUp(new MouseEvent('mouseup'))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange }
|
||||||
return {
|
|
||||||
isWeekColDragging,
|
|
||||||
handleWeekColMouseDown,
|
|
||||||
handlePointerLockChange,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createMonthScrollManager({
|
export function createMonthScrollManager({
|
||||||
@ -156,79 +308,32 @@ export function createMonthScrollManager({
|
|||||||
contentHeight,
|
contentHeight,
|
||||||
setScrollTop,
|
setScrollTop,
|
||||||
}) {
|
}) {
|
||||||
let dragging = false
|
const drag = createMomentumDrag({
|
||||||
let startY = 0
|
viewport,
|
||||||
let startScroll = 0
|
viewportHeight,
|
||||||
const SPEED = 10
|
contentHeight,
|
||||||
|
setScrollTop,
|
||||||
function applyDrag(clientY, reason) {
|
speed: 10,
|
||||||
const deltaY = clientY - startY
|
reasonDragPointer: 'month-scroll-drag',
|
||||||
const newScrollTop = startScroll - deltaY * SPEED
|
reasonDragTouch: 'month-scroll-touch',
|
||||||
const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
reasonMomentum: 'month-scroll-momentum',
|
||||||
const clamped = Math.max(0, Math.min(newScrollTop, maxScroll))
|
allowTouch: true,
|
||||||
setScrollTop(clamped, reason)
|
hitTest: null,
|
||||||
}
|
})
|
||||||
|
|
||||||
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) {
|
function handleMonthScrollPointerDown(e) {
|
||||||
if (e.button !== undefined && e.button !== 0) return
|
drag.handlePointerDown(e)
|
||||||
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) {
|
function handleMonthScrollTouchStart(e) {
|
||||||
if (e.touches.length !== 1) return
|
drag.handleTouchStart(e)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMonthScrollWheel(e) {
|
function handleMonthScrollWheel(e) {
|
||||||
|
drag.cancelMomentum()
|
||||||
const currentScroll = viewport.value?.scrollTop || 0
|
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 maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
||||||
const clamped = Math.max(0, Math.min(newScrollTop, maxScroll))
|
const clamped = Math.max(0, Math.min(newScrollTop, maxScroll))
|
||||||
setScrollTop(clamped, 'month-scroll-wheel')
|
setScrollTop(clamped, 'month-scroll-wheel')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
return { handleMonthScrollPointerDown, handleMonthScrollTouchStart, handleMonthScrollWheel }
|
return { handleMonthScrollPointerDown, handleMonthScrollTouchStart, handleMonthScrollWheel }
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user