fix: re-add scroll compensation (efficiently) (#22848)

* fix: re-add scroll compensation (efficient)

* Rename showSkeleton to invisible. Adjust skeleton margins, invisible support.

* Fix faulty logic, simplify

* Calculate ratios and determine compensation strategy: height comp for above/partiality visible, month-scroll comp within a fully visible month.

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis 2025-10-13 22:16:05 -04:00 committed by GitHub
parent e8ca7f235c
commit 146973b072
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 128 additions and 71 deletions

View file

@ -5,7 +5,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
import { CancellableTask } from '$lib/utils/cancellable-task';
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
import { debounce, isEqual } from 'lodash-es';
import { clamp, debounce, isEqual } from 'lodash-es';
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
@ -37,6 +37,13 @@ import type {
Viewport,
} from './types';
type ViewportTopMonthIntersection = {
month: MonthGroup | undefined;
// Where viewport top intersects month (0 = month top, 1 = month bottom)
viewportTopRatioInMonth: number;
// Where month bottom is in viewport (0 = viewport top, 1 = viewport bottom)
monthBottomViewportRatio: number;
};
export class TimelineManager {
isInitialized = $state(false);
months: MonthGroup[] = $state([]);
@ -49,6 +56,8 @@ export class TimelineManager {
scrubberMonths: ScrubberMonth[] = $state([]);
scrubberTimelineHeight: number = $state(0);
viewportTopMonthIntersection: ViewportTopMonthIntersection | undefined;
visibleWindow = $derived.by(() => ({
top: this.#scrollTop,
bottom: this.#scrollTop + this.viewportHeight,
@ -85,6 +94,8 @@ export class TimelineManager {
#suspendTransitions = $state(false);
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
#updatingIntersections = false;
#scrollableElement: HTMLElement | undefined = $state();
constructor() {}
@ -98,6 +109,20 @@ export class TimelineManager {
}
}
set scrollableElement(element: HTMLElement | undefined) {
this.#scrollableElement = element;
}
scrollTo(top: number) {
this.#scrollableElement?.scrollTo({ top });
this.updateSlidingWindow();
}
scrollBy(y: number) {
this.#scrollableElement?.scrollBy(0, y);
this.updateSlidingWindow();
}
#setHeaderHeight(value: number) {
if (this.#headerHeight == value) {
return false;
@ -161,7 +186,8 @@ export class TimelineManager {
const changed = value !== this.#viewportWidth;
this.#viewportWidth = value;
this.suspendTransitions = true;
void this.#updateViewportGeometry(changed);
this.#updateViewportGeometry(changed);
this.updateSlidingWindow();
}
get viewportWidth() {
@ -223,20 +249,52 @@ export class TimelineManager {
this.#websocketSupport = undefined;
}
updateSlidingWindow(scrollTop: number) {
updateSlidingWindow() {
const scrollTop = this.#scrollableElement?.scrollTop ?? 0;
if (this.#scrollTop !== scrollTop) {
this.#scrollTop = scrollTop;
this.updateIntersections();
}
}
#calculateMonthBottomViewportRatio(month: MonthGroup | undefined) {
if (!month) {
return 0;
}
const windowHeight = this.visibleWindow.bottom - this.visibleWindow.top;
const bottomOfMonth = month.top + month.height;
const bottomOfMonthInViewport = bottomOfMonth - this.visibleWindow.top;
return clamp(bottomOfMonthInViewport / windowHeight, 0, 1);
}
#calculateVewportTopRatioInMonth(month: MonthGroup | undefined) {
if (!month) {
return 0;
}
return clamp((this.visibleWindow.top - month.top) / month.height, 0, 1);
}
updateIntersections() {
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
if (this.#updatingIntersections || !this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
return;
}
this.#updatingIntersections = true;
for (const month of this.months) {
updateIntersectionMonthGroup(this, month);
}
const month = this.months.find((month) => month.actuallyIntersecting);
const viewportTopRatioInMonth = this.#calculateVewportTopRatioInMonth(month);
const monthBottomViewportRatio = this.#calculateMonthBottomViewportRatio(month);
this.viewportTopMonthIntersection = {
month,
monthBottomViewportRatio,
viewportTopRatioInMonth,
};
this.#updatingIntersections = false;
}
clearDeferredLayout(month: MonthGroup) {
@ -368,7 +426,8 @@ export class TimelineManager {
await loadFromTimeBuckets(this, monthGroup, this.#options, signal);
}, cancelable);
if (executionStatus === 'LOADED') {
updateIntersectionMonthGroup(this, monthGroup);
updateGeometry(this, monthGroup, { invalidateHeight: false });
this.updateIntersections();
}
}