diff --git a/package-lock.json b/package-lock.json index 9dc923b8..eca1e2e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4004,6 +4004,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastdom": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.9.tgz", + "integrity": "sha512-SSp4fbVzu8JkkG01NUX+0iOwe9M5PN3MGIQ84txLf4TkkJG4q30khkzumKgi4hUqO1+jX6wLHfnCPoZ6eSZ6Tg==", + "dev": true, + "requires": { + "strictdom": "^1.0.1" + } + }, "faye-websocket": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", @@ -12823,6 +12832,12 @@ "integrity": "sha512-IpXeZ67YxcsrfZHe3yg/IyZ5KPfRSn1teDy5mRX2e8M6K410NcJNcR+SFQ2Z92DO36VBUArQP4Vy3Qu33MwIOQ==", "dev": true }, + "strictdom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", + "integrity": "sha1-GJ3pFkn3PUTVm4Qy76aO+dJllGA=", + "dev": true + }, "string-length": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", diff --git a/package.json b/package.json index 0b35c076..b726d817 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "babel-jest": "^24.9.0", "compression-webpack-plugin": "^3.1.0", "css-loader": "^3.2.0", + "fastdom": "^1.0.9", "file-loader": "^4.3.0", "html-webpack-plugin": "^3.2.0", "install": "^0.13.0", diff --git a/src/components/pageIm.ts b/src/components/pageIm.ts index 87fef116..03b895b2 100644 --- a/src/components/pageIm.ts +++ b/src/components/pageIm.ts @@ -1,8 +1,6 @@ //import { appImManager, appMessagesManager, appDialogsManager, apiUpdatesManager, appUsersManager } from "../lib/services"; import { openBtnMenu } from "./misc"; - import {stackBlurImage} from '../lib/StackBlur'; - import appSidebarLeft from "../lib/appManagers/appSidebarLeft"; export default () => import('../lib/services').then(services => { diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index 1e63ddb2..86031a06 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -1,4 +1,19 @@ -import { isElementInViewport, isScrolledIntoView, cancelEvent } from "../lib/utils"; +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; @@ -27,7 +42,6 @@ export default class Scrollable { public paddingBottomDiv: HTMLDivElement; public splitUp: HTMLElement; - public splitOffset = 0; public onAddedBottom: () => void = null; @@ -37,8 +51,10 @@ export default class Scrollable { public isBottomIntersecting: boolean; public splitObserver: IntersectionObserver; + public splitMeasure: Promise = null; + public splitMutate: Promise = null; - constructor(public el: HTMLDivElement, x = false, y = true) { + constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300) { this.container = document.createElement('div'); this.container.classList.add('scrollable'); @@ -47,7 +63,7 @@ export default class Scrollable { this.topObserver = new IntersectionObserver(entries => { let entry = entries[0]; - // console.log('top intersection:', entries, this.isTopIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); + console.log('top intersection:', entries, this.isTopIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); if(this.isTopIntersecting = entry.isIntersecting) { this.onTopIntersection(entry); } @@ -57,7 +73,7 @@ export default class Scrollable { this.bottomObserver = new IntersectionObserver(entries => { let entry = entries[0]; - // console.log('bottom intersection:', entries, this.isBottomIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); + console.log('bottom intersection:', entries, this.isBottomIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); if(this.isBottomIntersecting = entry.isIntersecting) { this.onBottomIntersection(entry); @@ -66,48 +82,89 @@ export default class Scrollable { }, {threshold: arr}); this.splitObserver = new IntersectionObserver(entries => { - //console.log('splitObserver', entries); - - for(let entry of 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) { + 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 + this.splitOffset) <= 0; + 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) { - let sliced: Element[] = [child]; - - while(child.previousElementSibling) { - sliced.push(child = child.previousElementSibling); - } - - sliced.reverse(); - sliced.forEach(child => { - let height = child.scrollHeight; - this.paddings.up += height; - this.hiddenElements.up.push({element: child, height}); - child.parentElement.removeChild(child); - this.paddingTopDiv.style.height = this.paddings.up + 'px'; + 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; + + 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) { - let sliced: Element[] = [child]; - - while(child.nextElementSibling) { - sliced.push(child = child.nextElementSibling); - } - - sliced.reverse(); - sliced.forEach(child => { - let height = child.scrollHeight; - this.paddings.down += height; - this.hiddenElements.down.unshift({element: child, height}); - child.parentElement.removeChild(child); - this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + 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); @@ -159,6 +216,7 @@ export default class Scrollable { // @ts-ignore this.thumb.style[this.type] = '30px'; + // mouse scroll let onMouseMove = (e: MouseEvent) => { let rect = this.thumb.getBoundingClientRect(); @@ -194,101 +252,104 @@ export default class Scrollable { this.container.addEventListener('scroll', this.onScroll.bind(this)); - //this.container.append(this.paddingTopDiv); Array.from(el.children).forEach(c => this.container.append(c)); - //this.container.append(this.paddingBottomDiv); - el.append(this.container);//container.append(el); + el.append(this.container); this.container.parentElement.append(this.thumb); this.resize(); } - public resize() { - console.time('scroll resize'); - // @ts-ignore - this.scrollSize = this.container[this.scrollType]; - - let rect = this.container.getBoundingClientRect(); - - // @ts-ignore - this.size = rect[this.type]; + 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]; + }); - if(!this.size || this.size == this.scrollSize) { - this.thumbSize = 0; + 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'); - 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'); + //console.timeEnd('scroll resize'); // @ts-ignore //console.log('onresize', thumb.style[type], thumbHeight, height); } - public setVirtualContainer(el?: HTMLElement) { + 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) { - this.paddingTopDiv.style.height = ''; - this.paddingBottomDiv.style.height = ''; + fastdom.mutate(() => { + this.paddingTopDiv.style.height = ''; + this.paddingBottomDiv.style.height = ''; + }); } - /* this.topObserver.unobserve(this.paddingTopDiv); - this.bottomObserver.unobserve(this.paddingBottomDiv); - - this.topObserver.observe(this.paddingTopDiv); - this.bottomObserver.observe(this.paddingBottomDiv); */ - if(el) { - el.parentElement.insertBefore(this.paddingTopDiv, el); - el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); + fastdom.mutate(() => { + el.parentElement.insertBefore(this.paddingTopDiv, el); + el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); + }); } } - public onScroll() { - // @ts-ignore - //let st = container[scrollSide]; - - console.time('scroll onScroll'); + 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 - 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}; + }); - // @ts-ignore - let value = this.container[this.scrollSide] / (this.scrollSize - this.size) * 100; - let maxValue = 100 - (this.thumbSize / this.size * 100); - //console.log('onscroll', container.scrollHeight, thumbHeight, height, value, maxValue); - - // @ts-ignore - this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%'; + fastdom.mutate(() => { + // @ts-ignore + this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%'; + }); - console.timeEnd('scroll onScroll'); + //console.timeEnd('scroll onScroll'); } - public onTopIntersection(entry: IntersectionObserverEntry) { - // console.log('onTopIntersection'); + public async onTopIntersection(entry: IntersectionObserverEntry) { + console.log('onTopIntersection'); if(this.hiddenElements.up.length && this.paddings.up) { - let needHeight = entry.intersectionRect.height + this.splitOffset; + let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; + let fragment = document.createDocumentFragment(); while(needHeight > 0 && this.paddings.up) { let child = this.hiddenElements.up.pop(); @@ -300,29 +361,31 @@ export default class Scrollable { break; } - /* await new Promise((resolve, reject) => { - window.requestAnimationFrame(resolve); - }); */ - - this.splitUp.prepend(child.element); - let height = child.height || child.element.scrollHeight; + fragment.prepend(child.element); + let height = child.height; needHeight -= height; this.paddings.up -= height; + } + await fastdom.mutate(() => { + this.splitUp.prepend(fragment); this.paddingTopDiv.style.height = this.paddings.up + 'px'; - } + }); } else { - this.paddingTopDiv.style.height = '0px'; + await fastdom.mutate(() => { + this.paddingTopDiv.style.height = '0px'; + }); } } - public onBottomIntersection(entry: IntersectionObserverEntry) { - // console.log('onBottomIntersection'); + public async onBottomIntersection(entry: IntersectionObserverEntry) { + console.log('onBottomIntersection'); if(this.hiddenElements.down.length && this.paddings.down) { - let needHeight = entry.intersectionRect.height + this.splitOffset; + let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height; + let fragment = document.createDocumentFragment(); while(needHeight > 0 && this.paddings.down) { let child = this.hiddenElements.down.shift(); @@ -332,18 +395,22 @@ export default class Scrollable { break; } - this.splitUp.append(child.element); - let height = child.height || child.element.scrollHeight; + fragment.appendChild(child.element); + let height = child.height; needHeight -= height; this.paddings.down -= height; - - this.paddingBottomDiv.style.height = this.paddings.down + 'px'; } + await fastdom.mutate(() => { + this.splitUp.appendChild(fragment); + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + }); if(this.onAddedBottom) this.onAddedBottom(); } else { - this.paddingBottomDiv.style.height = '0px'; + await fastdom.mutate(() => { + this.paddingBottomDiv.style.height = '0px'; + }); } } @@ -351,16 +418,45 @@ export default class Scrollable { } - public splitAppend(...smth: (string | Node)[]) { - this.splitUp.append(...smth); + public prepend(...smth: (string | Node)[]) { + if(this.splitUp) { + 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) { + this.splitUp.append(...smth); - for(let node of smth) { - if(typeof(node) !== 'string') { - this.splitObserver.observe(node as Element); + 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); + return this.splitUp.insertBefore(newChild, refChild); + } + + return this.container.insertBefore(newChild, refChild); + } + set scrollTop(y: number) { this.container.scrollTop = y; } diff --git a/src/components/scrollable_good.ts b/src/components/scrollable_good.ts new file mode 100644 index 00000000..eaa6f4f4 --- /dev/null +++ b/src/components/scrollable_good.ts @@ -0,0 +1,428 @@ +import { cancelEvent } from "../lib/utils"; + +//import {measure} from 'fastdom/fastdom.min'; +import {measure} from 'fastdom'; + +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 splitOffset = 1500; + + public onAddedBottom: () => void = null; + + public topObserver: IntersectionObserver; + public isTopIntersecting: boolean; + public bottomObserver: IntersectionObserver; + public isBottomIntersecting: boolean; + + public splitObserver: IntersectionObserver; + + constructor(public el: HTMLDivElement, x = false, y = true) { + 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, this.isTopIntersecting, entry.isIntersecting, entry.intersectionRatio > 0); + if(this.isTopIntersecting = entry.isIntersecting) { + this.onTopIntersection(entry); + } + // console.log('top intersection end'); + }, {threshold: arr}); + + 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); + + if(this.onScrolledBottom) this.onScrolledBottom(); + } + }, {threshold: arr}); + + this.splitObserver = new IntersectionObserver(entries => { + //console.log('splitObserver', entries); + + for(let entry of entries) { + //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; + if(isTop) { + let sliced: Element[] = [/* child */]; + + do { + if(needHeight > 0) { + needHeight -= child.scrollHeight; + } else { + sliced.push(child); + } + } while(child = child.previousElementSibling); + + let length = sliced.length; + for(let i = length - 1; i >= 0; --i) { + let child = sliced[i]; + + let height = child.scrollHeight; + this.paddings.up += height; + this.hiddenElements.up.push({element: child, height}); + child.parentElement.removeChild(child); + } + + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + //console.log('onscroll sliced up', sliced); + } else if(isBottom) { + let sliced: Element[] = [/* child */]; + + do { + if(needHeight > 0) { + needHeight -= child.scrollHeight; + } else { + sliced.push(child); + } + } while(child = child.nextElementSibling); + + let length = sliced.length; + for(let i = length - 1; i >= 0; --i) { + let child = sliced[i]; + + let height = child.scrollHeight; + this.paddings.down += height; + this.hiddenElements.down.unshift({element: child, height}); + child.parentElement.removeChild(child); + } + + 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'; + 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'; + + 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)); + + //this.container.append(this.paddingTopDiv); + Array.from(el.children).forEach(c => this.container.append(c)); + //this.container.append(this.paddingBottomDiv); + + el.append(this.container);//container.append(el); + this.container.parentElement.append(this.thumb); + this.resize(); + } + + public resize() { + //console.time('scroll resize'); + // @ts-ignore + this.scrollSize = this.container[this.scrollType]; + + let rect = this.container.getBoundingClientRect(); + + // @ts-ignore + this.size = rect[this.type]; + + 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 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) { + this.paddingTopDiv.style.height = ''; + this.paddingBottomDiv.style.height = ''; + } + + /* this.topObserver.unobserve(this.paddingTopDiv); + this.bottomObserver.unobserve(this.paddingBottomDiv); + + this.topObserver.observe(this.paddingTopDiv); + this.bottomObserver.observe(this.paddingBottomDiv); */ + + if(el) { + el.parentElement.insertBefore(this.paddingTopDiv, el); + el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); + } + } + + public onScroll() { + // @ts-ignore + //let st = container[scrollSide]; + + //console.time('scroll onScroll'); + + // @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); + + //console.log('onscroll', container.scrollHeight, thumbHeight, height, value, maxValue); + + // @ts-ignore + this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%'; + + //console.timeEnd('scroll onScroll'); + } + + public onTopIntersection(entry: IntersectionObserverEntry) { + // console.log('onTopIntersection'); + + if(this.hiddenElements.up.length && this.paddings.up) { + let needHeight = entry.intersectionRect.height + this.splitOffset; + + 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; + } + + /* await new Promise((resolve, reject) => { + window.requestAnimationFrame(resolve); + }); */ + + this.splitUp.prepend(child.element); + let height = child.height || child.element.scrollHeight; + + needHeight -= height; + this.paddings.up -= height; + + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + } + } else { + this.paddingTopDiv.style.height = '0px'; + } + } + + public onBottomIntersection(entry: IntersectionObserverEntry) { + // console.log('onBottomIntersection'); + + if(this.hiddenElements.down.length && this.paddings.down) { + let needHeight = entry.intersectionRect.height + this.splitOffset; + + while(needHeight > 0 && this.paddings.down) { + let child = this.hiddenElements.down.shift(); + + if(!child) { + this.paddings.down = 0; + this.paddingBottomDiv.style.height = '0px'; + break; + } + + this.splitUp.append(child.element); + let height = child.height || child.element.scrollHeight; + + needHeight -= height; + this.paddings.down -= height; + + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + } + + if(this.onAddedBottom) this.onAddedBottom(); + } else { + this.paddingBottomDiv.style.height = '0px'; + } + } + + public onScrolledBottom() { + + } + + public prepend(...smth: (string | Node)[]) { + if(this.splitUp) { + 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) { + 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); + 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/wrappers.ts b/src/components/wrappers.ts index 5362e75b..59e7c3d0 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -329,7 +329,7 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { let lastIndex = 0; interval = setInterval(() => { - if(lastIndex >= svg.childElementCount) { + if(lastIndex > svg.childElementCount || isNaN(audio.duration)) { clearInterval(interval); return; } @@ -337,11 +337,14 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { // @ts-ignore timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true); + lastIndex = Math.round(audio.currentTime / audio.duration * 62); + //svg.children[lastIndex].setAttributeNS(null, 'fill', '#000'); svg.children[lastIndex].classList.add('active'); - ++lastIndex; + //++lastIndex; //console.log('lastIndex:', lastIndex, audio.currentTime); - }, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */); + //}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */); + }, 20); } else { audio.pause(); toggle.classList.add('tgico-largeplay'); diff --git a/src/lib/appManagers/appDialogsManager.ts b/src/lib/appManagers/appDialogsManager.ts index 36fcb5cf..df4f5434 100644 --- a/src/lib/appManagers/appDialogsManager.ts +++ b/src/lib/appManagers/appDialogsManager.ts @@ -1,6 +1,6 @@ import apiManager from "../mtproto/apiManager"; import apiFileManager from '../mtproto/apiFileManager'; -import { $rootScope, findUpTag, isElementInViewport, langPack } from "../utils"; +import { $rootScope, findUpTag, langPack } from "../utils"; import appImManager from "./appImManager"; import appPeersManager from './appPeersManager'; import appMessagesManager from "./appMessagesManager"; @@ -8,6 +8,7 @@ import appUsersManager from "./appUsersManager"; import { RichTextProcessor } from "../richtextprocessor"; import { ripple } from "../../components/misc"; import appSidebarLeft from "./appSidebarLeft"; +import Scrollable from "../../components/scrollable"; type DialogDom = { avatarDiv: HTMLDivElement, @@ -25,8 +26,8 @@ export class AppDialogsManager { public chatList = document.getElementById('dialogs') as HTMLUListElement; public chatListArchived = document.getElementById('dialogs-archived') as HTMLUListElement; public pinnedDelimiter: HTMLDivElement; - public chatsHidden: any; - public chatsArchivedHidden: any; + public chatsHidden: Scrollable["hiddenElements"]; + public chatsArchivedHidden: Scrollable["hiddenElements"]; public myID = 0; public doms: {[peerID: number]: DialogDom} = {}; @@ -153,7 +154,7 @@ export class AppDialogsManager { } public sortDom(archived = false) { - return; + // return; let dialogs = appMessagesManager.dialogsStorage.dialogs.slice(); @@ -205,6 +206,9 @@ export class AppDialogsManager { let hiddenLength: number = chatsHidden.up.length; let inViewportLength = chatList.childElementCount; + let hiddenConcated = chatsHidden.up.concat(chatsHidden.down); + + //console.log('sortDom clearing innerHTML', archived, hiddenLength, inViewportLength); chatList.innerHTML = ''; @@ -214,16 +218,18 @@ export class AppDialogsManager { if(!dom) return; if(inUpper.length < hiddenLength) { - inUpper.push({element: dom.listEl, height: 0}); + let child = hiddenConcated.find(obj => obj.element == dom.listEl); + inUpper.push({element: dom.listEl, height: child ? child.height : 0}); } else if(inViewportIndex <= inViewportLength - 1) { chatList.append(dom.listEl); ++inViewportIndex; } else { - inBottom.push({element: dom.listEl, height: 0}); + let child = hiddenConcated.find(obj => obj.element == dom.listEl); + inBottom.push({element: dom.listEl, height: child ? child.height : 0}); } }); - //////console.log('sortDom', sorted.length, inUpper.length, chatList.childElementCount, inBottom.length); + //console.log('sortDom', sorted.length, inUpper.length, chatList.childElementCount, inBottom.length); chatsHidden.up = inUpper; chatsHidden.down = inBottom; @@ -524,7 +530,7 @@ export class AppDialogsManager { this.domsArchived[dialog.peerID] = dom; } else { //this.chatList.append(li); - appSidebarLeft.scroll.splitAppend(li); + appSidebarLeft.scroll.append(li); this.doms[dialog.peerID] = dom; } diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index ad9814eb..b5c2b32a 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -70,8 +70,6 @@ export class AppImManager { public chatInner = document.getElementById('bubbles-inner') as HTMLDivElement; public searchBtn = this.pageEl.querySelector('.chat-search-button') as HTMLButtonElement; public goDownBtn = this.pageEl.querySelector('#bubbles-go-down') as HTMLButtonElement; - public firstContainerDiv: HTMLDivElement; - public lastContainerDiv: HTMLDivElement; private getHistoryPromise: Promise; private getHistoryTimeout = 0; @@ -185,7 +183,7 @@ export class AppImManager { this.renderMessagesByIDs(msgIDs); - appDialogsManager.sortDom(); + //appDialogsManager.sortDom(); }); $rootScope.$on('history_delete', (e: CustomEvent) => { @@ -627,6 +625,8 @@ export class AppImManager { } public deleteEmptySideDivs() { + return; + let nodes = Array.from(this.chatInner.childNodes) as HTMLDivElement[]; nodes.filter((node) => { let childElementCount = node.childElementCount; @@ -844,10 +844,10 @@ export class AppImManager { } public setScroll() { - this.scrollable = new Scrollable(this.bubblesContainer); + this.scrollable = new Scrollable(this.bubblesContainer, false, true, 1500); this.scroll = this.scrollable.container; - //this.scrollable.setVirtualContainer(this.chatInner); + this.scrollable.setVirtualContainer(this.chatInner); this.scrollPosition = new ScrollPosition(this.chatInner); this.scroll.addEventListener('scroll', this.onScroll.bind(this)); @@ -949,11 +949,6 @@ export class AppImManager { this.scrolledAllDown = false; this.muted = false; - if(this.lastContainerDiv) this.lastContainerDiv.remove(); - if(this.firstContainerDiv) this.firstContainerDiv.remove(); - this.lastContainerDiv = undefined; - this.firstContainerDiv = undefined; - for(let i in this.bubbles) { let bubble = this.bubbles[i]; bubble.remove(); @@ -975,6 +970,8 @@ export class AppImManager { // clear messages this.chatInner.innerHTML = ''; + this.scrollable.setVirtualContainer(this.chatInner); + //appSidebarRight.minMediaID = {}; } @@ -1635,10 +1632,15 @@ export class AppImManager { if(updatePosition) { bubble.classList.add(our ? 'is-out' : 'is-in'); - if(reverse) { + /* if(reverse) { this.chatInner.prepend(bubble); } else { this.chatInner.append(bubble); + } */ + if(reverse) { + this.scrollable.prepend(bubble); + } else { + this.scrollable.append(bubble); } let justDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); @@ -1673,12 +1675,12 @@ export class AppImManager { //this.chatInner.insertBefore(div, containerDiv); //containerDiv.insertBefore(div, bubble); - this.chatInner.insertBefore(div, bubble); + this.scrollable.insertBefore(div, bubble);// this.chatInner.insertBefore(div, bubble); } else { let dateMessage = this.dateMessages[dateTimestamp]; if(dateMessage.firstTimestamp > date.getTime()) { //this.chatInner.insertBefore(dateMessage.div, containerDiv); - this.chatInner.insertBefore(dateMessage.div, bubble); + this.scrollable.insertBefore(dateMessage.div, bubble);// this.chatInner.insertBefore(dateMessage.div, bubble); } } } @@ -1707,7 +1709,7 @@ export class AppImManager { let loadCount = Object.keys(this.bubbles).length > 0 ? 20 : - (this.chatInner.parentElement.parentElement.scrollHeight) / 30 * 1.25 | 0; + this.scrollable.container.parentElement.scrollHeight / 30 * 1.25 | 0; /* if(testScroll) { loadCount = 1; diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index d7420fb6..1f180b68 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -7,6 +7,9 @@ import apiFileManager from "../mtproto/apiFileManager"; import apiManager from "../mtproto/apiManager"; //import { MTPhotoSize } from "../../components/misc"; +//import fastdom from "fastdom"; +//import 'fastdom/fastdom-strict'; // exclude in production + type MTPhoto = { _: 'photo', pFlags: any, @@ -24,22 +27,26 @@ export class AppPhotosManager { private photos: { [id: string]: MTPhoto } = {}; - public windowW = document.body.scrollWidth; - public windowH = document.body.scrollHeight; + public windowW = 0; + public windowH = 0; public static jf = new Uint8Array(bytesFromHex('ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e19282321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2d2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc00011080000000003012200021101031101ffc4001f0000010501010101010100000000000000000102030405060708090a0bffc400b5100002010303020403050504040000017d01020300041105122131410613516107227114328191a1082342b1c11552d1f02433627282090a161718191a25262728292a3435363738393a434445464748494a535455565758595a636465666768696a737475767778797a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000102030405060708090a0bffc400b51100020102040403040705040400010277000102031104052131061241510761711322328108144291a1b1c109233352f0156272d10a162434e125f11718191a262728292a35363738393a434445464748494a535455565758595a636465666768696a737475767778797a82838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f9faffda000c03010002110311003f00')); public static Df = bytesFromHex('ffd9'); constructor() { window.addEventListener('resize', (e) => { - this.windowW = document.body.scrollWidth; - this.windowH = document.body.scrollHeight; - + //fastdom.measure(() => { + this.windowW = document.body.scrollWidth; + this.windowH = document.body.scrollHeight; + //}); //console.log(`Set windowW, windowH: ${this.windowW}x${this.windowH}`); }); - - /* $rootScope.openPhoto = openPhoto - $rootScope.preloadPhoto = preloadPhoto; */ + + //fastdom.measure(() => { + console.log('measure works'); + this.windowW = document.body.scrollWidth; + this.windowH = document.body.scrollHeight; + //}); } public savePhoto(apiPhoto: any, context?: any) { diff --git a/src/lib/appManagers/appSidebarLeft.ts b/src/lib/appManagers/appSidebarLeft.ts index 7f357ff1..8b513308 100644 --- a/src/lib/appManagers/appSidebarLeft.ts +++ b/src/lib/appManagers/appSidebarLeft.ts @@ -3,7 +3,7 @@ import { putPreloader, formatPhoneNumber } from "../../components/misc"; import Scrollable from '../../components/scrollable'; import appMessagesManager, { AppMessagesManager } from "./appMessagesManager"; import appDialogsManager from "./appDialogsManager"; -import { isElementInViewport, numberWithCommas, cancelEvent } from "../utils"; +import { isElementInViewport, numberWithCommas } from "../utils"; import appMessagesIDsManager from "./appMessagesIDsManager"; import appImManager from "./appImManager"; import appUsersManager from "./appUsersManager"; @@ -25,7 +25,7 @@ class SearchGroup { this.container.append(this.nameEl, this.list); this.container.style.display = 'none'; - appDialogsManager.setListClickListener(this.list); + //appDialogsManager.setListClickListener(this.list); } clear() { @@ -88,7 +88,7 @@ class AppSidebarLeft { this.chatsPreloader = document.createElement('div'); this.chatsPreloader.classList.add('preloader'); putPreloader(this.chatsPreloader); - this.chatsContainer.append(this.chatsPreloader); + //this.chatsContainer.append(this.chatsPreloader); this.chatsLoadCount = Math.round(document.body.scrollHeight / 70 * 1.5); @@ -128,20 +128,16 @@ class AppSidebarLeft { //this.toolsBtn.classList.add('tgico-back'); }); - /* this.listsContainer.insertBefore(this.searchMessagesList, this.listsContainer.lastElementChild); - for(let i = 0; i < 25; ++i) { + /* for(let i = 0; i < 100; ++i) { let li = document.createElement('li'); - li.innerHTML = `

