From a5b2f27b98c8182d4304627f7ff87628f9966b2a Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Sat, 22 Feb 2020 23:00:17 +0700 Subject: [PATCH] scroll try #3 --- src/components/scrollable.ts | 449 ++++++++++++---------- src/components/scrollable_goodnew.ts | 535 +++++++++++++++++++++++++++ src/lib/appManagers/appImManager.ts | 64 +--- src/lib/polyfill.ts | 19 +- 4 files changed, 824 insertions(+), 243 deletions(-) create mode 100644 src/components/scrollable_goodnew.ts diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 86031a06..b214df54 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -18,17 +18,17 @@ setTimeout(() => { export default class Scrollable { public container: HTMLDivElement; public thumb: HTMLDivElement; - + public type: string; public side: string; public scrollType: string; public scrollSide: string; public clientAxis: string; - + public scrollSize = -1; public size = 0; public thumbSize = 0; - + public hiddenElements: { up: {element: Element, height: number}[], down: {element: Element, height: number}[] @@ -37,144 +37,55 @@ export default class Scrollable { down: [] }; public paddings = {up: 0, down: 0}; - + public paddingTopDiv: HTMLDivElement; public paddingBottomDiv: HTMLDivElement; - + public splitUp: HTMLElement; - + public onAddedBottom: () => void = null; - + public onScrolledTop: () => void = null; + public onScrolledBottom: () => void = null; + public topObserver: IntersectionObserver; - public isTopIntersecting: boolean; public bottomObserver: IntersectionObserver; - public isBottomIntersecting: boolean; - + public splitObserver: IntersectionObserver; - public splitMeasure: Promise = null; - public splitMutate: Promise = null; - + public splitMeasure: Promise<{element: Element, height: number}[]> = null; + public splitMeasureAdd: Promise = null; + public splitMutate: Promise = null; + constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300) { this.container = document.createElement('div'); this.container.classList.add('scrollable'); - - let arr = []; - for(let i = 0.001; i < 1; i += 0.001) arr.push(i); + + //let arr = []; + //for(let i = 0.001; i < 1; i += 0.001) arr.push(i); this.topObserver = new IntersectionObserver(entries => { let entry = entries[0]; - console.log('top intersection:', entries, this.isTopIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); - if(this.isTopIntersecting = entry.isIntersecting) { - this.onTopIntersection(entry); + console.log('top intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0); + if(entry.isIntersecting) { + //this.onTopIntersection(entry); + this.onTopIntersection(entry.intersectionRect.height); + + if(this.onScrolledTop) this.onScrolledTop(); } // console.log('top intersection end'); - }, {threshold: arr}); - + }, {/* threshold: arr, */root: this.el}); + this.bottomObserver = new IntersectionObserver(entries => { let entry = entries[0]; - - console.log('bottom intersection:', entries, this.isBottomIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); - if(this.isBottomIntersecting = entry.isIntersecting) { - this.onBottomIntersection(entry); + + console.log('bottom intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0); + if(entry.isIntersecting) { + //this.onBottomIntersection(entry); + this.onBottomIntersection(entry.intersectionRect.height); if(this.onScrolledBottom) this.onScrolledBottom(); } - }, {threshold: arr}); - - this.splitObserver = new IntersectionObserver(entries => { - console.log('splitObserver', entries); - for(let entry of entries) { // there may be duplicates (1st - not intersecting, 2nd - intersecting) - //console.log('onscroll entry', entry.target, entry.isIntersecting, entry); - if(!entry.isIntersecting && entry.target.parentElement && entry.rootBounds) { - let child = entry.target; - //console.log('onscroll entry', entry.boundingClientRect, child, entry); - - let isTop = entry.boundingClientRect.top <= 0; - let isBottom = entry.rootBounds.height <= entry.boundingClientRect.top; - - let needHeight = this.splitOffset; - //console.log('will call measure'); - if(isTop) { - this.onBottomIntersection(entry); - - if(this.splitMeasure) fastdom.clear(this.splitMeasure); - this.splitMeasure = fastdom.measure(() => { - let sliced: {element: Element, height: number}[] = [/* child */]; - - do { - if(needHeight > 0) { - needHeight -= child.scrollHeight; - } else { - sliced.push({element: child, height: child.scrollHeight}); - } - } while(child = child.previousElementSibling); - return sliced; - }); - - this.splitMeasure.then(sliced => { - if(this.splitMutate) fastdom.clear(this.splitMutate); - this.splitMutate = fastdom.mutate(() => { - let length = sliced.length; - for(let i = length - 1; i >= 0; --i) { - let {element, height} = sliced[i]; - - if(!this.splitUp.contains(element)) continue; + }, {/* threshold: arr, */root: this.el}); - this.paddings.up += height; - this.hiddenElements.up.push(sliced[i]); - this.splitUp.removeChild(element); - //element.parentElement.removeChild(element); - } - - this.paddingTopDiv.style.height = this.paddings.up + 'px'; - }); - }); - - //console.log('onscroll sliced up', sliced); - } else if(isBottom) { - this.onTopIntersection(entry); - - if(this.splitMeasure) fastdom.clear(this.splitMeasure); - this.splitMeasure = fastdom.measure(() => { - let sliced: {element: Element, height: number}[] = [/* child */]; - - do { - if(needHeight > 0) { - needHeight -= child.scrollHeight; - } else { - sliced.push({element: child, height: child.scrollHeight}); - } - } while(child = child.nextElementSibling); - return sliced; - }); - - this.splitMeasure.then(sliced => { - if(this.splitMutate) fastdom.clear(this.splitMutate); - this.splitMutate = fastdom.mutate(() => { - let length = sliced.length; - for(let i = length - 1; i >= 0; --i) { - let {element, height} = sliced[i]; - - if(!this.splitUp.contains(element)) continue; - - this.paddings.down += height; - this.hiddenElements.down.unshift(sliced[i]); - this.splitUp.removeChild(element); - //element.parentElement.removeChild(element); - } - - this.paddingBottomDiv.style.height = this.paddings.down + 'px'; - }); - }); - - //console.log('onscroll sliced down', sliced); - } - - //console.log('splitObserver', entry, entry.target, isTop); - } - } - }); - if(x) { this.container.classList.add('scrollable-x'); this.type = 'width'; @@ -182,7 +93,7 @@ export default class Scrollable { this.scrollType = 'scrollWidth'; this.scrollSide = 'scrollLeft'; this.clientAxis = 'clientX'; - + let scrollHorizontally = (e: any) => { e = window.event || e; var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))); @@ -209,47 +120,47 @@ export default class Scrollable { } else { throw new Error('no side for scroll'); } - + this.thumb = document.createElement('div'); this.thumb.className = 'scrollbar-thumb'; // @ts-ignore this.thumb.style[this.type] = '30px'; - + // mouse scroll let onMouseMove = (e: MouseEvent) => { let rect = this.thumb.getBoundingClientRect(); - + let diff: number; // @ts-ignore diff = e[this.clientAxis] - rect[this.side]; // @ts-ignore this.container[this.scrollSide] += diff * 0.5; - + // console.log('onMouseMove', e, diff); - + cancelEvent(e); }; - + this.thumb.addEventListener('mousedown', () => { window.addEventListener('mousemove', onMouseMove); - + window.addEventListener('mouseup', () => { window.removeEventListener('mousemove', onMouseMove); }, {once: true}); }); - + //this.container.addEventListener('mouseover', this.resize.bind(this)); // omg window.addEventListener('resize', this.resize.bind(this)); - + this.paddingTopDiv = document.createElement('div'); this.paddingTopDiv.classList.add('scroll-padding'); this.paddingBottomDiv = document.createElement('div'); this.paddingBottomDiv.classList.add('scroll-padding'); - + this.topObserver.observe(this.paddingTopDiv); this.bottomObserver.observe(this.paddingBottomDiv); - + this.container.addEventListener('scroll', this.onScroll.bind(this)); Array.from(el.children).forEach(c => this.container.append(c)); @@ -258,16 +169,140 @@ export default class Scrollable { this.container.parentElement.append(this.thumb); this.resize(); } + + public splitObserve(entries: IntersectionObserverEntry[]) { + console.log('splitObserver', entries); + for(let entry of entries) { // there may be duplicates (1st - not intersecting, 2nd - intersecting) + //console.log('onscroll entry', entry.target, entry.isIntersecting, entry); + if(!entry.target.parentElement || !entry.rootBounds) continue; + + let child = entry.target; + let needHeight = this.splitOffset; + if(!entry.isIntersecting) { + let isTop = entry.boundingClientRect.top <= 0; + let isBottom = entry.rootBounds.height <= entry.boundingClientRect.top; + console.log('onscroll entry', isTop, isBottom, child, entry); + + //console.log('will call measure'); + if(isTop) { // when scrolling down + //this.onBottomIntersection(entry); + + if(this.splitMeasure) fastdom.clear(this.splitMeasure); + this.splitMeasure = fastdom.measure(() => { + let sliced: {element: Element, height: number}[] = []; + + do { + if(needHeight > 0) { + needHeight -= child.scrollHeight; + } else { + sliced.push({element: child, height: child.scrollHeight}); + } + } while(child = child.previousElementSibling); + return sliced; + }); + + this.splitMeasure.then(sliced => { + if(this.splitMutate) fastdom.clear(this.splitMutate); + + this.splitMutate = fastdom.mutate(() => { + sliced.forEachReverse((child) => { + let {element, height} = child; + if(!this.splitUp.contains(element)) return; + + this.paddings.up += height; + this.hiddenElements.up.push(child); + this.splitUp.removeChild(element); + //element.parentElement.removeChild(element); + }); + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + }); + }); + + //console.log('onscroll sliced up', sliced); + } else if(isBottom) { // when scrolling top + //this.onTopIntersection(entry); + + if(this.splitMeasure) fastdom.clear(this.splitMeasure); + this.splitMeasure = fastdom.measure(() => { + let sliced: {element: Element, height: number}[] = []; + + do { + if(needHeight > 0) { + needHeight -= child.scrollHeight; + } else { + sliced.push({element: child, height: child.scrollHeight}); + } + } while(child = child.nextElementSibling); + return sliced; + }); + + this.splitMeasure.then(sliced => { + if(this.splitMutate) fastdom.clear(this.splitMutate); + + this.splitMutate = fastdom.mutate(() => { + sliced.forEachReverse((child) => { + let {element, height} = child; + if(!this.splitUp.contains(element)) return; + + this.paddings.down += height; + this.hiddenElements.down.unshift(child); + this.splitUp.removeChild(element); + //element.parentElement.removeChild(element); + }); + + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + }); + }); + + //console.log('onscroll sliced down', sliced); + } + + //console.log('splitObserver', entry, entry.target, isTop); + } else { + let isTop = entry.boundingClientRect.top <= entry.rootBounds.top; + let isBottom = entry.boundingClientRect.bottom >= entry.rootBounds.bottom; + + if(isTop) { // when scrolling up + if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd); + this.splitMeasureAdd = fastdom.measure(() => { + while(child = child.previousElementSibling) { + needHeight -= child.scrollHeight; + } + + return needHeight; + }); + + this.splitMeasureAdd.then(needHeight => { + this.onTopIntersection(needHeight); + }); + } else if(isBottom) { // when scrolling down + if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd); + this.splitMeasureAdd = fastdom.measure(() => { + while(child = child.nextElementSibling) { + needHeight -= child.scrollHeight; + } + + return needHeight; + }); + + this.splitMeasureAdd.then(needHeight => { + this.onBottomIntersection(needHeight); + }); + } + } + } + } + public async resize() { //console.time('scroll resize'); - + await fastdom.measure(() => { // @ts-ignore this.scrollSize = this.container[this.scrollType]; - + let rect = this.container.getBoundingClientRect(); - + // @ts-ignore this.size = rect[this.type]; }); @@ -291,26 +326,32 @@ export default class Scrollable { // @ts-ignore this.thumb.style[this.type] = this.thumbSize + 'px'; }); - + //console.timeEnd('scroll resize'); // @ts-ignore //console.log('onresize', thumb.style[type], thumbHeight, height); } - + public async setVirtualContainer(el?: HTMLElement) { this.splitUp = el; this.hiddenElements.up.length = this.hiddenElements.down.length = 0; this.paddings.up = this.paddings.down = 0; - + if(this.paddingTopDiv.parentElement) { fastdom.mutate(() => { this.paddingTopDiv.style.height = ''; this.paddingBottomDiv.style.height = ''; }); } + + if(this.splitObserver) { + this.splitObserver.disconnect(); + } + this.splitObserver = new IntersectionObserver((entries) => this.splitObserve(entries), {root: this.el}); + if(el) { fastdom.mutate(() => { el.parentElement.insertBefore(this.paddingTopDiv, el); @@ -318,7 +359,7 @@ export default class Scrollable { }); } } - + public async onScroll() { //console.time('scroll onScroll'); let {value, maxValue} = await fastdom.measure(() => { @@ -326,48 +367,47 @@ export default class Scrollable { if(this.container[this.scrollType] != this.scrollSize || this.thumbSize == 0) { this.resize(); } - + // @ts-ignore let value = this.container[this.scrollSide] / (this.scrollSize - this.size) * 100; let maxValue = 100 - (this.thumbSize / this.size * 100); return {value, maxValue}; }); - + //console.log('onscroll', container.scrollHeight, thumbHeight, height, value, maxValue); fastdom.mutate(() => { // @ts-ignore this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%'; }); - + //console.timeEnd('scroll onScroll'); } - - public async onTopIntersection(entry: IntersectionObserverEntry) { - console.log('onTopIntersection'); - + + public async onTopIntersection(/* entry: IntersectionObserverEntry */needHeight: number) { + console.log('onTopIntersection', needHeight, this); + if(this.hiddenElements.up.length && this.paddings.up) { - let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; - + //let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; + //let needHeight = entry.intersectionRect.height || await fastdom.measure(() => this.splitUp.lastElementChild.scrollHeight); + let fragment = document.createDocumentFragment(); while(needHeight > 0 && this.paddings.up) { let child = this.hiddenElements.up.pop(); // console.log('top returning from hidden', child); - + if(!child) { this.paddings.up = 0; - this.paddingTopDiv.style.height = '0px'; break; } - + fragment.prepend(child.element); - let height = child.height; - - needHeight -= height; - this.paddings.up -= height; + + needHeight -= child.height; + this.paddings.up -= child.height; } - + await fastdom.mutate(() => { this.splitUp.prepend(fragment); this.paddingTopDiv.style.height = this.paddings.up + 'px'; @@ -378,30 +418,29 @@ export default class Scrollable { }); } } - - public async onBottomIntersection(entry: IntersectionObserverEntry) { - console.log('onBottomIntersection'); - + + public async onBottomIntersection(/* entry: IntersectionObserverEntry */needHeight: number) { + console.log('onBottomIntersection', needHeight, this); + if(this.hiddenElements.down.length && this.paddings.down) { - let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; - + //let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; + //let needHeight = entry.intersectionRect.height || await fastdom.measure(() => this.splitUp.firstElementChild.scrollHeight); + let fragment = document.createDocumentFragment(); while(needHeight > 0 && this.paddings.down) { let child = this.hiddenElements.down.shift(); - + if(!child) { this.paddings.down = 0; - this.paddingBottomDiv.style.height = '0px'; break; } - + fragment.appendChild(child.element); - let height = child.height; - - needHeight -= height; - this.paddings.down -= height; + + needHeight -= child.height; + this.paddings.down -= child.height; } - + await fastdom.mutate(() => { this.splitUp.appendChild(fragment); this.paddingBottomDiv.style.height = this.paddings.down + 'px'; @@ -413,15 +452,22 @@ export default class Scrollable { }); } } - - public onScrolledBottom() { - - } - + public prepend(...smth: (string | Node)[]) { if(this.splitUp) { - this.splitUp.prepend(...smth); - + if(this.hiddenElements.up.length) { + smth.forEach(node => { + if(typeof(node) !== 'string') { + this.hiddenElements.up.push({ + element: node as Element, + height: (node as Element).scrollHeight || 1 + }); + } + }); + } else { + this.splitUp.prepend(...smth); + } + for(let node of smth) { if(typeof(node) !== 'string') { this.splitObserver.unobserve(node as Element); @@ -432,11 +478,22 @@ export default class Scrollable { this.container.prepend(...smth); } } - + public append(...smth: (string | Node)[]) { if(this.splitUp) { - this.splitUp.append(...smth); - + if(this.hiddenElements.down.length) { + smth.forEachReverse(node => { + if(typeof(node) !== 'string') { + this.hiddenElements.down.unshift({ + element: node as Element, + height: (node as Element).scrollHeight || 1 + }); + } + }); + } else { + this.splitUp.append(...smth); + } + for(let node of smth) { if(typeof(node) !== 'string') { this.splitObserver.unobserve(node as Element); @@ -447,32 +504,50 @@ export default class Scrollable { this.container.append(...smth); } } - + public insertBefore(newChild: Element, refChild: Element) { if(this.splitUp) { this.splitObserver.unobserve(newChild); + this.splitObserver.observe(newChild); + + let index = -1; + index = this.hiddenElements.up.findIndex(c => c.element == refChild); + + // возможно здесь нужно очищать предыдущую высоту если newChild уже скрыт (но может и не нужно) + if(index !== -1) { + this.hiddenElements.up.splice(index, 0, {element: newChild, height: newChild.scrollHeight || 1}); + return index; + } else { + index = this.hiddenElements.down.findIndex(c => c.element == newChild); + + if(index !== -1) { + this.hiddenElements.down.splice(index, 0, {element: newChild, height: newChild.scrollHeight || 1}); + return index; + } + } + return this.splitUp.insertBefore(newChild, refChild); } - + return this.container.insertBefore(newChild, refChild); } - + set scrollTop(y: number) { this.container.scrollTop = y; } - + get scrollTop() { return this.container.scrollTop; } - + get scrollHeight() { return this.container.scrollHeight; } - + get parentElement() { return this.container.parentElement; } - + get offsetHeight() { return this.container.offsetHeight; } diff --git a/src/components/scrollable_goodnew.ts b/src/components/scrollable_goodnew.ts new file mode 100644 index 00000000..bd96abf4 --- /dev/null +++ b/src/components/scrollable_goodnew.ts @@ -0,0 +1,535 @@ +import { cancelEvent } from "../lib/utils"; + +//import {measure} from 'fastdom/fastdom.min'; +import FastDom from 'fastdom'; +import 'fastdom/src/fastdom-strict'; // exclude in production +import FastDomPromised from 'fastdom/extensions/fastdom-promised'; + +//const fastdom = FastDom.extend(FastDomPromised); +const fastdom = ((window as any).fastdom as typeof FastDom).extend(FastDomPromised); + +(window as any).fastdom.strict(false); + +setTimeout(() => { + //(window as any).fastdom.strict(true); +}, 5e3); + + +export default class Scrollable { + public container: HTMLDivElement; + public thumb: HTMLDivElement; + + public type: string; + public side: string; + public scrollType: string; + public scrollSide: string; + public clientAxis: string; + + public scrollSize = -1; + public size = 0; + public thumbSize = 0; + + public hiddenElements: { + up: {element: Element, height: number}[], + down: {element: Element, height: number}[] + } = { + up: [], + down: [] + }; + public paddings = {up: 0, down: 0}; + + public paddingTopDiv: HTMLDivElement; + public paddingBottomDiv: HTMLDivElement; + + public splitUp: HTMLElement; + + public onAddedBottom: () => void = null; + public onScrolledTop: () => void = null; + public onScrolledBottom: () => void = null; + + public topObserver: IntersectionObserver; + public bottomObserver: IntersectionObserver; + + public splitObserver: IntersectionObserver; + public splitMeasure: Promise<{element: Element, height: number}[]> = null; + public splitMutate: Promise = null; + + constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300) { + this.container = document.createElement('div'); + this.container.classList.add('scrollable'); + + //let arr = []; + //for(let i = 0.001; i < 1; i += 0.001) arr.push(i); + this.topObserver = new IntersectionObserver(entries => { + let entry = entries[0]; + + console.log('top intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0); + if(entry.isIntersecting) { + //this.onTopIntersection(entry); + this.onTopIntersection(entry.intersectionRect.height); + + if(this.onScrolledTop) this.onScrolledTop(); + } + // console.log('top intersection end'); + }, {/* threshold: arr, */root: this.el}); + + this.bottomObserver = new IntersectionObserver(entries => { + let entry = entries[0]; + + console.log('bottom intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0); + if(entry.isIntersecting) { + //this.onBottomIntersection(entry); + this.onBottomIntersection(entry.intersectionRect.height); + + if(this.onScrolledBottom) this.onScrolledBottom(); + } + }, {/* threshold: arr, */root: this.el}); + + if(x) { + this.container.classList.add('scrollable-x'); + this.type = 'width'; + this.side = 'left'; + this.scrollType = 'scrollWidth'; + this.scrollSide = 'scrollLeft'; + this.clientAxis = 'clientX'; + + let scrollHorizontally = (e: any) => { + e = window.event || e; + var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail))); + this.container.scrollLeft -= (delta * 20); + e.preventDefault(); + }; + if(this.container.addEventListener) { + // IE9, Chrome, Safari, Opera + this.container.addEventListener("mousewheel", scrollHorizontally, false); + // Firefox + this.container.addEventListener("DOMMouseScroll", scrollHorizontally, false); + } else { + // IE 6/7/8 + // @ts-ignore + this.container.attachEvent("onmousewheel", scrollHorizontally); + } + } else if(y) { + this.container.classList.add('scrollable-y'); + this.type = 'height'; + this.side = 'top'; + this.scrollType = 'scrollHeight'; + this.scrollSide = 'scrollTop'; + this.clientAxis = 'clientY'; + } else { + throw new Error('no side for scroll'); + } + + this.thumb = document.createElement('div'); + this.thumb.className = 'scrollbar-thumb'; + + // @ts-ignore + this.thumb.style[this.type] = '30px'; + + // mouse scroll + let onMouseMove = (e: MouseEvent) => { + let rect = this.thumb.getBoundingClientRect(); + + let diff: number; + // @ts-ignore + diff = e[this.clientAxis] - rect[this.side]; + // @ts-ignore + this.container[this.scrollSide] += diff * 0.5; + + // console.log('onMouseMove', e, diff); + + cancelEvent(e); + }; + + this.thumb.addEventListener('mousedown', () => { + window.addEventListener('mousemove', onMouseMove); + + window.addEventListener('mouseup', () => { + window.removeEventListener('mousemove', onMouseMove); + }, {once: true}); + }); + + //this.container.addEventListener('mouseover', this.resize.bind(this)); // omg + window.addEventListener('resize', this.resize.bind(this)); + + this.paddingTopDiv = document.createElement('div'); + this.paddingTopDiv.classList.add('scroll-padding'); + this.paddingBottomDiv = document.createElement('div'); + this.paddingBottomDiv.classList.add('scroll-padding'); + + this.topObserver.observe(this.paddingTopDiv); + this.bottomObserver.observe(this.paddingBottomDiv); + + this.container.addEventListener('scroll', this.onScroll.bind(this)); + + Array.from(el.children).forEach(c => this.container.append(c)); + + el.append(this.container); + this.container.parentElement.append(this.thumb); + this.resize(); + } + + public splitObserve(entries: IntersectionObserverEntry[]) { + console.log('splitObserver', entries); + for(let entry of entries) { // there may be duplicates (1st - not intersecting, 2nd - intersecting) + //console.log('onscroll entry', entry.target, entry.isIntersecting, entry); + if(!entry.isIntersecting && entry.target.parentElement && entry.rootBounds) { + let child = entry.target; + + let isTop = entry.boundingClientRect.top <= 0; + let isBottom = entry.rootBounds.height <= entry.boundingClientRect.top; + console.log('onscroll entry', isTop, isBottom, child, entry); + + let needHeight = this.splitOffset; + //console.log('will call measure'); + if(isTop) { // when scrolling down + //this.onBottomIntersection(entry); + + if(this.splitMeasure) fastdom.clear(this.splitMeasure); + this.splitMeasure = fastdom.measure(() => { + let sliced: {element: Element, height: number}[] = []; + + do { + if(needHeight > 0) { + needHeight -= child.scrollHeight; + } else { + sliced.push({element: child, height: child.scrollHeight}); + } + } while(child = child.previousElementSibling); + return sliced; + }); + + this.splitMeasure.then(sliced => { + if(this.splitMutate) fastdom.clear(this.splitMutate); + + this.splitMutate = fastdom.mutate(() => { + let sum = 0; + sliced.forEachReverse((child) => { + let {element, height} = child; + if(!this.splitUp.contains(element)) return; + + sum += height; + this.paddings.up += height; + this.hiddenElements.up.push(child); + this.splitUp.removeChild(element); + //element.parentElement.removeChild(element); + }); + + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + return sum; + }); + + this.splitMutate.then(sum => { + this.onBottomIntersection(sum); + }); + }); + + //console.log('onscroll sliced up', sliced); + } else if(isBottom) { // when scrolling top + //this.onTopIntersection(entry); + + if(this.splitMeasure) fastdom.clear(this.splitMeasure); + this.splitMeasure = fastdom.measure(() => { + let sliced: {element: Element, height: number}[] = []; + + do { + if(needHeight > 0) { + needHeight -= child.scrollHeight; + } else { + sliced.push({element: child, height: child.scrollHeight}); + } + } while(child = child.nextElementSibling); + return sliced; + }); + + this.splitMeasure.then(sliced => { + if(this.splitMutate) fastdom.clear(this.splitMutate); + + this.splitMutate = fastdom.mutate(() => { + let sum = 0; + sliced.forEachReverse((child) => { + let {element, height} = child; + if(!this.splitUp.contains(element)) return; + + sum += height; + this.paddings.down += height; + this.hiddenElements.down.unshift(child); + this.splitUp.removeChild(element); + //element.parentElement.removeChild(element); + }); + + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + return sum; + }); + + this.splitMutate.then(sum => { + this.onTopIntersection(sum); + }); + }); + + //console.log('onscroll sliced down', sliced); + } + + //console.log('splitObserver', entry, entry.target, isTop); + } + } + } + + public async resize() { + //console.time('scroll resize'); + + await fastdom.measure(() => { + // @ts-ignore + this.scrollSize = this.container[this.scrollType]; + + let rect = this.container.getBoundingClientRect(); + + // @ts-ignore + this.size = rect[this.type]; + }); + + await fastdom.mutate(() => { + if(!this.size || this.size == this.scrollSize) { + this.thumbSize = 0; + + // @ts-ignore + this.thumb.style[this.type] = this.thumbSize + 'px'; + //console.timeEnd('scroll resize'); + return; + } + //if(!height) return; + + let divider = this.scrollSize / this.size / 0.5; + this.thumbSize = this.size / divider; + + if(this.thumbSize < 20) this.thumbSize = 20; + + // @ts-ignore + this.thumb.style[this.type] = this.thumbSize + 'px'; + }); + + //console.timeEnd('scroll resize'); + + // @ts-ignore + //console.log('onresize', thumb.style[type], thumbHeight, height); + } + + public async setVirtualContainer(el?: HTMLElement) { + this.splitUp = el; + + this.hiddenElements.up.length = this.hiddenElements.down.length = 0; + this.paddings.up = this.paddings.down = 0; + + if(this.paddingTopDiv.parentElement) { + fastdom.mutate(() => { + this.paddingTopDiv.style.height = ''; + this.paddingBottomDiv.style.height = ''; + }); + } + + if(this.splitObserver) { + this.splitObserver.disconnect(); + } + + this.splitObserver = new IntersectionObserver((entries) => this.splitObserve(entries), {root: this.el}); + + if(el) { + fastdom.mutate(() => { + el.parentElement.insertBefore(this.paddingTopDiv, el); + el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); + }); + } + } + + public async onScroll() { + //console.time('scroll onScroll'); + let {value, maxValue} = await fastdom.measure(() => { + // @ts-ignore + if(this.container[this.scrollType] != this.scrollSize || this.thumbSize == 0) { + this.resize(); + } + + // @ts-ignore + let value = this.container[this.scrollSide] / (this.scrollSize - this.size) * 100; + let maxValue = 100 - (this.thumbSize / this.size * 100); + + return {value, maxValue}; + }); + + //console.log('onscroll', container.scrollHeight, thumbHeight, height, value, maxValue); + fastdom.mutate(() => { + // @ts-ignore + this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%'; + }); + + //console.timeEnd('scroll onScroll'); + } + + public async onTopIntersection(/* entry: IntersectionObserverEntry */needHeight: number) { + console.log('onTopIntersection', needHeight, this); + + if(this.hiddenElements.up.length && this.paddings.up) { + //let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; + //let needHeight = entry.intersectionRect.height || await fastdom.measure(() => this.splitUp.lastElementChild.scrollHeight); + + let fragment = document.createDocumentFragment(); + while(needHeight > 0 && this.paddings.up) { + let child = this.hiddenElements.up.pop(); + + // console.log('top returning from hidden', child); + + if(!child) { + this.paddings.up = 0; + break; + } + + fragment.prepend(child.element); + + needHeight -= child.height; + this.paddings.up -= child.height; + } + + await fastdom.mutate(() => { + this.splitUp.prepend(fragment); + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + }); + } else { + await fastdom.mutate(() => { + this.paddingTopDiv.style.height = '0px'; + }); + } + } + + public async onBottomIntersection(/* entry: IntersectionObserverEntry */needHeight: number) { + console.log('onBottomIntersection', needHeight, this); + + if(this.hiddenElements.down.length && this.paddings.down) { + //let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; + //let needHeight = entry.intersectionRect.height || await fastdom.measure(() => this.splitUp.firstElementChild.scrollHeight); + + let fragment = document.createDocumentFragment(); + while(needHeight > 0 && this.paddings.down) { + let child = this.hiddenElements.down.shift(); + + if(!child) { + this.paddings.down = 0; + break; + } + + fragment.appendChild(child.element); + + needHeight -= child.height; + this.paddings.down -= child.height; + } + + await fastdom.mutate(() => { + this.splitUp.appendChild(fragment); + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + }); + if(this.onAddedBottom) this.onAddedBottom(); + } else { + await fastdom.mutate(() => { + this.paddingBottomDiv.style.height = '0px'; + }); + } + } + + public prepend(...smth: (string | Node)[]) { + if(this.splitUp) { + if(this.hiddenElements.up.length) { + smth.forEach(node => { + if(typeof(node) !== 'string') { + this.hiddenElements.up.push({ + element: node as Element, + height: (node as Element).scrollHeight || 1 + }); + } + }); + } else { + this.splitUp.prepend(...smth); + } + + for(let node of smth) { + if(typeof(node) !== 'string') { + this.splitObserver.unobserve(node as Element); + this.splitObserver.observe(node as Element); + } + } + } else { + this.container.prepend(...smth); + } + } + + public append(...smth: (string | Node)[]) { + if(this.splitUp) { + if(this.hiddenElements.down.length) { + smth.forEachReverse(node => { + if(typeof(node) !== 'string') { + this.hiddenElements.down.unshift({ + element: node as Element, + height: (node as Element).scrollHeight || 1 + }); + } + }); + } else { + this.splitUp.append(...smth); + } + + for(let node of smth) { + if(typeof(node) !== 'string') { + this.splitObserver.unobserve(node as Element); + this.splitObserver.observe(node as Element); + } + } + } else { + this.container.append(...smth); + } + } + + public insertBefore(newChild: Element, refChild: Element) { + if(this.splitUp) { + this.splitObserver.unobserve(newChild); + this.splitObserver.observe(newChild); + + let index = -1; + index = this.hiddenElements.up.findIndex(c => c.element == refChild); + + // возможно здесь нужно очищать предыдущую высоту если newChild уже скрыт (но может и не нужно) + if(index !== -1) { + this.hiddenElements.up.splice(index, 0, {element: newChild, height: newChild.scrollHeight || 1}); + return index; + } else { + index = this.hiddenElements.down.findIndex(c => c.element == newChild); + + if(index !== -1) { + this.hiddenElements.down.splice(index, 0, {element: newChild, height: newChild.scrollHeight || 1}); + return index; + } + } + + return this.splitUp.insertBefore(newChild, refChild); + } + + return this.container.insertBefore(newChild, refChild); + } + + set scrollTop(y: number) { + this.container.scrollTop = y; + } + + get scrollTop() { + return this.container.scrollTop; + } + + get scrollHeight() { + return this.container.scrollHeight; + } + + get parentElement() { + return this.container.parentElement; + } + + get offsetHeight() { + return this.container.offsetHeight; + } +} diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index b5c2b32a..d34c30bc 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -256,10 +256,9 @@ export class AppImManager { this.pinnedMessageContent.innerHTML = RichTextProcessor.wrapEmojiText(message.message); } - let length = this.needUpdate.length; - for(let i = length - 1; i >= 0; --i) { - if(this.needUpdate[i].replyMid == mid) { - let {mid, replyMid} = this.needUpdate.splice(i, 1)[0]; + this.needUpdate.forEachReverse((obj, idx) => { + if(obj.replyMid == mid) { + let {mid, replyMid} = this.needUpdate.splice(idx, 1)[0]; //this.log('messages_downloaded', mid, replyMid, i, this.needUpdate, this.needUpdate.length, mids, this.bubbles[mid]); let bubble = this.bubbles[mid]; @@ -274,7 +273,7 @@ export class AppImManager { this.renderMessage(message, false, false, bubble, false); } - } + }); }); }); @@ -315,19 +314,6 @@ export class AppImManager { if(['IMG', 'VIDEO', 'SVG', 'DIV'].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV'); - /* if(target.tagName == 'VIDEO' && bubble.classList.contains('round')) { - let video = target as HTMLVideoElement; - video.currentTime = 0; - if(video.paused) { - video.play(); - video.volume = 1; - } else { - video.pause(); - video.volume = 0; - } - return; - } */ - if(target.tagName == 'DIV') { if(target.classList.contains('forward')) { let savedFrom = bubble.dataset.savedFrom; @@ -720,24 +706,20 @@ export class AppImManager { if(!this.myID) return Promise.resolve(); appUsersManager.setUserStatus(this.myID, this.offline); - return apiManager.invokeApi('account.updateStatus', { - offline: this.offline - }, {noErrorBox: true}); + return apiManager.invokeApi('account.updateStatus', {offline: this.offline}); } public onScroll() { - let length = this.unreaded.length; let readed: number[] = []; - for(let i = length - 1; i >= 0; --i) { - let msgID = this.unreaded[i]; + this.unreaded.forEachReverse((msgID, idx) => { let bubble = this.bubbles[msgID]; if(isElementInViewport(bubble)) { readed.push(msgID); - this.unreaded.splice(i, 1); + this.unreaded.splice(idx, 1); } - } + }); lottieLoader.checkAnimations(); @@ -844,7 +826,7 @@ export class AppImManager { } public setScroll() { - this.scrollable = new Scrollable(this.bubblesContainer, false, true, 1500); + this.scrollable = new Scrollable(this.bubblesContainer, false, true, 750/* 1500 */); this.scroll = this.scrollable.container; this.scrollable.setVirtualContainer(this.chatInner); @@ -1056,7 +1038,7 @@ export class AppImManager { this.titleEl.innerHTML = appSidebarRight.profileElements.name.innerHTML = title; this.topbar.style.display = this.goDownBtn.style.display = ''; - appSidebarRight.toggleSidebar(true); + //appSidebarRight.toggleSidebar(true); this.chatInput.style.display = appPeersManager.isChannel(peerID) && !appPeersManager.isMegagroup(peerID) ? 'none' : ''; @@ -1142,15 +1124,14 @@ export class AppImManager { ///////this.log('updateUnreadByDialog', maxID, dialog, this.unreadOut); let length = this.unreadOut.length; - for(let i = length - 1; i >= 0; --i) { - let msgID = this.unreadOut[i]; + this.unreadOut.forEachReverse((msgID, idx) => { if(msgID > 0 && msgID <= maxID) { let bubble = this.bubbles[msgID]; bubble.classList.remove('is-sent'); bubble.classList.add('is-read'); - this.unreadOut.splice(i, 1); + this.unreadOut.splice(idx, 1); } - } + }); } public deleteMessagesByIDs(msgIDs: number[]) { @@ -1632,11 +1613,6 @@ export class AppImManager { if(updatePosition) { bubble.classList.add(our ? 'is-out' : 'is-in'); - /* if(reverse) { - this.chatInner.prepend(bubble); - } else { - this.chatInner.append(bubble); - } */ if(reverse) { this.scrollable.prepend(bubble); } else { @@ -1673,14 +1649,11 @@ export class AppImManager { firstTimestamp: date.getTime() }; - //this.chatInner.insertBefore(div, containerDiv); - //containerDiv.insertBefore(div, bubble); - this.scrollable.insertBefore(div, bubble);// this.chatInner.insertBefore(div, bubble); + this.scrollable.insertBefore(div, bubble); } else { let dateMessage = this.dateMessages[dateTimestamp]; if(dateMessage.firstTimestamp > date.getTime()) { - //this.chatInner.insertBefore(dateMessage.div, containerDiv); - this.scrollable.insertBefore(dateMessage.div, bubble);// this.chatInner.insertBefore(dateMessage.div, bubble); + this.scrollable.insertBefore(dateMessage.div, bubble); } } } @@ -1765,14 +1738,11 @@ export class AppImManager { this.scrollPosition.prepareFor(reverse ? 'up' : 'down'); } - let length = history.length; - for(let i = length - 1; i >= 0; --i) { - let msgID = history[i]; - + history.forEachReverse((msgID: number) => { let message = appMessagesManager.getMessage(msgID); this.renderMessage(message, reverse, true); - } + }); if(!isBackLimit) { this.scrollPosition.restore(); diff --git a/src/lib/polyfill.ts b/src/lib/polyfill.ts index fdcaa97e..dafe952a 100644 --- a/src/lib/polyfill.ts +++ b/src/lib/polyfill.ts @@ -46,20 +46,21 @@ Uint8Array.prototype.concat = function(...args: Array(callback: (value: T, index?: number, array?: Array) => void) { + let length = this.length; + for(var i = length - 1; i >= 0; --i) { + callback(this[i], i, this); + } +}; declare global { interface Uint8Array { hex: string; randomize: () => Uint8Array, - //concat: (array: number[] | ArrayBuffer | Uint8Array) => Uint8Array concat: (...args: Array) => Uint8Array } + + interface Array { + forEachReverse(callback: (value: T, index?: number, array?: Array) => void): void; + } }