2020-10-11 16:14:58 +03:00
|
|
|
import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise";
|
2020-10-02 23:33:32 +03:00
|
|
|
import { isTouchSupported } from "../helpers/touchSupport";
|
2020-06-21 15:25:17 +03:00
|
|
|
import { logger, LogLevels } from "../lib/logger";
|
2020-09-23 23:29:53 +03:00
|
|
|
import smoothscroll, { SCROLL_TIME, SmoothScrollToOptions } from '../vendor/smoothscroll';
|
2020-05-10 04:23:21 +03:00
|
|
|
(window as any).__forceSmoothScrollPolyfill__ = true;
|
2020-09-23 23:29:53 +03:00
|
|
|
smoothscroll();
|
2020-05-03 15:46:05 +03:00
|
|
|
/*
|
|
|
|
var el = $0;
|
|
|
|
var height = 0;
|
|
|
|
var checkUp = false;
|
|
|
|
|
|
|
|
do {
|
|
|
|
height += el.scrollHeight;
|
|
|
|
} while(el = (checkUp ? el.previousElementSibling : el.nextElementSibling));
|
|
|
|
console.log(height);
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
Array.from($0.querySelectorAll('.bubble__container')).forEach(_el => {
|
|
|
|
//_el.style.display = '';
|
|
|
|
//return;
|
|
|
|
|
|
|
|
let el = _el.parentElement;
|
|
|
|
let height = el.scrollHeight;
|
|
|
|
let width = el.scrollWidth;
|
|
|
|
el.style.width = width + 'px';
|
|
|
|
el.style.height = height + 'px';
|
|
|
|
_el.style.display = 'none';
|
|
|
|
});
|
|
|
|
*/
|
|
|
|
|
2020-06-05 19:01:06 +03:00
|
|
|
/* const scrollables: Map<HTMLElement, Scrollable> = new Map();
|
|
|
|
const scrollsIntersector = new IntersectionObserver(entries => {
|
|
|
|
for(let entry of entries) {
|
|
|
|
const scrollable = scrollables.get(entry.target as HTMLElement);
|
|
|
|
|
|
|
|
if(entry.isIntersecting) {
|
|
|
|
scrollable.isVisible = true;
|
|
|
|
} else {
|
|
|
|
scrollable.isVisible = false;
|
|
|
|
|
|
|
|
if(!isInDOM(entry.target)) {
|
|
|
|
scrollsIntersector.unobserve(scrollable.container);
|
|
|
|
scrollables.delete(scrollable.container);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}); */
|
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
export class ScrollableBase {
|
|
|
|
protected log: ReturnType<typeof logger>;
|
2020-05-03 15:46:05 +03:00
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
protected onScroll: () => void;
|
|
|
|
public getScrollValue: () => number;
|
|
|
|
|
|
|
|
public scrollLocked = 0;
|
2020-10-11 16:14:58 +03:00
|
|
|
public scrollLockedPromise: CancellablePromise<void> = Promise.resolve();
|
2020-09-23 23:29:53 +03:00
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
constructor(public el: HTMLElement, logPrefix = '', public container: HTMLElement = document.createElement('div')) {
|
2020-09-23 23:29:53 +03:00
|
|
|
this.container.classList.add('scrollable');
|
|
|
|
|
|
|
|
this.log = logger('SCROLL' + (logPrefix ? '-' + logPrefix : ''), LogLevels.error);
|
|
|
|
|
|
|
|
if(el) {
|
|
|
|
Array.from(el.children).forEach(c => this.container.append(c));
|
|
|
|
|
|
|
|
el.append(this.container);
|
|
|
|
}
|
|
|
|
//this.onScroll();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected setListeners() {
|
|
|
|
window.addEventListener('resize', this.onScroll);
|
|
|
|
this.container.addEventListener('scroll', this.onScroll, {passive: true, capture: true});
|
|
|
|
}
|
|
|
|
|
|
|
|
public append(element: HTMLElement) {
|
2020-11-11 19:01:38 +02:00
|
|
|
this.container.append(element);
|
2020-09-23 23:29:53 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
public scrollTo(value: number, side: 'top' | 'left', smooth = true, important = false, scrollTime = SCROLL_TIME) {
|
|
|
|
if(this.scrollLocked && !important) return;
|
|
|
|
|
|
|
|
const scrollValue = this.getScrollValue();
|
|
|
|
if(scrollValue == Math.floor(value)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-10-11 22:12:17 +03:00
|
|
|
const wasLocked = !!this.scrollLocked;
|
|
|
|
if(wasLocked) clearTimeout(this.scrollLocked);
|
|
|
|
if(smooth) {
|
|
|
|
if(!wasLocked) {
|
|
|
|
this.scrollLockedPromise = deferredPromise<void>();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.scrollLocked = window.setTimeout(() => {
|
|
|
|
this.scrollLocked = 0;
|
|
|
|
this.scrollLockedPromise.resolve();
|
|
|
|
//this.onScroll();
|
|
|
|
this.container.dispatchEvent(new CustomEvent('scroll'));
|
|
|
|
}, scrollTime);
|
|
|
|
} else if(wasLocked) {
|
2020-10-11 16:14:58 +03:00
|
|
|
this.scrollLockedPromise.resolve();
|
2020-10-11 22:12:17 +03:00
|
|
|
}
|
2020-09-23 23:29:53 +03:00
|
|
|
|
|
|
|
const options: SmoothScrollToOptions = {
|
|
|
|
behavior: smooth ? 'smooth' : 'auto',
|
|
|
|
scrollTime
|
|
|
|
};
|
|
|
|
|
|
|
|
options[side] = value;
|
|
|
|
|
|
|
|
this.container.scrollTo(options as any);
|
2020-10-11 22:12:17 +03:00
|
|
|
|
|
|
|
if(!smooth) {
|
|
|
|
this.container.dispatchEvent(new CustomEvent('scroll'));
|
|
|
|
}
|
2020-09-23 23:29:53 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
export type SliceSides = 'top' | 'bottom';
|
|
|
|
export type SliceSidesContainer = {[k in SliceSides]: boolean};
|
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
export default class Scrollable extends ScrollableBase {
|
2020-05-03 15:46:05 +03:00
|
|
|
public splitUp: HTMLElement;
|
|
|
|
|
|
|
|
public onScrolledTop: () => void = null;
|
|
|
|
public onScrolledBottom: () => void = null;
|
|
|
|
|
|
|
|
public onScrollMeasure: number = null;
|
|
|
|
|
|
|
|
public lastScrollTop: number = 0;
|
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
public loadedAll: SliceSidesContainer = {top: true, bottom: false};
|
2020-05-03 15:46:05 +03:00
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
constructor(el: HTMLElement, logPrefix = '', public onScrollOffset = 300) {
|
|
|
|
super(el, logPrefix);
|
2020-05-03 15:46:05 +03:00
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
this.container.classList.add('scrollable-y');
|
|
|
|
this.setListeners();
|
2020-05-03 15:46:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
public setVirtualContainer(el?: HTMLElement) {
|
|
|
|
this.splitUp = el;
|
|
|
|
this.log('setVirtualContainer:', el, this);
|
|
|
|
}
|
|
|
|
|
2020-09-21 20:34:19 +03:00
|
|
|
public onScroll = () => {
|
2020-06-05 19:01:06 +03:00
|
|
|
//if(this.debug) {
|
|
|
|
//this.log('onScroll call', this.onScrollMeasure);
|
|
|
|
//}
|
|
|
|
|
2020-06-13 11:19:39 +03:00
|
|
|
if(this.onScrollMeasure || ((this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) && !this.splitUp)) return;
|
2020-05-20 17:25:23 +03:00
|
|
|
this.onScrollMeasure = window.requestAnimationFrame(() => {
|
2020-09-23 23:29:53 +03:00
|
|
|
this.checkForTriggers();
|
2020-05-23 08:31:18 +03:00
|
|
|
|
|
|
|
this.onScrollMeasure = 0;
|
2020-05-03 15:46:05 +03:00
|
|
|
});
|
2020-09-21 20:34:19 +03:00
|
|
|
};
|
2020-05-03 15:46:05 +03:00
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
public checkForTriggers() {
|
2020-05-23 08:31:18 +03:00
|
|
|
if(this.scrollLocked || (!this.onScrolledTop && !this.onScrolledBottom)) return;
|
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
const container = this.container;
|
2020-08-27 21:25:47 +03:00
|
|
|
const scrollHeight = container.scrollHeight;
|
|
|
|
if(!scrollHeight) { // незачем вызывать триггеры если блок пустой или не виден
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {clientHeight, scrollTop} = container;
|
|
|
|
const maxScrollTop = scrollHeight - clientHeight;
|
2020-06-05 19:01:06 +03:00
|
|
|
|
|
|
|
//this.log('checkForTriggers:', scrollTop, maxScrollTop);
|
2020-05-23 08:31:18 +03:00
|
|
|
|
|
|
|
if(this.onScrolledTop && scrollTop <= this.onScrollOffset) {
|
|
|
|
this.onScrolledTop();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this.onScrolledBottom && (maxScrollTop - scrollTop) <= this.onScrollOffset) {
|
|
|
|
this.onScrolledBottom();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
public prepend(element: HTMLElement) {
|
|
|
|
(this.splitUp || this.container).prepend(element);
|
2020-05-03 15:46:05 +03:00
|
|
|
}
|
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
public append(element: HTMLElement) {
|
|
|
|
(this.splitUp || this.container).append(element);
|
2020-05-03 15:46:05 +03:00
|
|
|
}
|
|
|
|
|
2020-05-18 04:21:58 +03:00
|
|
|
public scrollIntoView(element: HTMLElement, smooth = true) {
|
2020-05-13 18:26:40 +03:00
|
|
|
if(element.parentElement && !this.scrollLocked) {
|
2020-06-16 23:48:08 +03:00
|
|
|
const isFirstUnread = element.classList.contains('is-first-unread');
|
2020-05-27 09:21:16 +03:00
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
let offset = element.getBoundingClientRect().top - this.container.getBoundingClientRect().top;
|
|
|
|
offset = this.scrollTop + offset;
|
2020-05-27 09:21:16 +03:00
|
|
|
|
2020-05-18 04:21:58 +03:00
|
|
|
if(!smooth && isFirstUnread) {
|
2020-09-23 23:29:53 +03:00
|
|
|
this.scrollTo(offset, 'top', false);
|
2020-05-18 04:21:58 +03:00
|
|
|
return;
|
|
|
|
}
|
2020-05-10 04:23:21 +03:00
|
|
|
|
2020-06-16 23:48:08 +03:00
|
|
|
const clientHeight = this.container.clientHeight;
|
|
|
|
const height = element.scrollHeight;
|
2020-05-26 18:04:06 +03:00
|
|
|
|
2020-11-06 19:51:51 +02:00
|
|
|
const d = height >= clientHeight ? 0 : (clientHeight - height) / 2;
|
2020-09-23 23:29:53 +03:00
|
|
|
offset -= d;
|
2020-05-18 04:21:58 +03:00
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
this.scrollTo(offset, 'top', smooth);
|
2020-09-21 20:34:19 +03:00
|
|
|
}
|
2020-05-03 15:46:05 +03:00
|
|
|
}
|
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
public getScrollValue = () => {
|
|
|
|
return this.scrollTop;
|
|
|
|
};
|
|
|
|
|
2020-11-11 19:01:38 +02:00
|
|
|
public slice(side: SliceSides, safeCount: number/* sliceLength: number */) {
|
|
|
|
//const isOtherSideLoaded = this.loadedAll[side == 'top' ? 'bottom' : 'top'];
|
|
|
|
//const multiplier = 2 - +isOtherSideLoaded;
|
|
|
|
const multiplier = 2;
|
|
|
|
safeCount *= multiplier;
|
|
|
|
|
|
|
|
const length = this.splitUp.childElementCount;
|
|
|
|
|
|
|
|
if(length <= safeCount) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const children = Array.from(this.splitUp.children) as HTMLElement[];
|
|
|
|
const sliced = side == 'top' ? children.slice(0, length - safeCount) : children.slice(safeCount);
|
|
|
|
for(const el of sliced) {
|
|
|
|
el.remove();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.log.error('slice', side, length, sliced.length, this.splitUp.childElementCount);
|
|
|
|
|
|
|
|
if(sliced.length) {
|
|
|
|
this.loadedAll[side] = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return sliced;
|
|
|
|
}
|
|
|
|
|
2020-10-11 22:12:17 +03:00
|
|
|
get isScrolledDown() {
|
|
|
|
return this.scrollHeight - Math.round(this.scrollTop + this.container.offsetHeight) <= 1;
|
|
|
|
}
|
|
|
|
|
2020-05-03 15:46:05 +03:00
|
|
|
set scrollTop(y: number) {
|
|
|
|
this.container.scrollTop = y;
|
|
|
|
}
|
|
|
|
|
|
|
|
get scrollTop() {
|
|
|
|
//this.log.trace('get scrollTop');
|
|
|
|
return this.container.scrollTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
get scrollHeight() {
|
|
|
|
return this.container.scrollHeight;
|
|
|
|
}
|
2020-09-23 23:29:53 +03:00
|
|
|
}
|
2020-05-03 15:46:05 +03:00
|
|
|
|
2020-09-23 23:29:53 +03:00
|
|
|
export class ScrollableX extends ScrollableBase {
|
2020-11-11 19:01:38 +02:00
|
|
|
constructor(el: HTMLElement, logPrefix = '', public onScrollOffset = 300, public splitCount = 15, public container: HTMLElement = document.createElement('div')) {
|
|
|
|
super(el, logPrefix, container);
|
2020-09-23 23:29:53 +03:00
|
|
|
|
|
|
|
this.container.classList.add('scrollable-x');
|
|
|
|
|
2020-10-02 23:33:32 +03:00
|
|
|
if(!isTouchSupported) {
|
2020-09-23 23:29:53 +03:00
|
|
|
const scrollHorizontally = (e: any) => {
|
|
|
|
e = window.event || e;
|
|
|
|
if(e.which == 1) {
|
|
|
|
// maybe horizontal scroll is natively supports, works on macbook
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setListeners();
|
2020-05-03 15:46:05 +03:00
|
|
|
}
|
2020-09-23 23:29:53 +03:00
|
|
|
|
|
|
|
public scrollIntoView(element: HTMLElement, smooth = true, scrollTime?: number) {
|
|
|
|
if(element.parentElement && !this.scrollLocked) {
|
|
|
|
let offset = element.getBoundingClientRect().left - this.container.getBoundingClientRect().left;
|
|
|
|
offset = this.getScrollValue() + offset;
|
|
|
|
|
|
|
|
const clientWidth = this.container.clientWidth;
|
|
|
|
const width = element.scrollWidth;
|
|
|
|
|
2020-11-06 19:51:51 +02:00
|
|
|
const d = width >= clientWidth ? 0 : (clientWidth - width) / 2;
|
2020-09-23 23:29:53 +03:00
|
|
|
offset -= d;
|
|
|
|
|
|
|
|
this.scrollTo(offset, 'left', smooth, undefined, scrollTime);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getScrollValue = () => {
|
|
|
|
return this.container.scrollLeft;
|
|
|
|
};
|
2020-05-03 15:46:05 +03:00
|
|
|
}
|