/* * 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 default function fastSmoothScroll( container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, maxDistance = LONG_TRANSITION_MAX_DISTANCE, forceDirection?: FocusDirection, forceDuration?: number, axis: 'x' | 'y' = 'y', getNormalSize?: ScrollGetNormalSizeCallback ) { //return; if(!rootScope.settings.animationsEnabled) { forceDirection = FocusDirection.Static; } if(forceDirection === FocusDirection.Static) { forceDuration = 0; return scrollWithJs(container, element, position, margin, forceDuration, axis, getNormalSize); /* return Promise.resolve(); element.scrollIntoView({ block: position }); cancelAnimationByKey(container); return Promise.resolve(); */ } if(axis === 'y' && element !== container && isInDOM(element) && container.getBoundingClientRect) { const elementRect = element.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); const offsetTop = elementRect.top - containerRect.top; if(forceDirection === undefined) { if(offsetTop < -maxDistance) { container.scrollTop += (offsetTop + maxDistance); } else if(offsetTop > maxDistance) { container.scrollTop += (offsetTop - 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); } /* const { offsetTop } = element; if(forceDirection === undefined) { const offset = offsetTop - container.scrollTop; if(offset < -maxDistance) { container.scrollTop += (offset + maxDistance); } else if(offset > maxDistance) { container.scrollTop += (offset - maxDistance); } } else if(forceDirection === FocusDirection.Up) { container.scrollTop = offsetTop + maxDistance; } else if(forceDirection === FocusDirection.Down) { container.scrollTop = Math.max(0, offsetTop - maxDistance); } */ } const promise = new Promise((resolve) => { fastRaf(() => { scrollWithJs(container, element, position, margin, forceDuration, axis, getNormalSize) .then(resolve); }); }); return axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise; } function scrollWithJs( container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, forceDuration?: number, axis: 'x' | 'y' = 'y', getNormalSize?: ScrollGetNormalSizeCallback ) { 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]; break; // 'nearest' is not supported yet case 'nearest': case 'center': path = elementSize < containerSize ? (elementPosition + elementSize / 2) - (containerSize / 2) : 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; } */ // 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); }