/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import Scrollable from '../components/scrollable'; import {MOUNT_CLASS_TO} from '../config/debug'; import {IS_SAFARI} from '../environment/userAgent'; import getVisibleRect from './dom/getVisibleRect'; import {fastRaf} from './schedulers'; // let USE_REFLOW = false; // if(IS_SAFARI) { // try { // // throw ''; // const match = navigator.userAgent.match(/Version\/(.+?) /); // USE_REFLOW = +match[1] < 15.4; // } catch(err) { // USE_REFLOW = true; // } // } export default class ScrollSaver { private scrollHeight: number; private scrollHeightMinusTop: number; private scrollTop: number; private clientHeight: number; private elements: {element: HTMLElement, rect: DOMRect}[]; /** * * @param scrollable to reset scroll position and direction * @param reverse true means top */ constructor( private scrollable: Scrollable, private query: string, private reverse: boolean ) { } private get container() { return this.scrollable.container; } public getSaved() { return { scrollHeight: this.scrollHeight, scrollTop: this.scrollTop, clientHeight: this.clientHeight }; } public findElements() { if(!this.query) return []; const {container} = this; const containerRect = container.getBoundingClientRect(); const bubbles = Array.from(container.querySelectorAll(this.query)) as HTMLElement[]; const elements: ScrollSaver['elements'] = []; for(const bubble of bubbles) { const elementRect = bubble.getBoundingClientRect(); const visibleRect = getVisibleRect(bubble, container, undefined, elementRect, containerRect); if(visibleRect) { elements.push({element: bubble, rect: elementRect}); // break; // find first } else if(elements.length) { // find last break; } } if(!elements.length) { const bubble = bubbles[0]; if(bubble) { elements.push({element: bubble, rect: bubble.getBoundingClientRect()}); } } return elements; } public replaceSaved(from: HTMLElement, to: HTMLElement) { if(!this.elements) { return; } const idx = this.elements.findIndex(({element}) => from === element); if(idx !== -1) { this.elements[idx].element = to; } } public findAndSetElements() { this.elements = this.findElements(); } public save() { this.findAndSetElements(); // console.warn('scroll save', this.elements); this._save(); } public _save() { const {scrollTop, scrollHeight, clientHeight} = this.container; // previousScrollHeight = scrollHeight; // previousScrollHeight = scrollHeight + padding; this.scrollHeight = scrollHeight; this.scrollTop = scrollTop; this.clientHeight = clientHeight; this.scrollHeightMinusTop = this.reverse ? scrollHeight - scrollTop : scrollTop; // this.chatInner.style.paddingTop = padding + 'px'; /* if(reverse) { previousScrollHeightMinusTop = this.scrollable.scrollHeight - scrollTop; } else { previousScrollHeightMinusTop = scrollTop; } */ } private onRestore(useReflow?: boolean) { // if(USE_REFLOW && useReflow/* && !isAppleMobile */) { // * fix blinking and jumping // reflowScrollableElement(this.container); // } this.scrollable.onSizeChange(); } private setScrollTop(newScrollTop: number, useReflow?: boolean) { // touchSupport for safari iOS // isTouchSupported && isApple && (container.container.style.overflow = 'hidden'); this.scrollable.setScrollPositionSilently(this.scrollTop = newScrollTop); // container.scrollTop = scrollHeight; // isTouchSupported && isApple && (container.container.style.overflow = ''); if(IS_SAFARI) { fastRaf(() => { if(this.scrollTop === newScrollTop) { this.scrollable.setScrollPositionSilently(this.scrollTop = newScrollTop); } }); } this.onRestore(useReflow); } public restore(useReflow?: boolean) { const {scrollPosition: scrollTop, scrollSize: scrollHeight} = this.scrollable; this.scrollHeight = scrollHeight; if(!this.elements.length) { // maybe all messages have been deleted or adding first message // this._restore(useReflow); this.setScrollTop(this.reverse ? scrollHeight : 0, useReflow); // fix scrolling to first new message return; } let anchor: ScrollSaver['elements'][0]; // for(let i = this.elements.length - 1; i >= 0; --i) { // const _anchor = this.elements[i]; // if(_anchor.element.parentElement) { // anchor = _anchor; // break; // } // } anchor = this.elements[this.elements.length - 1]; if(!anchor?.element?.parentElement) { // try to find new anchor this.findAndSetElements(); anchor = this.elements[this.elements.length - 1]; if(!anchor) { // fallback to old method if smth is really strange this._restore(useReflow); return; } } const {element, rect} = anchor; const newRect = element.getBoundingClientRect(); const diff = newRect.bottom - rect.bottom; this.setScrollTop(scrollTop + diff, useReflow); // if(diff) debugger; // console.warn('scroll restore', rect, diff, newRect); } public _restore(useReflow?: boolean) { const {scrollHeightMinusTop: previousScrollHeightMinusTop, scrollable} = this; // if(previousScrollHeightMinusTop === undefined) { // throw new Error('scroll was not saved'); // } // const scrollHeight = container.scrollHeight; const scrollHeight = this.scrollHeight; // if(scrollHeight === this.scrollHeight) { // return; // } // this.scrollHeight = scrollHeight; /* const scrollHeight = container.scrollHeight; const addedHeight = scrollHeight - previousScrollHeight; this.chatInner.style.paddingTop = (10000 - addedHeight) + 'px'; */ /* const scrollHeight = scrollHeight; const addedHeight = scrollHeight - previousScrollHeight; this.chatInner.style.paddingTop = (padding - addedHeight) + 'px'; //const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; const newScrollTop = reverse ? scrollHeight - addedHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; this.log('performHistoryResult: will set scrollTop', previousScrollHeightMinusTop, scrollHeight, newScrollTop, container.container.clientHeight); */ // const newScrollTop = reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; const newScrollTop = this.reverse ? scrollHeight - previousScrollHeightMinusTop : previousScrollHeightMinusTop; /* if(DEBUG) { this.log('performHistoryResult: will set up scrollTop:', newScrollTop, this.isHeavyAnimationInProgress); } */ this.setScrollTop(newScrollTop, useReflow); /* if(DEBUG) { this.log('performHistoryResult: have set up scrollTop:', newScrollTop, container.scrollTop, container.scrollHeight, this.isHeavyAnimationInProgress); } */ } } MOUNT_CLASS_TO && (MOUNT_CLASS_TO.ScrollSaver = ScrollSaver);