Влад14:41

это важно

`; - this.searchMessagesList.append(li); + li.dataset.id = '' + i; + li.innerHTML = `

${i}18:33

Ильяс: Гагагагга

`; + this.scroll.append(li); } */ this.listsContainer.addEventListener('scroll', this.onSidebarScroll.bind(this)); - - //this.searchContainer.append(this.listsContainer); - + this.searchInput.addEventListener('focus', (e) => { - /* this.toolsBtn.classList.remove('tgico-menu', 'btn-menu-toggle'); - this.toolsBtn.classList.add('tgico-back'); */ this.toolsBtn.classList.remove('active'); this.backBtn.classList.add('active'); this.searchContainer.classList.add('active'); @@ -154,17 +150,10 @@ class AppSidebarLeft { this.searchInput.addEventListener('blur', (e) => { if(!this.searchInput.value) { - /* this.toolsBtn.classList.add('tgico-menu'); - this.toolsBtn.classList.remove('tgico-back'); */ this.toolsBtn.classList.add('active'); this.backBtn.classList.remove('active'); this.searchContainer.classList.remove('active'); this.backBtn.click(); - - /* setTimeout(() => { - //this.toolsBtn.click(); - this.toolsBtn.classList.add('btn-menu-toggle'); - }, 200); */ } /* this.peerID = 0; @@ -223,6 +212,8 @@ class AppSidebarLeft { } public async loadDialogs(archived = false) { + //return; + if(this.loadDialogsPromise/* || 1 == 1 */) return this.loadDialogsPromise; (archived ? this.chatsArchivedContainer : this.chatsContainer).append(this.chatsPreloader); diff --git a/src/lib/services.ts b/src/lib/services.ts index 1f9cfb33..16afce0f 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -32,7 +32,7 @@ export const appDocsManager = AppDocsManager; export const appSidebarRight = AppSidebarRight; export const appSidebarLeft = AppSidebarLeft; -/* (window as any).Services = { +(window as any).Services = { appUsersManager, appChatsManager, apiUpdatesManager, @@ -48,4 +48,4 @@ export const appSidebarLeft = AppSidebarLeft; appSidebarRight, appSidebarLeft //appSharedMediaManager -}; */ +}; diff --git a/src/scss/partials/_chatlist.scss b/src/scss/partials/_chatlist.scss index 52bec98a..e82bd31a 100644 --- a/src/scss/partials/_chatlist.scss +++ b/src/scss/partials/_chatlist.scss @@ -70,9 +70,9 @@ margin: 0 8.5px 0 8px; overflow: hidden; - /* &:hover { + &:hover { background: rgba(112, 117, 121, .08); - } */ + } } li.active > .rp {