|
|
|
/*
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import rootScope from "../lib/rootScope";
|
|
|
|
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
|
|
|
|
import { dispatchHeavyAnimationEvent } from "../hooks/useHeavyAnimationCheck";
|
|
|
|
import whichChild from "../helpers/dom/whichChild";
|
|
|
|
import { cancelEvent } from "../helpers/dom/cancelEvent";
|
|
|
|
|
|
|
|
function slideNavigation(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
|
|
|
|
const width = prevTabContent.getBoundingClientRect().width;
|
|
|
|
const elements = [tabContent, prevTabContent];
|
|
|
|
if(toRight) elements.reverse();
|
|
|
|
elements[0].style.filter = `brightness(80%)`;
|
|
|
|
elements[0].style.transform = `translate3d(${-width * .25}px, 0, 0)`;
|
|
|
|
elements[1].style.transform = `translate3d(${width}px, 0, 0)`;
|
|
|
|
|
|
|
|
tabContent.classList.add('active');
|
|
|
|
void tabContent.offsetWidth; // reflow
|
|
|
|
|
|
|
|
tabContent.style.transform = '';
|
|
|
|
tabContent.style.filter = '';
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
prevTabContent.style.transform = prevTabContent.style.filter = '';
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function slideTabs(tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) {
|
|
|
|
// Jolly Cobra's // Workaround for scrollable content flickering during animation.
|
|
|
|
// const scrollableContainer = findUpClassName(tabContent, 'scrollable-y');
|
|
|
|
// if(scrollableContainer && scrollableContainer.style.overflowY !== 'hidden') {
|
|
|
|
// // const scrollBarWidth = scrollableContainer.offsetWidth - scrollableContainer.clientWidth;
|
|
|
|
// scrollableContainer.style.overflowY = 'hidden';
|
|
|
|
// // scrollableContainer.style.paddingRight = `${scrollBarWidth}px`;
|
|
|
|
// // this.container.classList.add('sliding');
|
|
|
|
// }
|
|
|
|
|
|
|
|
//window.requestAnimationFrame(() => {
|
|
|
|
const width = prevTabContent.getBoundingClientRect().width;
|
|
|
|
/* tabContent.style.setProperty('--width', width + 'px');
|
|
|
|
prevTabContent.style.setProperty('--width', width + 'px');
|
|
|
|
|
|
|
|
tabContent.classList.add('active'); */
|
|
|
|
//void tabContent.offsetWidth; // reflow
|
|
|
|
const elements = [tabContent, prevTabContent];
|
|
|
|
if(toRight) elements.reverse();
|
|
|
|
elements[0].style.transform = `translate3d(${-width}px, 0, 0)`;
|
|
|
|
elements[1].style.transform = `translate3d(${width}px, 0, 0)`;
|
|
|
|
|
|
|
|
tabContent.classList.add('active');
|
|
|
|
void tabContent.offsetWidth; // reflow
|
|
|
|
|
|
|
|
tabContent.style.transform = '';
|
|
|
|
//});
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
prevTabContent.style.transform = '';
|
|
|
|
|
|
|
|
// if(scrollableContainer) {
|
|
|
|
// // Jolly Cobra's // Workaround for scrollable content flickering during animation.
|
|
|
|
// if(isSafari) { // ! safari doesn't respect sticky header, so it flicks when overflow is changing
|
|
|
|
// scrollableContainer.style.display = 'none';
|
|
|
|
// }
|
|
|
|
|
|
|
|
// scrollableContainer.style.overflowY = '';
|
|
|
|
|
|
|
|
// if(isSafari) {
|
|
|
|
// void scrollableContainer.offsetLeft; // reflow
|
|
|
|
// scrollableContainer.style.display = '';
|
|
|
|
// }
|
|
|
|
|
|
|
|
// // scrollableContainer.style.paddingRight = '0';
|
|
|
|
// // this.container.classList.remove('sliding');
|
|
|
|
// }
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export const TransitionSlider = (
|
|
|
|
content: HTMLElement,
|
|
|
|
type: 'tabs' | 'navigation' | 'zoom-fade' | 'slide-fade' | 'none'/* | 'counter' */,
|
|
|
|
transitionTime: number,
|
|
|
|
onTransitionEnd?: (id: number) => void,
|
|
|
|
isHeavy = true
|
|
|
|
) => {
|
|
|
|
let animationFunction: TransitionFunction = null;
|
|
|
|
|
|
|
|
switch(type) {
|
|
|
|
case 'tabs':
|
|
|
|
animationFunction = slideTabs;
|
|
|
|
break;
|
|
|
|
case 'navigation':
|
|
|
|
animationFunction = slideNavigation;
|
|
|
|
break;
|
|
|
|
/* default:
|
|
|
|
break; */
|
|
|
|
}
|
|
|
|
|
|
|
|
content.dataset.animation = type;
|
|
|
|
|
|
|
|
return Transition(content, animationFunction, transitionTime, onTransitionEnd, isHeavy);
|
|
|
|
};
|
|
|
|
|
|
|
|
type TransitionFunction = (tabContent: HTMLElement, prevTabContent: HTMLElement, toRight: boolean) => void | (() => void);
|
|
|
|
|
|
|
|
const Transition = (
|
|
|
|
content: HTMLElement,
|
|
|
|
animationFunction: TransitionFunction,
|
|
|
|
transitionTime: number,
|
|
|
|
onTransitionEnd?: (id: number) => void,
|
|
|
|
isHeavy = true,
|
|
|
|
once = false,
|
|
|
|
withAnimationListener = true
|
|
|
|
) => {
|
|
|
|
const onTransitionEndCallbacks: Map<HTMLElement, Function> = new Map();
|
|
|
|
let animationDeferred: CancellablePromise<void>;
|
|
|
|
// let animationStarted = 0;
|
|
|
|
let from: HTMLElement = null;
|
|
|
|
|
|
|
|
if(withAnimationListener) {
|
|
|
|
const listenerName = animationFunction ? 'transitionend' : 'animationend';
|
|
|
|
|
|
|
|
const onEndEvent = (e: TransitionEvent | AnimationEvent) => {
|
|
|
|
cancelEvent(e);
|
|
|
|
|
|
|
|
if((e.target as HTMLElement).parentElement !== content) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
//console.log('Transition: transitionend', /* content, */ e, selectTab.prevId, performance.now() - animationStarted);
|
|
|
|
|
|
|
|
const callback = onTransitionEndCallbacks.get(e.target as HTMLElement);
|
|
|
|
if(callback) callback();
|
|
|
|
|
|
|
|
if(e.target !== from) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!animationDeferred && isHeavy) return;
|
|
|
|
|
|
|
|
if(animationDeferred) {
|
|
|
|
animationDeferred.resolve();
|
|
|
|
animationDeferred = undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(onTransitionEnd) {
|
|
|
|
onTransitionEnd(selectTab.prevId());
|
|
|
|
}
|
|
|
|
|
|
|
|
content.classList.remove('animating', 'backwards', 'disable-hover');
|
|
|
|
|
|
|
|
if(once) {
|
|
|
|
content.removeEventListener(listenerName, onEndEvent/* , {capture: false} */);
|
|
|
|
from = animationDeferred = undefined;
|
|
|
|
onTransitionEndCallbacks.clear();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// TODO: check for transition type (transform, etc) using by animationFunction
|
|
|
|
content.addEventListener(listenerName, onEndEvent/* , {passive: true, capture: false} */);
|
|
|
|
}
|
|
|
|
|
|
|
|
function selectTab(id: number | HTMLElement, animate = true, overrideFrom?: typeof from) {
|
|
|
|
if(overrideFrom) {
|
|
|
|
from = overrideFrom;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(id instanceof HTMLElement) {
|
|
|
|
id = whichChild(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
const prevId = selectTab.prevId();
|
|
|
|
if(id === prevId) return false;
|
|
|
|
|
|
|
|
//console.log('selectTab id:', id);
|
|
|
|
|
|
|
|
const to = content.children[id] as HTMLElement;
|
|
|
|
|
|
|
|
if(!rootScope.settings.animationsEnabled || prevId === -1) {
|
|
|
|
animate = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!withAnimationListener) {
|
|
|
|
const timeout = content.dataset.timeout;
|
|
|
|
if(timeout !== undefined) {
|
|
|
|
clearTimeout(+timeout);
|
|
|
|
}
|
|
|
|
|
|
|
|
delete content.dataset.timeout;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!animate) {
|
|
|
|
if(from) from.classList.remove('active', 'to', 'from');
|
|
|
|
else if(to) { // fix instant opening back from closed slider (e.g. instant closening and opening right sidebar)
|
|
|
|
const callback = onTransitionEndCallbacks.get(to);
|
|
|
|
if(callback) {
|
|
|
|
callback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(to) {
|
|
|
|
to.classList.remove('to', 'from');
|
|
|
|
to.classList.add('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
content.classList.remove('animating', 'backwards', 'disable-hover');
|
|
|
|
|
|
|
|
from = to;
|
|
|
|
|
|
|
|
if(onTransitionEnd) onTransitionEnd(id);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!withAnimationListener) {
|
|
|
|
content.dataset.timeout = '' + window.setTimeout(() => {
|
|
|
|
to.classList.remove('to');
|
|
|
|
from && from.classList.remove('from');
|
|
|
|
content.classList.remove('animating', 'backwards', 'disable-hover');
|
|
|
|
delete content.dataset.timeout;
|
|
|
|
}, transitionTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(from) {
|
|
|
|
from.classList.remove('to');
|
|
|
|
from.classList.add('from');
|
|
|
|
}
|
|
|
|
|
|
|
|
content.classList.add('animating'/* , 'disable-hover' */);
|
|
|
|
const toRight = prevId < id;
|
|
|
|
content.classList.toggle('backwards', !toRight);
|
|
|
|
|
|
|
|
let onTransitionEndCallback: ReturnType<TransitionFunction>;
|
|
|
|
if(!to) {
|
|
|
|
//prevTabContent.classList.remove('active');
|
|
|
|
} else {
|
|
|
|
if(animationFunction) {
|
|
|
|
onTransitionEndCallback = animationFunction(to, from, toRight);
|
|
|
|
} else {
|
|
|
|
to.classList.add('active');
|
|
|
|
}
|
|
|
|
|
|
|
|
to.classList.remove('from');
|
|
|
|
to.classList.add('to');
|
|
|
|
}
|
|
|
|
|
|
|
|
if(to) {
|
|
|
|
onTransitionEndCallbacks.set(to, () => {
|
|
|
|
to.classList.remove('to');
|
|
|
|
onTransitionEndCallbacks.delete(to);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if(from/* && false */) {
|
|
|
|
const _from = from;
|
|
|
|
const callback = () => {
|
|
|
|
_from.classList.remove('active', 'from');
|
|
|
|
|
|
|
|
if(onTransitionEndCallback) {
|
|
|
|
onTransitionEndCallback();
|
|
|
|
}
|
|
|
|
|
|
|
|
onTransitionEndCallbacks.delete(_from);
|
|
|
|
};
|
|
|
|
|
|
|
|
if(to) {
|
|
|
|
onTransitionEndCallbacks.set(_from, callback);
|
|
|
|
} else {
|
|
|
|
const timeout = window.setTimeout(callback, transitionTime);
|
|
|
|
onTransitionEndCallbacks.set(_from, () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
onTransitionEndCallbacks.delete(_from);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if(isHeavy) {
|
|
|
|
if(!animationDeferred) {
|
|
|
|
animationDeferred = deferredPromise<void>();
|
|
|
|
// animationStarted = performance.now();
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
from = to;
|
|
|
|
}
|
|
|
|
|
|
|
|
//selectTab.prevId = -1;
|
|
|
|
selectTab.prevId = () => from ? whichChild(from) : -1;
|
|
|
|
|
|
|
|
return selectTab;
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Transition;
|