Telegram Web K with changes to work inside I2P https://web.telegram.i2p/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

555 lines
18 KiB

import { cancelEvent } from "../lib/utils";
//import {measure} from 'fastdom/fastdom.min';
import FastDom from 'fastdom';
import 'fastdom/src/fastdom-strict'; // exclude in production
import FastDomPromised from 'fastdom/extensions/fastdom-promised';
//const fastdom = FastDom.extend(FastDomPromised);
const fastdom = ((window as any).fastdom as typeof FastDom).extend(FastDomPromised);
(window as any).fastdom.strict(false);
setTimeout(() => {
//(window as any).fastdom.strict(true);
}, 5e3);
export default class Scrollable {
public container: HTMLDivElement;
public thumb: HTMLDivElement;
4 years ago
public type: string;
public side: string;
public scrollType: string;
public scrollSide: string;
public clientAxis: string;
4 years ago
public scrollSize = -1;
public size = 0;
public thumbSize = 0;
4 years ago
public hiddenElements: {
up: {element: Element, height: number}[],
down: {element: Element, height: number}[]
} = {
up: [],
down: []
};
public paddings = {up: 0, down: 0};
4 years ago
public paddingTopDiv: HTMLDivElement;
public paddingBottomDiv: HTMLDivElement;
4 years ago
public splitUp: HTMLElement;
4 years ago
public onAddedBottom: () => void = null;
4 years ago
public onScrolledTop: () => void = null;
public onScrolledBottom: () => void = null;
public topObserver: IntersectionObserver;
public bottomObserver: IntersectionObserver;
4 years ago
public splitObserver: IntersectionObserver;
4 years ago
public splitMeasure: Promise<{element: Element, height: number}[]> = null;
public splitMeasureAdd: Promise<number> = null;
public splitMutate: Promise<void> = null;
constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300) {
this.container = document.createElement('div');
this.container.classList.add('scrollable');
4 years ago
//let arr = [];
//for(let i = 0.001; i < 1; i += 0.001) arr.push(i);
this.topObserver = new IntersectionObserver(entries => {
let entry = entries[0];
4 years ago
console.log('top intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0);
if(entry.isIntersecting) {
//this.onTopIntersection(entry);
this.onTopIntersection(entry.intersectionRect.height);
if(this.onScrolledTop) this.onScrolledTop();
}
// console.log('top intersection end');
4 years ago
}, {/* threshold: arr, */root: this.el});
this.bottomObserver = new IntersectionObserver(entries => {
let entry = entries[0];
4 years ago
console.log('bottom intersection:', entries, entry.isIntersecting, entry.intersectionRatio > 0);
if(entry.isIntersecting) {
//this.onBottomIntersection(entry);
this.onBottomIntersection(entry.intersectionRect.height);
if(this.onScrolledBottom) this.onScrolledBottom();
}
4 years ago
}, {/* threshold: arr, */root: this.el});
if(x) {
this.container.classList.add('scrollable-x');
this.type = 'width';
this.side = 'left';
this.scrollType = 'scrollWidth';
this.scrollSide = 'scrollLeft';
this.clientAxis = 'clientX';
4 years ago
let scrollHorizontally = (e: any) => {
e = window.event || e;
var delta = Math.max(-1, Math.min(1, (e.wheelDelta || -e.detail)));
this.container.scrollLeft -= (delta * 20);
e.preventDefault();
};
if(this.container.addEventListener) {
// IE9, Chrome, Safari, Opera
this.container.addEventListener("mousewheel", scrollHorizontally, false);
// Firefox
this.container.addEventListener("DOMMouseScroll", scrollHorizontally, false);
} else {
// IE 6/7/8
// @ts-ignore
this.container.attachEvent("onmousewheel", scrollHorizontally);
}
} else if(y) {
this.container.classList.add('scrollable-y');
this.type = 'height';
this.side = 'top';
this.scrollType = 'scrollHeight';
this.scrollSide = 'scrollTop';
this.clientAxis = 'clientY';
} else {
throw new Error('no side for scroll');
}
4 years ago
this.thumb = document.createElement('div');
this.thumb.className = 'scrollbar-thumb';
// @ts-ignore
this.thumb.style[this.type] = '30px';
4 years ago
// mouse scroll
let onMouseMove = (e: MouseEvent) => {
let rect = this.thumb.getBoundingClientRect();
4 years ago
let diff: number;
// @ts-ignore
diff = e[this.clientAxis] - rect[this.side];
// @ts-ignore
this.container[this.scrollSide] += diff * 0.5;
4 years ago
// console.log('onMouseMove', e, diff);
4 years ago
cancelEvent(e);
};
4 years ago
this.thumb.addEventListener('mousedown', () => {
window.addEventListener('mousemove', onMouseMove);
4 years ago
window.addEventListener('mouseup', () => {
window.removeEventListener('mousemove', onMouseMove);
}, {once: true});
});
4 years ago
//this.container.addEventListener('mouseover', this.resize.bind(this)); // omg
window.addEventListener('resize', this.resize.bind(this));
4 years ago
this.paddingTopDiv = document.createElement('div');
this.paddingTopDiv.classList.add('scroll-padding');
this.paddingBottomDiv = document.createElement('div');
this.paddingBottomDiv.classList.add('scroll-padding');
4 years ago
this.topObserver.observe(this.paddingTopDiv);
this.bottomObserver.observe(this.paddingBottomDiv);
4 years ago
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();
}
4 years ago
public splitObserve(entries: IntersectionObserverEntry[]) {
console.log('splitObserver', entries);
for(let entry of entries) { // there may be duplicates (1st - not intersecting, 2nd - intersecting)
//console.log('onscroll entry', entry.target, entry.isIntersecting, entry);
if(!entry.target.parentElement || !entry.rootBounds) continue;
let child = entry.target;
let needHeight = this.splitOffset;
if(!entry.isIntersecting) {
let isTop = entry.boundingClientRect.top <= 0;
let isBottom = entry.rootBounds.height <= entry.boundingClientRect.top;
console.log('onscroll entry', isTop, isBottom, child, entry);
//console.log('will call measure');
if(isTop) { // when scrolling down
//this.onBottomIntersection(entry);
if(this.splitMeasure) fastdom.clear(this.splitMeasure);
this.splitMeasure = fastdom.measure(() => {
let sliced: {element: Element, height: number}[] = [];
do {
if(needHeight > 0) {
needHeight -= child.scrollHeight;
} else {
sliced.push({element: child, height: child.scrollHeight});
}
} while(child = child.previousElementSibling);
return sliced;
});
this.splitMeasure.then(sliced => {
if(this.splitMutate) fastdom.clear(this.splitMutate);
this.splitMutate = fastdom.mutate(() => {
sliced.forEachReverse((child) => {
let {element, height} = child;
if(!this.splitUp.contains(element)) return;
this.paddings.up += height;
this.hiddenElements.up.push(child);
this.splitUp.removeChild(element);
//element.parentElement.removeChild(element);
});
4 years ago
this.paddingTopDiv.style.height = this.paddings.up + 'px';
});
});
//console.log('onscroll sliced up', sliced);
} else if(isBottom) { // when scrolling top
//this.onTopIntersection(entry);
if(this.splitMeasure) fastdom.clear(this.splitMeasure);
this.splitMeasure = fastdom.measure(() => {
let sliced: {element: Element, height: number}[] = [];
do {
if(needHeight > 0) {
needHeight -= child.scrollHeight;
} else {
sliced.push({element: child, height: child.scrollHeight});
}
} while(child = child.nextElementSibling);
return sliced;
});
this.splitMeasure.then(sliced => {
if(this.splitMutate) fastdom.clear(this.splitMutate);
this.splitMutate = fastdom.mutate(() => {
sliced.forEachReverse((child) => {
let {element, height} = child;
if(!this.splitUp.contains(element)) return;
this.paddings.down += height;
this.hiddenElements.down.unshift(child);
this.splitUp.removeChild(element);
//element.parentElement.removeChild(element);
});
this.paddingBottomDiv.style.height = this.paddings.down + 'px';
});
});
//console.log('onscroll sliced down', sliced);
}
//console.log('splitObserver', entry, entry.target, isTop);
} else {
let isTop = entry.boundingClientRect.top <= entry.rootBounds.top;
let isBottom = entry.boundingClientRect.bottom >= entry.rootBounds.bottom;
if(isTop) { // when scrolling up
if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd);
this.splitMeasureAdd = fastdom.measure(() => {
while(child = child.previousElementSibling) {
needHeight -= child.scrollHeight;
}
return needHeight;
});
this.splitMeasureAdd.then(needHeight => {
this.onTopIntersection(needHeight);
});
} else if(isBottom) { // when scrolling down
if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd);
this.splitMeasureAdd = fastdom.measure(() => {
while(child = child.nextElementSibling) {
needHeight -= child.scrollHeight;
}
return needHeight;
});
this.splitMeasureAdd.then(needHeight => {
this.onBottomIntersection(needHeight);
});
}
}
}
}
public async resize() {
//console.time('scroll resize');
4 years ago
await fastdom.measure(() => {
// @ts-ignore
this.scrollSize = this.container[this.scrollType];
4 years ago
let rect = this.container.getBoundingClientRect();
4 years ago
// @ts-ignore
this.size = rect[this.type];
});
await fastdom.mutate(() => {
if(!this.size || this.size == this.scrollSize) {
this.thumbSize = 0;
// @ts-ignore
this.thumb.style[this.type] = this.thumbSize + 'px';
//console.timeEnd('scroll resize');
return;
}
//if(!height) return;
let divider = this.scrollSize / this.size / 0.5;
this.thumbSize = this.size / divider;
if(this.thumbSize < 20) this.thumbSize = 20;
// @ts-ignore
this.thumb.style[this.type] = this.thumbSize + 'px';
});
4 years ago
//console.timeEnd('scroll resize');
// @ts-ignore
//console.log('onresize', thumb.style[type], thumbHeight, height);
}
4 years ago
public async setVirtualContainer(el?: HTMLElement) {
this.splitUp = el;
this.hiddenElements.up.length = this.hiddenElements.down.length = 0;
this.paddings.up = this.paddings.down = 0;
4 years ago
if(this.paddingTopDiv.parentElement) {
fastdom.mutate(() => {
this.paddingTopDiv.style.height = '';
this.paddingBottomDiv.style.height = '';
});
}
4 years ago
if(this.splitObserver) {
this.splitObserver.disconnect();
}
4 years ago
this.splitObserver = new IntersectionObserver((entries) => this.splitObserve(entries), {root: this.el});
if(el) {
fastdom.mutate(() => {
el.parentElement.insertBefore(this.paddingTopDiv, el);
el.parentNode.insertBefore(this.paddingBottomDiv, el.nextSibling);
});
}
}
4 years ago
public async onScroll() {
//console.time('scroll onScroll');
let {value, maxValue} = await fastdom.measure(() => {
// @ts-ignore
if(this.container[this.scrollType] != this.scrollSize || this.thumbSize == 0) {
this.resize();
}
4 years ago
// @ts-ignore
let value = this.container[this.scrollSide] / (this.scrollSize - this.size) * 100;
let maxValue = 100 - (this.thumbSize / this.size * 100);
return {value, maxValue};
});
4 years ago
//console.log('onscroll', container.scrollHeight, thumbHeight, height, value, maxValue);
fastdom.mutate(() => {
// @ts-ignore
this.thumb.style[this.side] = (value >= maxValue ? maxValue : value) + '%';
});
4 years ago
//console.timeEnd('scroll onScroll');
}
4 years ago
public async onTopIntersection(/* entry: IntersectionObserverEntry */needHeight: number) {
console.log('onTopIntersection', needHeight, this);
if(this.hiddenElements.up.length && this.paddings.up) {
4 years ago
//let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height;
//let needHeight = entry.intersectionRect.height || await fastdom.measure(() => this.splitUp.lastElementChild.scrollHeight);
let fragment = document.createDocumentFragment();
while(needHeight > 0 && this.paddings.up) {
let child = this.hiddenElements.up.pop();
// console.log('top returning from hidden', child);
4 years ago
if(!child) {
this.paddings.up = 0;
break;
}
4 years ago
fragment.prepend(child.element);
4 years ago
needHeight -= child.height;
this.paddings.up -= child.height;
}
4 years ago
await fastdom.mutate(() => {
this.splitUp.prepend(fragment);
this.paddingTopDiv.style.height = this.paddings.up + 'px';
});
} else {
await fastdom.mutate(() => {
this.paddingTopDiv.style.height = '0px';
});
}
}
4 years ago
public async onBottomIntersection(/* entry: IntersectionObserverEntry */needHeight: number) {
console.log('onBottomIntersection', needHeight, this);
if(this.hiddenElements.down.length && this.paddings.down) {
4 years ago
//let needHeight = entry.intersectionRect.height || entry.boundingClientRect.height;
//let needHeight = entry.intersectionRect.height || await fastdom.measure(() => this.splitUp.firstElementChild.scrollHeight);
let fragment = document.createDocumentFragment();
while(needHeight > 0 && this.paddings.down) {
let child = this.hiddenElements.down.shift();
4 years ago
if(!child) {
this.paddings.down = 0;
break;
}
4 years ago
fragment.appendChild(child.element);
4 years ago
needHeight -= child.height;
this.paddings.down -= child.height;
}
4 years ago
await fastdom.mutate(() => {
this.splitUp.appendChild(fragment);
this.paddingBottomDiv.style.height = this.paddings.down + 'px';
});
if(this.onAddedBottom) this.onAddedBottom();
} else {
await fastdom.mutate(() => {
this.paddingBottomDiv.style.height = '0px';
});
}
}
4 years ago
public prepend(...smth: (string | Node)[]) {
if(this.splitUp) {
4 years ago
if(this.hiddenElements.up.length) {
smth.forEach(node => {
if(typeof(node) !== 'string') {
this.hiddenElements.up.push({
element: node as Element,
height: (node as Element).scrollHeight || 1
});
}
});
} else {
this.splitUp.prepend(...smth);
}
for(let node of smth) {
if(typeof(node) !== 'string') {
this.splitObserver.unobserve(node as Element);
this.splitObserver.observe(node as Element);
}
}
} else {
this.container.prepend(...smth);
}
}
4 years ago
public append(...smth: (string | Node)[]) {
if(this.splitUp) {
4 years ago
if(this.hiddenElements.down.length) {
smth.forEachReverse(node => {
if(typeof(node) !== 'string') {
this.hiddenElements.down.unshift({
element: node as Element,
height: (node as Element).scrollHeight || 1
});
}
});
} else {
this.splitUp.append(...smth);
}
for(let node of smth) {
if(typeof(node) !== 'string') {
this.splitObserver.unobserve(node as Element);
this.splitObserver.observe(node as Element);
}
}
} else {
this.container.append(...smth);
}
}
4 years ago
public insertBefore(newChild: Element, refChild: Element) {
if(this.splitUp) {
this.splitObserver.unobserve(newChild);
4 years ago
this.splitObserver.observe(newChild);
let index = -1;
index = this.hiddenElements.up.findIndex(c => c.element == refChild);
// возможно здесь нужно очищать предыдущую высоту если newChild уже скрыт (но может и не нужно)
if(index !== -1) {
this.hiddenElements.up.splice(index, 0, {element: newChild, height: newChild.scrollHeight || 1});
return index;
} else {
index = this.hiddenElements.down.findIndex(c => c.element == newChild);
if(index !== -1) {
this.hiddenElements.down.splice(index, 0, {element: newChild, height: newChild.scrollHeight || 1});
return index;
}
}
return this.splitUp.insertBefore(newChild, refChild);
}
4 years ago
return this.container.insertBefore(newChild, refChild);
}
4 years ago
4 years ago
set scrollTop(y: number) {
this.container.scrollTop = y;
}
4 years ago
4 years ago
get scrollTop() {
return this.container.scrollTop;
}
4 years ago
4 years ago
get scrollHeight() {
return this.container.scrollHeight;
}
4 years ago
4 years ago
get parentElement() {
return this.container.parentElement;
}
4 years ago
4 years ago
get offsetHeight() {
return this.container.offsetHeight;
}
}