mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
102 lines
4.6 KiB
TypeScript
102 lines
4.6 KiB
TypeScript
![]() |
import ScrollerFactory from 'libraries/scroller';
|
||
|
import globalize from 'scripts/globalize';
|
||
|
|
||
|
export enum ScrollDirection {
|
||
|
RIGHT = 'right',
|
||
|
LEFT = 'left',
|
||
|
}
|
||
|
|
||
|
interface ScrollState {
|
||
|
scrollPosition: number;
|
||
|
}
|
||
|
interface ScrollerSlideViewWindowProps {
|
||
|
direction: ScrollDirection;
|
||
|
scroller: ScrollerFactory | null;
|
||
|
scrollState: ScrollState;
|
||
|
}
|
||
|
export function scrollerItemSlideIntoView({ direction, scroller, scrollState }: ScrollerSlideViewWindowProps) {
|
||
|
if (!scroller) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const slider: HTMLElement = scroller.getScrollSlider();
|
||
|
const items = [...slider.children];
|
||
|
|
||
|
if (direction === ScrollDirection.LEFT) {
|
||
|
scrollToPreviousVisibleWindow(scroller, items as HTMLElement[], scrollState);
|
||
|
} else {
|
||
|
scrollToNextVisibleWindow(scroller, items as HTMLElement[], scrollState);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getFirstAndLastVisible(scrollFrame: HTMLElement, items: HTMLElement[], { scrollPosition }: ScrollState) {
|
||
|
const isRTL = globalize.getIsRTL();
|
||
|
const localeModifier = isRTL ? -1 : 1;
|
||
|
|
||
|
const currentScrollPos = scrollPosition * localeModifier;
|
||
|
const scrollerWidth = scrollFrame.offsetWidth;
|
||
|
const itemWidth = items[0].offsetWidth;
|
||
|
|
||
|
// Rounding down here will give us the first item index which is fully visible. We want the first partially visible
|
||
|
// index so we'll subtract one.
|
||
|
const firstVisibleIndex = Math.max(Math.floor(currentScrollPos / itemWidth) - 1, 0);
|
||
|
// Rounding up will give us the last index which is at least partially visible (overflows at container end).
|
||
|
const lastVisibleIndex = Math.floor((currentScrollPos + scrollerWidth) / itemWidth);
|
||
|
|
||
|
return [firstVisibleIndex, lastVisibleIndex];
|
||
|
}
|
||
|
|
||
|
function scrollToNextVisibleWindow(scroller: ScrollerFactory, items: HTMLElement[], scrollState: ScrollState) {
|
||
|
const isRTL = globalize.getIsRTL();
|
||
|
// When we're rendering RTL, scrolling toward the end of the container is toward the left so all of our scroll
|
||
|
// positions need to be negative.
|
||
|
const localeModifier = isRTL ? -1 : 1;
|
||
|
// NOTE: Legacy scroller passing in an Element which is the frame element and has some of the scroller factory
|
||
|
// functions on it, but is not a true scroller factory. For legacy, we need to pass `scroller` directly instead
|
||
|
// of getting the frame from the factory instance.
|
||
|
const [, lastVisibleIndex] = getFirstAndLastVisible(scroller.getScrollFrame?.() || scroller, items, scrollState);
|
||
|
|
||
|
const nextItem = items[lastVisibleIndex];
|
||
|
const nextItemScrollOffset = lastVisibleIndex * nextItem.offsetWidth;
|
||
|
|
||
|
// This will be the position to anchor the item at `lastVisibleIndex` to the start of the view window.
|
||
|
const nextItemScrollPos = (nextItemScrollOffset) * localeModifier;
|
||
|
|
||
|
if (scroller.slideTo) {
|
||
|
scroller.slideTo(nextItemScrollPos, false, undefined);
|
||
|
} else {
|
||
|
// @ts-expect-error Legacy support passes in a `scroller` that isn't a ScrollFactory
|
||
|
scroller.scrollToPosition(nextItemScrollPos);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function scrollToPreviousVisibleWindow(scroller: ScrollerFactory, items: HTMLElement[], scrollState: ScrollState) {
|
||
|
// NOTE: Legacy scroller passing in an Element which is the frame element and has some of the scroller factory
|
||
|
// functions on it, but is not a true scroller factory. For legacy, we need to pass `scroller` directly instead
|
||
|
// of getting the frame from the factory instance.
|
||
|
const frame = scroller.getScrollFrame?.() || scroller;
|
||
|
const isRTL = globalize.getIsRTL();
|
||
|
// When we're rendering RTL, scrolling toward the end of the container is toward the left so all of our scroll
|
||
|
// positions need to be negative.
|
||
|
const localeModifier = isRTL ? -1 : 1;
|
||
|
|
||
|
const [firstVisibleIndex] = getFirstAndLastVisible(frame, items, scrollState);
|
||
|
|
||
|
const previousItem = items[firstVisibleIndex];
|
||
|
const previousItemScrollOffset = firstVisibleIndex * previousItem.offsetWidth;
|
||
|
|
||
|
// Find the total number of items that can fit in a view window and subtract one to account for item at
|
||
|
// `firstVisibleIndex`. The total width of these items is the amount that we need to adjust the scroll position by
|
||
|
// to anchor item at `firstVisibleIndex` to the end of the view window.
|
||
|
const offsetAdjustment = (Math.floor(frame.offsetWidth / previousItem.offsetWidth) - 1) * previousItem.offsetWidth;
|
||
|
|
||
|
const previousItemScrollPos = (previousItemScrollOffset - offsetAdjustment) * localeModifier;
|
||
|
|
||
|
if (scroller.slideTo) {
|
||
|
scroller.slideTo(previousItemScrollPos, false, undefined);
|
||
|
} else {
|
||
|
// @ts-expect-error Legacy support passes in a `scroller` that isn't a ScrollFactory
|
||
|
scroller.scrollToPosition(previousItemScrollPos);
|
||
|
}
|
||
|
}
|