mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	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:
		
							parent
							
								
									e8ca7f235c
								
							
						
					
					
						commit
						146973b072
					
				| @ -89,10 +89,10 @@ | ||||
| 
 | ||||
|   let { isViewing: showAssetViewer, asset: viewingAsset, gridScrollTarget } = assetViewingStore; | ||||
| 
 | ||||
|   let element: HTMLElement | undefined = $state(); | ||||
|   let scrollableElement: HTMLElement | undefined = $state(); | ||||
| 
 | ||||
|   let timelineElement: HTMLElement | undefined = $state(); | ||||
|   let showSkeleton = $state(true); | ||||
|   let invisible = $state(true); | ||||
|   // The percentage of scroll through the month that is currently intersecting the top boundary of the viewport. | ||||
|   // Note: There may be multiple months visible within the viewport at any given time. | ||||
|   let viewportTopMonthScrollPercent = $state(0); | ||||
| @ -124,29 +124,22 @@ | ||||
|     timelineManager.setLayoutOptions(layoutOptions); | ||||
|   }); | ||||
| 
 | ||||
|   const scrollTo = (top: number) => { | ||||
|     if (element) { | ||||
|       element.scrollTo({ top }); | ||||
|     } | ||||
|   }; | ||||
|   const scrollTop = (top: number) => { | ||||
|     if (element) { | ||||
|       element.scrollTop = top; | ||||
|     } | ||||
|   }; | ||||
|   $effect(() => { | ||||
|     timelineManager.scrollableElement = scrollableElement; | ||||
|   }); | ||||
| 
 | ||||
|   const scrollToTop = () => { | ||||
|     scrollTo(0); | ||||
|     timelineManager.scrollTo(0); | ||||
|   }; | ||||
| 
 | ||||
|   const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId); | ||||
| 
 | ||||
|   const assetIsVisible = (assetTop: number): boolean => { | ||||
|     if (!element) { | ||||
|     if (!scrollableElement) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|     const { clientHeight, scrollTop } = element; | ||||
|     const { clientHeight, scrollTop } = scrollableElement; | ||||
|     return assetTop >= scrollTop && assetTop < scrollTop + clientHeight; | ||||
|   }; | ||||
| 
 | ||||
| @ -163,8 +156,7 @@ | ||||
|       return true; | ||||
|     } | ||||
| 
 | ||||
|     scrollTo(height); | ||||
|     updateSlidingWindow(); | ||||
|     timelineManager.scrollTo(height); | ||||
|     return true; | ||||
|   }; | ||||
| 
 | ||||
| @ -174,8 +166,7 @@ | ||||
|       return false; | ||||
|     } | ||||
|     const height = getAssetHeight(asset.id, monthGroup); | ||||
|     scrollTo(height); | ||||
|     updateSlidingWindow(); | ||||
|     timelineManager.scrollTo(height); | ||||
|     return true; | ||||
|   }; | ||||
| 
 | ||||
| @ -189,7 +180,7 @@ | ||||
|       // if the asset is not found, scroll to the top | ||||
|       scrollToTop(); | ||||
|     } | ||||
|     showSkeleton = false; | ||||
|     invisible = false; | ||||
|   }; | ||||
| 
 | ||||
|   beforeNavigate(() => (timelineManager.suspendTransitions = true)); | ||||
| @ -216,7 +207,7 @@ | ||||
|         } else { | ||||
|           scrollToTop(); | ||||
|         } | ||||
|         showSkeleton = false; | ||||
|         invisible = false; | ||||
|       }, 500); | ||||
|     } | ||||
|   }; | ||||
| @ -230,13 +221,12 @@ | ||||
| 
 | ||||
|   const updateIsScrolling = () => (timelineManager.scrolling = true); | ||||
|   // note: don't throttle, debounch, or otherwise do this function async - it causes flicker | ||||
|   const updateSlidingWindow = () => timelineManager.updateSlidingWindow(element?.scrollTop || 0); | ||||
| 
 | ||||
|   const topSectionResizeObserver: OnResizeCallback = ({ height }) => (timelineManager.topSectionHeight = height); | ||||
| 
 | ||||
