Fixed a nasty landscape/portrait rotation bug (clamping while updating caused wrong year).
This commit is contained in:
parent
4367480f1c
commit
7c94fcbb45
@ -4,7 +4,11 @@ import { useCalendarStore } from '@/stores/CalendarStore'
|
|||||||
import CalendarHeader from '@/components/CalendarHeader.vue'
|
import CalendarHeader from '@/components/CalendarHeader.vue'
|
||||||
import CalendarWeek from '@/components/CalendarWeek.vue'
|
import CalendarWeek from '@/components/CalendarWeek.vue'
|
||||||
import HeaderControls from '@/components/HeaderControls.vue'
|
import HeaderControls from '@/components/HeaderControls.vue'
|
||||||
import Jogwheel from '@/components/Jogwheel.vue'
|
import {
|
||||||
|
createScrollManager,
|
||||||
|
createWeekColumnScrollManager,
|
||||||
|
createMonthScrollManager,
|
||||||
|
} from '@/plugins/scrollManager'
|
||||||
import {
|
import {
|
||||||
getLocalizedMonthName,
|
getLocalizedMonthName,
|
||||||
monthAbbr,
|
monthAbbr,
|
||||||
@ -38,10 +42,8 @@ function createEventFromSelection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollTop = ref(0)
|
|
||||||
const viewportHeight = ref(600)
|
const viewportHeight = ref(600)
|
||||||
const rowHeight = ref(64)
|
const rowHeight = ref(64)
|
||||||
// Probe element & observer for dynamic var(--row-h) changes
|
|
||||||
const rowProbe = ref(null)
|
const rowProbe = ref(null)
|
||||||
let rowProbeObserver = null
|
let rowProbeObserver = null
|
||||||
|
|
||||||
@ -51,17 +53,14 @@ const selection = ref({ startDate: null, dayCount: 0 })
|
|||||||
const isDragging = ref(false)
|
const isDragging = ref(false)
|
||||||
const dragAnchor = ref(null)
|
const dragAnchor = ref(null)
|
||||||
|
|
||||||
// Rebuild visible weeks whenever selection changes so day.isSelected stays in sync.
|
|
||||||
watch(
|
watch(
|
||||||
() => [selection.value.startDate, selection.value.dayCount],
|
() => [calendarStore.selectedDate, calendarStore.rangeStartDate],
|
||||||
() => {
|
() => {
|
||||||
// Skip if no selection (both null/0) to avoid unnecessary work.
|
if (calendarStore.selectedDate || calendarStore.rangeStartDate) {
|
||||||
if (!selection.value.startDate || selection.value.dayCount === 0) {
|
scheduleRebuild('selection-change')
|
||||||
scheduleRebuild('selection-clear')
|
|
||||||
} else {
|
|
||||||
scheduleRebuild('selection')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ flush: 'sync' },
|
||||||
)
|
)
|
||||||
|
|
||||||
const DOUBLE_TAP_DELAY = 300
|
const DOUBLE_TAP_DELAY = 300
|
||||||
@ -110,6 +109,52 @@ const totalVirtualWeeks = computed(() => {
|
|||||||
return maxVirtualWeek.value - minVirtualWeek.value + 1
|
return maxVirtualWeek.value - minVirtualWeek.value + 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const contentHeight = computed(() => {
|
||||||
|
return totalVirtualWeeks.value * rowHeight.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleWeeks = ref([])
|
||||||
|
let lastScrollRange = { startVW: null, endVW: null }
|
||||||
|
let pendingRebuild = false
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollManager = createScrollManager({ viewport, scheduleRebuild })
|
||||||
|
|
||||||
|
const { scrollTop, setScrollTop, onScroll } = scrollManager
|
||||||
|
|
||||||
|
const weekColumnScrollManager = createWeekColumnScrollManager({
|
||||||
|
viewport,
|
||||||
|
viewportHeight,
|
||||||
|
contentHeight,
|
||||||
|
setScrollTop,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { isWeekColDragging, handleWeekColMouseDown, handlePointerLockChange } =
|
||||||
|
weekColumnScrollManager
|
||||||
|
|
||||||
|
const monthScrollManager = createMonthScrollManager({
|
||||||
|
viewport,
|
||||||
|
viewportHeight,
|
||||||
|
contentHeight,
|
||||||
|
setScrollTop,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { handleMonthScrollMouseDown, handleMonthScrollTouchStart, handleMonthScrollWheel } =
|
||||||
|
monthScrollManager
|
||||||
|
|
||||||
const initialScrollTop = computed(() => {
|
const initialScrollTop = computed(() => {
|
||||||
const nowDate = new Date(calendarStore.now)
|
const nowDate = new Date(calendarStore.now)
|
||||||
const targetWeekIndex = getWeekIndex(nowDate) - 3
|
const targetWeekIndex = getWeekIndex(nowDate) - 3
|
||||||
@ -124,42 +169,25 @@ const selectedDateRange = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// PERFORMANCE: Maintain a manual cache of computed weeks instead of relying on
|
|
||||||
// deep reactive tracking of every event & day object. We rebuild lazily when
|
|
||||||
// (a) scrolling changes the needed range or (b) eventsMutation counter bumps.
|
|
||||||
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 weekColDragStartScroll = 0
|
|
||||||
let weekColAccum = 0
|
|
||||||
let weekColPointerLocked = false
|
|
||||||
let weekColLastY = 0
|
|
||||||
|
|
||||||
function scheduleRebuild(reason) {
|
|
||||||
if (pendingRebuild) return
|
|
||||||
pendingRebuild = true
|
|
||||||
// Use requestIdleCallback when available, else fallback to rAF
|
|
||||||
const cb = () => {
|
|
||||||
pendingRebuild = false
|
|
||||||
rebuildVisibleWeeks(reason)
|
|
||||||
}
|
|
||||||
if ('requestIdleCallback' in window) {
|
|
||||||
requestIdleCallback(cb, { timeout: 120 })
|
|
||||||
} else {
|
|
||||||
requestAnimationFrame(cb)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function rebuildVisibleWeeks(reason) {
|
function rebuildVisibleWeeks(reason) {
|
||||||
const buffer = 10
|
const buffer = 10
|
||||||
const startIdx = Math.floor((scrollTop.value - buffer * rowHeight.value) / rowHeight.value)
|
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
||||||
|
const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value)
|
||||||
const endIdx = Math.ceil(
|
const endIdx = Math.ceil(
|
||||||
(scrollTop.value + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value,
|
(currentScrollTop + viewportHeight.value + buffer * rowHeight.value) / rowHeight.value,
|
||||||
)
|
)
|
||||||
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.log('[CalendarView] rebuildVisibleWeeks', {
|
||||||
|
reason,
|
||||||
|
currentScrollTop,
|
||||||
|
startIdx,
|
||||||
|
endIdx,
|
||||||
|
startVW,
|
||||||
|
endVW,
|
||||||
|
rowHeight: rowHeight.value,
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
reason === 'scroll' &&
|
reason === 'scroll' &&
|
||||||
lastScrollRange.startVW === startVW &&
|
lastScrollRange.startVW === startVW &&
|
||||||
@ -174,10 +202,6 @@ function rebuildVisibleWeeks(reason) {
|
|||||||
lastScrollRange = { startVW, endVW }
|
lastScrollRange = { startVW, endVW }
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentHeight = computed(() => {
|
|
||||||
return totalVirtualWeeks.value * rowHeight.value
|
|
||||||
})
|
|
||||||
|
|
||||||
function computeRowHeight() {
|
function computeRowHeight() {
|
||||||
if (rowProbe.value) {
|
if (rowProbe.value) {
|
||||||
const h = rowProbe.value.getBoundingClientRect().height || 64
|
const h = rowProbe.value.getBoundingClientRect().height || 64
|
||||||
@ -201,12 +225,18 @@ function measureFromProbe() {
|
|||||||
const newH = Math.round(h)
|
const newH = Math.round(h)
|
||||||
if (newH !== rowHeight.value) {
|
if (newH !== rowHeight.value) {
|
||||||
const oldH = rowHeight.value
|
const oldH = rowHeight.value
|
||||||
const currentTopVW = Math.floor(scrollTop.value / oldH) + minVirtualWeek.value
|
// Anchor: keep the same top virtual week visible.
|
||||||
|
const topVirtualWeek = Math.floor(scrollTop.value / oldH) + minVirtualWeek.value
|
||||||
rowHeight.value = newH
|
rowHeight.value = newH
|
||||||
const newScrollTop = (currentTopVW - minVirtualWeek.value) * newH
|
const newScrollTop = (topVirtualWeek - minVirtualWeek.value) * newH
|
||||||
scrollTop.value = newScrollTop
|
console.log('[CalendarView] measureFromProbe rowHeight change', {
|
||||||
if (viewport.value) viewport.value.scrollTop = newScrollTop
|
oldH,
|
||||||
scheduleRebuild('row-height-change')
|
newH,
|
||||||
|
topVirtualWeek,
|
||||||
|
oldScrollTop: scrollTop.value,
|
||||||
|
newScrollTop,
|
||||||
|
})
|
||||||
|
setScrollTop(newScrollTop, 'row-height-change')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,10 +406,8 @@ function createWeek(virtualWeek) {
|
|||||||
function goToToday() {
|
function goToToday() {
|
||||||
const top = addDays(new Date(calendarStore.now), -21)
|
const top = addDays(new Date(calendarStore.now), -21)
|
||||||
const targetWeekIndex = getWeekIndex(top)
|
const targetWeekIndex = getWeekIndex(top)
|
||||||
scrollTop.value = (targetWeekIndex - minVirtualWeek.value) * rowHeight.value
|
const newScrollTop = (targetWeekIndex - minVirtualWeek.value) * rowHeight.value
|
||||||
if (viewport.value) {
|
setScrollTop(newScrollTop, 'go-to-today')
|
||||||
viewport.value.scrollTop = scrollTop.value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSelection() {
|
function clearSelection() {
|
||||||
@ -478,10 +506,9 @@ function getDateFromCoordinates(clientX, clientY) {
|
|||||||
const sampleWeek = viewport.value.querySelector('.week-row')
|
const sampleWeek = viewport.value.querySelector('.week-row')
|
||||||
if (!sampleWeek) return null
|
if (!sampleWeek) return null
|
||||||
const labelEl = sampleWeek.querySelector('.week-label')
|
const labelEl = sampleWeek.querySelector('.week-label')
|
||||||
const jogwheelWidth = 48
|
|
||||||
const wrRect = sampleWeek.getBoundingClientRect()
|
const wrRect = sampleWeek.getBoundingClientRect()
|
||||||
const labelRight = labelEl ? labelEl.getBoundingClientRect().right : wrRect.left
|
const labelRight = labelEl ? labelEl.getBoundingClientRect().right : wrRect.left
|
||||||
const daysAreaRight = wrRect.right - jogwheelWidth
|
const daysAreaRight = wrRect.right
|
||||||
const daysWidth = daysAreaRight - labelRight
|
const daysWidth = daysAreaRight - labelRight
|
||||||
if (clientX < labelRight || clientX > daysAreaRight) return null
|
if (clientX < labelRight || clientX > daysAreaRight) return null
|
||||||
const col = Math.min(6, Math.max(0, Math.floor(((clientX - labelRight) / daysWidth) * 7)))
|
const col = Math.min(6, Math.max(0, Math.floor(((clientX - labelRight) / daysWidth) * 7)))
|
||||||
@ -519,76 +546,13 @@ function getWeekLabelRect() {
|
|||||||
return weekLabel ? weekLabel.getBoundingClientRect() : null
|
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
|
|
||||||
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.vale)
|
|
||||||
if (desired > maxScroll) desired = maxScroll
|
|
||||||
viewport.value.scrollTop = desired
|
|
||||||
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() {
|
|
||||||
weekColPointerLocked = document.pointerLockElement === viewport.value
|
|
||||||
if (!weekColPointerLocked && isWeekColDragging.value) {
|
|
||||||
handleWeekColMouseUp(new MouseEvent('mouseup'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onScroll = () => {
|
|
||||||
if (viewport.value) scrollTop.value = viewport.value.scrollTop
|
|
||||||
scheduleRebuild('scroll')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleJogwheelScrollTo = (newScrollTop) => {
|
|
||||||
if (viewport.value) {
|
|
||||||
viewport.value.scrollTop = newScrollTop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
computeRowHeight()
|
computeRowHeight()
|
||||||
calendarStore.updateCurrentDate()
|
calendarStore.updateCurrentDate()
|
||||||
|
|
||||||
if (viewport.value) {
|
if (viewport.value) {
|
||||||
viewportHeight.value = viewport.value.clientHeight
|
viewportHeight.value = viewport.value.clientHeight
|
||||||
viewport.value.scrollTop = initialScrollTop.value
|
setScrollTop(initialScrollTop.value, 'initial-mount')
|
||||||
viewport.value.addEventListener('scroll', onScroll)
|
viewport.value.addEventListener('scroll', onScroll)
|
||||||
// Capture mousedown in viewport to allow dragging via week label column
|
// Capture mousedown in viewport to allow dragging via week label column
|
||||||
viewport.value.addEventListener('mousedown', handleWeekColMouseDown, true)
|
viewport.value.addEventListener('mousedown', handleWeekColMouseDown, true)
|
||||||
@ -658,13 +622,22 @@ const handleEventClick = (payload) => {
|
|||||||
const handleHeaderYearChange = ({ scrollTop: st }) => {
|
const handleHeaderYearChange = ({ scrollTop: st }) => {
|
||||||
const maxScroll = contentHeight.value - viewportHeight.value
|
const maxScroll = contentHeight.value - viewportHeight.value
|
||||||
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
|
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
|
||||||
scrollTop.value = clamped
|
setScrollTop(clamped, 'header-year-change')
|
||||||
viewport.value && (viewport.value.scrollTop = clamped)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
||||||
// We explicitly avoid locale detection; rely solely on characters present.
|
// We explicitly avoid locale detection; rely solely on characters present.
|
||||||
// Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears.
|
// Disable rotation if any CJK Unified Ideograph or Compatibility Ideograph appears.
|
||||||
|
function shouldRotateMonth(label) {
|
||||||
|
if (!label) return false
|
||||||
|
try {
|
||||||
|
return /\p{Script=Latin}/u.test(label)
|
||||||
|
} catch (e) {
|
||||||
|
return /[A-Za-z\u00C0-\u024F\u1E00-\u1EFF]/u.test(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch first day changes (e.g., first_day config update) to adjust scroll
|
||||||
// Keep roughly same visible date when first_day setting changes.
|
// Keep roughly same visible date when first_day setting changes.
|
||||||
watch(
|
watch(
|
||||||
() => calendarStore.config.first_day,
|
() => calendarStore.config.first_day,
|
||||||
@ -674,8 +647,7 @@ watch(
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const newTopWeekIndex = getWeekIndex(currentTopDate)
|
const newTopWeekIndex = getWeekIndex(currentTopDate)
|
||||||
const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
|
const newScroll = (newTopWeekIndex - minVirtualWeek.value) * rowHeight.value
|
||||||
scrollTop.value = newScroll
|
setScrollTop(newScroll, 'first-day-change')
|
||||||
if (viewport.value) viewport.value.scrollTop = newScroll
|
|
||||||
scheduleRebuild('first-day-change')
|
scheduleRebuild('first-day-change')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -710,6 +682,8 @@ window.addEventListener('resize', () => {
|
|||||||
/>
|
/>
|
||||||
<div class="calendar-container">
|
<div class="calendar-container">
|
||||||
<div class="calendar-viewport" ref="viewport">
|
<div class="calendar-viewport" ref="viewport">
|
||||||
|
<!-- Main calendar content (weeks and days) -->
|
||||||
|
<div class="main-calendar-area">
|
||||||
<div class="calendar-content" :style="{ height: contentHeight + 'px' }">
|
<div class="calendar-content" :style="{ height: contentHeight + 'px' }">
|
||||||
<CalendarWeek
|
<CalendarWeek
|
||||||
v-for="week in visibleWeeks"
|
v-for="week in visibleWeeks"
|
||||||
@ -725,13 +699,31 @@ window.addEventListener('resize', () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Jogwheel
|
<!-- Month column area -->
|
||||||
:total-virtual-weeks="totalVirtualWeeks"
|
<div class="month-column-area">
|
||||||
:row-height="rowHeight"
|
<!-- Month labels -->
|
||||||
:viewport-height="viewportHeight"
|
<div class="month-labels-container" :style="{ height: contentHeight + 'px' }">
|
||||||
:scroll-top="scrollTop"
|
<template v-for="monthWeek in visibleWeeks" :key="monthWeek.virtualWeek + '-month'">
|
||||||
@scroll-to="handleJogwheelScrollTo"
|
<div
|
||||||
/>
|
v-if="monthWeek && monthWeek.monthLabel"
|
||||||
|
class="month-label"
|
||||||
|
:class="monthWeek.monthLabel?.monthClass"
|
||||||
|
:style="{
|
||||||
|
height: `calc(var(--row-h) * ${monthWeek.monthLabel?.weeksSpan || 1})`,
|
||||||
|
top: (monthWeek.top || 0) + 'px',
|
||||||
|
}"
|
||||||
|
@mousedown="handleMonthScrollMouseDown"
|
||||||
|
@touchstart="handleMonthScrollTouchStart"
|
||||||
|
@wheel="handleMonthScrollWheel"
|
||||||
|
>
|
||||||
|
<span :class="{ bottomup: shouldRotateMonth(monthWeek.monthLabel?.text) }">{{
|
||||||
|
monthWeek.monthLabel?.text || ''
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -773,7 +765,13 @@ header h1 {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr var(--month-w);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-calendar-area {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-content {
|
.calendar-content {
|
||||||
@ -781,6 +779,47 @@ header h1 {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.month-column-area {
|
||||||
|
position: relative;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-labels-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background-image: linear-gradient(to bottom, rgba(186, 186, 186, 0.3), rgba(186, 186, 186, 0.2));
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--muted);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 15;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: ns-resize;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-label > span {
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: mixed;
|
||||||
|
transform-origin: center;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomup {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
.row-height-probe {
|
.row-height-probe {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
@ -61,23 +61,13 @@ function shouldRotateMonth(label) {
|
|||||||
/>
|
/>
|
||||||
<EventOverlay :week="props.week" @event-click="handleEventClick" />
|
<EventOverlay :week="props.week" @event-click="handleEventClick" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="props.week.monthLabel"
|
|
||||||
class="month-label"
|
|
||||||
:class="props.week.monthLabel.monthClass"
|
|
||||||
:style="{ height: `calc(var(--row-h) * ${props.week.monthLabel.weeksSpan})` }"
|
|
||||||
>
|
|
||||||
<span :class="{ bottomup: shouldRotateMonth(props.week.monthLabel.text) }">{{
|
|
||||||
props.week.monthLabel.text
|
|
||||||
}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.week-row {
|
.week-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--week-w) repeat(7, 1fr) var(--month-w);
|
grid-template-columns: var(--week-w) repeat(7, 1fr);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: var(--row-h);
|
height: var(--row-h);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -91,7 +81,9 @@ function shouldRotateMonth(label) {
|
|||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
height: var(--row-h);
|
||||||
}
|
}
|
||||||
|
|
||||||
.days-grid {
|
.days-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, 1fr);
|
grid-template-columns: repeat(7, 1fr);
|
||||||
@ -99,32 +91,4 @@ function shouldRotateMonth(label) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.week-label {
|
|
||||||
height: var(--row-h);
|
|
||||||
}
|
|
||||||
.month-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: var(--month-w);
|
|
||||||
background-image: linear-gradient(to bottom, rgba(186, 186, 186, 0.3), rgba(186, 186, 186, 0.2));
|
|
||||||
font-size: 2em;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--muted);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 15;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.month-label > span {
|
|
||||||
display: inline-block;
|
|
||||||
white-space: nowrap;
|
|
||||||
writing-mode: vertical-rl;
|
|
||||||
text-orientation: mixed;
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
.bottomup {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
234
src/plugins/scrollManager.js
Normal file
234
src/plugins/scrollManager.js
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export function createScrollManager({ viewport, scheduleRebuild }) {
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
let lastProgrammatic = null
|
||||||
|
let pendingTarget = null
|
||||||
|
let pendingAttempts = 0
|
||||||
|
let pendingLoopActive = false
|
||||||
|
|
||||||
|
function setScrollTop(val, reason = 'programmatic') {
|
||||||
|
let applied = val
|
||||||
|
if (viewport.value) {
|
||||||
|
const maxScroll = viewport.value.scrollHeight - viewport.value.clientHeight
|
||||||
|
if (applied > maxScroll) {
|
||||||
|
applied = maxScroll < 0 ? 0 : maxScroll
|
||||||
|
pendingTarget = val
|
||||||
|
pendingAttempts = 0
|
||||||
|
startPendingLoop()
|
||||||
|
}
|
||||||
|
if (applied < 0) applied = 0
|
||||||
|
viewport.value.scrollTop = applied
|
||||||
|
}
|
||||||
|
scrollTop.value = applied
|
||||||
|
lastProgrammatic = applied
|
||||||
|
scheduleRebuild(reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
if (!viewport.value) return
|
||||||
|
const cur = viewport.value.scrollTop
|
||||||
|
const maxScroll = viewport.value.scrollHeight - viewport.value.clientHeight
|
||||||
|
let effective = cur
|
||||||
|
if (cur < 0) effective = 0
|
||||||
|
else if (cur > maxScroll) effective = maxScroll
|
||||||
|
scrollTop.value = effective
|
||||||
|
if (lastProgrammatic !== null && effective === lastProgrammatic) {
|
||||||
|
lastProgrammatic = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (pendingTarget !== null && Math.abs(effective - pendingTarget) > 4) {
|
||||||
|
pendingTarget = null
|
||||||
|
}
|
||||||
|
scheduleRebuild('scroll')
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPendingLoop() {
|
||||||
|
if (pendingLoopActive || !viewport.value) return
|
||||||
|
pendingLoopActive = true
|
||||||
|
const loop = () => {
|
||||||
|
if (pendingTarget == null || !viewport.value) {
|
||||||
|
pendingLoopActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const maxScroll = viewport.value.scrollHeight - viewport.value.clientHeight
|
||||||
|
if (pendingTarget <= maxScroll) {
|
||||||
|
setScrollTop(pendingTarget, 'pending-fulfill')
|
||||||
|
pendingTarget = null
|
||||||
|
pendingLoopActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingAttempts++
|
||||||
|
if (pendingAttempts > 120) {
|
||||||
|
pendingTarget = null
|
||||||
|
pendingLoopActive = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
requestAnimationFrame(loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { scrollTop, setScrollTop, onScroll }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWeekColumnScrollManager({
|
||||||
|
viewport,
|
||||||
|
viewportHeight,
|
||||||
|
contentHeight,
|
||||||
|
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 rect = getWeekLabelRect()
|
||||||
|
if (!rect) return
|
||||||
|
if (e.clientX < rect.left || e.clientX > rect.right) 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
|
||||||
|
isWeekColDragging.value = false
|
||||||
|
window.removeEventListener('mousemove', handleWeekColMouseMove)
|
||||||
|
window.removeEventListener('mouseup', handleWeekColMouseUp)
|
||||||
|
if (weekColPointerLocked && document.exitPointerLock) document.exitPointerLock()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerLockChange() {
|
||||||
|
weekColPointerLocked = document.pointerLockElement === viewport.value
|
||||||
|
if (!weekColPointerLocked && isWeekColDragging.value) {
|
||||||
|
handleWeekColMouseUp(new MouseEvent('mouseup'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isWeekColDragging,
|
||||||
|
handleWeekColMouseDown,
|
||||||
|
handlePointerLockChange,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createMonthScrollManager({
|
||||||
|
viewport,
|
||||||
|
viewportHeight,
|
||||||
|
contentHeight,
|
||||||
|
setScrollTop,
|
||||||
|
}) {
|
||||||
|
let isMonthScrolling = false
|
||||||
|
let monthScrollStartY = 0
|
||||||
|
let monthScrollStartScroll = 0
|
||||||
|
|
||||||
|
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
|
||||||
|
const maxScroll = Math.max(0, contentHeight.value - viewportHeight.value)
|
||||||
|
const clampedScroll = Math.max(0, Math.min(newScrollTop, maxScroll))
|
||||||
|
|
||||||
|
setScrollTop(clampedScroll, 'month-scroll-wheel')
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleMonthScrollMouseDown,
|
||||||
|
handleMonthScrollTouchStart,
|
||||||
|
handleMonthScrollWheel,
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user