diff --git a/src/components/emoticonsDropdown.ts b/src/components/emoticonsDropdown.ts index 6a7a8ee5..48118e62 100644 --- a/src/components/emoticonsDropdown.ts +++ b/src/components/emoticonsDropdown.ts @@ -249,7 +249,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, docs.forEach(doc => { let div = document.createElement('div'); - wrapSticker(doc, div, undefined, lazyLoadQueue, EMOTICONSSTICKERGROUP, true); + wrapSticker(doc, div, undefined, lazyLoadQueue, EMOTICONSSTICKERGROUP, true, false, true); categoryDiv.append(div); }); @@ -293,7 +293,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, let categoryDiv = document.createElement('div'); categoryDiv.classList.add('sticker-category'); - stickersDiv.prepend(categoryDiv); + stickersScroll.prepend(categoryDiv); categoryPush(categoryDiv, stickers.stickers, true); }); @@ -314,14 +314,14 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, menu.append(li); - stickersDiv.append(categoryDiv); + stickersScroll.append(categoryDiv); let stickerSet = await appStickersManager.getStickerSet(set); if(stickerSet.set.thumb) { let thumb = stickerSet.set.thumb; - appStickersManager.getStickerSetThumb(stickerSet.set).then(async(blob) => { + appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => { if(thumb.w == 1 && thumb.h == 1) { // means animated const reader = new FileReader(); diff --git a/src/components/pageIm.ts b/src/components/pageIm.ts index 03b895b2..b6906741 100644 --- a/src/components/pageIm.ts +++ b/src/components/pageIm.ts @@ -69,7 +69,7 @@ export default () => import('../lib/services').then(services => { appDialogsManager.setLastMessage(dialog); } - if(performed) { + if(performed/* && false */) { /////////console.log('will sortDom'); appDialogsManager.sortDom(); appDialogsManager.sortDom(true); diff --git a/src/components/scrollable.ts b/src/components/scrollable.ts index b214df54..79f14507 100644 --- a/src/components/scrollable.ts +++ b/src/components/scrollable.ts @@ -4,6 +4,7 @@ import { cancelEvent } from "../lib/utils"; import FastDom from 'fastdom'; import 'fastdom/src/fastdom-strict'; // exclude in production import FastDomPromised from 'fastdom/extensions/fastdom-promised'; +import { logger } from "../lib/polyfill"; //const fastdom = FastDom.extend(FastDomPromised); const fastdom = ((window as any).fastdom as typeof FastDom).extend(FastDomPromised); @@ -14,6 +15,16 @@ setTimeout(() => { //(window as any).fastdom.strict(true); }, 5e3); +/* +var el = $0; +var height = 0; +var checkUp = false; + +do { + height += el.scrollHeight; +} while(el = (checkUp ? el.previousElementSibling : el.nextElementSibling)); +console.log(height); +*/ export default class Scrollable { public container: HTMLDivElement; @@ -51,40 +62,51 @@ export default class Scrollable { public bottomObserver: IntersectionObserver; public splitObserver: IntersectionObserver; - public splitMeasure: Promise<{element: Element, height: number}[]> = null; + public splitMeasureTop: Promise<{element: Element, height: number}[]> = null; + public splitMeasureBottom: Scrollable['splitMeasureTop'] = null; public splitMeasureAdd: Promise = null; - public splitMutate: Promise = null; + public splitMeasureRemoveBad: Promise = null; + public splitMutateTop: Promise = null; + public splitMutateBottom: Scrollable['splitMutateTop'] = null; + public splitMutateRemoveBad: Promise = null; + + public splitMutateIntersectionTop: Promise = null; + public splitMutateIntersectionBottom: Promise = null; + + public onScrollMeasure: Promise = null; + + public lastScrollTop: number = 0; + public scrollTopOffset: number = 0; + + private log: ReturnType; + private debug = false; - constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300) { + constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300, logPrefix = '') { 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.log = logger('SCROLL' + (logPrefix ? '-' + logPrefix : '')); + + let arr = []; + for(let i = 0.001; i < 1; i += 0.001) arr.push(i); this.topObserver = new IntersectionObserver(entries => { - let entry = entries[0]; + let entry = entries[entries.length - 1]; - console.log('top intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 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}); + }, {root: this.el}); this.bottomObserver = new IntersectionObserver(entries => { - let entry = entries[0]; + let entry = entries[entries.length - 1]; - console.log('bottom intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0); + //console.log('bottom intersection:', entries, entry, 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}); + }, {root: this.el}); if(x) { this.container.classList.add('scrollable-x'); @@ -151,7 +173,10 @@ export default class Scrollable { }); //this.container.addEventListener('mouseover', this.resize.bind(this)); // omg - window.addEventListener('resize', this.resize.bind(this)); + window.addEventListener('resize', () => { + //this.resize.bind(this); + this.onScroll(); + }); this.paddingTopDiv = document.createElement('div'); this.paddingTopDiv.classList.add('scroll-padding'); @@ -170,128 +195,245 @@ export default class Scrollable { this.resize(); } + public detachTop(child: Element, needHeight = 0) { + if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom); + if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); + + this.splitMeasureBottom = 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; + }); + + return this.splitMeasureBottom.then(sliced => { + if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); + + return this.splitMutateBottom = 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); + }); + + if(this.debug) { + this.log('sliced up', sliced); + } + + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + }); + }); + } + + public detachBottom(child: Element, needHeight = 0) { + if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom); + if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); + + this.splitMeasureBottom = 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; + }); + + return this.splitMeasureBottom.then(sliced => { + if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); + + return this.splitMutateBottom = 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); + }); + + if(this.debug) { + this.log('sliced down', sliced); + } + + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + }); + }); + } + + public detachByPrevScroll(child: Element, prevScrollTop: number, needHeight = 0) { + if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom); + if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); + + let attachToTop = this.paddings.up < prevScrollTop; + + this.splitMeasureBottom = 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; + }); + + return this.splitMeasureBottom.then(sliced => { + if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); + + return this.splitMutateBottom = 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.log('sliced down', sliced); + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + }); + }); + } + public splitObserve(entries: IntersectionObserverEntry[]) { - console.log('splitObserver', entries); + let sorted: { + intersecting: { + top?: IntersectionObserverEntry, + bottom?: IntersectionObserverEntry + }, + notIntersecting: { + top?: IntersectionObserverEntry, + bottom?: IntersectionObserverEntry + } + } = { + intersecting: {}, + notIntersecting: {} + }; + for(let entry of entries) { // there may be duplicates (1st - not intersecting, 2nd - intersecting) - //console.log('onscroll entry', entry.target, entry.isIntersecting, entry); + //console.log('onscroll entry 1', 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('onscroll entry notIntersecting', isTop, isBottom, entry.target, 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); + if(isTop) { + sorted.notIntersecting.top = entry; + } else if(isBottom && !sorted.notIntersecting.bottom) { + sorted.notIntersecting.bottom = entry; } //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; + + if(isTop) { + sorted.intersecting.top = entry; + } else if(isBottom && !sorted.intersecting.bottom) { + sorted.intersecting.bottom = entry; + } + + // if(isTop) { + // this.onTopIntersection(entry.boundingClientRect.height); + // } else if(isBottom) { + // this.onTopIntersection(entry.boundingClientRect.height); + // } + } + } + + console.log('splitObserve', entries, sorted); + + let needHeight = this.splitOffset; + let isOutOfView: boolean; + let entry: IntersectionObserverEntry; + if(entry = sorted.notIntersecting.top) { // scrolled bottom + let child = entry.target; + + let diff = entry.boundingClientRect.bottom + needHeight; + if(diff < 0) { // maybe need <=, means out of view + if(!(child = child.nextElementSibling)) { + this.detachTop(this.splitUp.lastElementChild, 0); + } else { + if(this.splitMeasureRemoveBad) fastdom.clear(this.splitMeasureRemoveBad); + this.splitMeasureRemoveBad = fastdom.measure(() => { + do { + diff += child.scrollHeight; + } while(diff < 0 && (child = child.nextElementSibling)); + + return child || this.splitUp.lastElementChild; }); - - this.splitMeasureAdd.then(needHeight => { - this.onBottomIntersection(needHeight); + + this.splitMeasureRemoveBad.then(child => { + this.detachTop(child, 0); }); } + } else { + this.detachTop(child, needHeight); } } + + if(entry = sorted.notIntersecting.bottom) { // scrolled top + isOutOfView = (entry.boundingClientRect.top - needHeight) >= entry.rootBounds.height; + this.detachBottom(entry.target, isOutOfView ? 0 : needHeight); + } + + if(entry = sorted.intersecting.top) { // scrolling top + let needHeight = this.splitOffset; + + let child = entry.target; + 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); + }); + } + + if(entry = sorted.intersecting.bottom) { // scrolling bottom + let needHeight = this.splitOffset; + + let child = entry.target; + 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() { @@ -338,6 +480,7 @@ export default class Scrollable { this.hiddenElements.up.length = this.hiddenElements.down.length = 0; this.paddings.up = this.paddings.down = 0; + this.lastScrollTop = 0; if(this.paddingTopDiv.parentElement) { fastdom.mutate(() => { @@ -346,12 +489,16 @@ export default class Scrollable { }); } - if(this.splitObserver) { + /* if(this.splitObserver) { this.splitObserver.disconnect(); } - - this.splitObserver = new IntersectionObserver((entries) => this.splitObserve(entries), {root: this.el}); + this.splitObserver = new IntersectionObserver((entries) => this.splitObserve(entries), {root: this.el}); */ + + this.log('setVirtualContainer:', el, this); + + this.getScrollTopOffset(); + if(el) { fastdom.mutate(() => { el.parentElement.insertBefore(this.paddingTopDiv, el); @@ -359,10 +506,24 @@ export default class Scrollable { }); } } + + public getScrollTopOffset() { + if(this.splitUp && this.splitUp.parentElement && this.splitUp.parentElement != this.container) { // need to find offset + fastdom.measure(() => { + let rect = this.splitUp.getBoundingClientRect(); + let containerRect = this.container.getBoundingClientRect(); - public async onScroll() { - //console.time('scroll onScroll'); - let {value, maxValue} = await fastdom.measure(() => { + this.scrollTopOffset = rect.top - containerRect.top; + this.log('set scrollTopOffset to:', this.scrollTopOffset); + }); + } else { + this.scrollTopOffset = 0; + } + } + + public onScroll() { + if(this.onScrollMeasure) fastdom.clear(this.onScrollMeasure); + this.onScrollMeasure = fastdom.measure(() => { // @ts-ignore if(this.container[this.scrollType] != this.scrollSize || this.thumbSize == 0) { this.resize(); @@ -372,85 +533,272 @@ export default class Scrollable { let value = this.container[this.scrollSide] / (this.scrollSize - this.size) * 100; let maxValue = 100 - (this.thumbSize / this.size * 100); + let ret = {value, maxValue}; + + if(!this.splitUp) { + return ret; + } + + let perf = performance.now(); + let scrollTop = this.scrollTop - this.scrollTopOffset; + let outerHeight = this.parentElement.scrollHeight; + + let maxScrollTop = this.scrollHeight - this.scrollTopOffset - outerHeight; + if(scrollTop < 0) scrollTop = 0; + else if(scrollTop > maxScrollTop) scrollTop = maxScrollTop; + + let toBottom = scrollTop > this.lastScrollTop; + + let visibleFrom = /* scrollTop < this.paddings.up ? scrollTop : */scrollTop - this.paddings.up; + let visibleUntil = visibleFrom + outerHeight; + let sum = 0; + + let firstVisibleElement: Element; + let lastVisibleElement: Element; + + let needHeight = this.splitOffset; + + let children = this.splitUp.children; + let length = children.length; + for(let i = 0; i < length; ++i) { + let element = children[i]; + + let height = element.scrollHeight; + if(sum < visibleUntil && (sum + height) >= visibleFrom && !firstVisibleElement) { // if any part is in viewport + firstVisibleElement = element; + } + + if(sum < visibleUntil && firstVisibleElement) { + lastVisibleElement = element; + } + + sum += element.scrollHeight; + + //this.log(sum, element); + } + + if(!lastVisibleElement && firstVisibleElement) { + lastVisibleElement = firstVisibleElement; + } + + // возможно устанавливать прошлый скролл нужно уже после этого промиса, т.к. он может очиститься + if(scrollTop == this.lastScrollTop) { + this.lastScrollTop = scrollTop; + if(firstVisibleElement) this.detachTop(firstVisibleElement, needHeight); + if(lastVisibleElement) this.detachBottom(lastVisibleElement, needHeight); + return ret; + } + + /* { + this.log('onScroll', (performance.now() - perf).toFixed(3), length, scrollTop, + toBottom, firstVisibleElement, lastVisibleElement, visibleFrom, visibleUntil); + return {value, maxValue}; + } */ + + if(toBottom) { // scrolling bottom + if(firstVisibleElement) { + if(this.debug) { + this.log('will detach top by:', firstVisibleElement, needHeight); + } + + this.detachTop(firstVisibleElement, needHeight); + + if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd); + + let child = lastVisibleElement; + this.splitMeasureAdd = fastdom.measure(() => { + while(child = child.nextElementSibling) { + needHeight -= child.scrollHeight; + } + + this.onBottomIntersection(needHeight); + return needHeight; + }); + + /* this.splitMeasureAdd.then(needHeight => { + this.onBottomIntersection(needHeight); + }); */ + } else if(length) { // scrolled manually or safari + if(this.debug) { + this.log.warn('will detach all of top', length, this.splitUp.childElementCount, maxScrollTop, this.paddings, this.lastScrollTop); + } + + this.detachTop(children[length - 1], 0).then(() => { // now need to move from one hidden array to another one + this.onManualScrollBottom(scrollTop, needHeight); + }); + } else if(this.paddings.down) { // scrolled manually or safari + this.onManualScrollBottom(scrollTop, needHeight); + } + } else { // scrolling top + if(lastVisibleElement) { + if(this.debug) { + this.log('will detach bottom by:', lastVisibleElement, needHeight); + } + + this.detachBottom(lastVisibleElement, needHeight); + + let child = firstVisibleElement; + if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd); + this.splitMeasureAdd = fastdom.measure(() => { + while(child = child.previousElementSibling) { + needHeight -= child.scrollHeight; + } + + this.onTopIntersection(needHeight); + return needHeight; + }); + + /* this.splitMeasureAdd.then(needHeight => { + this.onTopIntersection(needHeight); + }); */ + } else if(length) { // scrolled manually or safari + if(this.debug) { + this.log.warn('will detach all of bottom', length, this.splitUp.childElementCount, maxScrollTop, this.paddings, this.lastScrollTop); + } + + this.detachBottom(children[0], 0).then(() => { // now need to move from one hidden array to another one + this.onManualScrollTop(scrollTop, needHeight, maxScrollTop); + }); + } else if(this.paddings.up) { + this.onManualScrollTop(scrollTop, needHeight, maxScrollTop); + } + } + + if(this.debug) { + this.log('onScroll', (performance.now() - perf).toFixed(3), length, scrollTop, maxScrollTop, toBottom, firstVisibleElement, lastVisibleElement, visibleFrom, visibleUntil, this.scrollTopOffset); + } + + this.lastScrollTop = scrollTop; + 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) + '%'; + this.onScrollMeasure.then(({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); + public onManualScrollTop(scrollTop: number, needHeight: number, maxScrollTop: number) { + //if(this.splitMutateRemoveBad) fastdom.clear(this.splitMutateRemoveBad); + this.splitMutateRemoveBad = fastdom.mutate(() => { + let h = maxScrollTop - (scrollTop + outerHeight); - let fragment = document.createDocumentFragment(); - while(needHeight > 0 && this.paddings.up) { + while(this.paddings.down < h && 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.hiddenElements.down.unshift(child); + this.paddings.down += child.height; this.paddings.up -= child.height; } - await fastdom.mutate(() => { + if(this.debug) { + this.log.warn('bait it off now', this, length, this.splitUp.childElementCount, scrollTop, this.paddings.up, h); + } + + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + this.onTopIntersection((outerHeight * 2) + (needHeight * 2)); + }); + + /* this.splitMutateRemoveBad.then(() => { + }); */ + } + + public onManualScrollBottom(scrollTop: number, needHeight: number) { + //if(this.splitMutateRemoveBad) fastdom.clear(this.splitMutateRemoveBad); + this.splitMutateRemoveBad = fastdom.mutate(() => { + let h = scrollTop - needHeight; + + while(this.paddings.up < h && this.paddings.down) { + let child = this.hiddenElements.down.shift(); + this.hiddenElements.up.push(child); + this.paddings.up += child.height; + this.paddings.down -= child.height; + } + + if(this.debug) { + this.log.warn('shake it off now', this, length, this.splitUp.childElementCount); + } + + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + this.onBottomIntersection(outerHeight + (needHeight * 2)); + }); + + /* this.splitMutateRemoveBad.then(() => { + }); */ + } + + public onTopIntersection(needHeight: number) { + if(this.debug) { + this.log('onTopIntersection', needHeight, this); + } + + if(this.splitMutateIntersectionTop) fastdom.clear(this.splitMutateIntersectionTop); + this.splitMutateIntersectionTop = fastdom.mutate(() => { + if(this.hiddenElements.up.length && this.paddings.up) { + 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; + } + this.splitUp.prepend(fragment); this.paddingTopDiv.style.height = this.paddings.up + 'px'; - }); - } else { - await fastdom.mutate(() => { + } else { this.paddingTopDiv.style.height = '0px'; - }); - } + } + }); } - public async onBottomIntersection(/* entry: IntersectionObserverEntry */needHeight: number) { - console.log('onBottomIntersection', needHeight, this); + public onBottomIntersection(needHeight: number) { + if(this.debug) { + this.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; + if(this.splitMutateIntersectionBottom) fastdom.clear(this.splitMutateIntersectionBottom); + this.splitMutateIntersectionBottom = fastdom.mutate(() => { + if(this.hiddenElements.down.length && this.paddings.down) { + 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; } - 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(() => { + + /* if(this.debug) { + this.log('onBottomIntersection append:', fragment, needHeight); + } */ + + if(this.onAddedBottom) this.onAddedBottom(); + } else { this.paddingBottomDiv.style.height = '0px'; - }); - } + } + }); } public prepend(...smth: (string | Node)[]) { @@ -467,16 +815,11 @@ export default class Scrollable { } 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); } + + this.onScroll(); } public append(...smth: (string | Node)[]) { @@ -493,47 +836,45 @@ export default class Scrollable { } 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); } + + this.onScroll(); } 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}); + this.onScroll(); 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}); + this.onScroll(); return index; } } + this.onScroll(); return this.splitUp.insertBefore(newChild, refChild); } + this.onScroll(); return this.container.insertBefore(newChild, refChild); } set scrollTop(y: number) { - this.container.scrollTop = y; + fastdom.mutate(() => { + this.container.scrollTop = y; + }); } get scrollTop() { diff --git a/src/components/scrollable_backwards.ts b/src/components/scrollable_backwards.ts new file mode 100644 index 00000000..629fcfcf --- /dev/null +++ b/src/components/scrollable_backwards.ts @@ -0,0 +1,660 @@ +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); + +/* +var el = $0; +var height = 0; +var checkUp = false; + +do { + height += el.scrollHeight; +} while(el = (checkUp ? el.previousElementSibling : el.nextElementSibling)); +console.log(height); +*/ + +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 splitMeasureAdd: Promise = null; + public splitMeasureRemoveBad: 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); + this.topObserver = new IntersectionObserver(entries => { + let entry = entries[entries.length - 1]; + + 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[entries.length - 1]; + + console.log('bottom intersection:', entries, entry, entry.isIntersecting, entry.intersectionRatio > 0); + if(entry.isIntersecting) { + //this.onBottomIntersection(entry); + let scrollTop = entry.boundingClientRect.top; + + console.log('bottom intersection scrollTop', scrollTop); + + if(scrollTop < 0) { + + } //else this.onBottomIntersection(entry.intersectionRect.height); + + //this.onBottomIntersection(entry.boundingClientRect.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 detachTop(child: Element, needHeight = 0) { + 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; + }); + + return this.splitMeasure.then(sliced => { + if(this.splitMutate) fastdom.clear(this.splitMutate); + + return 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); + }); + + console.log('onscroll sliced up', sliced); + + this.paddingTopDiv.style.height = this.paddings.up + 'px'; + }); + }); + } + + public detachBottom(child: Element, needHeight = 0) { + 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; + }); + + return this.splitMeasure.then(sliced => { + if(this.splitMutate) fastdom.clear(this.splitMutate); + + return 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); + }); + + console.log('onscroll sliced down', sliced); + + this.paddingBottomDiv.style.height = this.paddings.down + 'px'; + }); + }); + } + + public splitObserve(entries: IntersectionObserverEntry[]) { + let sorted: { + intersecting: { + top?: IntersectionObserverEntry, + bottom?: IntersectionObserverEntry + }, + notIntersecting: { + top?: IntersectionObserverEntry, + bottom?: IntersectionObserverEntry + } + } = { + intersecting: {}, + notIntersecting: {} + }; + + for(let entry of entries) { // there may be duplicates (1st - not intersecting, 2nd - intersecting) + //console.log('onscroll entry 1', entry.target, entry.isIntersecting, entry); + if(!entry.target.parentElement || !entry.rootBounds) continue; + + if(!entry.isIntersecting) { + let isTop = entry.boundingClientRect.top <= 0; + let isBottom = entry.rootBounds.height <= entry.boundingClientRect.top; + //console.log('onscroll entry notIntersecting', isTop, isBottom, entry.target, entry); + + if(isTop) { + sorted.notIntersecting.top = entry; + } else if(isBottom && !sorted.notIntersecting.bottom) { + sorted.notIntersecting.bottom = entry; + } + + //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) { + sorted.intersecting.top = entry; + } else if(isBottom && !sorted.intersecting.bottom) { + sorted.intersecting.bottom = entry; + } + + // if(isTop) { + // this.onTopIntersection(entry.boundingClientRect.height); + // } else if(isBottom) { + // this.onTopIntersection(entry.boundingClientRect.height); + // } + } + } + + console.log('splitObserve', entries, sorted); + + let needHeight = this.splitOffset; + let isOutOfView: boolean; + let entry: IntersectionObserverEntry; + if(entry = sorted.notIntersecting.top) { // scrolled bottom + let child = entry.target; + + let diff = entry.boundingClientRect.bottom + needHeight; + if(diff < 0) { // maybe need <=, means out of view + if(!(child = child.nextElementSibling)) { + this.detachTop(this.splitUp.lastElementChild, 0); + } else { + if(this.splitMeasureRemoveBad) fastdom.clear(this.splitMeasureRemoveBad); + this.splitMeasureRemoveBad = fastdom.measure(() => { + do { + diff += child.scrollHeight; + } while(diff < 0 && (child = child.nextElementSibling)); + + return child || this.splitUp.lastElementChild; + }); + + this.splitMeasureRemoveBad.then(child => { + this.detachTop(child, 0); + }); + } + } else { + this.detachTop(child, needHeight); + } + } + + if(entry = sorted.notIntersecting.bottom) { // scrolled top + isOutOfView = (entry.boundingClientRect.top - needHeight) >= entry.rootBounds.height; + this.detachBottom(entry.target, isOutOfView ? 0 : needHeight); + } + + if(entry = sorted.intersecting.top) { // scrolling top + let needHeight = this.splitOffset; + + let child = entry.target; + 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); + }); + } + + if(entry = sorted.intersecting.bottom) { // scrolling bottom + let needHeight = this.splitOffset; + + let child = entry.target; + 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]; + }); + + 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(e?: any) { + //console.log('scroll onScroll', e, this.container.scrollTop); + 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) { + fastdom.measure(() => { + let scrollTop = this.container.scrollTop; + if(scrollTop == y) return scrollTop; + + this.container.scrollTop = y; + + if(this.splitUp) { + let isBottom = y > scrollTop; + let diff = Math.abs(y - scrollTop); + + if(isBottom) { + let el = this.splitUp.lastElementChild; + let rect = el.getBoundingClientRect(); + } else { + + } + } + }); + } + + 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 59e7c3d0..79e461a6 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -423,24 +423,51 @@ export function wrapPhoto(this: AppImManager, photo: any, message: any, containe return this.loadMediaQueue ? this.loadMediaQueuePush(load) : load(); } -export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, group?: string, canvas?: boolean, play = false) { +export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: () => boolean, lazyLoadQueue?: LazyLoadQueue, group?: string, canvas?: boolean, play = false, onlyThumb = false) { let stickerType = doc.mime_type == "application/x-tgsticker" ? 2 : (doc.mime_type == "image/webp" ? 1 : 0); if(!stickerType) { console.error('wrong doc for wrapSticker!', doc, div); } - ///////console.log('wrap sticker', doc); + //////console.log('wrap sticker', doc, onlyThumb); if(doc.thumbs && !div.firstElementChild) { let thumb = doc.thumbs[0]; + + ///////console.log('wrap sticker', thumb); if(thumb.bytes) { apiFileManager.saveSmallFile(thumb.location, thumb.bytes); appPhotosManager.setAttachmentPreview(thumb.bytes, div, true); + + if(onlyThumb) return Promise.resolve(); } } + + if(onlyThumb && doc.thumbs) { + let thumb = doc.thumbs[0]; + + let load = () => apiFileManager.downloadSmallFile({ + _: 'inputDocumentFileLocation', + access_hash: doc.access_hash, + file_reference: doc.file_reference, + thumb_size: thumb.type, + id: doc.id + }, {dcID: doc.dc_id}).then(blob => { + let img = new Image(); + + appWebpManager.polyfillImage(img, blob); + + div.append(img); + + div.setAttribute('file-id', doc.id); + appStickersManager.saveSticker(doc); + }); + + return lazyLoadQueue ? (lazyLoadQueue.push({div, load}), Promise.resolve()) : load(); + } let load = () => apiFileManager.downloadSmallFile({ _: 'inputDocumentFileLocation', diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index d34c30bc..1232d50c 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -826,7 +826,7 @@ export class AppImManager { } public setScroll() { - this.scrollable = new Scrollable(this.bubblesContainer, false, true, 750/* 1500 */); + this.scrollable = new Scrollable(this.bubblesContainer, false, true, 750, 'IM'/* 1500 */); this.scroll = this.scrollable.container; this.scrollable.setVirtualContainer(this.chatInner); @@ -1192,14 +1192,22 @@ export class AppImManager { this.scrollPosition.prepareFor(reverse ? 'up' : 'down'); // лагает из-за этого } + let bubbleContainer: HTMLDivElement; + // bubble if(!bubble) { + bubbleContainer = document.createElement('div'); + bubbleContainer.classList.add('bubble__container'); + bubble = document.createElement('div'); bubble.classList.add('bubble'); + bubble.appendChild(bubbleContainer); this.bubbles[+message.mid] = bubble; } else { bubble.className = 'bubble'; - bubble.innerHTML = ''; + bubbleContainer = bubble.firstElementChild as HTMLDivElement; + bubbleContainer.innerHTML = ''; + //bubble.innerHTML = ''; } // time section @@ -1243,7 +1251,7 @@ export class AppImManager { messageDiv.classList.add('message-empty'); bubble.classList.add('emoji-' + emojiEntities.length + 'x', 'emoji-big'); - bubble.append(attachmentDiv); + bubbleContainer.append(attachmentDiv); } else { messageDiv.innerHTML = richText; } @@ -1260,7 +1268,7 @@ export class AppImManager { timeSpan.appendChild(timeInner); messageDiv.append(timeSpan); - bubble.prepend(messageDiv); + bubbleContainer.prepend(messageDiv); //bubble.prepend(timeSpan, messageDiv); // that's bad if(our) { @@ -1409,7 +1417,7 @@ export class AppImManager { box.append(quote); //bubble.prepend(box); - bubble.prepend(timeSpan, box); + bubbleContainer.prepend(timeSpan, box); //this.log('night running', bubble.scrollHeight); @@ -1433,8 +1441,8 @@ export class AppImManager { appPhotosManager.setAttachmentSize(doc, attachmentDiv, undefined, undefined, true); let preloader = new ProgressivePreloader(attachmentDiv, false); - bubble.style.height = attachmentDiv.style.height; - bubble.style.width = attachmentDiv.style.width; + bubbleContainer.style.height = attachmentDiv.style.height; + bubbleContainer.style.width = attachmentDiv.style.width; //appPhotosManager.setAttachmentSize(doc, bubble); let load = () => wrapSticker(doc, attachmentDiv, () => { if(this.peerID != peerID) { @@ -1483,7 +1491,7 @@ export class AppImManager { } if(!processingWebPage) { - bubble.append(attachmentDiv); + bubbleContainer.append(attachmentDiv); } } @@ -1512,7 +1520,7 @@ export class AppImManager { `; - bubble.append(fwd); + bubbleContainer.append(fwd); bubble.dataset.savedFrom = message.savedFrom; } @@ -1522,7 +1530,7 @@ export class AppImManager { nameDiv.innerHTML = 'Forwarded from ' + title; nameDiv.dataset.peerID = message.fwdFromID; //nameDiv.style.color = appPeersManager.getPeerColorByID(message.fromID, false); - bubble.append(nameDiv); + bubbleContainer.append(nameDiv); } } else { if(message.reply_to_mid) { @@ -1546,7 +1554,7 @@ export class AppImManager { bubble.setAttribute('data-original-mid', message.reply_to_mid); } - bubble.append(wrapReply(originalPeerTitle, originalMessage.message || '', originalMessage.media)); + bubbleContainer.append(wrapReply(originalPeerTitle, originalMessage.message || '', originalMessage.media)); bubble.classList.add('is-reply'); } @@ -1556,7 +1564,7 @@ export class AppImManager { nameDiv.innerHTML = title; nameDiv.style.color = appPeersManager.getPeerColorByID(message.fromID, false); nameDiv.dataset.peerID = message.fromID; - bubble.append(nameDiv); + bubbleContainer.append(nameDiv); } else /* if(!message.reply_to_mid) */ { bubble.classList.add('hide-name'); } @@ -1585,14 +1593,14 @@ export class AppImManager { avatarDiv.dataset.peerID = message.fromID; - bubble.append(avatarDiv); + bubbleContainer.append(avatarDiv); } } else { bubble.classList.add('hide-name'); } if(message._ == 'messageService') { - bubble.className = 'service'; + bubble.className = 'bubble service'; let action = message.action; @@ -1608,11 +1616,11 @@ export class AppImManager { } // @ts-ignore let str = (name.innerText ? name.outerHTML + ' ' : '') + langPack[_]; - bubble.innerHTML = `
${str}
`; + bubbleContainer.innerHTML = `
${str}
`; } + bubble.classList.add(our ? 'is-out' : 'is-in'); if(updatePosition) { - bubble.classList.add(our ? 'is-out' : 'is-in'); if(reverse) { this.scrollable.prepend(bubble); } else { @@ -1640,7 +1648,7 @@ export class AppImManager { } let div = document.createElement('div'); - div.classList.add('service'); + div.className = 'bubble service'; div.innerHTML = `
${str}
`; ////////this.log('need to render date message', dateTimestamp, str); @@ -1737,8 +1745,8 @@ export class AppImManager { if(!isBackLimit) { this.scrollPosition.prepareFor(reverse ? 'up' : 'down'); } - - history.forEachReverse((msgID: number) => { + + /* for(let i = 0; i < 25; ++i) */ history.forEachReverse((msgID: number) => { let message = appMessagesManager.getMessage(msgID); this.renderMessage(message, reverse, true); diff --git a/src/lib/appManagers/appPhotosManager.ts b/src/lib/appManagers/appPhotosManager.ts index 1f180b68..17b2875f 100644 --- a/src/lib/appManagers/appPhotosManager.ts +++ b/src/lib/appManagers/appPhotosManager.ts @@ -139,7 +139,7 @@ export class AppPhotosManager { }); } - public setAttachmentPreview(bytes: Uint8Array, div: HTMLDivElement, isSticker = false, background = false) { + public setAttachmentPreview(bytes: Uint8Array, div: HTMLElement, isSticker = false, background = false) { //image.src = "data:image/jpeg;base64," + bytesToBase64(photo.sizes[0].bytes); //photo.sizes[0].bytes = new Uint8Array([...photo.sizes[0].bytes].reverse()); diff --git a/src/lib/appManagers/appSidebarLeft.ts b/src/lib/appManagers/appSidebarLeft.ts index 8b513308..8b825a1e 100644 --- a/src/lib/appManagers/appSidebarLeft.ts +++ b/src/lib/appManagers/appSidebarLeft.ts @@ -9,6 +9,8 @@ import appImManager from "./appImManager"; import appUsersManager from "./appUsersManager"; import { appPeersManager } from "../services"; +let testScroll = false; + class SearchGroup { container: HTMLDivElement; nameEl: HTMLDivElement; @@ -57,7 +59,7 @@ class AppSidebarLeft { private chatsArchivedOffsetIndex = 0; private chatsOffsetIndex = 0; private chatsPreloader: HTMLDivElement; - private chatsLoadCount = 0; + //private chatsLoadCount = 0; //private loadDialogsPromise: Promise; private loadDialogsPromise: ReturnType; @@ -90,15 +92,15 @@ class AppSidebarLeft { putPreloader(this.chatsPreloader); //this.chatsContainer.append(this.chatsPreloader); - this.chatsLoadCount = Math.round(document.body.scrollHeight / 70 * 1.5); + //this.chatsLoadCount = Math.round(document.body.scrollHeight / 70 * 1.5); - this.scroll = new Scrollable(this.chatsContainer as HTMLDivElement); + this.scroll = new Scrollable(this.chatsContainer as HTMLDivElement, false, true, 300, 'CL'); this.scroll.setVirtualContainer(appDialogsManager.chatList); this.scroll.onScrolledBottom = this.onChatsScroll.bind(this); appDialogsManager.chatsHidden = this.scroll.hiddenElements; //this.scroll.container.addEventListener('scroll', this.onChatsScroll.bind(this)); - this.scrollArchived = new Scrollable(this.chatsArchivedContainer as HTMLDivElement); + this.scrollArchived = new Scrollable(this.chatsArchivedContainer as HTMLDivElement, false, true, 300, 'CLA'); this.scrollArchived.setVirtualContainer(appDialogsManager.chatListArchived); appDialogsManager.chatsArchivedHidden = this.scrollArchived.hiddenElements; //this.scrollArchived.container.addEventListener('scroll', this.onChatsArchivedScroll.bind(this)); @@ -128,12 +130,14 @@ class AppSidebarLeft { //this.toolsBtn.classList.add('tgico-back'); }); - /* for(let i = 0; i < 100; ++i) { - let li = document.createElement('li'); - li.dataset.id = '' + i; - li.innerHTML = `

${i}18:33

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

`; - this.scroll.append(li); - } */ + if(testScroll) { + for(let i = 0; i < 1000; ++i) { + let li = document.createElement('li'); + li.dataset.id = '' + i; + li.innerHTML = `

${i}18:33

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

`; + this.scroll.append(li); + } + } this.listsContainer.addEventListener('scroll', this.onSidebarScroll.bind(this)); @@ -196,7 +200,7 @@ class AppSidebarLeft { }); window.addEventListener('resize', () => { - this.chatsLoadCount = Math.round(document.body.scrollHeight / 70 * 1.5); + //this.chatsLoadCount = Math.round(document.body.scrollHeight / 70 * 1.5); setTimeout(() => { this.onSidebarScroll(); @@ -212,7 +216,9 @@ class AppSidebarLeft { } public async loadDialogs(archived = false) { - //return; + if(testScroll) { + return; + } if(this.loadDialogsPromise/* || 1 == 1 */) return this.loadDialogsPromise; @@ -225,9 +231,12 @@ class AppSidebarLeft { try { - this.loadDialogsPromise = appMessagesManager.getConversations('', offset, this.chatsLoadCount, +archived); + console.time('getDialogs time'); + this.loadDialogsPromise = appMessagesManager.getConversations('', offset, 50/*this.chatsLoadCount */, +archived); let result = await this.loadDialogsPromise; + + console.timeEnd('getDialogs time'); if(result && result.dialogs && result.dialogs.length) { let index = result.dialogs[result.dialogs.length - 1].index; @@ -245,7 +254,7 @@ class AppSidebarLeft { this.archivedCount.innerText = '' + count; } */ - /////this.log('loaded ' + this.chatsLoadCount + ' dialogs by offset:', offset, result, this.scroll.hiddenElements); + //this.log('getDialogs ' + this.chatsLoadCount + ' dialogs by offset:', offset, result, this.scroll.hiddenElements); this.scroll.onScroll(); } catch(err) { this.log.error(err); diff --git a/src/lib/appManagers/appSidebarRight.ts b/src/lib/appManagers/appSidebarRight.ts index c0ba6f39..f0130343 100644 --- a/src/lib/appManagers/appSidebarRight.ts +++ b/src/lib/appManagers/appSidebarRight.ts @@ -13,6 +13,8 @@ import appMediaViewer from "./appMediaViewer"; import LazyLoadQueue from "../../components/lazyLoadQueue"; import { wrapDocument, wrapAudio } from "../../components/wrappers"; +const testScroll = false; + class AppSidebarRight { public sidebarEl = document.querySelector('.profile-container') as HTMLDivElement; public profileContentEl = document.querySelector('.profile-content') as HTMLDivElement; @@ -77,7 +79,7 @@ class AppSidebarRight { hiddenElements: any, paddings: any } - } + } = {}; private profileTabs: HTMLUListElement; private prevTabID = -1; @@ -90,7 +92,7 @@ class AppSidebarRight { let container = this.profileContentEl.querySelector('.profile-tabs-content') as HTMLDivElement; this.profileTabs = this.profileContentEl.querySelector('.profile-tabs') as HTMLUListElement; - this.sidebarScroll = new Scrollable(this.sidebarEl); + this.sidebarScroll = new Scrollable(this.sidebarEl, false, true, 500, 'SR'); this.sidebarScroll.container.addEventListener('scroll', this.onSidebarScroll.bind(this)); horizontalMenu(this.profileTabs, container, (id, tabContent) => { @@ -160,6 +162,23 @@ class AppSidebarRight { this.onSidebarScroll(); }, 0); }); + + if(testScroll) { + let div = document.createElement('div'); + for(let i = 0; i < 500; ++i) { + //div.insertAdjacentHTML('beforeend', `
`); + div.insertAdjacentHTML('beforeend', `
${i / 3 | 0}
`); + + if((i + 1) % 3 == 0) { + this.sharedMedia.contentMedia.append(div); + div = document.createElement('div'); + } + + div.dataset.id = '' + (i / 3 | 0); + } + this.sharedMedia.contentMedia.append(div); + (this.profileTabs.children[1] as HTMLLIElement).click(); // set media + } } public onSidebarScroll() { @@ -196,6 +215,10 @@ class AppSidebarRight { } public loadSidebarMedia(single = false) { + if(testScroll) { + return; + } + let peerID = this.peerID; let typesToLoad = single ? [this.sharedMediaType] : this.sharedMediaTypes; @@ -467,6 +490,21 @@ class AppSidebarRight { } } + if(peerID != appImManager.myID) { + let dialog: any = appMessagesManager.getDialogByPeerID(peerID); + if(dialog.length) { + dialog = dialog[0]; + let muted = false; + if(dialog.notify_settings && dialog.notify_settings.mute_until) { + muted = new Date(dialog.notify_settings.mute_until * 1000) > new Date(); + } + + appImManager.setMutedState(muted); + } + } else { + this.profileElements.notificationsRow.style.display = 'none'; + } + if(peerID > 0) { let user = appUsersManager.getUser(peerID); if(user.phone && peerID != appImManager.myID) { @@ -489,6 +527,8 @@ class AppSidebarRight { appImManager.pinnedMsgID = userFull.pinned_msg_id; appMessagesManager.wrapSingleMessage(userFull.pinned_msg_id); } + + this.sidebarScroll.getScrollTopOffset(); }); } else { let chat = appPeersManager.getPeer(peerID); @@ -504,24 +544,12 @@ class AppSidebarRight { if(chatFull.about) { setText(RichTextProcessor.wrapRichText(chatFull.about), this.profileElements.bio); } - }); - } - if(peerID != appImManager.myID) { - let dialog: any = appMessagesManager.getDialogByPeerID(peerID); - if(dialog.length) { - dialog = dialog[0]; - let muted = false; - if(dialog.notify_settings && dialog.notify_settings.mute_until) { - muted = new Date(dialog.notify_settings.mute_until * 1000) > new Date(); - } - - appImManager.setMutedState(muted); - } - } else { - this.profileElements.notificationsRow.style.display = 'none'; + this.sidebarScroll.getScrollTopOffset(); + }); } + this.sidebarScroll.getScrollTopOffset(); //this.loadSidebarMedia(); } } diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index e9ec56ca..5eedc733 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -136,7 +136,7 @@ } #bubbles-inner { - max-width: 700px; + //max-width: 700px; width: 100%; display: flex; flex-direction: column; @@ -148,7 +148,7 @@ justify-content: flex-end; &.is-chat { - .is-in { + .is-in .bubble__container { margin-left: 45px; } } @@ -180,7 +180,6 @@ } .service { - margin: 1rem 0; align-self: center; .service-msg { @@ -203,16 +202,33 @@ } .bubble { - min-width: 60px; - max-width: 85%; - border-radius: 12px; - box-shadow: 0 1px 2px 0 rgba(16, 35, 47, 0.15); - position: relative; - display: flex; - flex-direction: column-reverse; - font-size: 0; - width: max-content; - height: fit-content; + padding-top: 5px; + display: grid; + grid-template-columns: 1fr 700px 1fr; + grid-row-gap: 0px; + + &:before, &:after { + content: " "; + width: 100%; + } + + &__container { + min-width: 60px; + max-width: 85%; + border-radius: 12px; + box-shadow: 0 1px 2px 0 rgba(16, 35, 47, 0.15); + position: relative; + display: flex; + flex-direction: column-reverse; + /* font-size: 0; */ + width: max-content; + height: fit-content; + } + + &.service { + display: block; + padding: 1rem 0; + } &.forwarded { .forward { @@ -261,7 +277,9 @@ } &.photo, &.video { - width: min-content; + .bubble__container { + width: min-content; + } .box.web { /* width: max-content; */ // commented 10.02.2020 @@ -292,11 +310,14 @@ &.emoji-big { font-size: 0; - background: none!important; - box-shadow: none; - line-height: 1; - user-select: none; - -webkit-user-select: none; + + .bubble__container { + background: none!important; + box-shadow: none; + line-height: 1; + user-select: none; + -webkit-user-select: none; + } .attachment { padding-top: .5rem; @@ -345,11 +366,13 @@ } &.sticker, &.round { - cursor: pointer; - background: none!important; - box-shadow: none; - max-width: 300px; - max-height: 300px; + .bubble__container { + cursor: pointer; + background: none!important; + box-shadow: none; + max-width: 300px; + max-height: 300px; + } img { object-fit: contain; @@ -672,7 +695,7 @@ cursor: pointer; } - > .name { + &__container > .name { /* padding: .2675rem .6rem 0 .6rem; */ padding: .32rem .6rem 0 .6rem; font-weight: 500; @@ -709,7 +732,7 @@ margin-top: 6px; } - &:not(.sticker):not(.emoji-big):not(.round):last-child:after { + &:not(.sticker):not(.emoji-big):not(.round):last-child .bubble__container:after { position: absolute; bottom: -1px; width: 11px; @@ -720,9 +743,9 @@ } } - .bubble + .bubble { + /* .bubble + .bubble { margin-top: 5px; - } + } */ .in, .out { @@ -732,36 +755,37 @@ } .is-in { - align-self: flex-start; - - background-color: #ffffff; + .bubble__container { + margin-right: auto; + background-color: #ffffff; border-radius: 6px 12px 12px 6px; + } - &:first-child { - border-radius: 12px 12px 12px 6px; - } - - &:last-child { - border-radius: 6px 12px 12px 0px; - //border-radius: 12px 12px 12px 0px; - - &:after { - left: -8.4px; - background-image: url('../../assets/img/msg-tail-left.svg'); - } - } + &:first-child .bubble__container { + border-radius: 12px 12px 12px 6px; + } + + &:last-child .bubble__container { + border-radius: 6px 12px 12px 0px; + //border-radius: 12px 12px 12px 0px; - &:first-child:last-child { - border-radius: 12px 12px 12px 0px; + &:after { + left: -8.4px; + background-image: url('../../assets/img/msg-tail-left.svg'); } + } - &.forwarded .attachment, - &.is-reply .attachment, - &:not(.hide-name) .message-empty + .attachment/* , - &:not(.hide-name):not(.sticker) .attachment */ { - border-top-left-radius: 0; - border-top-right-radius: 0; - } + &:first-child:last-child .bubble__container { + border-radius: 12px 12px 12px 0px; + } + + &.forwarded .attachment, + &.is-reply .attachment, + &:not(.hide-name) .message-empty + .attachment/* , + &:not(.hide-name):not(.sticker) .attachment */ { + border-top-left-radius: 0; + border-top-right-radius: 0; + } a { color: $darkblue; @@ -773,7 +797,7 @@ &.is-reply { &.emoji-big, &.sticker { - .box, .reply { + .reply { left: calc(100% + 10px); background-color: #fff; } @@ -805,33 +829,34 @@ } .is-out { - align-self: flex-end; - - background-color: #eeffde; + .bubble__container { + margin-left: auto; + background-color: #eeffde; border-radius: 12px 6px 6px 12px; + } - &:first-child { - border-radius: 12px 12px 6px 12px; - } + &:first-child .bubble__container { + border-radius: 12px 12px 6px 12px; + } - &:last-child { - border-radius: 12px 6px 0px 12px; + &:last-child .bubble__container { + border-radius: 12px 6px 0px 12px; - &:after { - right: -8.4px; - background-image: url('../../assets/img/msg-tail-right.svg'); - } + &:after { + right: -8.4px; + background-image: url('../../assets/img/msg-tail-right.svg'); } + } - &:first-child:last-child { - border-radius: 12px 12px 0px 12px; - } + &:first-child:last-child .bubble__container { + border-radius: 12px 12px 0px 12px; + } - &.forwarded .attachment, - &.is-reply .attachment { - border-top-left-radius: 0; - border-top-right-radius: 0; - } + &.forwarded .attachment, + &.is-reply .attachment { + border-top-left-radius: 0; + border-top-right-radius: 0; + } .quote:hover, .reply:hover { background-color: rgba($green, 0.12); diff --git a/src/scss/partials/_emojiDropdown.scss b/src/scss/partials/_emojiDropdown.scss index f7afd69c..12ea4fe0 100644 --- a/src/scss/partials/_emojiDropdown.scss +++ b/src/scss/partials/_emojiDropdown.scss @@ -172,7 +172,7 @@ padding: 0; } */ - .emoji-padding { + .emoji-padding, .stickers-padding { .menu-horizontal > li { font-size: 1.65rem; } diff --git a/src/scss/partials/_sidebar.scss b/src/scss/partials/_sidebar.scss index 235401fe..99247fe6 100644 --- a/src/scss/partials/_sidebar.scss +++ b/src/scss/partials/_sidebar.scss @@ -130,6 +130,7 @@ width: 100%; display: flex; flex-direction: column; + padding-top: 4px; > div { display: grid; @@ -137,10 +138,7 @@ grid-auto-rows: max-content; grid-gap: 3.5px; place-items: start; - - & + div { - margin-top: 3.5px; - } + padding-top: 3.5px; > div { width: 100%; @@ -149,6 +147,11 @@ background-repeat: no-repeat; background-size: cover; background-position: center center; + + display: flex; + background-color: #cecece; + justify-content: center; + align-items: center; &::before { content: "";