Major new version #2
@ -115,27 +115,42 @@ 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 updating = 0 // 0 idle, 1 window incremental, 2 full rebuild
|
||||||
let dataTimer = null
|
|
||||||
const WINDOW_DEBOUNCE_MS = 30
|
|
||||||
const DATA_DEBOUNCE_MS = 40
|
|
||||||
function scheduleWindowUpdate(reason) {
|
function scheduleWindowUpdate(reason) {
|
||||||
if (windowTimer) return
|
if (updating !== 0) return
|
||||||
windowTimer = setTimeout(() => {
|
updating = 1
|
||||||
windowTimer = null
|
const run = () => {
|
||||||
const fn = () => updateVisibleWeeks(reason)
|
let complete = true
|
||||||
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 80 })
|
try {
|
||||||
else requestAnimationFrame(fn)
|
complete = updateVisibleWeeks(reason)
|
||||||
}, WINDOW_DEBOUNCE_MS)
|
} finally {
|
||||||
|
updating = 0
|
||||||
|
}
|
||||||
|
if (!complete) scheduleWindowUpdate('incremental-build')
|
||||||
|
}
|
||||||
|
if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 16 })
|
||||||
|
else requestAnimationFrame(run)
|
||||||
}
|
}
|
||||||
function scheduleDataRebuild(reason) {
|
function scheduleDataRebuild(reason) {
|
||||||
if (dataTimer) return
|
if (updating === 2) return // already rebuilding
|
||||||
dataTimer = setTimeout(() => {
|
const doRebuild = () => {
|
||||||
dataTimer = null
|
updating = 2
|
||||||
const fn = () => rebuildVisibleWeeks(reason)
|
const run = () => {
|
||||||
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 120 })
|
try {
|
||||||
else requestAnimationFrame(fn)
|
rebuildVisibleWeeks(reason)
|
||||||
}, DATA_DEBOUNCE_MS)
|
} finally {
|
||||||
|
updating = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 60 })
|
||||||
|
else requestAnimationFrame(run)
|
||||||
|
}
|
||||||
|
// If we're mid incremental window update, defer slightly to next frame
|
||||||
|
if (updating === 1) {
|
||||||
|
requestAnimationFrame(doRebuild)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
doRebuild()
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
|
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
|
||||||
@ -177,6 +192,7 @@ const selectedDateRange = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function updateVisibleWeeks(reason) {
|
function updateVisibleWeeks(reason) {
|
||||||
|
// Compute desired virtual week window with buffer
|
||||||
const buffer = 4
|
const buffer = 4
|
||||||
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
|
||||||
const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value)
|
const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value)
|
||||||
@ -185,34 +201,86 @@ function updateVisibleWeeks(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)
|
||||||
if (
|
|
||||||
lastScrollRange.startVW === startVW &&
|
// Step 1: prune anything outside the desired window
|
||||||
lastScrollRange.endVW === endVW &&
|
|
||||||
visibleWeeks.value.length
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if (visibleWeeks.value.length) {
|
if (visibleWeeks.value.length) {
|
||||||
while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW)
|
while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW) {
|
||||||
visibleWeeks.value.shift()
|
visibleWeeks.value.shift()
|
||||||
|
}
|
||||||
while (
|
while (
|
||||||
visibleWeeks.value.length &&
|
visibleWeeks.value.length &&
|
||||||
visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek > endVW
|
visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek > endVW
|
||||||
)
|
) {
|
||||||
visibleWeeks.value.pop()
|
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))
|
// Step 2: ensure no gaps; add at most one adjacent missing week each pass
|
||||||
let needLast = visibleWeeks.value[visibleWeeks.value.length - 1]?.virtualWeek
|
let added = false
|
||||||
for (let vw = (needLast ?? startVW - 1) + 1; vw <= endVW; vw++)
|
const len = visibleWeeks.value.length
|
||||||
visibleWeeks.value.push(createWeek(vw))
|
if (len === 0) {
|
||||||
lastScrollRange = { startVW, endVW }
|
visibleWeeks.value.push(createWeek(startVW))
|
||||||
return
|
added = true
|
||||||
|
} else {
|
||||||
|
// Sort defensively (should already be sorted)
|
||||||
|
visibleWeeks.value.sort((a, b) => a.virtualWeek - b.virtualWeek)
|
||||||
|
const firstVW = visibleWeeks.value[0].virtualWeek
|
||||||
|
const lastVW = visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek
|
||||||
|
if (firstVW > startVW) {
|
||||||
|
// Add one week just before current first to close front gap gradually
|
||||||
|
visibleWeeks.value.unshift(createWeek(firstVW - 1))
|
||||||
|
added = true
|
||||||
|
} else {
|
||||||
|
// Look for first internal gap
|
||||||
|
let gapInserted = false
|
||||||
|
for (let i = 0; i < visibleWeeks.value.length - 1; i++) {
|
||||||
|
const curVW = visibleWeeks.value[i].virtualWeek
|
||||||
|
const nextVW = visibleWeeks.value[i + 1].virtualWeek
|
||||||
|
if (nextVW - curVW > 1 && curVW < endVW) {
|
||||||
|
// Insert the immediate missing week after curVW
|
||||||
|
visibleWeeks.value.splice(i + 1, 0, createWeek(curVW + 1))
|
||||||
|
added = true
|
||||||
|
gapInserted = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!gapInserted && lastVW < endVW) {
|
||||||
|
// Extend at end
|
||||||
|
visibleWeeks.value.push(createWeek(lastVW + 1))
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: assess coverage
|
||||||
|
const firstAfter = visibleWeeks.value[0].virtualWeek
|
||||||
|
const lastAfter = visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek
|
||||||
|
const contiguous = (() => {
|
||||||
|
for (let i = 0; i < visibleWeeks.value.length - 1; i++) {
|
||||||
|
if (visibleWeeks.value[i + 1].virtualWeek !== visibleWeeks.value[i].virtualWeek + 1)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})()
|
||||||
|
const coverageComplete =
|
||||||
|
firstAfter <= startVW &&
|
||||||
|
lastAfter >= endVW &&
|
||||||
|
contiguous &&
|
||||||
|
visibleWeeks.value.length === endVW - startVW + 1
|
||||||
|
if (!coverageComplete) {
|
||||||
|
// Incomplete; do not update lastScrollRange so subsequent runs keep adding
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
lastScrollRange.startVW === startVW &&
|
||||||
|
lastScrollRange.endVW === endVW &&
|
||||||
|
!added &&
|
||||||
|
visibleWeeks.value.length
|
||||||
|
) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
const weeks = []
|
|
||||||
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
|
|
||||||
visibleWeeks.value = weeks
|
|
||||||
lastScrollRange = { startVW, endVW }
|
lastScrollRange = { startVW, endVW }
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
function rebuildVisibleWeeks(reason) {
|
function rebuildVisibleWeeks(reason) {
|
||||||
const buffer = 4
|
const buffer = 4
|
||||||
@ -650,6 +718,8 @@ 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))
|
||||||
setScrollTop(clamped, 'header-year-change')
|
setScrollTop(clamped, 'header-year-change')
|
||||||
|
// Force a full rebuild so the new year range appears instantly
|
||||||
|
scheduleDataRebuild('header-year-change')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
// Heuristic: rotate month label (180deg) only for predominantly Latin text.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user