/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ // * Jolly Cobra's fastSmoothScroll slightly patched import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck'; import { fastRaf } from './schedulers'; import { animateSingle, cancelAnimationByKey } from './animation'; import rootScope from '../lib/rootScope'; import isInDOM from './dom/isInDOM'; const MIN_JS_DURATION = 250; const MAX_JS_DURATION = 600; const LONG_TRANSITION_MAX_DISTANCE = 1500; const SHORT_TRANSITION_MAX_DISTANCE = 500; export enum FocusDirection { Up, Down, Static, }; export type ScrollGetNormalSizeCallback = (options: {rect: DOMRect}) => number; export type ScrollOptions = { container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin?: number, maxDistance?: number, forceDirection?: FocusDirection, forceDuration?: number, axis?: 'x' | 'y', getNormalSize?: ScrollGetNormalSizeCallback, fallbackToElementStartWhenCentering?: HTMLElement }; export default function fastSmoothScroll(options: ScrollOptions) { if(options.margin === undefined) { options.margin = 0; } if(options.maxDistance === undefined) { options.maxDistance = LONG_TRANSITION_MAX_DISTANCE; } if(options.axis === undefined) { options.axis = 'y'; } //return; if(!rootScope.settings.animationsEnabled) { options.forceDirection = FocusDirection.Static; } if(options.forceDirection === FocusDirection.Static) { options.forceDuration = 0; return scrollWithJs(options); /* return Promise.resolve(); element.scrollIntoView({ block: position }); cancelAnimationByKey(container); return Promise.resolve(); */ } const promise = new Promise((resolve) => { fastRaf(() => { scrollWithJs(options).then(resolve); }); }); return options.axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise; } function scrollWithJs(options: ScrollOptions): Promise { const {element, container, getNormalSize, axis, margin, position, forceDirection, maxDistance, forceDuration} = options; if(!isInDOM(element)) { cancelAnimationByKey(container); return Promise.resolve(); } const rectStartKey = axis === 'y' ? 'top' : 'left'; const rectEndKey = axis === 'y' ? 'bottom' : 'right'; const sizeKey = axis === 'y' ? 'height' : 'width'; const scrollSizeKey = axis === 'y' ? 'scrollHeight' : 'scrollWidth'; const scrollPositionKey = axis === 'y' ? 'scrollTop' : 'scrollLeft'; //const { offsetTop: elementTop, offsetHeight: elementHeight } = element; const elementRect = element.getBoundingClientRect(); const containerRect = container.getBoundingClientRect ? container.getBoundingClientRect() : document.body.getBoundingClientRect(); //const transformable = container.firstElementChild as HTMLElement; const elementPosition = elementRect[rectStartKey] - containerRect[rectStartKey]; const elementSize = element[scrollSizeKey]; // margin is exclusive in DOMRect const containerSize = getNormalSize ? getNormalSize({rect: containerRect}) : containerRect[sizeKey]; const scrollPosition = container[scrollPositionKey]; const scrollSize = container[scrollSizeKey]; /* const elementPosition = element.offsetTop; const elementSize = element.offsetHeight; const scrollPosition = container[scrollPositionKey]; const scrollSize = container[scrollSizeKey]; const containerSize = container.offsetHeight; */ let path!: number; switch(position) { case 'start': path = elementPosition - margin; break; case 'end': path = elementRect[rectEndKey] /* + (elementSize - elementRect[sizeKey]) */ - containerRect[rectEndKey] + margin; break; // 'nearest' is not supported yet case 'nearest': case 'center': if(elementSize < containerSize) { path = (elementPosition + elementSize / 2) - (containerSize / 2); } else { if(options.fallbackToElementStartWhenCentering && options.fallbackToElementStartWhenCentering !== element) { options.element = options.fallbackToElementStartWhenCentering; options.position = 'start'; return scrollWithJs(options); } path = elementPosition - margin; } break; } /* switch (position) { case 'start': path = (elementPosition - margin) - scrollPosition; break; case 'end': path = (elementPosition + elementSize + margin) - (scrollPosition + containerSize); break; // 'nearest' is not supported yet case 'nearest': case 'center': path = elementSize < containerSize ? (elementPosition + elementSize / 2) - (scrollPosition + containerSize / 2) : (elementPosition - margin) - scrollPosition; break; } */ if(axis === 'y') { if(forceDirection === undefined) { if(path > maxDistance) { container.scrollTop += path - maxDistance; path = maxDistance; } else if(path < -maxDistance) { container.scrollTop += path + maxDistance; path = -maxDistance; } }/* else if(forceDirection === FocusDirection.Up) { // * not tested yet container.scrollTop = offsetTop + container.scrollTop + maxDistance; } else if(forceDirection === FocusDirection.Down) { // * not tested yet container.scrollTop = Math.max(0, offsetTop + container.scrollTop - maxDistance); } */ } // console.log('scrollWithJs: will scroll path:', path, element); /* let existsTransform = 0; const currentTransform = transformable.style.transform; if(currentTransform) { existsTransform = parseInt(currentTransform.match(/\((.+?), (.+?), .+\)/)[2]); //path += existsTransform; } */ if(path < 0) { const remainingPath = -scrollPosition; path = Math.max(path, remainingPath); } else if(path > 0) { const remainingPath = scrollSize - (scrollPosition + containerSize); path = Math.min(path, remainingPath); } const target = container[scrollPositionKey] + path; const absPath = Math.abs(path); const duration = forceDuration ?? ( MIN_JS_DURATION + (absPath / LONG_TRANSITION_MAX_DISTANCE) * (MAX_JS_DURATION - MIN_JS_DURATION) ); const startAt = Date.now(); /* transformable.classList.add('no-transition'); const tickTransform = () => { const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; const currentPath = path * transition(t); transformable.style.transform = `translate3d(0, ${-currentPath}px, 0)`; container.dataset.translate = '' + -currentPath; const willContinue = t < 1; if(!willContinue) { fastRaf(() => { delete container.dataset.transform; container.dataset.transform = ''; transformable.style.transform = ''; void transformable.offsetLeft; // reflow transformable.classList.remove('no-transition'); void transformable.offsetLeft; // reflow container[scrollPositionKey] = Math.round(target); }); } return willContinue; }; return animateSingle(tickTransform, container); */ /* return new Promise((resolve) => { fastRaf(() => { transformable.style.transform = ''; transformable.style.transition = ''; setTimeout(resolve, duration); }); }); const transformableHeight = transformable.scrollHeight; //transformable.style.minHeight = `${transformableHeight}px`; */ const transition = absPath < SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition; const tick = () => { const t = duration ? Math.min((Date.now() - startAt) / duration, 1) : 1; const currentPath = path * (1 - transition(t)); container[scrollPositionKey] = Math.round(target - currentPath); return t < 1; }; if(!duration || !path) { cancelAnimationByKey(container); tick(); return Promise.resolve(); } /* return new Promise((resolve) => { setTimeout(resolve, duration); }).then(() => { transformable.classList.add('no-transition'); void transformable.offsetLeft; // reflow transformable.style.transform = ''; transformable.style.transition = ''; void transformable.offsetLeft; // reflow transformable.classList.remove('no-transition'); void transformable.offsetLeft; // reflow fastRaf(() => { container[scrollPositionKey] = Math.round(target); //transformable.style.minHeight = ``; }); }); */ return animateSingle(tick, container); } function longTransition(t: number) { return 1 - ((1 - t) ** 5); } function shortTransition(t: number) { return 1 - ((1 - t) ** 3.5); }