|   onMount(() => { | ||||
|     if (!enableRouting) { | ||||
|       showSkeleton = false; | ||||
|       invisible = false; | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
| @ -246,11 +236,13 @@ | ||||
|   }; | ||||
| 
 | ||||
|   const getMaxScroll = () => { | ||||
|     if (!element || !timelineElement) { | ||||
|     if (!scrollableElement || !timelineElement) { | ||||
|       return 0; | ||||
|     } | ||||
|     return ( | ||||
|       timelineManager.topSectionHeight + bottomSectionHeight + (timelineElement.clientHeight - element.clientHeight) | ||||
|       timelineManager.topSectionHeight + | ||||
|       bottomSectionHeight + | ||||
|       (timelineElement.clientHeight - scrollableElement.clientHeight) | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
| @ -260,7 +252,7 @@ | ||||
|     const delta = monthGroup.height * monthGroupScrollPercent; | ||||
|     const scrollToTop = (topOffset + delta) * maxScrollPercent; | ||||
| 
 | ||||
|     scrollTop(scrollToTop); | ||||
|     timelineManager.scrollTo(scrollToTop); | ||||
|   }; | ||||
| 
 | ||||
|   // note: don't throttle, debounce, or otherwise make this function async - it causes flicker | ||||
| @ -272,7 +264,7 @@ | ||||
|       // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead | ||||
|       const maxScroll = getMaxScroll(); | ||||
|       const offset = maxScroll * overallScrollPercent; | ||||
|       scrollTop(offset); | ||||
|       timelineManager.scrollTo(offset); | ||||
|     } else { | ||||
|       const monthGroup = timelineManager.months.find( | ||||
|         ({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month, | ||||
| @ -288,26 +280,26 @@ | ||||
|   const handleTimelineScroll = () => { | ||||
|     isInLeadOutSection = false; | ||||
| 
 | ||||
|     if (!element) { | ||||
|     if (!scrollableElement) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) { | ||||
|       // edge case - scroll limited due to size of content, must adjust -  use the overall percent instead | ||||
|       const maxScroll = getMaxScroll(); | ||||
|       timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll); | ||||
|       timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll); | ||||
| 
 | ||||
|       viewportTopMonth = undefined; | ||||
|       viewportTopMonthScrollPercent = 0; | ||||
|     } else { | ||||
|       let top = element.scrollTop; | ||||
|       let top = scrollableElement.scrollTop; | ||||
|       if (top < timelineManager.topSectionHeight) { | ||||
|         // in the lead-in area | ||||
|         viewportTopMonth = undefined; | ||||
|         viewportTopMonthScrollPercent = 0; | ||||
|         const maxScroll = getMaxScroll(); | ||||
| 
 | ||||
|         timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll); | ||||
|         timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
| @ -414,7 +406,7 @@ | ||||
|     onSelect(asset); | ||||
| 
 | ||||
|     if (singleSelect) { | ||||
|       scrollTop(0); | ||||
|       timelineManager.scrollTo(0); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
| @ -564,10 +556,10 @@ | ||||
|       if (evt.key === 'ArrowUp') { | ||||
|         amount = -amount; | ||||
|         if (shiftKeyIsDown) { | ||||
|           element?.scrollBy({ top: amount, behavior: 'smooth' }); | ||||
|           scrollableElement?.scrollBy({ top: amount, behavior: 'smooth' }); | ||||
|         } | ||||
|       } else if (evt.key === 'ArrowDown') { | ||||
|         element?.scrollBy({ top: amount, behavior: 'smooth' }); | ||||
|         scrollableElement?.scrollBy({ top: amount, behavior: 'smooth' }); | ||||
|       } | ||||
|     }} | ||||
|   /> | ||||
| @ -580,19 +572,19 @@ | ||||
|   style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'} | ||||
|   tabindex="-1" | ||||
|   bind:clientHeight={timelineManager.viewportHeight} | ||||
|   bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())} | ||||
|   bind:this={element} | ||||
|   onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())} | ||||
|   bind:clientWidth={timelineManager.viewportWidth} | ||||
|   bind:this={scrollableElement} | ||||
|   onscroll={() => (handleTimelineScroll(), timelineManager.updateSlidingWindow(), updateIsScrolling())} | ||||
| > | ||||
|   <section | ||||
|     bind:this={timelineElement} | ||||
|     id="virtual-timeline" | ||||
|     class:invisible={showSkeleton} | ||||
|     class:invisible | ||||
|     style:height={timelineManager.timelineHeight + 'px'} | ||||
|   > | ||||
|     <section | ||||
|       use:resizeObserver={topSectionResizeObserver} | ||||
|       class:invisible={showSkeleton} | ||||
|       class:invisible | ||||
|       style:position="absolute" | ||||
|       style:left="0" | ||||
|       style:right="0" | ||||
| @ -615,10 +607,7 @@ | ||||
|           style:transform={`translate3d(0,${absoluteHeight}px,0)`} | ||||
|           style:width="100%" | ||||
|         > | ||||
|           <Skeleton | ||||
|             height={monthGroup.height - monthGroup.timelineManager.headerHeight} | ||||
|             title={monthGroup.monthGroupTitle} | ||||
|           /> | ||||
|           <Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} /> | ||||
|         </div> | ||||
|       {:else if display} | ||||
|         <div | ||||
| @ -658,7 +647,7 @@ | ||||
| 
 | ||||
| <Portal target="body"> | ||||
|   {#if $showAssetViewer} | ||||
|     <TimelineAssetViewer bind:showSkeleton {timelineManager} {removeAction} {withStacked} {isShared} {album} {person} /> | ||||
|     <TimelineAssetViewer bind:invisible {timelineManager} {removeAction} {withStacked} {isShared} {album} {person} /> | ||||
|   {/if} | ||||
| </Portal> | ||||
| 
 | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| 
 | ||||
|   interface Props { | ||||
|     timelineManager: TimelineManager; | ||||
|     showSkeleton: boolean; | ||||
|     invisible: boolean; | ||||
|     withStacked?: boolean; | ||||
|     isShared?: boolean; | ||||
|     album?: AlbumResponseDto | null; | ||||
| @ -30,7 +30,7 @@ | ||||
| 
 | ||||
|   let { | ||||
|     timelineManager, | ||||
|     showSkeleton = $bindable(false), | ||||
|     invisible = $bindable(false), | ||||
|     removeAction, | ||||
|     withStacked = false, | ||||
|     isShared = false, | ||||
| @ -81,7 +81,7 @@ | ||||
| 
 | ||||
|   const handleClose = async (asset: { id: string }) => { | ||||
|     assetViewingStore.showAssetViewer(false); | ||||
|     showSkeleton = true; | ||||
|     invisible = true; | ||||
|     $gridScrollTarget = { at: asset.id }; | ||||
|     await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }); | ||||
|   }; | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
| 
 | ||||
|   import { fromTimelinePlainDate, getDateLocaleString } from '$lib/utils/timeline-util'; | ||||
|   import { Icon } from '@immich/ui'; | ||||
|   import type { Snippet } from 'svelte'; | ||||
|   import { type Snippet } from 'svelte'; | ||||
|   import { flip } from 'svelte/animate'; | ||||
|   import { scale } from 'svelte/transition'; | ||||
| 
 | ||||
|  | ||||
| @ -2,24 +2,21 @@ | ||||
|   interface Props { | ||||
|     height: number; | ||||
|     title?: string; | ||||
|     invisible?: boolean; | ||||
|   } | ||||
| 
 | ||||
|   let { height = 0, title }: Props = $props(); | ||||
|   let { height = 0, title, invisible = false }: Props = $props(); | ||||
| </script> | ||||
| 
 | ||||
| <div class="overflow-clip" style:height={height + 'px'}> | ||||
| <div class={['overflow-clip', { invisible }]} style:height={height + 'px'}> | ||||
|   {#if title} | ||||
|     <div | ||||
|       class="flex pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-light dark:text-immich-dark-fg md:text-sm" | ||||
|       class="flex pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm" | ||||
|     > | ||||
|       {title} | ||||
|     </div> | ||||
|   {/if} | ||||
|   <div | ||||
|     class="animate-pulse absolute h-full ms-[10px] me-[10px]" | ||||
|     style:width="calc(100% - 20px)" | ||||
|     data-skeleton="true" | ||||
|   ></div> | ||||
|   <div class="animate-pulse h-full w-full" data-skeleton="true"></div> | ||||
| </div> | ||||
| 
 | ||||
| <style> | ||||
| @ -47,4 +44,7 @@ | ||||
|       0s linear 0.1s forwards delayedVisibility, | ||||
|       pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | ||||
|   } | ||||
|   .invisible [data-skeleton] { | ||||
|     visibility: hidden !important; | ||||
|   } | ||||
| </style> | ||||
|  | ||||
| @ -4,7 +4,6 @@ import { getTimeBucket } from '@immich/sdk'; | ||||
| import type { MonthGroup } from '../month-group.svelte'; | ||||
| import type { TimelineManager } from '../timeline-manager.svelte'; | ||||
| import type { TimelineManagerOptions } from '../types'; | ||||
| import { layoutMonthGroup } from './layout-support.svelte'; | ||||
| 
 | ||||
| export async function loadFromTimeBuckets( | ||||
|   timelineManager: TimelineManager, | ||||
| @ -55,6 +54,4 @@ export async function loadFromTimeBuckets( | ||||
|       )}`,
 | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   layoutMonthGroup(timelineManager, monthGroup); | ||||
| } | ||||
|  | ||||
| @ -36,6 +36,7 @@ export class MonthGroup { | ||||
| 
 | ||||
|   #initialCount: number = 0; | ||||
|   #sortOrder: AssetOrder = AssetOrder.Desc; | ||||
|   percent: number = $state(0); | ||||
| 
 | ||||
|   assetsCount: number = $derived( | ||||
|     this.isLoaded | ||||
| @ -241,7 +242,6 @@ export class MonthGroup { | ||||
|     if (this.#height === height) { | ||||
|       return; | ||||
|     } | ||||
|     let needsIntersectionUpdate = false; | ||||
|     const timelineManager = this.timelineManager; | ||||
|     const index = timelineManager.months.indexOf(this); | ||||
|     const heightDelta = height - this.#height; | ||||
| @ -261,11 +261,21 @@ export class MonthGroup { | ||||
|       const newTop = monthGroup.#top + heightDelta; | ||||
|       if (monthGroup.#top !== newTop) { | ||||
|         monthGroup.#top = newTop; | ||||
|         needsIntersectionUpdate = true; | ||||
|       } | ||||
|     } | ||||
|     if (needsIntersectionUpdate) { | ||||
|       timelineManager.updateIntersections(); | ||||
|     if (!timelineManager.viewportTopMonthIntersection) { | ||||
|       return; | ||||
|     } | ||||
|     const { month, monthBottomViewportRatio, viewportTopRatioInMonth } = timelineManager.viewportTopMonthIntersection; | ||||
|     const currentIndex = month ? timelineManager.months.indexOf(month) : -1; | ||||
|     if (!month || currentIndex <= 0 || index > currentIndex) { | ||||
|       return; | ||||
|     } | ||||
|     if (index < currentIndex || monthBottomViewportRatio < 1) { | ||||
|       timelineManager.scrollBy(heightDelta); | ||||
|     } else if (index === currentIndex) { | ||||
|       const scrollTo = this.top + height * viewportTopRatioInMonth; | ||||
|       timelineManager.scrollTo(scrollTo); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -4,6 +4,7 @@ import { AbortError } from '$lib/utils'; | ||||
| import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util'; | ||||
| import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk'; | ||||
| import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory'; | ||||
| import { tick } from 'svelte'; | ||||
| import { TimelineManager } from './timeline-manager.svelte'; | ||||
| import type { TimelineAsset } from './types'; | ||||
| 
 | ||||
| @ -64,11 +65,12 @@ describe('TimelineManager', () => { | ||||
| 
 | ||||
|       sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssetsResponse[timeBucket])); | ||||
|       await timelineManager.updateViewport({ width: 1588, height: 1000 }); | ||||
|       await tick(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should load months in viewport', () => { | ||||
|       expect(sdkMock.getTimeBuckets).toBeCalledTimes(1); | ||||
|       expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(3); | ||||
|       expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2); | ||||
|     }); | ||||
| 
 | ||||
|     it('calculates month height', () => { | ||||
| @ -82,13 +84,13 @@ describe('TimelineManager', () => { | ||||
|         expect.arrayContaining([ | ||||
|           expect.objectContaining({ year: 2024, month: 3, height: 165.5 }), | ||||
|           expect.objectContaining({ year: 2024, month: 2, height: 11_996 }), | ||||
|           expect.objectContaining({ year: 2024, month: 1, height: 48 }), | ||||
|           expect.objectContaining({ year: 2024, month: 1, height: 286 }), | ||||
|         ]), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('calculates timeline height', () => { | ||||
|       expect(timelineManager.timelineHeight).toBe(12_209.5); | ||||
|       expect(timelineManager.timelineHeight).toBe(12_447.5); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user