/* * 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 deferredPromise, {CancellablePromise} 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 = new Map(); let animationDeferred: CancellablePromise; // 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; 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(); // animationStarted = performance.now(); } dispatchHeavyAnimationEvent(animationDeferred, transitionTime * 2); } } from = to; } // selectTab.prevId = -1; selectTab.prevId = () => from ? whichChild(from) : -1; return selectTab; }; export default Transition;