Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

302 lines
9.8 KiB

/*
* 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 {fastRafPromise} from './schedulers';
import {animateSingle, cancelAnimationByKey} from './animation';
import rootScope from '../lib/rootScope';
import isInDOM from './dom/isInDOM';
import liteMode from './liteMode';
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 ScrollGetElementPositionCallback = (options: {elementRect: DOMRect, containerRect: DOMRect, elementPosition: number}) => number;
export type ScrollStartCallbackDimensions = {
scrollSize: number,
scrollPosition: number,
distanceToEnd: number,
path: number,
duration: number,
containerRect: DOMRect,
elementRect: DOMRect,
getProgress: () => number
};
export type ScrollOptions = {
container: HTMLElement,
element: HTMLElement,
position: ScrollLogicalPosition,
margin?: number,
maxDistance?: number,
forceDirection?: FocusDirection,
forceDuration?: number,
axis?: 'x' | 'y',
getNormalSize?: ScrollGetNormalSizeCallback,
getElementPosition?: ScrollGetElementPositionCallback,
fallbackToElementStartWhenCentering?: HTMLElement,
startCallback?: (dimensions: ScrollStartCallbackDimensions) => void,
transitionFunction?: (value: number) => number
};
export default function fastSmoothScroll(options: ScrollOptions) {
options.margin ??= 0;
options.maxDistance ??= LONG_TRANSITION_MAX_DISTANCE;
options.axis ??= 'y';
// return;
if(!liteMode.isAvailable('animations') || options.forceDuration === 0) {
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 = fastRafPromise().then(() => scrollWithJs(options));
return options.axis === 'y' ? dispatchHeavyAnimationEvent(promise) : promise;
}
function scrollWithJs(options: ScrollOptions): Promise<void> {
const {element, container, getNormalSize, getElementPosition, transitionFunction, 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 elementScrollSizeKey = axis === 'y' ? 'scrollHeight' : 'offsetWidth'; // can use offsetWidth for X, since it's almost same as 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 possibleElementPosition = elementRect[rectStartKey] - containerRect[rectStartKey];
const elementPosition = getElementPosition ? getElementPosition({elementRect, containerRect, elementPosition: possibleElementPosition}) : possibleElementPosition;
const elementSize = element[elementScrollSizeKey]; // margin is exclusive in DOMRect
const containerSize = getNormalSize ? getNormalSize({rect: containerRect}) : containerRect[sizeKey];
let 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(Math.abs(path - (margin || 0)) < 1) {
cancelAnimationByKey(container);
return Promise.resolve();
}
if(axis === 'y') {
if(forceDirection === undefined) {
if(path > maxDistance) {
scrollPosition = container.scrollTop += path - maxDistance;
path = maxDistance;
} else if(path < -maxDistance) {
scrollPosition = 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 = transitionFunction ?? (absPath < SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition);
const getProgress = () => duration ? Math.min((Date.now() - startAt) / duration, 1) : 1;
const tick = () => {
const t = getProgress();
const value = transition(t);
const currentPath = path * (1 - value);
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 = ``;
});
}); */
if(options.startCallback) {
const distanceToEnd = scrollSize - Math.round(target + container[axis === 'y' ? 'offsetHeight' : 'offsetWidth']);
options.startCallback({
scrollSize,
scrollPosition,
distanceToEnd,
path,
duration,
containerRect,
elementRect,
getProgress
});
}
return animateSingle(tick, container);
}
function longTransition(t: number) {
return 1 - ((1 - t) ** 5);
}
function shortTransition(t: number) {
return 1 - ((1 - t) ** 3.5);
}