Eduard Kuzmenko
5 years ago
27 changed files with 4415 additions and 1185 deletions
@ -0,0 +1,922 @@ |
|||||||
|
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'; |
||||||
|
import { logger } from "../lib/polyfill"; |
||||||
|
|
||||||
|
//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 translate: string; |
||||||
|
public scrollType: string; |
||||||
|
public scrollSide: string; |
||||||
|
public clientAxis: string; |
||||||
|
|
||||||
|
public scrollSize = -1; // it will be scrollHeight
|
||||||
|
public size = 0; // it will be outerHeight of container (not scrollHeight)
|
||||||
|
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 onScrolledTopFired = false; |
||||||
|
public onScrolledBottomFired = false; |
||||||
|
|
||||||
|
public topObserver: IntersectionObserver; |
||||||
|
public bottomObserver: IntersectionObserver; |
||||||
|
|
||||||
|
public splitMeasureTop: Promise<{element: Element, height: number}[]> = null; |
||||||
|
public splitMeasureBottom: Scrollable['splitMeasureTop'] = null; |
||||||
|
public splitMeasureAdd: Promise<number> = null; |
||||||
|
public splitMeasureRemoveBad: Promise<Element> = null; |
||||||
|
public splitMutateTop: Promise<void> = null; |
||||||
|
public splitMutateBottom: Scrollable['splitMutateTop'] = null; |
||||||
|
public splitMutateRemoveBad: Promise<void> = null; |
||||||
|
|
||||||
|
public splitMutateIntersectionTop: Promise<void> = null; |
||||||
|
public splitMutateIntersectionBottom: Promise<void> = null; |
||||||
|
|
||||||
|
public getScrollHeightPromises: Array<{ |
||||||
|
element: Element, |
||||||
|
task: Promise<any> |
||||||
|
}> = []; |
||||||
|
|
||||||
|
public onScrollMeasure: Promise<any> = null; |
||||||
|
|
||||||
|
public lastScrollTop: number = 0; |
||||||
|
public scrollTopOffset: number = 0; |
||||||
|
|
||||||
|
private log: ReturnType<typeof logger>; |
||||||
|
private debug = false; |
||||||
|
|
||||||
|
constructor(public el: HTMLElement, x = false, y = true, public splitOffset = 300, logPrefix = '', public appendTo = el, public onScrollOffset = splitOffset) { |
||||||
|
this.container = document.createElement('div'); |
||||||
|
this.container.classList.add('scrollable'); |
||||||
|
|
||||||
|
this.log = logger('SCROLL' + (logPrefix ? '-' + logPrefix : '')); |
||||||
|
|
||||||
|
if(x) { |
||||||
|
this.container.classList.add('scrollable-x'); |
||||||
|
this.type = 'width'; |
||||||
|
this.side = 'left'; |
||||||
|
this.translate = 'translateX'; |
||||||
|
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.translate = 'translateY'; |
||||||
|
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.onScroll(); |
||||||
|
this.resize(); |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingTopDiv = document.createElement('div'); |
||||||
|
this.paddingTopDiv.classList.add('scroll-padding'); |
||||||
|
this.paddingBottomDiv = document.createElement('div'); |
||||||
|
this.paddingBottomDiv.classList.add('scroll-padding'); |
||||||
|
|
||||||
|
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.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 resize() { |
||||||
|
//console.time('scroll resize');
|
||||||
|
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 setVirtualContainer(el?: HTMLElement) { |
||||||
|
this.splitUp = el; |
||||||
|
|
||||||
|
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(() => { |
||||||
|
this.paddingTopDiv.style.height = ''; |
||||||
|
this.paddingBottomDiv.style.height = ''; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
this.log('setVirtualContainer:', el, this); |
||||||
|
|
||||||
|
this.getScrollTopOffset(); |
||||||
|
|
||||||
|
if(el) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
el.parentElement.insertBefore(this.paddingTopDiv, el); |
||||||
|
el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.paddingTopDiv.remove(); |
||||||
|
this.paddingBottomDiv.remove(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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(); |
||||||
|
|
||||||
|
this.scrollTopOffset = rect.top - containerRect.top; |
||||||
|
this.log('set scrollTopOffset to:', this.scrollTopOffset); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.scrollTopOffset = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public onScroll() { |
||||||
|
return; |
||||||
|
|
||||||
|
if(this.debug) { |
||||||
|
this.log('onScroll call'); |
||||||
|
} |
||||||
|
|
||||||
|
if(this.onScrollMeasure) fastdom.clear(this.onScrollMeasure); |
||||||
|
this.onScrollMeasure = fastdom.measure(() => { |
||||||
|
// @ts-ignore quick brown fix
|
||||||
|
this.size = this.parentElement[this.scrollType]; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
let scrollSize = this.container[this.scrollType]; |
||||||
|
if(scrollSize != this.scrollSize || this.thumbSize == 0) { |
||||||
|
this.resize(); |
||||||
|
} |
||||||
|
this.scrollSize = scrollSize; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
let scrollPos = this.container[this.scrollSide]; |
||||||
|
|
||||||
|
// let value = scrollPos / (this.scrollSize - this.size) * 100;
|
||||||
|
// let maxValue = 100 - (this.thumbSize / this.size * 100);
|
||||||
|
let value = scrollPos / (this.scrollSize - this.size) * this.size; |
||||||
|
let maxValue = this.size - this.thumbSize; |
||||||
|
|
||||||
|
//this.log(scrollPos, this.scrollSize, this.size, value, scrollPos / (this.scrollSize - this.size) * this.size);
|
||||||
|
let ret = {value, maxValue}; |
||||||
|
|
||||||
|
let scrollTop = scrollPos - this.scrollTopOffset; |
||||||
|
let maxScrollTop = this.scrollSize - this.scrollTopOffset - this.size; |
||||||
|
|
||||||
|
if(this.onScrolledBottom) { |
||||||
|
if(!this.hiddenElements.down.length && (maxScrollTop - scrollTop) <= this.onScrollOffset) { |
||||||
|
if(!this.onScrolledBottomFired) { |
||||||
|
this.onScrolledBottomFired = true; |
||||||
|
this.onScrolledBottom(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.onScrolledBottomFired = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(this.onScrolledTop) { |
||||||
|
//this.log('onScrolledTop:', scrollTop, this.onScrollOffset);
|
||||||
|
if(!this.hiddenElements.up.length && scrollTop <= this.onScrollOffset) { |
||||||
|
if(!this.onScrolledTopFired) { |
||||||
|
this.onScrolledTopFired = true; |
||||||
|
this.onScrolledTop(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.onScrolledTopFired = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!this.splitUp) { |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
let perf = performance.now(); |
||||||
|
|
||||||
|
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 + this.size; |
||||||
|
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; |
||||||
|
}); |
||||||
|
} 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
|
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('seems manually scrolled bottom', this.paddings.up, this.lastScrollTop); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
}); |
||||||
|
} 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) { |
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('seems manually scrolled top', this.paddings.down, this.lastScrollTop); |
||||||
|
} |
||||||
|
|
||||||
|
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}; |
||||||
|
}); |
||||||
|
|
||||||
|
this.onScrollMeasure.then(({value, maxValue}) => { |
||||||
|
//fastdom.mutate(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
//this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%';
|
||||||
|
this.thumb.style.transform = this.translate + '(' + (value >= maxValue ? maxValue : value) + 'px)'; |
||||||
|
//});
|
||||||
|
}); |
||||||
|
|
||||||
|
//console.timeEnd('scroll onScroll');
|
||||||
|
} |
||||||
|
|
||||||
|
public onManualScrollTop(scrollTop: number, needHeight: number, maxScrollTop: number) { |
||||||
|
//if(this.splitMutateRemoveBad) fastdom.clear(this.splitMutateRemoveBad);
|
||||||
|
this.splitMutateRemoveBad = fastdom.mutate(() => { |
||||||
|
let h = maxScrollTop - (scrollTop + this.size); |
||||||
|
|
||||||
|
while(this.paddings.down < h && this.paddings.up) { |
||||||
|
let child = this.hiddenElements.up.pop(); |
||||||
|
this.hiddenElements.down.unshift(child); |
||||||
|
this.paddings.down += child.height; |
||||||
|
this.paddings.up -= child.height; |
||||||
|
} |
||||||
|
|
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('bait it off now', this, length, this.splitUp.childElementCount, scrollTop, this.paddings.up, h); |
||||||
|
} |
||||||
|
|
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onTopIntersection((this.size * 2) + (needHeight * 2)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
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.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onBottomIntersection(this.size + (needHeight * 2)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
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 { |
||||||
|
this.paddingTopDiv.style.height = '0px'; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public onBottomIntersection(needHeight: number) { |
||||||
|
if(this.debug) { |
||||||
|
this.log('onBottomIntersection', needHeight, this); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.appendChild(fragment); |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
|
||||||
|
/* if(this.debug) { |
||||||
|
this.log('onBottomIntersection append:', fragment, needHeight); |
||||||
|
} */ |
||||||
|
|
||||||
|
if(this.onAddedBottom) this.onAddedBottom(); |
||||||
|
} else { |
||||||
|
this.paddingBottomDiv.style.height = '0px'; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public prepend(...smth: Element[]) { |
||||||
|
if(this.splitUp) { |
||||||
|
smth.forEach(node => { |
||||||
|
this.removeElement(node); |
||||||
|
}); |
||||||
|
|
||||||
|
if(this.hiddenElements.up.length) { |
||||||
|
/* fastdom.mutate(() => { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
}).then(() => { |
||||||
|
return fastdom.measure(() => { |
||||||
|
smth.forEachReverse(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to up hidden', node, height); |
||||||
|
this.paddings.up += height; |
||||||
|
this.hiddenElements.up.unshift({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}).then(() => { |
||||||
|
fastdom.mutate(() => { |
||||||
|
smth.forEachReverse(node => { |
||||||
|
if(node.parentElement) { |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
|
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
}); */ |
||||||
|
this.splitUp.prepend(...smth); |
||||||
|
smth.forEachReverse(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to up hidden', node, height); |
||||||
|
this.paddings.up += height; |
||||||
|
this.hiddenElements.up.unshift({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
}); |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
} else { |
||||||
|
this.splitUp.prepend(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.appendTo.prepend(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
//this.onScroll();
|
||||||
|
} |
||||||
|
|
||||||
|
public append(...smth: Element[]) { |
||||||
|
if(this.splitUp) { |
||||||
|
smth.forEach(node => { |
||||||
|
this.removeElement(node); |
||||||
|
}); |
||||||
|
|
||||||
|
if(this.hiddenElements.down.length) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
}).then(() => { |
||||||
|
return fastdom.measure(() => { |
||||||
|
smth.forEach(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to down hidden', node, height); |
||||||
|
this.paddings.down += height; |
||||||
|
this.hiddenElements.down.push({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}).then(() => { |
||||||
|
fastdom.mutate(() => { |
||||||
|
smth.forEach(node => { |
||||||
|
if(node.parentElement) { |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
|
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.appendTo.append(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
//this.onScroll();
|
||||||
|
} |
||||||
|
|
||||||
|
public removeElement(element: Element) { |
||||||
|
if(!this.splitUp) { |
||||||
|
if(this.container.contains(element)) { |
||||||
|
//fastdom.mutate(() => this.container.removeChild(element));
|
||||||
|
this.container.removeChild(element); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} else { |
||||||
|
if(this.splitUp.contains(element)) { |
||||||
|
//fastdom.mutate(() => this.splitUp.removeChild(element));
|
||||||
|
this.splitUp.removeChild(element); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let child = this.hiddenElements.up.findAndSplice(c => c.element == element); |
||||||
|
let foundUp = false; |
||||||
|
if(child) { |
||||||
|
this.paddings.up -= child.height; |
||||||
|
foundUp = true; |
||||||
|
} else { |
||||||
|
child = this.hiddenElements.down.findAndSplice(c => c.element == element); |
||||||
|
if(child) { |
||||||
|
this.paddings.down -= child.height; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!child) return; |
||||||
|
|
||||||
|
//fastdom.mutate(() => {
|
||||||
|
if(foundUp) { |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
} else { |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
} |
||||||
|
//});
|
||||||
|
|
||||||
|
return child; |
||||||
|
} |
||||||
|
|
||||||
|
public insertBefore(newChild: Element, refChild: Element, height?: number) { |
||||||
|
//this.log('insertBefore', newChild, refChild);
|
||||||
|
return; |
||||||
|
|
||||||
|
if(this.splitUp) { |
||||||
|
let index = -1; |
||||||
|
index = this.hiddenElements.up.findIndex(c => c.element == refChild); |
||||||
|
|
||||||
|
let child = this.removeElement(newChild); |
||||||
|
if(child) { |
||||||
|
height = child.height; |
||||||
|
} else if(height === undefined) { |
||||||
|
let p = this.getScrollHeightPromises.find(p => p.element == newChild); |
||||||
|
if(!p) p = {element: newChild, task: null}; |
||||||
|
else fastdom.clear(p.task); |
||||||
|
|
||||||
|
let promise: any; |
||||||
|
|
||||||
|
return p.task = promise = fastdom.mutate(() => { |
||||||
|
this.splitUp.append(newChild); |
||||||
|
|
||||||
|
return fastdom.measure(() => { |
||||||
|
if(p.task != promise) return; |
||||||
|
|
||||||
|
let height = newChild.scrollHeight; |
||||||
|
|
||||||
|
return fastdom.mutate(() => { |
||||||
|
if(p.task != promise || !newChild.parentElement) return; |
||||||
|
|
||||||
|
this.splitUp.removeChild(newChild); |
||||||
|
|
||||||
|
this.insertBefore(newChild, refChild, height); |
||||||
|
|
||||||
|
this.getScrollHeightPromises = this.getScrollHeightPromises.filter(p => p.element != newChild); |
||||||
|
|
||||||
|
return height; |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if(index !== -1) { |
||||||
|
this.hiddenElements.up.splice(index, 0, {element: newChild, height: height}); |
||||||
|
this.paddings.up += height; |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return index; |
||||||
|
} else { |
||||||
|
index = this.hiddenElements.down.findIndex(c => c.element == refChild); |
||||||
|
|
||||||
|
if(index !== -1) { |
||||||
|
this.hiddenElements.down.splice(index, 0, {element: newChild, height: height}); |
||||||
|
this.paddings.down += height; |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return index; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fastdom.mutate(() => { |
||||||
|
this.log('inserting', newChild, 'before', refChild, this.splitUp.contains(refChild)); |
||||||
|
if(!this.splitUp.contains(refChild)) { |
||||||
|
this.log.error('no refChild in splitUp', refChild, newChild, this.hiddenElements); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.insertBefore(newChild, refChild); |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let ret = this.container.insertBefore(newChild, refChild); |
||||||
|
this.onScroll(); |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
public scrollIntoView(element: Element) { |
||||||
|
if(element.parentElement) { |
||||||
|
element.scrollIntoView(); |
||||||
|
} else if(this.splitUp) { |
||||||
|
let index = this.hiddenElements.up.findIndex(e => e.element == element); |
||||||
|
let y = 0; |
||||||
|
if(index !== -1) { |
||||||
|
for(let i = 0; i < index; ++i) { |
||||||
|
y += this.hiddenElements.up[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
this.scrollTop = y; |
||||||
|
} else if((index = this.hiddenElements.down.findIndex(e => e.element == element)) !== -1) { |
||||||
|
y += this.paddings.up + this.size; |
||||||
|
for(let i = 0; i < index; ++i) { |
||||||
|
y += this.hiddenElements.down[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
this.scrollTop = y; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
set scrollTop(y: number) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,974 @@ |
|||||||
|
import { cancelEvent } from "../lib/utils"; |
||||||
|
|
||||||
|
import 'fastdom/fastdom.min'; |
||||||
|
import FastDom from 'fastdom'; |
||||||
|
//import 'fastdom/src/fastdom-strict'; // exclude in production
|
||||||
|
import FastDomPromised from 'fastdom/extensions/fastdom-promised'; |
||||||
|
import { logger, deferredPromise, CancellablePromise } from "../lib/polyfill"; |
||||||
|
|
||||||
|
//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 translate: string; |
||||||
|
public scrollType: string; |
||||||
|
public scrollSide: string; |
||||||
|
public clientAxis: string; |
||||||
|
public clientSize: string; |
||||||
|
|
||||||
|
public scrollSize = -1; // it will be scrollHeight
|
||||||
|
public size = 0; // it will be outerHeight of container (not scrollHeight)
|
||||||
|
public thumbSize = 0; |
||||||
|
|
||||||
|
public visibleElements: Array<{element: Element, height: number}> = []; |
||||||
|
public hiddenElements: { |
||||||
|
up: Scrollable['visibleElements'], |
||||||
|
down: Scrollable['visibleElements'] |
||||||
|
} = { |
||||||
|
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 onScrolledTopFired = false; |
||||||
|
public onScrolledBottomFired = false; |
||||||
|
|
||||||
|
public topObserver: IntersectionObserver; |
||||||
|
public bottomObserver: IntersectionObserver; |
||||||
|
|
||||||
|
public splitMeasureTop: Promise<Promise<void>> = null; |
||||||
|
public splitMeasureBottom: Scrollable['splitMeasureTop'] = null; |
||||||
|
public splitMeasureAdd: Promise<void> = null; |
||||||
|
public splitMeasureRemoveBad: Promise<Element> = null; |
||||||
|
public splitMutateTop: Promise<void> = null; |
||||||
|
public splitMutateBottom: Scrollable['splitMutateTop'] = null; |
||||||
|
public splitMutateRemoveBad: Promise<void> = null; |
||||||
|
|
||||||
|
public splitMutateIntersectionTop: Promise<void> = null; |
||||||
|
public splitMutateIntersectionBottom: Promise<void> = null; |
||||||
|
|
||||||
|
public getScrollHeightPromises: Array<{ |
||||||
|
element: Element, |
||||||
|
task: Promise<any> |
||||||
|
}> = []; |
||||||
|
|
||||||
|
public onScrollMeasure: number = null; |
||||||
|
|
||||||
|
public lastScrollTop: number = 0; |
||||||
|
public scrollTopOffset: number = 0; |
||||||
|
|
||||||
|
private disableHoverTimeout: number = 0; |
||||||
|
|
||||||
|
private log: ReturnType<typeof logger>; |
||||||
|
private debug = true; |
||||||
|
|
||||||
|
private measureMutex: CancellablePromise<void>; |
||||||
|
|
||||||
|
constructor(public el: HTMLElement, axis: 'y' | 'x' = 'y', public splitOffset = 300, logPrefix = '', public appendTo = el, public onScrollOffset = splitOffset) { |
||||||
|
this.container = document.createElement('div'); |
||||||
|
this.container.classList.add('scrollable'); |
||||||
|
|
||||||
|
this.log = logger('SCROLL' + (logPrefix ? '-' + logPrefix : '')); |
||||||
|
|
||||||
|
this.measureMutex = deferredPromise<void>(); |
||||||
|
this.measureMutex.resolve(); |
||||||
|
|
||||||
|
if(axis == 'x') { |
||||||
|
this.container.classList.add('scrollable-x'); |
||||||
|
this.type = 'width'; |
||||||
|
this.side = 'left'; |
||||||
|
this.translate = 'translateX'; |
||||||
|
this.scrollType = 'scrollWidth'; |
||||||
|
this.scrollSide = 'scrollLeft'; |
||||||
|
this.clientAxis = 'clientX'; |
||||||
|
this.clientSize = 'clientWidth'; |
||||||
|
|
||||||
|
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(axis == 'y') { |
||||||
|
this.container.classList.add('scrollable-y'); |
||||||
|
this.type = 'height'; |
||||||
|
this.side = 'top'; |
||||||
|
this.translate = 'translateY'; |
||||||
|
this.scrollType = 'scrollHeight'; |
||||||
|
this.scrollSide = 'scrollTop'; |
||||||
|
this.clientAxis = 'clientY'; |
||||||
|
this.clientSize = 'clientHeight'; |
||||||
|
} 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', () => { |
||||||
|
setTimeout(() => { |
||||||
|
// @ts-ignore
|
||||||
|
this.size = this.container[this.clientSize]; |
||||||
|
this.onScroll(); |
||||||
|
this.resize(); |
||||||
|
}, 0); |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingTopDiv = document.createElement('div'); |
||||||
|
this.paddingTopDiv.classList.add('scroll-padding'); |
||||||
|
this.paddingBottomDiv = document.createElement('div'); |
||||||
|
this.paddingBottomDiv.classList.add('scroll-padding'); |
||||||
|
|
||||||
|
this.container.addEventListener('scroll', () => this.onScroll(), {passive: true, capture: true}); |
||||||
|
|
||||||
|
Array.from(el.children).forEach(c => this.container.append(c)); |
||||||
|
|
||||||
|
el.append(this.container); |
||||||
|
|
||||||
|
setTimeout(() => { |
||||||
|
// @ts-ignore
|
||||||
|
this.size = this.container[this.clientSize]; |
||||||
|
}, 0); |
||||||
|
|
||||||
|
this.container.parentElement.append(this.thumb); |
||||||
|
this.resize(); |
||||||
|
} |
||||||
|
|
||||||
|
public detachTop(fromIndex: number, needHeight = 0, detachAll = false) { |
||||||
|
//if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom);
|
||||||
|
if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); |
||||||
|
|
||||||
|
//return this.splitMeasureBottom = fastdom.measure(() => {
|
||||||
|
return this.splitMutateBottom = fastdom.mutate(() => { |
||||||
|
let spliceTo = -1; |
||||||
|
|
||||||
|
let needToDetachHeight = needHeight; |
||||||
|
for(; fromIndex >= 0; --fromIndex) { |
||||||
|
let child = this.visibleElements[fromIndex]; |
||||||
|
if(needHeight > 0) { |
||||||
|
needHeight -= child.height; |
||||||
|
} else { |
||||||
|
needToDetachHeight -= child.height; |
||||||
|
if(spliceTo === -1) { |
||||||
|
spliceTo = fromIndex; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if((needToDetachHeight > 0 && !detachAll) || spliceTo === -1) return; |
||||||
|
|
||||||
|
//if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
|
||||||
|
//return this.splitMutateBottom = fastdom.mutate(() => {
|
||||||
|
let spliced = this.visibleElements.splice(0, spliceTo + 1); |
||||||
|
spliced.forEach(child => { |
||||||
|
//if(!this.splitUp.contains(child.element)) return false;
|
||||||
|
|
||||||
|
this.paddings.up += child.height; |
||||||
|
this.splitUp.removeChild(child.element); |
||||||
|
}); |
||||||
|
|
||||||
|
this.hiddenElements.up.push(...spliced); |
||||||
|
|
||||||
|
if(this.debug) { |
||||||
|
this.log('spliced up', spliced); |
||||||
|
} |
||||||
|
|
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
//});
|
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public detachBottom(fromIndex: number, needHeight = 0, detachAll = false) { |
||||||
|
//if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom);
|
||||||
|
if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom); |
||||||
|
|
||||||
|
//return this.splitMeasureBottom = fastdom.measure(() => {
|
||||||
|
return this.splitMutateBottom = fastdom.mutate(() => { |
||||||
|
let spliceFrom = -1; |
||||||
|
let spliceTo = 0; |
||||||
|
|
||||||
|
let needToDetachHeight = needHeight; |
||||||
|
let length = this.visibleElements.length; |
||||||
|
for(; fromIndex < length; ++fromIndex) { |
||||||
|
let child = this.visibleElements[fromIndex]; |
||||||
|
if(needHeight > 0) { |
||||||
|
needHeight -= child.height; |
||||||
|
} else { |
||||||
|
needToDetachHeight -= child.height; |
||||||
|
if(spliceFrom === -1) spliceFrom = fromIndex; |
||||||
|
spliceTo = fromIndex; |
||||||
|
} |
||||||
|
} |
||||||
|
if((needToDetachHeight > 0 && !detachAll) || spliceFrom === -1) return; |
||||||
|
|
||||||
|
//if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
|
||||||
|
//return this.splitMutateBottom = fastdom.mutate(() => {
|
||||||
|
let spliced = this.visibleElements.splice(spliceFrom, spliceTo - spliceFrom + 1); |
||||||
|
spliced.forEach((child) => { |
||||||
|
//if(!this.splitUp.contains(child.element)) return false;
|
||||||
|
|
||||||
|
this.paddings.down += child.height; |
||||||
|
this.splitUp.removeChild(child.element); |
||||||
|
}); |
||||||
|
|
||||||
|
this.hiddenElements.down.unshift(...spliced); |
||||||
|
|
||||||
|
if(this.debug) { |
||||||
|
this.log('spliced down', spliced, spliceFrom, spliceTo - spliceFrom + 1, length); |
||||||
|
} |
||||||
|
|
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
//});
|
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public resize() { |
||||||
|
//console.time('scroll resize');
|
||||||
|
//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 setVirtualContainer(el?: HTMLElement) { |
||||||
|
this.splitUp = el; |
||||||
|
|
||||||
|
this.onScrolledBottomFired = this.onScrolledTopFired = false; |
||||||
|
this.hiddenElements.up.length = this.hiddenElements.down.length = this.visibleElements.length = 0; |
||||||
|
this.paddings.up = this.paddings.down = 0; |
||||||
|
this.lastScrollTop = 0; |
||||||
|
|
||||||
|
if(this.paddingTopDiv.parentElement) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingTopDiv.style.height = ''; |
||||||
|
this.paddingBottomDiv.style.height = ''; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
this.log('setVirtualContainer:', el, this); |
||||||
|
|
||||||
|
this.getScrollTopOffset(); |
||||||
|
|
||||||
|
if(el) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
el.parentElement.insertBefore(this.paddingTopDiv, el); |
||||||
|
el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.paddingTopDiv.remove(); |
||||||
|
this.paddingBottomDiv.remove(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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(); |
||||||
|
|
||||||
|
this.scrollTopOffset = rect.top - containerRect.top; |
||||||
|
this.log('set scrollTopOffset to:', this.scrollTopOffset); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.scrollTopOffset = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public onScroll() { |
||||||
|
//return;
|
||||||
|
if(this.debug) { |
||||||
|
this.log('onScroll call'); |
||||||
|
} |
||||||
|
|
||||||
|
let appendTo = this.splitUp || this.appendTo; |
||||||
|
|
||||||
|
clearTimeout(this.disableHoverTimeout); |
||||||
|
if(this.el != this.appendTo) { |
||||||
|
if(!appendTo.classList.contains('disable-hover')) { |
||||||
|
appendTo.classList.add('disable-hover'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.disableHoverTimeout = setTimeout(() => { |
||||||
|
appendTo.classList.remove('disable-hover'); |
||||||
|
|
||||||
|
if(!this.measureMutex.isFulfilled) { |
||||||
|
this.measureMutex.resolve(); |
||||||
|
} |
||||||
|
}, 100); |
||||||
|
|
||||||
|
if(this.onScrollMeasure) return; //window.cancelAnimationFrame(this.onScrollMeasure);
|
||||||
|
this.onScrollMeasure = window.requestAnimationFrame(() => { |
||||||
|
// @ts-ignore
|
||||||
|
let scrollPos = this.container[this.scrollSide]; |
||||||
|
|
||||||
|
if(this.measureMutex.isFulfilled) { |
||||||
|
// @ts-ignore quick brown fix
|
||||||
|
this.size = this.container[this.clientSize]; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
let scrollSize = this.container[this.scrollType]; |
||||||
|
if(scrollSize != this.scrollSize || this.thumbSize == 0) { |
||||||
|
this.scrollSize = scrollSize; |
||||||
|
this.resize(); |
||||||
|
} else this.scrollSize = scrollSize; |
||||||
|
|
||||||
|
this.measureMutex = deferredPromise<void>(); |
||||||
|
} |
||||||
|
|
||||||
|
// let value = scrollPos / (this.scrollSize - this.size) * 100;
|
||||||
|
// let maxValue = 100 - (this.thumbSize / this.size * 100);
|
||||||
|
let value = scrollPos / (this.scrollSize - this.size) * this.size; |
||||||
|
let maxValue = this.size - this.thumbSize; |
||||||
|
|
||||||
|
//this.log(scrollPos, this.scrollSize, this.size, value, scrollPos / (this.scrollSize - this.size) * this.size);
|
||||||
|
|
||||||
|
let scrollTop = scrollPos - this.scrollTopOffset; |
||||||
|
let maxScrollTop = this.scrollSize - this.scrollTopOffset - this.size; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
this.thumb.style.transform = this.translate + '(' + (value >= maxValue ? maxValue : value) + 'px)'; |
||||||
|
|
||||||
|
if(this.onScrolledBottom) { |
||||||
|
if(!this.hiddenElements.down.length && (maxScrollTop - scrollTop) <= this.onScrollOffset) { |
||||||
|
//if(!this.onScrolledBottomFired) {
|
||||||
|
this.onScrolledBottomFired = true; |
||||||
|
this.onScrolledBottom(); |
||||||
|
//}
|
||||||
|
} else { |
||||||
|
this.onScrolledBottomFired = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(this.onScrolledTop) { |
||||||
|
//this.log('onScrolledTop:', scrollTop, this.onScrollOffset);
|
||||||
|
if(!this.hiddenElements.up.length && scrollTop <= this.onScrollOffset) { |
||||||
|
//if(!this.onScrolledTopFired) {
|
||||||
|
this.onScrolledTopFired = true; |
||||||
|
this.onScrolledTop(); |
||||||
|
//}
|
||||||
|
} else { |
||||||
|
this.onScrolledTopFired = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!this.splitUp) { |
||||||
|
this.onScrollMeasure = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let perf = performance.now(); |
||||||
|
|
||||||
|
if(scrollTop < 0) scrollTop = 0; |
||||||
|
else if(scrollTop > maxScrollTop) scrollTop = maxScrollTop; |
||||||
|
|
||||||
|
let toBottom = scrollTop > this.lastScrollTop; |
||||||
|
|
||||||
|
let visibleFrom = scrollTop - this.paddings.up; |
||||||
|
let visibleUntil = visibleFrom + this.size; |
||||||
|
let sum = 0; |
||||||
|
|
||||||
|
let firstVisibleElementIndex = -1; |
||||||
|
let lastVisibleElementIndex = -1; |
||||||
|
|
||||||
|
let needHeight = this.splitOffset; |
||||||
|
let length = this.visibleElements.length; |
||||||
|
this.visibleElements.forEach((child, idx) => { |
||||||
|
if(sum < visibleUntil && (sum + child.height) >= visibleFrom && firstVisibleElementIndex === -1) { // if any part is in viewport
|
||||||
|
firstVisibleElementIndex = idx; |
||||||
|
} |
||||||
|
|
||||||
|
if(sum < visibleUntil && firstVisibleElementIndex !== -1) { |
||||||
|
lastVisibleElementIndex = idx; |
||||||
|
} |
||||||
|
|
||||||
|
sum += child.height; |
||||||
|
|
||||||
|
//this.log(sum, element);
|
||||||
|
}); |
||||||
|
|
||||||
|
if(lastVisibleElementIndex === -1 && firstVisibleElementIndex !== -1) { |
||||||
|
lastVisibleElementIndex = firstVisibleElementIndex; |
||||||
|
} |
||||||
|
|
||||||
|
// возможно устанавливать прошлый скролл нужно уже после этого промиса, т.к. он может очиститься
|
||||||
|
if(scrollTop == this.lastScrollTop) { |
||||||
|
this.lastScrollTop = scrollTop; |
||||||
|
if(firstVisibleElementIndex !== -1) this.detachTop(firstVisibleElementIndex, needHeight); |
||||||
|
if(lastVisibleElementIndex !== -1) this.detachBottom(lastVisibleElementIndex, needHeight); |
||||||
|
this.onScrollMeasure = 0; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
/* { |
||||||
|
this.log('onScroll', (performance.now() - perf).toFixed(3), length, scrollTop, |
||||||
|
toBottom, firstVisibleElement, lastVisibleElement, visibleFrom, visibleUntil); |
||||||
|
return {value, maxValue}; |
||||||
|
} */ |
||||||
|
|
||||||
|
if(toBottom) { // scrolling bottom
|
||||||
|
if(firstVisibleElementIndex !== -1) { |
||||||
|
if(this.debug) { |
||||||
|
this.log('will detach top by:', firstVisibleElementIndex, needHeight); |
||||||
|
} |
||||||
|
|
||||||
|
this.detachTop(firstVisibleElementIndex, needHeight); |
||||||
|
|
||||||
|
for(let i = lastVisibleElementIndex + 1; i < length; ++i) { |
||||||
|
needHeight -= this.visibleElements[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
if(needHeight >= this.splitOffset) { |
||||||
|
//this.detachTop(firstVisibleElementIndex, this.splitOffset);
|
||||||
|
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(this.visibleElements.length - 1, 0, true).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
|
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('seems manually scrolled bottom', this.paddings.up, this.lastScrollTop); |
||||||
|
} |
||||||
|
|
||||||
|
this.onManualScrollBottom(scrollTop, needHeight); |
||||||
|
} |
||||||
|
} else { // scrolling top
|
||||||
|
if(lastVisibleElementIndex !== -1) { |
||||||
|
if(this.debug) { |
||||||
|
this.log('will detach bottom by:', lastVisibleElementIndex, needHeight); |
||||||
|
} |
||||||
|
|
||||||
|
//if((lastVisibleElementIndex + 1) < length) {
|
||||||
|
this.detachBottom(lastVisibleElementIndex, needHeight); |
||||||
|
//}
|
||||||
|
|
||||||
|
for(let i = firstVisibleElementIndex - 1; i >= 0; --i) { |
||||||
|
needHeight -= this.visibleElements[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
if(needHeight >= this.splitOffset) { |
||||||
|
//this.detachBottom(lastVisibleElementIndex, this.splitOffset);
|
||||||
|
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(0, 0, true).then(() => { // now need to move from one hidden array to another one
|
||||||
|
this.onManualScrollTop(scrollTop, needHeight, maxScrollTop); |
||||||
|
}); |
||||||
|
} else if(this.paddings.up) { |
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('seems manually scrolled top', this.paddings.down, this.lastScrollTop); |
||||||
|
} |
||||||
|
|
||||||
|
this.onManualScrollTop(scrollTop, needHeight, maxScrollTop); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(this.debug) { |
||||||
|
this.log('onScroll', (performance.now() - perf).toFixed(3), length, scrollTop, maxScrollTop, toBottom, firstVisibleElementIndex, lastVisibleElementIndex, visibleFrom, visibleUntil, this.scrollTopOffset); |
||||||
|
} |
||||||
|
|
||||||
|
this.lastScrollTop = scrollTop; |
||||||
|
|
||||||
|
this.onScrollMeasure = 0; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public onManualScrollTop(scrollTop: number, needHeight: number, maxScrollTop: number) { |
||||||
|
//if(this.splitMutateRemoveBad) fastdom.clear(this.splitMutateRemoveBad);
|
||||||
|
this.splitMutateRemoveBad = fastdom.mutate(() => { |
||||||
|
let h = maxScrollTop - (scrollTop + this.size); |
||||||
|
|
||||||
|
while(this.paddings.down < h && this.paddings.up) { |
||||||
|
let child = this.hiddenElements.up.pop(); |
||||||
|
this.hiddenElements.down.unshift(child); |
||||||
|
this.paddings.down += child.height; |
||||||
|
this.paddings.up -= child.height; |
||||||
|
} |
||||||
|
|
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('manual scroll top', this, length, this.splitUp.childElementCount, scrollTop, this.paddings.up, h); |
||||||
|
} |
||||||
|
|
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onTopIntersection((this.size * 2) + (needHeight * 2)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
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('manual scroll bottom', this, length, this.splitUp.childElementCount); |
||||||
|
} |
||||||
|
|
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onBottomIntersection(this.size + (needHeight * 2)); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public onTopIntersection(needHeight: number) { |
||||||
|
if(this.debug) { |
||||||
|
this.log('onTopIntersection', needHeight, this); |
||||||
|
} |
||||||
|
|
||||||
|
if(this.splitMutateIntersectionBottom) fastdom.clear(this.splitMutateIntersectionBottom); |
||||||
|
this.splitMutateIntersectionBottom = 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; |
||||||
|
} |
||||||
|
|
||||||
|
this.visibleElements.unshift(child); |
||||||
|
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 { |
||||||
|
this.paddingTopDiv.style.height = '0px'; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public onBottomIntersection(needHeight: number) { |
||||||
|
if(this.debug) { |
||||||
|
this.log('onBottomIntersection', needHeight, this); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
this.visibleElements.push(child); |
||||||
|
fragment.appendChild(child.element); |
||||||
|
|
||||||
|
needHeight -= child.height; |
||||||
|
this.paddings.down -= child.height; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.appendChild(fragment); |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
|
||||||
|
/* if(this.debug) { |
||||||
|
this.log('onBottomIntersection append:', fragment, needHeight); |
||||||
|
} */ |
||||||
|
|
||||||
|
if(this.onAddedBottom) this.onAddedBottom(); |
||||||
|
} else { |
||||||
|
this.paddingBottomDiv.style.height = '0px'; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public prepend(...smth: Element[]) { |
||||||
|
if(this.splitUp) { |
||||||
|
smth.forEach(node => { |
||||||
|
this.removeElement(node); |
||||||
|
}); |
||||||
|
|
||||||
|
if(this.hiddenElements.up.length) { |
||||||
|
/* fastdom.mutate(() => { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
}).then(() => { |
||||||
|
return fastdom.measure(() => { |
||||||
|
smth.forEachReverse(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to up hidden', node, height); |
||||||
|
this.paddings.up += height; |
||||||
|
this.hiddenElements.up.unshift({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}).then(() => { |
||||||
|
fastdom.mutate(() => { |
||||||
|
smth.forEachReverse(node => { |
||||||
|
if(node.parentElement) { |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
|
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
}); */ |
||||||
|
this.splitUp.prepend(...smth); |
||||||
|
smth.forEachReverse(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to up hidden', node, height); |
||||||
|
this.paddings.up += height; |
||||||
|
this.hiddenElements.up.unshift({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
}); |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
} else { |
||||||
|
this.splitUp.prepend(...smth); |
||||||
|
fastdom.measure(() => { |
||||||
|
smth.forEachReverse(element => { |
||||||
|
if(!element.parentElement) return; |
||||||
|
this.visibleElements.unshift({element: element, height: element.scrollHeight}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.appendTo.prepend(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
//this.onScroll();
|
||||||
|
} |
||||||
|
|
||||||
|
public append(...smth: Element[]) { |
||||||
|
if(this.splitUp) { |
||||||
|
smth.forEach(node => { |
||||||
|
this.removeElement(node); |
||||||
|
}); |
||||||
|
|
||||||
|
if(this.hiddenElements.down.length) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
}).then(() => { |
||||||
|
return fastdom.measure(() => { |
||||||
|
smth.forEach(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to down hidden', node, height); |
||||||
|
this.paddings.down += height; |
||||||
|
this.hiddenElements.down.push({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}).then(() => { |
||||||
|
fastdom.mutate(() => { |
||||||
|
smth.forEach(node => { |
||||||
|
if(node.parentElement) { |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
|
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
fastdom.measure(() => { |
||||||
|
smth.forEach(element => { |
||||||
|
if(!element.parentElement) return; |
||||||
|
this.visibleElements.push({element: element, height: element.scrollHeight}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.appendTo.append(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
//this.onScroll();
|
||||||
|
} |
||||||
|
|
||||||
|
public removeElement(element: Element) { |
||||||
|
if(!this.splitUp) { |
||||||
|
if(this.container.contains(element)) { |
||||||
|
//fastdom.mutate(() => this.container.removeChild(element));
|
||||||
|
this.container.removeChild(element); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} else { |
||||||
|
if(this.splitUp.contains(element)) { |
||||||
|
//fastdom.mutate(() => this.splitUp.removeChild(element));
|
||||||
|
this.splitUp.removeChild(element); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let child = this.hiddenElements.up.findAndSplice(c => c.element == element); |
||||||
|
let foundUp = false; |
||||||
|
if(child) { |
||||||
|
this.paddings.up -= child.height; |
||||||
|
foundUp = true; |
||||||
|
} else { |
||||||
|
child = this.hiddenElements.down.findAndSplice(c => c.element == element); |
||||||
|
if(child) { |
||||||
|
this.paddings.down -= child.height; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!child) return; |
||||||
|
|
||||||
|
//fastdom.mutate(() => {
|
||||||
|
if(foundUp) { |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
} else { |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
} |
||||||
|
//});
|
||||||
|
|
||||||
|
return child; |
||||||
|
} |
||||||
|
|
||||||
|
public insertBefore(newChild: Element, refChild: Element, height?: number) { |
||||||
|
//this.log('insertBefore', newChild, refChild);
|
||||||
|
return; |
||||||
|
|
||||||
|
if(this.splitUp) { |
||||||
|
let index = -1; |
||||||
|
index = this.hiddenElements.up.findIndex(c => c.element == refChild); |
||||||
|
|
||||||
|
let child = this.removeElement(newChild); |
||||||
|
if(child) { |
||||||
|
height = child.height; |
||||||
|
} else if(height === undefined) { |
||||||
|
let p = this.getScrollHeightPromises.find(p => p.element == newChild); |
||||||
|
if(!p) p = {element: newChild, task: null}; |
||||||
|
else fastdom.clear(p.task); |
||||||
|
|
||||||
|
let promise: any; |
||||||
|
|
||||||
|
return p.task = promise = fastdom.mutate(() => { |
||||||
|
this.splitUp.append(newChild); |
||||||
|
|
||||||
|
return fastdom.measure(() => { |
||||||
|
if(p.task != promise) return; |
||||||
|
|
||||||
|
let height = newChild.scrollHeight; |
||||||
|
|
||||||
|
return fastdom.mutate(() => { |
||||||
|
if(p.task != promise || !newChild.parentElement) return; |
||||||
|
|
||||||
|
this.splitUp.removeChild(newChild); |
||||||
|
|
||||||
|
this.insertBefore(newChild, refChild, height); |
||||||
|
|
||||||
|
this.getScrollHeightPromises = this.getScrollHeightPromises.filter(p => p.element != newChild); |
||||||
|
|
||||||
|
return height; |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if(index !== -1) { |
||||||
|
this.hiddenElements.up.splice(index, 0, {element: newChild, height: height}); |
||||||
|
this.paddings.up += height; |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return index; |
||||||
|
} else { |
||||||
|
index = this.hiddenElements.down.findIndex(c => c.element == refChild); |
||||||
|
|
||||||
|
if(index !== -1) { |
||||||
|
this.hiddenElements.down.splice(index, 0, {element: newChild, height: height}); |
||||||
|
this.paddings.down += height; |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return index; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fastdom.mutate(() => { |
||||||
|
this.log('inserting', newChild, 'before', refChild, this.splitUp.contains(refChild)); |
||||||
|
if(!this.splitUp.contains(refChild)) { |
||||||
|
this.log.error('no refChild in splitUp', refChild, newChild, this.hiddenElements); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.insertBefore(newChild, refChild); |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let ret = this.container.insertBefore(newChild, refChild); |
||||||
|
this.onScroll(); |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
public scrollIntoView(element: Element) { |
||||||
|
if(element.parentElement) { |
||||||
|
element.scrollIntoView(); |
||||||
|
} else if(this.splitUp) { |
||||||
|
let index = this.hiddenElements.up.findIndex(e => e.element == element); |
||||||
|
let y = 0; |
||||||
|
if(index !== -1) { |
||||||
|
for(let i = 0; i < index; ++i) { |
||||||
|
y += this.hiddenElements.up[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
this.scrollTop = y; |
||||||
|
} else if((index = this.hiddenElements.down.findIndex(e => e.element == element)) !== -1) { |
||||||
|
y += this.paddings.up + this.size; |
||||||
|
for(let i = 0; i < index; ++i) { |
||||||
|
y += this.hiddenElements.down[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
this.scrollTop = y; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
set scrollTop(y: number) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
get length() { |
||||||
|
return this.hiddenElements.up.length + this.visibleElements.length + this.hiddenElements.down.length; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,828 @@ |
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
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'; |
||||||
|
import { logger } from "../lib/polyfill"; |
||||||
|
|
||||||
|
//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 translate: string; |
||||||
|
public scrollType: string; |
||||||
|
public scrollSide: string; |
||||||
|
public clientAxis: string; |
||||||
|
|
||||||
|
public scrollSize = -1; // it will be scrollHeight
|
||||||
|
public size = 0; // it will be outerHeight of container (not scrollHeight)
|
||||||
|
public thumbSize = 0; |
||||||
|
|
||||||
|
public hiddenElements: { |
||||||
|
up: Element[], |
||||||
|
down: Element[] |
||||||
|
} = { |
||||||
|
up: [], |
||||||
|
down: [] |
||||||
|
}; |
||||||
|
|
||||||
|
public splitUp: HTMLElement; |
||||||
|
|
||||||
|
public onAddedBottom: () => void = null; |
||||||
|
public onScrolledTop: () => void = null; |
||||||
|
public onScrolledBottom: () => void = null; |
||||||
|
public onScrolledTopFired = false; |
||||||
|
public onScrolledBottomFired = false; |
||||||
|
|
||||||
|
public topObserver: IntersectionObserver; |
||||||
|
public bottomObserver: IntersectionObserver; |
||||||
|
|
||||||
|
public splitMeasureTop: Promise<{element: Element, height: number}[]> = null; |
||||||
|
public splitMeasureBottom: Scrollable['splitMeasureTop'] = null; |
||||||
|
public splitMeasureAdd: Promise<number> = null; |
||||||
|
public splitMeasureRemoveBad: Promise<Element> = null; |
||||||
|
public splitMutateTop: Promise<void> = null; |
||||||
|
public splitMutateBottom: Scrollable['splitMutateTop'] = null; |
||||||
|
public splitMutateRemoveBad: Promise<void> = null; |
||||||
|
|
||||||
|
public splitMutateIntersectionTop: Promise<void> = null; |
||||||
|
public splitMutateIntersectionBottom: Promise<void> = null; |
||||||
|
|
||||||
|
public getScrollHeightPromises: Array<{ |
||||||
|
element: Element, |
||||||
|
task: Promise<any> |
||||||
|
}> = []; |
||||||
|
|
||||||
|
public onScrollMeasure: Promise<any> = null; |
||||||
|
|
||||||
|
public lastScrollTop: number = 0; |
||||||
|
public scrollTopOffset: number = 0; |
||||||
|
|
||||||
|
private log: ReturnType<typeof logger>; |
||||||
|
private debug = false; |
||||||
|
|
||||||
|
constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300, logPrefix = '', public appendTo = el, public onScrollOffset = splitOffset) { |
||||||
|
this.container = document.createElement('div'); |
||||||
|
this.container.classList.add('scrollable'); |
||||||
|
|
||||||
|
this.log = logger('SCROLL' + (logPrefix ? '-' + logPrefix : '')); |
||||||
|
|
||||||
|
if(x) { |
||||||
|
this.container.classList.add('scrollable-x'); |
||||||
|
this.type = 'width'; |
||||||
|
this.side = 'left'; |
||||||
|
this.translate = 'translateX'; |
||||||
|
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.translate = 'translateY'; |
||||||
|
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.onScroll(); |
||||||
|
this.resize(); |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingTopDiv = document.createElement('div'); |
||||||
|
this.paddingTopDiv.classList.add('scroll-padding'); |
||||||
|
this.paddingBottomDiv = document.createElement('div'); |
||||||
|
this.paddingBottomDiv.classList.add('scroll-padding'); |
||||||
|
|
||||||
|
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.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 resize() { |
||||||
|
//console.time('scroll resize');
|
||||||
|
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 setVirtualContainer(el?: HTMLElement) { |
||||||
|
this.splitUp = el; |
||||||
|
|
||||||
|
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(() => { |
||||||
|
this.paddingTopDiv.style.height = ''; |
||||||
|
this.paddingBottomDiv.style.height = ''; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
this.log('setVirtualContainer:', el, this); |
||||||
|
|
||||||
|
this.getScrollTopOffset(); |
||||||
|
|
||||||
|
if(el) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
el.parentElement.insertBefore(this.paddingTopDiv, el); |
||||||
|
el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.paddingTopDiv.remove(); |
||||||
|
this.paddingBottomDiv.remove(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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(); |
||||||
|
|
||||||
|
this.scrollTopOffset = rect.top - containerRect.top; |
||||||
|
this.log('set scrollTopOffset to:', this.scrollTopOffset); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.scrollTopOffset = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public onScroll() { |
||||||
|
if(this.debug) { |
||||||
|
this.log('onScroll call'); |
||||||
|
} |
||||||
|
|
||||||
|
if(this.onScrollMeasure) fastdom.clear(this.onScrollMeasure); |
||||||
|
this.onScrollMeasure = fastdom.measure(() => { |
||||||
|
// @ts-ignore quick brown fix
|
||||||
|
this.size = this.parentElement[this.scrollType]; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
let scrollSize = this.container[this.scrollType]; |
||||||
|
if(scrollSize != this.scrollSize || this.thumbSize == 0) { |
||||||
|
this.resize(); |
||||||
|
} |
||||||
|
this.scrollSize = scrollSize; |
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
let scrollPos = this.container[this.scrollSide]; |
||||||
|
|
||||||
|
// let value = scrollPos / (this.scrollSize - this.size) * 100;
|
||||||
|
// let maxValue = 100 - (this.thumbSize / this.size * 100);
|
||||||
|
let value = scrollPos / (this.scrollSize - this.size) * this.size; |
||||||
|
let maxValue = this.size - this.thumbSize; |
||||||
|
|
||||||
|
//this.log(scrollPos, this.scrollSize, this.size, value, scrollPos / (this.scrollSize - this.size) * this.size);
|
||||||
|
let ret = {value, maxValue}; |
||||||
|
|
||||||
|
let scrollTop = scrollPos - this.scrollTopOffset; |
||||||
|
let maxScrollTop = this.scrollSize - this.scrollTopOffset - this.size; |
||||||
|
|
||||||
|
if(this.onScrolledBottom) { |
||||||
|
if(!this.hiddenElements.down.length && (maxScrollTop - scrollTop) <= this.onScrollOffset) { |
||||||
|
if(!this.onScrolledBottomFired) { |
||||||
|
this.onScrolledBottomFired = true; |
||||||
|
this.onScrolledBottom(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.onScrolledBottomFired = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(this.onScrolledTop) { |
||||||
|
//this.log('onScrolledTop:', scrollTop, this.onScrollOffset);
|
||||||
|
if(!this.hiddenElements.up.length && scrollTop <= this.onScrollOffset) { |
||||||
|
if(!this.onScrolledTopFired) { |
||||||
|
this.onScrolledTopFired = true; |
||||||
|
this.onScrolledTop(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.onScrolledTopFired = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if(!this.splitUp) { |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
let perf = performance.now(); |
||||||
|
|
||||||
|
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 + this.size; |
||||||
|
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; |
||||||
|
}); |
||||||
|
} 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
|
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('seems manually scrolled bottom', this.paddings.up, this.lastScrollTop); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
}); |
||||||
|
} 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) { |
||||||
|
if(this.debug) { |
||||||
|
this.log.warn('seems manually scrolled top', this.paddings.down, this.lastScrollTop); |
||||||
|
} |
||||||
|
|
||||||
|
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}; |
||||||
|
}); |
||||||
|
|
||||||
|
this.onScrollMeasure.then(({value, maxValue}) => { |
||||||
|
//fastdom.mutate(() => {
|
||||||
|
// @ts-ignore
|
||||||
|
//this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%';
|
||||||
|
this.thumb.style.transform = this.translate + '(' + (value >= maxValue ? maxValue : value) + 'px)'; |
||||||
|
//});
|
||||||
|
}); |
||||||
|
|
||||||
|
//console.timeEnd('scroll onScroll');
|
||||||
|
} |
||||||
|
|
||||||
|
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) { |
||||||
|
let fragment = document.createDocumentFragment(); |
||||||
|
while(needHeight > 0 && this.hiddenElements.up.length) { |
||||||
|
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.element.scrollHeight; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.prepend(fragment); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public onBottomIntersection(needHeight: number) { |
||||||
|
if(this.debug) { |
||||||
|
this.log('onBottomIntersection', needHeight, this); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.appendChild(fragment); |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
|
||||||
|
/* if(this.debug) { |
||||||
|
this.log('onBottomIntersection append:', fragment, needHeight); |
||||||
|
} */ |
||||||
|
|
||||||
|
if(this.onAddedBottom) this.onAddedBottom(); |
||||||
|
} else { |
||||||
|
this.paddingBottomDiv.style.height = '0px'; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public prepend(...smth: Element[]) { |
||||||
|
if(this.splitUp) { |
||||||
|
smth.forEach(node => { |
||||||
|
this.removeElement(node); |
||||||
|
}); |
||||||
|
|
||||||
|
if(this.hiddenElements.up.length) { |
||||||
|
this.splitUp.prepend(...smth); |
||||||
|
smth.forEachReverse(node => { |
||||||
|
this.log('will append element to up hidden', node); |
||||||
|
this.hiddenElements.up.unshift(node); |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
}); |
||||||
|
this.onScroll(); |
||||||
|
} else { |
||||||
|
this.splitUp.prepend(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.appendTo.prepend(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
//this.onScroll();
|
||||||
|
} |
||||||
|
|
||||||
|
public append(...smth: Element[]) { |
||||||
|
if(this.splitUp) { |
||||||
|
smth.forEach(node => { |
||||||
|
this.removeElement(node); |
||||||
|
}); |
||||||
|
|
||||||
|
if(this.hiddenElements.down.length) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
}).then(() => { |
||||||
|
return fastdom.measure(() => { |
||||||
|
smth.forEach(node => { |
||||||
|
let height = node.scrollHeight; |
||||||
|
this.log('will append element to down hidden', node, height); |
||||||
|
this.paddings.down += height; |
||||||
|
this.hiddenElements.down.push({ |
||||||
|
element: node, |
||||||
|
height: height |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}).then(() => { |
||||||
|
fastdom.mutate(() => { |
||||||
|
smth.forEach(node => { |
||||||
|
if(node.parentElement) { |
||||||
|
node.parentElement.removeChild(node); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
|
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} else { |
||||||
|
this.splitUp.append(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.appendTo.append(...smth); |
||||||
|
this.onScroll(); |
||||||
|
} |
||||||
|
|
||||||
|
//this.onScroll();
|
||||||
|
} |
||||||
|
|
||||||
|
public removeElement(element: Element) { |
||||||
|
if(!this.splitUp) { |
||||||
|
if(this.container.contains(element)) { |
||||||
|
//fastdom.mutate(() => this.container.removeChild(element));
|
||||||
|
this.container.removeChild(element); |
||||||
|
} |
||||||
|
|
||||||
|
return; |
||||||
|
} else { |
||||||
|
if(this.splitUp.contains(element)) { |
||||||
|
//fastdom.mutate(() => this.splitUp.removeChild(element));
|
||||||
|
this.splitUp.removeChild(element); |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let child = this.hiddenElements.up.findAndSplice(e => e == element); |
||||||
|
let foundUp = false; |
||||||
|
if(child) { |
||||||
|
foundUp = true; |
||||||
|
} else { |
||||||
|
child = this.hiddenElements.down.findAndSplice(e => e == element); |
||||||
|
} |
||||||
|
|
||||||
|
if(!child) return; |
||||||
|
|
||||||
|
//fastdom.mutate(() => {
|
||||||
|
//});
|
||||||
|
|
||||||
|
return child; |
||||||
|
} |
||||||
|
|
||||||
|
public insertBefore(newChild: Element, refChild: Element, height?: number) { |
||||||
|
//this.log('insertBefore', newChild, refChild);
|
||||||
|
return; |
||||||
|
|
||||||
|
if(this.splitUp) { |
||||||
|
let index = -1; |
||||||
|
index = this.hiddenElements.up.findIndex(c => c.element == refChild); |
||||||
|
|
||||||
|
let child = this.removeElement(newChild); |
||||||
|
if(child) { |
||||||
|
height = child.height; |
||||||
|
} else if(height === undefined) { |
||||||
|
let p = this.getScrollHeightPromises.find(p => p.element == newChild); |
||||||
|
if(!p) p = {element: newChild, task: null}; |
||||||
|
else fastdom.clear(p.task); |
||||||
|
|
||||||
|
let promise: any; |
||||||
|
|
||||||
|
return p.task = promise = fastdom.mutate(() => { |
||||||
|
this.splitUp.append(newChild); |
||||||
|
|
||||||
|
return fastdom.measure(() => { |
||||||
|
if(p.task != promise) return; |
||||||
|
|
||||||
|
let height = newChild.scrollHeight; |
||||||
|
|
||||||
|
return fastdom.mutate(() => { |
||||||
|
if(p.task != promise || !newChild.parentElement) return; |
||||||
|
|
||||||
|
this.splitUp.removeChild(newChild); |
||||||
|
|
||||||
|
this.insertBefore(newChild, refChild, height); |
||||||
|
|
||||||
|
this.getScrollHeightPromises = this.getScrollHeightPromises.filter(p => p.element != newChild); |
||||||
|
|
||||||
|
return height; |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if(index !== -1) { |
||||||
|
this.hiddenElements.up.splice(index, 0, {element: newChild, height: height}); |
||||||
|
this.paddings.up += height; |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingTopDiv.style.height = this.paddings.up + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return index; |
||||||
|
} else { |
||||||
|
index = this.hiddenElements.down.findIndex(c => c.element == refChild); |
||||||
|
|
||||||
|
if(index !== -1) { |
||||||
|
this.hiddenElements.down.splice(index, 0, {element: newChild, height: height}); |
||||||
|
this.paddings.down += height; |
||||||
|
fastdom.mutate(() => { |
||||||
|
this.paddingBottomDiv.style.height = this.paddings.down + 'px'; |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return index; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fastdom.mutate(() => { |
||||||
|
this.log('inserting', newChild, 'before', refChild, this.splitUp.contains(refChild)); |
||||||
|
if(!this.splitUp.contains(refChild)) { |
||||||
|
this.log.error('no refChild in splitUp', refChild, newChild, this.hiddenElements); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
this.splitUp.insertBefore(newChild, refChild); |
||||||
|
this.onScroll(); |
||||||
|
}); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let ret = this.container.insertBefore(newChild, refChild); |
||||||
|
this.onScroll(); |
||||||
|
return ret; |
||||||
|
} |
||||||
|
|
||||||
|
public scrollIntoView(element: Element) { |
||||||
|
if(element.parentElement) { |
||||||
|
element.scrollIntoView(); |
||||||
|
} else if(this.splitUp) { |
||||||
|
let index = this.hiddenElements.up.findIndex(e => e.element == element); |
||||||
|
let y = 0; |
||||||
|
if(index !== -1) { |
||||||
|
for(let i = 0; i < index; ++i) { |
||||||
|
y += this.hiddenElements.up[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
this.scrollTop = y; |
||||||
|
} else if((index = this.hiddenElements.down.findIndex(e => e.element == element)) !== -1) { |
||||||
|
y += this.paddings.up + this.size; |
||||||
|
for(let i = 0; i < index; ++i) { |
||||||
|
y += this.hiddenElements.down[i].height; |
||||||
|
} |
||||||
|
|
||||||
|
this.scrollTop = y; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
set scrollTop(y: number) { |
||||||
|
fastdom.mutate(() => { |
||||||
|
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; |
||||||
|
} |
||||||
|
} |
@ -1,330 +0,0 @@ |
|||||||
//import { MTDocument, ProgressivePreloader, wrapVideo } from "../../components/misc";
|
|
||||||
import appPeersManager from "./appPeersManager"; |
|
||||||
import appDialogsManager from "./appDialogsManager"; |
|
||||||
import appPhotosManager from "./appPhotosManager"; |
|
||||||
import appSidebarRight from "./appSidebarRight"; |
|
||||||
import { $rootScope } from "../utils"; |
|
||||||
import appMessagesManager from "./appMessagesManager"; |
|
||||||
//import { CancellablePromise } from "../mtproto/apiFileManager";
|
|
||||||
import { RichTextProcessor } from "../richtextprocessor"; |
|
||||||
import { logger } from "../polyfill"; |
|
||||||
import ProgressivePreloader from "../../components/preloader"; |
|
||||||
import { wrapVideo } from "../../components/wrappers"; |
|
||||||
|
|
||||||
export class AppMediaViewer { |
|
||||||
private overlaysDiv = document.querySelector('.overlays') as HTMLDivElement; |
|
||||||
private author = { |
|
||||||
avatarEl: this.overlaysDiv.querySelector('.user-avatar') as HTMLDivElement, |
|
||||||
nameEl: this.overlaysDiv.querySelector('.media-viewer-name') as HTMLDivElement, |
|
||||||
date: this.overlaysDiv.querySelector('.media-viewer-date') as HTMLDivElement |
|
||||||
}; |
|
||||||
private buttons = { |
|
||||||
delete: this.overlaysDiv.querySelector('.media-viewer-delete-button') as HTMLDivElement, |
|
||||||
forward: this.overlaysDiv.querySelector('.media-viewer-forward-button') as HTMLDivElement, |
|
||||||
download: this.overlaysDiv.querySelector('.media-viewer-download-button') as HTMLDivElement, |
|
||||||
close: this.overlaysDiv.querySelector('.media-viewer-close-button') as HTMLDivElement, |
|
||||||
prev: this.overlaysDiv.querySelector('.media-viewer-switcher-left') as HTMLDivElement, |
|
||||||
next: this.overlaysDiv.querySelector('.media-viewer-switcher-right') as HTMLDivElement, |
|
||||||
}; |
|
||||||
private content = { |
|
||||||
container: this.overlaysDiv.querySelector('.media-viewer-media') as HTMLDivElement, |
|
||||||
caption: this.overlaysDiv.querySelector('.media-viewer-caption') as HTMLDivElement, |
|
||||||
mover: this.overlaysDiv.querySelector('.media-viewer-mover') as HTMLDivElement |
|
||||||
}; |
|
||||||
|
|
||||||
private reverse = false; |
|
||||||
public currentMessageID = 0; |
|
||||||
private higherMsgID: number | undefined = 0; |
|
||||||
private lowerMsgID: number | undefined = 0; |
|
||||||
private preloader: ProgressivePreloader = null; |
|
||||||
private lastTarget: HTMLElement = null; |
|
||||||
|
|
||||||
public log: ReturnType<typeof logger>; |
|
||||||
|
|
||||||
constructor() { |
|
||||||
this.log = logger('AMV'); |
|
||||||
this.preloader = new ProgressivePreloader(); |
|
||||||
|
|
||||||
this.buttons.close.addEventListener('click', () => { |
|
||||||
//this.overlaysDiv.classList.remove('active');
|
|
||||||
this.content.container.innerHTML = ''; |
|
||||||
this.currentMessageID = 0; |
|
||||||
|
|
||||||
this.setMoverToTarget(this.lastTarget, true); |
|
||||||
}); |
|
||||||
|
|
||||||
this.buttons.prev.addEventListener('click', () => { |
|
||||||
let id = this.reverse ? this.lowerMsgID : this.higherMsgID; |
|
||||||
if(id) { |
|
||||||
this.openMedia(appMessagesManager.getMessage(id), this.reverse); |
|
||||||
} else { |
|
||||||
this.buttons.prev.style.display = 'none'; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
this.buttons.next.addEventListener('click', () => { |
|
||||||
let id = this.reverse ? this.higherMsgID : this.lowerMsgID; |
|
||||||
if(id) { |
|
||||||
this.openMedia(appMessagesManager.getMessage(id), this.reverse); |
|
||||||
} else { |
|
||||||
this.buttons.next.style.display = 'none'; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
this.buttons.download.addEventListener('click', () => { |
|
||||||
let message = appMessagesManager.getMessage(this.currentMessageID); |
|
||||||
appPhotosManager.downloadPhoto(message.media.photo.id); |
|
||||||
}); |
|
||||||
/* this.buttons.prev.onclick = (e) => { |
|
||||||
let history = appSidebarRight.historiesStorage[$rootScope.selectedPeerID]['inputMessagesFilterPhotoVideo'].slice(); |
|
||||||
|
|
||||||
let message: any; |
|
||||||
|
|
||||||
if(!this.reverse) { |
|
||||||
for(let mid of history) { |
|
||||||
if(mid > this.currentMessageID) { |
|
||||||
let _message = appMessagesManager.getMessage(mid); |
|
||||||
if(_message.media && _message.media.photo) { |
|
||||||
message = _message; |
|
||||||
} |
|
||||||
} else break; |
|
||||||
} |
|
||||||
} else { |
|
||||||
for(let mid of history) { |
|
||||||
if(mid < this.currentMessageID) { |
|
||||||
let _message = appMessagesManager.getMessage(mid); |
|
||||||
if(_message.media && _message.media.photo) { |
|
||||||
message = _message; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if(message) { |
|
||||||
this.openMedia(message.media.photo, message.mid, this.reverse); |
|
||||||
} else { |
|
||||||
this.buttons.prev.style.display = 'none'; |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
this.buttons.next.onclick = (e) => { |
|
||||||
let history = appSidebarRight.historiesStorage[$rootScope.selectedPeerID]['inputMessagesFilterPhotoVideo'].slice(); |
|
||||||
|
|
||||||
let message: any; |
|
||||||
|
|
||||||
if(this.reverse) { |
|
||||||
for(let mid of history) { |
|
||||||
if(mid > this.currentMessageID) { |
|
||||||
let _message = appMessagesManager.getMessage(mid); |
|
||||||
if(_message.media && _message.media.photo) { |
|
||||||
message = _message; |
|
||||||
} |
|
||||||
} else break; |
|
||||||
} |
|
||||||
} else { |
|
||||||
for(let mid of history) { |
|
||||||
if(mid < this.currentMessageID) { |
|
||||||
let _message = appMessagesManager.getMessage(mid); |
|
||||||
if(_message.media && _message.media.photo) { |
|
||||||
message = _message; |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if(message) { |
|
||||||
this.openMedia(message.media.photo, message.mid, this.reverse); |
|
||||||
} else { |
|
||||||
this.buttons.next.style.display = 'none'; |
|
||||||
} |
|
||||||
}; */ |
|
||||||
} |
|
||||||
|
|
||||||
public setMoverToTarget(target: HTMLElement, closing = false) { |
|
||||||
let mover = this.content.mover; |
|
||||||
|
|
||||||
if(!closing) { |
|
||||||
mover.innerHTML = ''; |
|
||||||
} |
|
||||||
|
|
||||||
let rect = target.getBoundingClientRect(); |
|
||||||
mover.style.transform = `translate(${rect.left}px, ${rect.top}px)`; |
|
||||||
mover.style.width = rect.width + 'px'; |
|
||||||
mover.style.height = rect.height + 'px'; |
|
||||||
|
|
||||||
if(!closing) { |
|
||||||
let img: HTMLImageElement; |
|
||||||
let video: HTMLVideoElement; |
|
||||||
|
|
||||||
if(target.tagName == 'DIV') { // means backgrounded with cover
|
|
||||||
//img.style.objectFit = 'cover';
|
|
||||||
img = new Image(); |
|
||||||
img.src = target.style.backgroundImage.slice(5, -2); |
|
||||||
} else if(target.tagName == 'IMG') { |
|
||||||
img = new Image(); |
|
||||||
img.src = (target as HTMLImageElement).src; |
|
||||||
img.style.objectFit = 'contain'; |
|
||||||
}/* else if(target.tagName == 'VIDEO') { |
|
||||||
let video = document.createElement('video'); |
|
||||||
let source = document.createElement('source'); |
|
||||||
source.src = target.querySelector('source').src; |
|
||||||
video.append(source); |
|
||||||
} */ |
|
||||||
|
|
||||||
if(img) { |
|
||||||
mover.appendChild(img); |
|
||||||
} else if(video) { |
|
||||||
mover.appendChild(video); |
|
||||||
} |
|
||||||
|
|
||||||
mover.style.display = ''; |
|
||||||
mover.classList.add('active'); |
|
||||||
} else { |
|
||||||
setTimeout(() => { |
|
||||||
this.overlaysDiv.classList.remove('active'); |
|
||||||
mover.classList.remove('active'); |
|
||||||
mover.style.display = 'none'; |
|
||||||
}, 250); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public openMedia(message: any, reverse = false, target?: HTMLElement) { |
|
||||||
this.log('openMedia doc:', message); |
|
||||||
let media = message.media.photo || message.media.document || message.media.webpage.document || message.media.webpage.photo; |
|
||||||
|
|
||||||
let isVideo = media.mime_type == 'video/mp4'; |
|
||||||
|
|
||||||
this.currentMessageID = message.mid; |
|
||||||
this.reverse = reverse; |
|
||||||
|
|
||||||
let container = this.content.container; |
|
||||||
|
|
||||||
if(container.firstElementChild) { |
|
||||||
container.innerHTML = ''; |
|
||||||
} |
|
||||||
|
|
||||||
let date = new Date(media.date * 1000); |
|
||||||
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
|
||||||
|
|
||||||
let dateStr = months[date.getMonth()] + ' ' + date.getDate() + ' at '+ date.getHours() + ':' + ('0' + date.getMinutes()).slice(-2); |
|
||||||
this.author.date.innerText = dateStr; |
|
||||||
|
|
||||||
let name = appPeersManager.getPeerTitle(message.fromID); |
|
||||||
this.author.nameEl.innerHTML = name; |
|
||||||
|
|
||||||
if(message.message) { |
|
||||||
this.content.caption.innerHTML = RichTextProcessor.wrapRichText(message.message, { |
|
||||||
entities: message.totalEntities |
|
||||||
}); |
|
||||||
} else { |
|
||||||
this.content.caption.innerHTML = ''; |
|
||||||
} |
|
||||||
|
|
||||||
appDialogsManager.loadDialogPhoto(this.author.avatarEl, message.fromID); |
|
||||||
|
|
||||||
this.overlaysDiv.classList.add('active'); |
|
||||||
|
|
||||||
container.classList.add('loading'); |
|
||||||
|
|
||||||
// ok set
|
|
||||||
let mover = this.content.mover; |
|
||||||
|
|
||||||
let rect = target.getBoundingClientRect(); |
|
||||||
|
|
||||||
this.lastTarget = target; |
|
||||||
this.setMoverToTarget(target); |
|
||||||
let maxWidth = appPhotosManager.windowW - 16; |
|
||||||
let maxHeight = appPhotosManager.windowH - 100; |
|
||||||
if(isVideo) { |
|
||||||
//this.preloader.attach(container);
|
|
||||||
//this.preloader.setProgress(75);
|
|
||||||
|
|
||||||
this.log('will wrap video'); |
|
||||||
|
|
||||||
let size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight); |
|
||||||
let containerRect = container.getBoundingClientRect(); |
|
||||||
let scaleX = containerRect.width / rect.width; |
|
||||||
let scaleY = containerRect.height / rect.height; |
|
||||||
mover.style.transform = `translate(${containerRect.left}px, ${containerRect.top}px) scale(${scaleX}, ${scaleY})`; |
|
||||||
wrapVideo.call(this, media, mover, message, false, this.preloader).then(() => { |
|
||||||
if(this.currentMessageID != message.mid) { |
|
||||||
this.log.warn('media viewer changed video'); |
|
||||||
return; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
|
|
||||||
/* appPhotosManager.setAttachmentSize(media, container, appPhotosManager.windowW, appPhotosManager.windowH); |
|
||||||
wrapVideo.call(this, media, container, message, false, this.preloader).then(() => { |
|
||||||
if(this.currentMessageID != message.mid) { |
|
||||||
this.log.warn('media viewer changed video'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
container.classList.remove('loading'); |
|
||||||
container.style.width = ''; |
|
||||||
container.style.height = ''; |
|
||||||
}); */ |
|
||||||
} else { |
|
||||||
let size = appPhotosManager.setAttachmentSize(media.id, container, maxWidth, maxHeight); |
|
||||||
|
|
||||||
let containerRect = container.getBoundingClientRect(); |
|
||||||
let scaleX = containerRect.width / rect.width; |
|
||||||
let scaleY = containerRect.height / rect.height; |
|
||||||
mover.style.transform = `translate(${containerRect.left}px, ${containerRect.top}px) scale(${scaleX}, ${scaleY})`; |
|
||||||
|
|
||||||
this.preloader.attach(mover); |
|
||||||
//this.preloader.setProgress(75);
|
|
||||||
|
|
||||||
let cancellablePromise = appPhotosManager.preloadPhoto(media.id, size); |
|
||||||
cancellablePromise.then((blob) => { |
|
||||||
if(this.currentMessageID != message.mid) { |
|
||||||
this.log.warn('media viewer changed photo'); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
this.log('indochina', blob); |
|
||||||
|
|
||||||
let image = mover.firstElementChild as HTMLImageElement || new Image(); |
|
||||||
image.src = URL.createObjectURL(blob); |
|
||||||
mover.append(image); |
|
||||||
|
|
||||||
/* container.classList.remove('loading'); |
|
||||||
|
|
||||||
container.style.width = ''; |
|
||||||
container.style.height = ''; */ |
|
||||||
|
|
||||||
this.preloader.detach(); |
|
||||||
}).catch(err => { |
|
||||||
this.log.error(err); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
let history = appSidebarRight.historiesStorage[$rootScope.selectedPeerID]['inputMessagesFilterPhotoVideo'].slice(); |
|
||||||
let index = history.findIndex(m => m == message.mid); |
|
||||||
let comparer = (mid: number) => { |
|
||||||
let _message = appMessagesManager.getMessage(mid); |
|
||||||
let media = _message.media; |
|
||||||
|
|
||||||
if(media && (media.photo || (media.document && ['video', 'gif'].indexOf(media.document.type) !== -1))) return true; |
|
||||||
return false; |
|
||||||
}; |
|
||||||
|
|
||||||
this.higherMsgID = history.slice(0, index).reverse().find(comparer); |
|
||||||
this.lowerMsgID = history.slice(index + 1).find(comparer); |
|
||||||
|
|
||||||
if(this.reverse) { |
|
||||||
this.buttons.prev.style.display = this.lowerMsgID !== undefined ? '' : 'none'; |
|
||||||
this.buttons.next.style.display = this.higherMsgID !== undefined ? '' : 'none'; |
|
||||||
} else { |
|
||||||
this.buttons.prev.style.display = this.higherMsgID !== undefined ? '' : 'none'; |
|
||||||
this.buttons.next.style.display = this.lowerMsgID !== undefined ? '' : 'none'; |
|
||||||
} |
|
||||||
|
|
||||||
//console.log('prev and next', prevMsgID, nextMsgID);
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export default new AppMediaViewer(); |
|
Loading…
Reference in new issue