Implement mouse-drag scrolling on both sides (alike touch drag).

This commit is contained in:
Leo Vasanko 2025-08-24 22:51:53 -06:00
parent 4e933e4128
commit 9b3b6f62a3
2 changed files with 108 additions and 1 deletions

View File

@ -124,6 +124,10 @@ const todayString = computed(() => {
const visibleWeeks = ref([])
let lastScrollRange = { startVW: null, endVW: null }
let pendingRebuild = false
// Week label column drag scrolling state (no momentum)
const isWeekColDragging = ref(false)
let weekColDragStartY = 0
let weekColDragStartScroll = 0
function scheduleRebuild(reason) {
if (pendingRebuild) return
@ -498,6 +502,49 @@ function calculateSelection(anchorStr, otherStr) {
}
}
// ---------------- Week label column drag scrolling ----------------
function getWeekLabelRect() {
// Prefer header year label width as stable reference
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 rect = getWeekLabelRect()
if (!rect) return
if (e.clientX < rect.left || e.clientX > rect.right) return
isWeekColDragging.value = true
weekColDragStartY = e.clientY
weekColDragStartScroll = viewport.value.scrollTop
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
const dy = e.clientY - weekColDragStartY
// Natural: drag down moves view to earlier content (scroll up)
viewport.value.scrollTop = Math.max(0, weekColDragStartScroll - dy)
e.preventDefault()
}
// (momentum removed per requirements)
function handleWeekColMouseUp(e) {
if (!isWeekColDragging.value) return
isWeekColDragging.value = false
window.removeEventListener('mousemove', handleWeekColMouseMove)
window.removeEventListener('mouseup', handleWeekColMouseUp)
e.preventDefault()
}
const onScroll = () => {
if (viewport.value) scrollTop.value = viewport.value.scrollTop
scheduleRebuild('scroll')
@ -517,6 +564,8 @@ onMounted(() => {
viewportHeight.value = viewport.value.clientHeight
viewport.value.scrollTop = initialScrollTop.value
viewport.value.addEventListener('scroll', onScroll)
// Capture mousedown in viewport to allow dragging via week label column
viewport.value.addEventListener('mousedown', handleWeekColMouseDown, true)
}
const timer = setInterval(() => {
@ -541,6 +590,7 @@ onMounted(() => {
onBeforeUnmount(() => {
if (viewport.value) {
viewport.value.removeEventListener('scroll', onScroll)
viewport.value.removeEventListener('mousedown', handleWeekColMouseDown, true)
}
if (rowProbeObserver && rowProbe.value) {
try {

View File

@ -5,7 +5,7 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
totalVirtualWeeks: { type: Number, required: true },
@ -19,6 +19,11 @@ const emit = defineEmits(['scroll-to'])
const jogwheelViewport = ref(null)
const jogwheelContent = ref(null)
const syncLock = ref(null)
// Drag state (no momentum, 1:1 mapping)
const isDragging = ref(false)
let dragStartY = 0
let mainStartScroll = 0
let dragScale = 1 // mainScrollPixels per mouse pixel
// Jogwheel content height is 1/10th of main calendar
const jogwheelHeight = computed(() => {
@ -30,6 +35,58 @@ const handleJogwheelScroll = () => {
syncFromJogwheel()
}
function onDragMouseDown(e) {
if (e.button !== 0) return
isDragging.value = true
dragStartY = e.clientY
mainStartScroll = props.scrollTop
// Precompute scale between jogwheel scrollable range and main scrollable range
const mainScrollable = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight)
let jogScrollable = 0
if (jogwheelViewport.value && jogwheelContent.value) {
jogScrollable = Math.max(0, jogwheelContent.value.scrollHeight - jogwheelViewport.value.clientHeight)
}
dragScale = jogScrollable > 0 ? mainScrollable / jogScrollable : 1
if (!isFinite(dragScale) || dragScale <= 0) dragScale = 1
window.addEventListener('mousemove', onDragMouseMove, { passive: false })
window.addEventListener('mouseup', onDragMouseUp, { passive: false })
e.preventDefault()
}
function onDragMouseMove(e) {
if (!isDragging.value) return
const dy = e.clientY - dragStartY
// Natural content drag (drag down => scrollTop decreases)
let desired = mainStartScroll - dy * dragScale
if (desired < 0) desired = 0
const maxScroll = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight)
if (desired > maxScroll) desired = maxScroll
emit('scroll-to', desired)
e.preventDefault()
}
function onDragMouseUp(e) {
if (!isDragging.value) return
isDragging.value = false
window.removeEventListener('mousemove', onDragMouseMove)
window.removeEventListener('mouseup', onDragMouseUp)
e.preventDefault()
}
onMounted(() => {
if (jogwheelViewport.value) {
jogwheelViewport.value.addEventListener('mousedown', onDragMouseDown)
}
})
onBeforeUnmount(() => {
if (jogwheelViewport.value) {
jogwheelViewport.value.removeEventListener('mousedown', onDragMouseDown)
}
window.removeEventListener('mousemove', onDragMouseMove)
window.removeEventListener('mouseup', onDragMouseUp)
})
const syncFromJogwheel = () => {
if (!jogwheelViewport.value || !jogwheelContent.value) return