Browse Source

unstable beta commit

master
Eduard Kuzmenko 4 years ago
parent
commit
34cc14d3f5
  1. 189
      src/components/emoticonsDropdown.ts
  2. 6
      src/components/misc.ts
  3. 2
      src/components/pageIm.ts
  4. 2
      src/components/preloader.ts
  5. 618
      src/components/scrollable.ts
  6. 922
      src/components/scrollable_almostgood_fastdom.ts
  7. 974
      src/components/scrollable_hzchtoetotakoe.ts
  8. 828
      src/components/scrollable_noPaddings.ts
  9. 244
      src/components/wrappers.ts
  10. 73
      src/lib/appManagers/appDialogsManager.ts
  11. 3
      src/lib/appManagers/appDocsManager.ts
  12. 344
      src/lib/appManagers/appImManager.ts
  13. 330
      src/lib/appManagers/appMediaViewer copy.ts
  14. 389
      src/lib/appManagers/appMediaViewer.ts
  15. 26
      src/lib/appManagers/appMessagesManager.ts
  16. 102
      src/lib/appManagers/appPhotosManager.ts
  17. 60
      src/lib/appManagers/appSidebarLeft.ts
  18. 107
      src/lib/appManagers/appSidebarRight.ts
  19. 16
      src/lib/mtproto/apiFileManager.ts
  20. 47
      src/lib/polyfill.ts
  21. 5
      src/lib/services.ts
  22. 62
      src/lib/utils.js
  23. 145
      src/scss/partials/_chat.scss
  24. 58
      src/scss/partials/_chatlist.scss
  25. 1
      src/scss/partials/_mediaViewer.scss
  26. 8
      src/scss/partials/_rightSIdebar.scss
  27. 39
      src/scss/style.scss

189
src/components/emoticonsDropdown.ts

@ -49,10 +49,10 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -49,10 +49,10 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
}
console.log('emoticonsMenuOnClick', menu.getBoundingClientRect(), target.getBoundingClientRect());
*/
scroll.onAddedBottom = () => { // привет, костыль, давно не виделись!
/* scroll.onAddedBottom = () => { // привет, костыль, давно не виделись!
scroll.container.scrollTop = y;
scroll.onAddedBottom = () => {};
};
}; */
scroll.container.scrollTop = y;
});
};
@ -186,7 +186,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -186,7 +186,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
let prevCategoryIndex = 1;
let menu = contentEmojiDiv.nextElementSibling as HTMLUListElement;
let emojiScroll = new Scrollable(contentEmojiDiv);
let emojiScroll = new Scrollable(contentEmojiDiv, 'y', 500, 'EMOJI', contentEmojiDiv);
emojiScroll.container.addEventListener('scroll', (e) => {
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, emojiScroll.container);
});
@ -202,7 +202,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -202,7 +202,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
let menuWrapper = contentStickersDiv.nextElementSibling as HTMLDivElement;
let menu = menuWrapper.firstElementChild as HTMLUListElement;
let menuScroll = new Scrollable(menuWrapper, true, false);
let menuScroll = new Scrollable(menuWrapper, 'x');
let stickersDiv = document.createElement('div');
stickersDiv.classList.add('stickers-categories');
@ -244,6 +244,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -244,6 +244,7 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
let heights: number[] = [];
let heightRAF = 0;
let categoryPush = (categoryDiv: HTMLDivElement, docs: MTDocument[], prepend?: boolean) => {
//if((docs.length % 5) != 0) categoryDiv.classList.add('not-full');
@ -254,11 +255,8 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -254,11 +255,8 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
categoryDiv.append(div);
});
/* if(prepend) {
stickersDiv.prepend(categoryDiv);
} else {
stickersDiv.append(categoryDiv);
} */
if(prepend) stickersScroll.prepend(categoryDiv);
else stickersScroll.append(categoryDiv);
setTimeout(() => lazyLoadQueue.check(), 0);
@ -271,92 +269,30 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -271,92 +269,30 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
return heights.unshift(scrollHeight) - 1;
} */
heights.length = 0;
Array.from(stickersDiv.children).forEach((div, i) => {
heights[i] = (heights[i - 1] || 0) + div.scrollHeight;
if(heightRAF) window.cancelAnimationFrame(heightRAF);
heightRAF = window.requestAnimationFrame(() => {
heightRAF = 0;
heights.length = 0;
let concated = stickersScroll.hiddenElements.up.concat(stickersScroll.visibleElements, stickersScroll.hiddenElements.down);
concated.forEach((el, i) => {
heights[i] = (heights[i - 1] || 0) + el.height;
});
console.log('stickers concated', concated, heights);
});
/* Array.from(stickersDiv.children).forEach((div, i) => {
heights[i] = (heights[i - 1] || 0) + div.scrollHeight;
}); */
//stickersScroll.onScroll();
//return heights.push(prevHeight + scrollHeight) - 1;
};
apiManager.invokeApi('messages.getRecentStickers', {flags: 0, hash: 0}).then((res) => {
let stickers: {
_: string,
hash: number,
packs: any[],
stickers: MTDocument[],
dates: number[]
} = res as any;
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
stickersScroll.prepend(categoryDiv);
categoryPush(categoryDiv, stickers.stickers, true);
});
apiManager.invokeApi('messages.getAllStickers', {hash: 0}).then((res) => {
let stickers: {
_: 'messages.allStickers',
hash: number,
sets: Array<MTStickerSet>
} = res as any;
stickers.sets/* .slice(0, 10) */.forEach(async(set) => {
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
let li = document.createElement('li');
li.classList.add('btn-icon');
menu.append(li);
stickersScroll.append(categoryDiv);
let stickerSet = await appStickersManager.getStickerSet(set);
if(stickerSet.set.thumb) {
let thumb = stickerSet.set.thumb;
appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => {
if(thumb.w == 1 && thumb.h == 1) { // means animated
const reader = new FileReader();
reader.addEventListener('loadend', async(e) => {
// @ts-ignore
const text = e.srcElement.result;
let json = await CryptoWorker.gzipUncompress<string>(text, true);
let animation = await lottieLoader.loadAnimation({
container: li,
loop: true,
autoplay: false,
animationData: JSON.parse(json)
}, EMOTICONSSTICKERGROUP);
});
reader.readAsArrayBuffer(blob);
} else {
let image = new Image();
//image.src = URL.createObjectURL(blob);
appWebpManager.polyfillImage(image, blob);
li.append(image);
}
});
} else { // as thumb will be used first sticker
wrapSticker(stickerSet.documents[0], li as any, undefined, undefined, EMOTICONSSTICKERGROUP); // kostil
}
categoryPush(categoryDiv, stickerSet.documents);
});
});
let prevCategoryIndex = 0;
let stickersScroll = new Scrollable(contentStickersDiv);
let stickersScroll = new Scrollable(contentStickersDiv, 'y', 500, 'STICKERS');
stickersScroll.container.addEventListener('scroll', (e) => {
lazyLoadQueue.check();
lottieLoader.checkAnimations();
@ -364,10 +300,89 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement, @@ -364,10 +300,89 @@ const initEmoticonsDropdown = (pageEl: HTMLDivElement,
prevCategoryIndex = emoticonsContentOnScroll(menu, heights, prevCategoryIndex, stickersScroll.container, menuScroll);
});
stickersScroll.setVirtualContainer(stickersDiv);
stickersScroll.lock('both');
emoticonsMenuOnClick(menu, heights, stickersScroll, menuScroll);
stickersInit = null;
Promise.all([
apiManager.invokeApi('messages.getRecentStickers', {flags: 0, hash: 0}).then((res) => {
let stickers: {
_: string,
hash: number,
packs: any[],
stickers: MTDocument[],
dates: number[]
} = res as any;
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
//stickersScroll.prepend(categoryDiv);
categoryPush(categoryDiv, stickers.stickers, true);
}),
apiManager.invokeApi('messages.getAllStickers', {hash: 0}).then(async(res) => {
let stickers: {
_: 'messages.allStickers',
hash: number,
sets: Array<MTStickerSet>
} = res as any;
for(let set of stickers.sets) {
let categoryDiv = document.createElement('div');
categoryDiv.classList.add('sticker-category');
let li = document.createElement('li');
li.classList.add('btn-icon');
menu.append(li);
//stickersScroll.append(categoryDiv);
let stickerSet = await appStickersManager.getStickerSet(set);
if(stickerSet.set.thumb) {
let thumb = stickerSet.set.thumb;
appStickersManager.getStickerSetThumb(stickerSet.set).then((blob) => {
if(thumb.w == 1 && thumb.h == 1) { // means animated
const reader = new FileReader();
reader.addEventListener('loadend', async(e) => {
// @ts-ignore
const text = e.srcElement.result;
let json = await CryptoWorker.gzipUncompress<string>(text, true);
let animation = await lottieLoader.loadAnimation({
container: li,
loop: true,
autoplay: false,
animationData: JSON.parse(json)
}, EMOTICONSSTICKERGROUP);
});
reader.readAsArrayBuffer(blob);
} else {
let image = new Image();
//image.src = URL.createObjectURL(blob);
appWebpManager.polyfillImage(image, blob);
li.append(image);
}
});
} else { // as thumb will be used first sticker
wrapSticker(stickerSet.documents[0], li as any, undefined, undefined, EMOTICONSSTICKERGROUP); // kostil
}
categoryPush(categoryDiv, stickerSet.documents, false);
}
})
]).then(() => {
stickersScroll.unlock('both');
});
};
return {dropdown, lazyLoadQueue};

6
src/components/misc.ts

@ -1,8 +1,8 @@ @@ -1,8 +1,8 @@
import apiManager from "../lib/mtproto/apiManager";
import { whichChild, isElementInViewport, isInDOM, findUpTag } from "../lib/utils";
import { whichChild, findUpTag } from "../lib/utils";
let onRippleClick = function(this: HTMLElement, e: MouseEvent) {
var $circle = this.firstElementChild as HTMLSpanElement;//this.querySelector('.c-ripple__circle') as HTMLSpanElement;
var $circle = this.firstElementChild as HTMLSpanElement;
var rect = this.parentElement.getBoundingClientRect();
var x = e.clientX - rect.left; //x position within the element.
@ -66,7 +66,7 @@ export function putPreloader(elem: Element, returnDiv = false) { @@ -66,7 +66,7 @@ export function putPreloader(elem: Element, returnDiv = false) {
if(elem) {
elem.appendChild(div);
}
return div;
}

2
src/components/pageIm.ts

@ -24,7 +24,7 @@ export default () => import('../lib/services').then(services => { @@ -24,7 +24,7 @@ export default () => import('../lib/services').then(services => {
//console.log('updating user:', user, dialog);
if(dialog && !appUsersManager.isBot(dialog.peerID) && dialog.peerID != appImManager.myID) {
let online = user.status._ == 'userStatusOnline';
let online = user.status && user.status._ == 'userStatusOnline';
let dom = appDialogsManager.getDialogDom(dialog.peerID);
if(dom) {

2
src/components/preloader.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { isInDOM } from "../lib/utils";
import { CancellablePromise } from "../lib/mtproto/apiFileManager";
import { CancellablePromise } from "../lib/polyfill";
export default class ProgressivePreloader {
public preloader: HTMLDivElement = null;

618
src/components/scrollable.ts

@ -1,15 +1,15 @@ @@ -1,15 +1,15 @@
import { cancelEvent } from "../lib/utils";
//import {measure} from 'fastdom/fastdom.min';
import 'fastdom/fastdom.min';
import FastDom from 'fastdom';
import 'fastdom/src/fastdom-strict'; // exclude in production
//import 'fastdom/src/fastdom-strict'; // exclude in production
import FastDomPromised from 'fastdom/extensions/fastdom-promised';
import { logger } from "../lib/polyfill";
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);
//(window as any).fastdom.strict(false);
setTimeout(() => {
//(window as any).fastdom.strict(true);
@ -36,14 +36,16 @@ export default class Scrollable { @@ -36,14 +36,16 @@ export default class Scrollable {
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: {element: Element, height: number}[],
down: {element: Element, height: number}[]
up: Scrollable['visibleElements'],
down: Scrollable['visibleElements']
} = {
up: [],
down: []
@ -64,9 +66,9 @@ export default class Scrollable { @@ -64,9 +66,9 @@ export default class Scrollable {
public topObserver: IntersectionObserver;
public bottomObserver: IntersectionObserver;
public splitMeasureTop: Promise<{element: Element, height: number}[]> = null;
public splitMeasureTop: Promise<Promise<void>> = null;
public splitMeasureBottom: Scrollable['splitMeasureTop'] = null;
public splitMeasureAdd: Promise<number> = null;
public splitMeasureAdd: Promise<void> = null;
public splitMeasureRemoveBad: Promise<Element> = null;
public splitMutateTop: Promise<void> = null;
public splitMutateBottom: Scrollable['splitMutateTop'] = null;
@ -80,21 +82,30 @@ export default class Scrollable { @@ -80,21 +82,30 @@ export default class Scrollable {
task: Promise<any>
}> = [];
public onScrollMeasure: Promise<any> = null;
public onScrollMeasure: number = null;
public lastScrollTop: number = 0;
public scrollTopOffset: number = 0;
private disableHoverTimeout: number = 0;
private log: ReturnType<typeof logger>;
private debug = true;
constructor(public el: HTMLDivElement, x = false, y = true, public splitOffset = 300, logPrefix = '', public appendTo = el, public onScrollOffset = splitOffset) {
private measureMutex: CancellablePromise<void>;
private prependLocked = false;
private appendLocked = false;
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 : ''));
if(x) {
this.measureMutex = deferredPromise<void>();
this.measureMutex.resolve();
if(axis == 'x') {
this.container.classList.add('scrollable-x');
this.type = 'width';
this.side = 'left';
@ -102,6 +113,7 @@ export default class Scrollable { @@ -102,6 +113,7 @@ export default class Scrollable {
this.scrollType = 'scrollWidth';
this.scrollSide = 'scrollLeft';
this.clientAxis = 'clientX';
this.clientSize = 'clientWidth';
let scrollHorizontally = (e: any) => {
e = window.event || e;
@ -119,7 +131,7 @@ export default class Scrollable { @@ -119,7 +131,7 @@ export default class Scrollable {
// @ts-ignore
this.container.attachEvent("onmousewheel", scrollHorizontally);
}
} else if(y) {
} else if(axis == 'y') {
this.container.classList.add('scrollable-y');
this.type = 'height';
this.side = 'top';
@ -127,6 +139,7 @@ export default class Scrollable { @@ -127,6 +139,7 @@ export default class Scrollable {
this.scrollType = 'scrollHeight';
this.scrollSide = 'scrollTop';
this.clientAxis = 'clientY';
this.clientSize = 'clientHeight';
} else {
throw new Error('no side for scroll');
}
@ -162,126 +175,149 @@ export default class Scrollable { @@ -162,126 +175,149 @@ export default class Scrollable {
//this.container.addEventListener('mouseover', this.resize.bind(this)); // omg
window.addEventListener('resize', () => {
//this.resize.bind(this);
this.onScroll();
this.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.bind(this));
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];
this.resize();
}, 0);
this.container.parentElement.append(this.thumb);
this.resize();
}
public detachTop(child: Element, needHeight = 0) {
if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom);
public detachTop(fromIndex: number, needHeight = 0, detachAll = false) {
//if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom);
if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
if(this.prependLocked) return;
this.splitMeasureBottom = fastdom.measure(() => {
let sliced: {element: Element, height: number}[] = [];
//return this.splitMeasureBottom = fastdom.measure(() => {
return this.splitMutateBottom = fastdom.mutate(() => {
if(this.prependLocked) return;
let spliceTo = -1;
do {
let needToDetachHeight = needHeight;
for(; fromIndex >= 0; --fromIndex) {
let child = this.visibleElements[fromIndex];
if(needHeight > 0) {
needHeight -= child.scrollHeight;
needHeight -= child.height;
} else {
sliced.push({element: child, height: child.scrollHeight});
needToDetachHeight -= child.height;
if(spliceTo === -1) {
spliceTo = fromIndex;
}
}
} while(child = child.previousElementSibling);
return sliced;
});
return this.splitMeasureBottom.then(sliced => {
if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
}
if((needToDetachHeight > 0 && !detachAll) || spliceTo === -1) return;
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);
//if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
//return this.splitMutateBottom = fastdom.mutate(() => {
let spliced = this.visibleElements.splice(0, spliceTo + 1);
if(this.debug) {
this.log('spliced up', spliced);
}
spliced.forEach((child, idx) => {
if(!child.element.parentElement) {
this.log.error('no child in splitUp (up):', child, child.element, 0, spliceTo + 1, idx, spliced);
}
this.paddingTopDiv.style.height = this.paddings.up + 'px';
this.paddings.up += child.height;
this.splitUp.removeChild(child.element);
});
this.hiddenElements.up.push(...spliced);
this.paddingTopDiv.style.height = this.paddings.up + 'px';
//});
});
}
public detachBottom(child: Element, needHeight = 0) {
if(this.splitMeasureBottom) fastdom.clear(this.splitMeasureBottom);
public detachBottom(fromIndex: number, needHeight = 0, detachAll = false) {
//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(this.appendLocked) return;
//return this.splitMeasureBottom = fastdom.measure(() => {
return this.splitMutateBottom = fastdom.mutate(() => {
if(this.appendLocked) return;
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.scrollHeight;
needHeight -= child.height;
} else {
sliced.push({element: child, height: child.scrollHeight});
needToDetachHeight -= child.height;
if(spliceFrom === -1) spliceFrom = fromIndex;
spliceTo = fromIndex;
}
} while(child = child.nextElementSibling);
return sliced;
});
return this.splitMeasureBottom.then(sliced => {
if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
}
if((needToDetachHeight > 0 && !detachAll) || spliceFrom === -1) return;
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);
//if(this.splitMutateBottom) fastdom.clear(this.splitMutateBottom);
//return this.splitMutateBottom = fastdom.mutate(() => {
let spliced = this.visibleElements.splice(spliceFrom, spliceTo - spliceFrom + 1);
if(this.debug) {
this.log('spliced down', spliced, spliceFrom, spliceTo - spliceFrom + 1, length);
}
spliced.forEach((child, idx) => {
if(!child.element.parentElement) {
this.log.error('no child in splitUp (down):', child, child.element, spliceFrom, spliceTo - spliceFrom + 1, idx, spliced);
}
this.paddingBottomDiv.style.height = this.paddings.down + 'px';
this.paddings.down += child.height;
this.splitUp.removeChild(child.element);
});
this.hiddenElements.down.unshift(...spliced);
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;
//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');
@ -292,16 +328,13 @@ export default class Scrollable { @@ -292,16 +328,13 @@ export default class Scrollable {
public setVirtualContainer(el?: HTMLElement) {
this.splitUp = el;
this.hiddenElements.up.length = this.hiddenElements.down.length = 0;
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.paddingTopDiv.style.height = '';
this.paddingBottomDiv.style.height = '';
this.log('setVirtualContainer:', el, this);
@ -318,6 +351,40 @@ export default class Scrollable { @@ -318,6 +351,40 @@ export default class Scrollable {
}
}
get state() {
return {
hiddenElements: {
up: this.hiddenElements.up.slice(),
down: this.hiddenElements.down.slice(),
},
paddings: {
up: this.paddings.up,
down: this.paddings.down
},
visibleElements: this.visibleElements.slice(),
scrollSize: this.scrollSize
};
}
set state(state: {
visibleElements: Scrollable['visibleElements'],
hiddenElements: Scrollable['hiddenElements'],
paddings: Scrollable['paddings'],
scrollSize: Scrollable['scrollSize']
}) {
this.visibleElements = state.visibleElements;
this.hiddenElements = state.hiddenElements;
this.paddings = state.paddings;
this.scrollSize = state.scrollSize;
fastdom.mutate(() => {
this.paddingTopDiv.style.height = this.paddings.up + 'px';
this.paddingBottomDiv.style.height = this.paddings.down + 'px';
this.onScroll();
});
}
public getScrollTopOffset() {
if(this.splitUp && this.splitUp.parentElement && this.splitUp.parentElement != this.container) { // need to find offset
fastdom.measure(() => {
@ -333,49 +400,75 @@ export default class Scrollable { @@ -333,49 +400,75 @@ export default class Scrollable {
}
public onScroll() {
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];
//return;
if(this.debug) {
this.log('onScroll call', this.onScrollMeasure);
}
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');
// @ts-ignore
let scrollSize = this.container[this.scrollType];
if(scrollSize != this.scrollSize || this.thumbSize == 0) {
this.resize();
if(!this.measureMutex.isFulfilled) {
this.measureMutex.resolve();
}
this.scrollSize = scrollSize;
}, 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 ret = {value, maxValue};
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();
}
//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) {
if(/* !this.onScrolledTopFired */!this.prependLocked) {
this.onScrolledTopFired = true;
this.onScrolledTop();
}
@ -385,7 +478,8 @@ export default class Scrollable { @@ -385,7 +478,8 @@ export default class Scrollable {
}
if(!this.splitUp) {
return ret;
this.onScrollMeasure = 0;
return;
}
let perf = performance.now();
@ -395,44 +489,44 @@ export default class Scrollable { @@ -395,44 +489,44 @@ export default class Scrollable {
let toBottom = scrollTop > this.lastScrollTop;
let visibleFrom = /* scrollTop < this.paddings.up ? scrollTop : */scrollTop - this.paddings.up;
let visibleFrom = scrollTop - this.paddings.up;
let visibleUntil = visibleFrom + this.size;
let sum = 0;
let firstVisibleElement: Element;
let lastVisibleElement: Element;
let firstVisibleElementIndex = -1;
let lastVisibleElementIndex = -1;
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;
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 && firstVisibleElement) {
lastVisibleElement = element;
if(sum < visibleUntil && firstVisibleElementIndex !== -1) {
lastVisibleElementIndex = idx;
}
sum += element.scrollHeight;
sum += child.height;
//this.log(sum, element);
}
});
if(!lastVisibleElement && firstVisibleElement) {
lastVisibleElement = firstVisibleElement;
if(lastVisibleElementIndex === -1 && firstVisibleElementIndex !== -1) {
lastVisibleElementIndex = firstVisibleElementIndex;
}
// возможно устанавливать прошлый скролл нужно уже после этого промиса, т.к. он может очиститься
if(scrollTop == this.lastScrollTop) {
if(this.debug) {
this.log('onScroll ==', (performance.now() - perf).toFixed(3), length, scrollTop, maxScrollTop, toBottom, firstVisibleElementIndex, lastVisibleElementIndex, visibleFrom, visibleUntil, this.scrollTopOffset, this.scrollSize);
}
this.lastScrollTop = scrollTop;
if(firstVisibleElement) this.detachTop(firstVisibleElement, needHeight);
if(lastVisibleElement) this.detachBottom(lastVisibleElement, needHeight);
return ret;
if(firstVisibleElementIndex !== -1) this.detachTop(firstVisibleElementIndex, needHeight);
if(lastVisibleElementIndex !== -1) this.detachBottom(lastVisibleElementIndex, needHeight);
this.onScrollMeasure = 0;
return;
}
/* {
@ -442,92 +536,78 @@ export default class Scrollable { @@ -442,92 +536,78 @@ export default class Scrollable {
} */
if(toBottom) { // scrolling bottom
if(firstVisibleElement) {
if(firstVisibleElementIndex !== -1) {
if(this.debug) {
this.log('will detach top by:', firstVisibleElement, needHeight);
this.log('will detach top by:', firstVisibleElementIndex, needHeight);
}
this.detachTop(firstVisibleElement, needHeight);
this.detachTop(firstVisibleElementIndex, needHeight);
if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd);
for(let i = lastVisibleElementIndex + 1; i < length; ++i) {
needHeight -= this.visibleElements[i].height;
}
let child = lastVisibleElement;
this.splitMeasureAdd = fastdom.measure(() => {
while(child = child.nextElementSibling) {
needHeight -= child.scrollHeight;
}
if(needHeight >= this.splitOffset) {
//this.detachTop(firstVisibleElementIndex, this.splitOffset);
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.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.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(lastVisibleElement) {
if(lastVisibleElementIndex !== -1) {
if(this.debug) {
this.log('will detach bottom by:', lastVisibleElement, needHeight);
this.log('will detach bottom by:', lastVisibleElementIndex, needHeight);
}
this.detachBottom(lastVisibleElement, needHeight);
//if((lastVisibleElementIndex + 1) < length) {
this.detachBottom(lastVisibleElementIndex, needHeight);
//}
let child = firstVisibleElement;
if(this.splitMeasureAdd) fastdom.clear(this.splitMeasureAdd);
this.splitMeasureAdd = fastdom.measure(() => {
while(child = child.previousElementSibling) {
needHeight -= child.scrollHeight;
}
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);
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.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, firstVisibleElement, lastVisibleElement, visibleFrom, visibleUntil, this.scrollTopOffset);
this.log('onScroll', (performance.now() - perf).toFixed(3), length, scrollTop, maxScrollTop, toBottom, firstVisibleElementIndex, lastVisibleElementIndex, visibleFrom, visibleUntil, this.scrollTopOffset);
}
this.lastScrollTop = scrollTop;
return {value, maxValue};
this.onScrollMeasure = 0;
});
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) {
@ -543,12 +623,14 @@ export default class Scrollable { @@ -543,12 +623,14 @@ export default class Scrollable {
}
if(this.debug) {
this.log.warn('bait it off now', this, length, this.splitUp.childElementCount, scrollTop, this.paddings.up, h);
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));
if(!this.paddings.up) this.onBottomIntersection((this.size * 2) + (needHeight * 2));
else this.onTopIntersection((this.size * 2) + (needHeight * 2));
});
}
@ -565,12 +647,14 @@ export default class Scrollable { @@ -565,12 +647,14 @@ export default class Scrollable {
}
if(this.debug) {
this.log.warn('shake it off now', this, length, this.splitUp.childElementCount);
this.log.warn('manual scroll bottom', this, length, this.splitUp.childElementCount, scrollTop, this.paddings.down, h);
}
this.paddingTopDiv.style.height = this.paddings.up + 'px';
this.paddingBottomDiv.style.height = this.paddings.down + 'px';
this.onBottomIntersection(this.size + (needHeight * 2));
if(!this.paddings.down) this.onTopIntersection(this.size + (needHeight * 2));
else this.onBottomIntersection(this.size + (needHeight * 2));
});
}
@ -579,8 +663,8 @@ export default class Scrollable { @@ -579,8 +663,8 @@ export default class Scrollable {
this.log('onTopIntersection', needHeight, this);
}
if(this.splitMutateIntersectionTop) fastdom.clear(this.splitMutateIntersectionTop);
this.splitMutateIntersectionTop = fastdom.mutate(() => {
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) {
@ -593,6 +677,7 @@ export default class Scrollable { @@ -593,6 +677,7 @@ export default class Scrollable {
break;
}
this.visibleElements.unshift(child);
fragment.prepend(child.element);
needHeight -= child.height;
@ -624,6 +709,7 @@ export default class Scrollable { @@ -624,6 +709,7 @@ export default class Scrollable {
break;
}
this.visibleElements.push(child);
fragment.appendChild(child.element);
needHeight -= child.height;
@ -644,93 +730,67 @@ export default class Scrollable { @@ -644,93 +730,67 @@ export default class Scrollable {
});
}
public prepend(...smth: Element[]) {
public prepend(element: HTMLElement) {
if(this.splitUp) {
smth.forEach(node => {
this.removeElement(node);
});
this.removeElement(element);
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);
if(this.hiddenElements.up.length && !this.prependLocked) {
this.splitUp.prepend(element);
let height = element.scrollHeight;
this.log('will append element to up hidden', element, height);
this.paddings.up += height;
this.hiddenElements.up.unshift({
element: element,
height: height
});
element.parentElement.removeChild(element);
this.paddingTopDiv.style.height = this.paddings.up + 'px';
this.onScroll();
} else {
this.splitUp.prepend(...smth);
this.splitUp.prepend(element);
let el = {element, height: 0};
this.visibleElements.unshift(el);
fastdom.measure(() => {
if(!element.parentElement) return;
let height = element.scrollHeight;
el.height = height;
this.scrollSize += height;
});
this.onScroll();
}
} else {
this.appendTo.prepend(...smth);
this.appendTo.prepend(element);
this.onScroll();
}
//this.onScroll();
}
public append(...smth: Element[]) {
public append(element: HTMLElement) {
if(this.splitUp) {
smth.forEach(node => {
this.removeElement(node);
});
this.removeElement(element);
if(this.hiddenElements.down.length) {
if(this.hiddenElements.down.length && !this.appendLocked) {
fastdom.mutate(() => {
this.splitUp.append(...smth);
this.splitUp.append(element);
}).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
});
let height = element.scrollHeight;
this.log('will append element to down hidden', element, height);
this.paddings.down += height;
this.hiddenElements.down.push({
element: element,
height: height
});
});
}).then(() => {
fastdom.mutate(() => {
smth.forEach(node => {
if(node.parentElement) {
node.parentElement.removeChild(node);
}
});
if(element.parentElement) {
element.parentElement.removeChild(element);
}
this.paddingBottomDiv.style.height = this.paddings.down + 'px';
@ -738,17 +798,34 @@ export default class Scrollable { @@ -738,17 +798,34 @@ export default class Scrollable {
});
});
} else {
this.splitUp.append(...smth);
this.splitUp.append(element);
let el = {element, height: 0};
this.visibleElements.push(el);
fastdom.measure(() => {
if(!element.parentElement) return;
let height = element.scrollHeight;
el.height = height;
this.scrollSize += height;
});
this.onScroll();
}
} else {
this.appendTo.append(...smth);
this.appendTo.append(element);
this.onScroll();
}
//this.onScroll();
}
public contains(element: Element) {
if(!this.splitUp) {
return this.appendTo.contains(element);
}
return !!element.parentElement || !!this.hiddenElements.up.find(c => c.element == element) || !!this.hiddenElements.down.find(c => c.element == element);
}
public removeElement(element: Element) {
if(!this.splitUp) {
if(this.container.contains(element)) {
@ -791,8 +868,13 @@ export default class Scrollable { @@ -791,8 +868,13 @@ export default class Scrollable {
}
public insertBefore(newChild: Element, refChild: Element, height?: number) {
//this.log('insertBefore', newChild, refChild);
return;
this.log('insertBefore', newChild, newChild.textContent, refChild);
//return;
if(!this.splitUp) {
let ret = this.appendTo.insertBefore(newChild, refChild);
this.onScroll();
return ret;
}
if(this.splitUp) {
let index = -1;
@ -865,12 +947,8 @@ export default class Scrollable { @@ -865,12 +947,8 @@ export default class Scrollable {
});
return;
}
let ret = this.container.insertBefore(newChild, refChild);
this.onScroll();
return ret;
}
public scrollIntoView(element: Element) {
if(element.parentElement) {
element.scrollIntoView();
@ -881,19 +959,31 @@ export default class Scrollable { @@ -881,19 +959,31 @@ export default class Scrollable {
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;
}
}
}
public lock(side: 'top' | 'down' | 'both' = 'down') {
if(side == 'top') this.prependLocked = true;
else if(side == 'down') this.appendLocked = true;
else this.prependLocked = this.appendLocked = true;
}
public unlock(side: 'top' | 'down' | 'both' = 'down') {
if(side == 'top') this.prependLocked = false;
else if(side == 'down') this.appendLocked = false;
else this.prependLocked = this.appendLocked = false;
}
set scrollTop(y: number) {
fastdom.mutate(() => {
this.container.scrollTop = y;
@ -915,4 +1005,8 @@ export default class Scrollable { @@ -915,4 +1005,8 @@ export default class Scrollable {
get offsetHeight() {
return this.container.offsetHeight;
}
get length() {
return this.hiddenElements.up.length + this.visibleElements.length + this.hiddenElements.down.length;
}
}

922
src/components/scrollable_almostgood_fastdom.ts

@ -0,0 +1,922 @@ @@ -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;
}
}

974
src/components/scrollable_hzchtoetotakoe.ts

@ -0,0 +1,974 @@ @@ -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;
}
}

828
src/components/scrollable_noPaddings.ts

@ -0,0 +1,828 @@ @@ -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;
}
}

244
src/components/wrappers.ts

@ -7,10 +7,11 @@ import {AppImManager} from "../lib/appManagers/appImManager"; @@ -7,10 +7,11 @@ import {AppImManager} from "../lib/appManagers/appImManager";
import { formatBytes } from "../lib/utils";
import ProgressivePreloader from './preloader';
import LazyLoadQueue from './lazyLoadQueue';
import apiFileManager, { CancellablePromise } from '../lib/mtproto/apiFileManager';
import apiFileManager from '../lib/mtproto/apiFileManager';
import appWebpManager from '../lib/appManagers/appWebpManager';
import {wrapPlayer} from '../lib/ckin';
import { RichTextProcessor } from '../lib/richtextprocessor';
import { CancellablePromise } from '../lib/polyfill';
export type MTDocument = {
_: 'document',
@ -46,27 +47,43 @@ export type MTPhotoSize = { @@ -46,27 +47,43 @@ export type MTPhotoSize = {
preloaded?: boolean // custom added
};
export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement, message: any, justLoader = true, preloader?: ProgressivePreloader, controls = true, round = false) {
if(!container.firstElementChild || (container.firstElementChild.tagName != 'IMG' && container.firstElementChild.tagName != 'VIDEO')) {
let size = appPhotosManager.setAttachmentSize(doc, container);
}
let peerID = this.peerID ? this.peerID : this.currentMessageID;
//container.classList.add('video');
let img = container.firstElementChild as HTMLImageElement || new Image();
img.setAttribute('message-id', '' + message.mid);
if(!container.contains(img)) {
container.append(img);
export function wrapVideo(this: AppImManager, doc: MTDocument, container: HTMLDivElement, message: any, justLoader = true, preloader?: ProgressivePreloader, controls = true, round = false, boxWidth = 380, boxHeight = 380, withTail = false, isOut = false) {
let img: HTMLImageElement | SVGImageElement;
let peerID = this.peerID;
if(withTail) {
img = wrapMediaWithTail(doc, message, container, boxWidth, boxHeight, isOut);
} else {
if(!container.firstElementChild || (container.firstElementChild.tagName != 'IMG' && container.firstElementChild.tagName != 'VIDEO')) {
let size = appPhotosManager.setAttachmentSize(doc, container, boxWidth, boxHeight);
}
img = container.firstElementChild as HTMLImageElement || new Image();
if(!container.contains(img)) {
container.append(img);
}
}
//return Promise.resolve();
if(!preloader) {
preloader = new ProgressivePreloader(container, true);
}
let video = document.createElement('video');
if(withTail) {
let foreignObject = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject');
let width = img.getAttributeNS(null, 'width');
let height = img.getAttributeNS(null, 'height');
foreignObject.setAttributeNS(null, 'width', width);
foreignObject.setAttributeNS(null, 'height', height);
video.width = +width;
video.height = +height;
foreignObject.append(video);
img.parentElement.append(foreignObject);
}
let source = document.createElement('source');
video.append(source);
let loadVideo = () => {
let promise = appDocsManager.downloadDoc(doc);
@ -74,37 +91,27 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement, @@ -74,37 +91,27 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement,
preloader.attach(container, true, promise);
return promise.then(blob => {
if((this.peerID ? this.peerID : this.currentMessageID) != peerID) {
if(this.peerID != peerID) {
this.log.warn('peer changed');
return;
}
//return;
///////console.log('loaded doc:', doc, blob, container);
let video = document.createElement('video');
/* video.loop = controls;
video.autoplay = controls;
if(!justLoader) {
video.controls = controls;
} else {
video.volume = 0;
} */
video.setAttribute('message-id', '' + message.mid);
let source = document.createElement('source');
//source.src = doc.url;
source.src = URL.createObjectURL(blob);
source.type = doc.mime_type;
if(img && container.contains(img)) {
container.removeChild(img);
}
source.src = URL.createObjectURL(blob);
video.append(source);
container.append(video);
if(!withTail) {
if(img && container.contains(img)) {
container.removeChild(img);
}
container.append(video);
}
if(!justLoader || round) {
video.dataset.ckin = round ? 'circle' : 'default';
@ -118,15 +125,12 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement, @@ -118,15 +125,12 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement,
video.autoplay = true;
video.loop = true;
}
//container.style.width = '';
//container.style.height = '';
});
};
if(doc.type == 'gif' || true) { // extra fix
return this.peerID ? this.loadMediaQueuePush(loadVideo) : loadVideo();
} else { // if video
return this.loadMediaQueuePush(loadVideo);
} /* else { // if video
let load = () => appPhotosManager.preloadPhoto(doc).then((blob) => {
if((this.peerID ? this.peerID : this.currentMessageID) != peerID) {
this.log.warn('peer changed');
@ -135,14 +139,6 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement, @@ -135,14 +139,6 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement,
img.src = URL.createObjectURL(blob);
/* image.style.height = doc.h + 'px';
image.style.width = doc.w + 'px'; */
/* if(justLoader) { // extra fix
justLoader = false;
controls = false;
} */
if(!justLoader) {
return loadVideo();
} else {
@ -153,7 +149,7 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement, @@ -153,7 +149,7 @@ export function wrapVideo(this: any, doc: MTDocument, container: HTMLDivElement,
});
return this.peerID ? this.loadMediaQueuePush(load) : load();
}
} */
}
export function wrapDocument(doc: MTDocument, withTime = false, uploading = false): HTMLDivElement {
@ -194,7 +190,7 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals @@ -194,7 +190,7 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals
<div class="document-name">${fileName}</div>
<div class="document-size">${size}</div>
`;
if(!uploading) {
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
let preloader: ProgressivePreloader;
@ -289,7 +285,7 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { @@ -289,7 +285,7 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
}
let progress = div.querySelector('.audio-waveform') as HTMLDivElement;
let onClick = () => {
if(!promise) {
if(downloadDiv.classList.contains('downloading')) {
@ -311,7 +307,7 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { @@ -311,7 +307,7 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
let source = document.createElement('source');
source.src = URL.createObjectURL(blob);
source.type = doc.mime_type;
audio.volume = 1;
div.removeEventListener('click', onClick);
@ -346,13 +342,13 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { @@ -346,13 +342,13 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
timeDiv.innerText = String(audio.currentTime | 0).toHHMMSS(true);
lastIndex = Math.round(audio.currentTime / audio.duration * 47);
//svg.children[lastIndex].setAttributeNS(null, 'fill', '#000');
//svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски..
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
//++lastIndex;
//console.log('lastIndex:', lastIndex, audio.currentTime);
//}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */);
//}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */);
}, 20);
} else {
audio.pause();
@ -402,12 +398,12 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { @@ -402,12 +398,12 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
progress.addEventListener('click', (e) => {
if(!audio.paused) scrub(e, audio, progress);
});
function scrub(e: MouseEvent, audio: HTMLAudioElement, progress: HTMLDivElement) {
let scrubTime = e.offsetX / 190 /* width */ * audio.duration;
(Array.from(svg.children) as HTMLElement[]).forEach(node => node.classList.remove('active'));
lastIndex = Math.round(scrubTime / audio.duration * 47);
(Array.from(svg.children) as HTMLElement[]).slice(0,lastIndex+1).forEach(node => node.classList.add('active'));
audio.currentTime = scrubTime;
}
@ -431,22 +427,99 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement { @@ -431,22 +427,99 @@ export function wrapAudio(doc: MTDocument, withTime = false): HTMLDivElement {
return div;
}
export function wrapPhoto(this: AppImManager, photo: any, message: any, container: HTMLDivElement, boxWidth = 380, boxHeight = 380) {
//container.classList.add('photo');
function wrapMediaWithTail(photo: any, message: {mid: number, message: string}, container: HTMLDivElement, boxWidth: number, boxHeight: number, isOut: boolean) {
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.classList.add('bubble__media-container', isOut ? 'is-out' : 'is-in');
let peerID = this.peerID;
let size = appPhotosManager.setAttachmentSize(photo.type ? photo : photo.id, svg, boxWidth, boxHeight);
let image = svg.firstElementChild as SVGImageElement || document.createElementNS("http://www.w3.org/2000/svg", "image");
let width = +svg.getAttributeNS(null, 'width');
let height = +svg.getAttributeNS(null, 'height');
let size = appPhotosManager.setAttachmentSize(photo.id, container, boxWidth, boxHeight);
let image = container.firstElementChild as HTMLImageElement || new Image();
//let size = appPhotosManager.setAttachmentSize(photo.id, image);
image.setAttribute('message-id', message.mid);
//container.style.width = (width - 9) + 'px'; // maybe return
if(!container.contains(image)) {
container.append(image);
let clipID = 'clip' + message.mid;
svg.dataset.clipID = clipID;
//image.setAttributeNS(null, 'clip-path', `url(#${clipID})`);
if(!svg.contains(image)) {
image.setAttributeNS(null, 'width', '' + width);
image.setAttributeNS(null, 'height', '' + height);
svg.append(image);
}
let preloader = new ProgressivePreloader(container, false);
let defs = document.createElementNS("http://www.w3.org/2000/svg", 'defs');
let clipPathHTML: string = '';/* = `
<use href="#message-tail" transform="translate(${width - 2}, ${height}) scale(-1, -1)"></use>
<rect width="${width - 9}" height="${height}" rx="12"></rect>
`; */
//window.getComputedStyle(container).getPropertyValue('border-radius');
if(message.message) {
//clipPathHTML += `<rect width="${width}" height="${height}"></rect>`;
} else {
/* if(isOut) {
clipPathHTML += `
<use href="#message-tail" transform="translate(${width - 2}, ${height}) scale(-1, -1)"></use>
<path d="${generatePathData(0, 0, width - 9, height, 6, 0, 12, 12)}" />
`;
} else {
clipPathHTML += `
<use href="#message-tail" transform="translate(0, ${height}) scale(1, -1)"></use>
<path d="${generatePathData(0, 0, width - 9, height, 12, 12, 0, 6)}" />
`;
} */
if(isOut) {
clipPathHTML += `
<use href="#message-tail" transform="translate(${width - 2}, ${height}) scale(-1, -1)"></use>
<path />
`;
} else {
clipPathHTML += `
<use href="#message-tail" transform="translate(2, ${height}) scale(1, -1)"></use>
<path />
`;
}
}
/* if(isOut) { // top-right, bottom-right
clipPathHTML += `
<rect class="br-tr" width="12" height="12" x="${width - 9 - 12}" y="0"></rect>
<rect class="br-br" width="12" height="12" x="${width - 9 - 12}" y="${height - 12}"></rect>
`
} else { // top-left, bottom-left
clipPathHTML += `
<rect class="br-tl" width="12" height="12" x="0" y="0"></rect>
<rect class="br-bl" width="12" height="12" x="0" y="${height - 12}"></rect>
`;
} */
defs.innerHTML = `<clipPath id="${clipID}">${clipPathHTML}</clipPath>`;
svg.prepend(defs);
container.appendChild(svg);
return image;
}
export function wrapPhoto(this: AppImManager, photo: any, message: any, container: HTMLDivElement, boxWidth = 380, boxHeight = 380, withTail = true, isOut = false) {
let peerID = this.peerID;
let size: MTPhotoSize;
let image: SVGImageElement | HTMLImageElement;
if(withTail) {
image = wrapMediaWithTail(photo, message, container, boxWidth, boxHeight, isOut);
} else {
size = appPhotosManager.setAttachmentSize(photo.id, container, boxWidth, boxHeight);
image = container.firstElementChild as HTMLImageElement || new Image();
if(!container.contains(image)) {
container.appendChild(image);
}
}
let preloader = new ProgressivePreloader(container, false);
let load = () => {
let promise = appPhotosManager.preloadPhoto(photo.id, size);
@ -458,12 +531,11 @@ export function wrapPhoto(this: AppImManager, photo: any, message: any, containe @@ -458,12 +531,11 @@ export function wrapPhoto(this: AppImManager, photo: any, message: any, containe
return;
}
image.src = URL.createObjectURL(blob);
//image.style.width = '';
//image.style.height = '';
//container.style.width = '';
//container.style.height = '';
if(withTail) {
image.setAttributeNS(null, 'href', URL.createObjectURL(blob));
} else if(image instanceof Image) {
image.src = URL.createObjectURL(blob);
}
});
};
@ -483,21 +555,21 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( @@ -483,21 +555,21 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: (
if(doc.thumbs && !div.firstElementChild) {
let thumb = doc.thumbs[0];
///////console.log('wrap sticker', thumb);
if(thumb.bytes) {
apiFileManager.saveSmallFile(thumb.location, thumb.bytes);
appPhotosManager.setAttachmentPreview(thumb.bytes, div, true);
if(onlyThumb) return Promise.resolve();
}
}
if(onlyThumb && doc.thumbs) {
let thumb = doc.thumbs[0];
let load = () => apiFileManager.downloadSmallFile({
_: 'inputDocumentFileLocation',
access_hash: doc.access_hash,
@ -508,13 +580,13 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: ( @@ -508,13 +580,13 @@ export function wrapSticker(doc: MTDocument, div: HTMLDivElement, middleware?: (
let img = new Image();
appWebpManager.polyfillImage(img, blob);
div.append(img);
div.setAttribute('file-id', doc.id);
appStickersManager.saveSticker(doc);
});
return lazyLoadQueue ? (lazyLoadQueue.push({div, load}), Promise.resolve()) : load();
}
@ -635,11 +707,11 @@ export function wrapReply(title: string, subtitle: string, media?: any) { @@ -635,11 +707,11 @@ export function wrapReply(title: string, subtitle: string, media?: any) {
} else {
replySubtitle.innerHTML = media._;
}
if(media.photo || (media.document && ['video'].indexOf(media.document.type) !== -1)) {
let replyMedia = document.createElement('div');
replyMedia.classList.add('reply-media');
let photo = media.photo || media.document;
let sizes = photo.sizes || photo.thumbs;
@ -651,7 +723,7 @@ export function wrapReply(title: string, subtitle: string, media?: any) { @@ -651,7 +723,7 @@ export function wrapReply(title: string, subtitle: string, media?: any) {
.then((blob) => {
replyMedia.style.backgroundImage = 'url(' + URL.createObjectURL(blob) + ')';
});
replyContent.append(replyMedia);
div.classList.add('is-reply-media');
}

73
src/lib/appManagers/appDialogsManager.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import apiManager from "../mtproto/apiManager";
import apiFileManager from '../mtproto/apiFileManager';
import { $rootScope, findUpTag, langPack } from "../utils";
import { $rootScope, findUpTag, langPack, findUpClassName } from "../utils";
import appImManager from "./appImManager";
import appPeersManager from './appPeersManager';
import appMessagesManager from "./appMessagesManager";
@ -27,7 +27,9 @@ export class AppDialogsManager { @@ -27,7 +27,9 @@ export class AppDialogsManager {
public chatListArchived = document.getElementById('dialogs-archived') as HTMLUListElement;
public pinnedDelimiter: HTMLDivElement;
public chatsHidden: Scrollable["hiddenElements"];
public chatsVisible: Scrollable["visibleElements"];
public chatsArchivedHidden: Scrollable["hiddenElements"];
public chatsArchivedVisible: Scrollable["visibleElements"];
public myID = 0;
public doms: {[peerID: number]: DialogDom} = {};
@ -59,15 +61,17 @@ export class AppDialogsManager { @@ -59,15 +61,17 @@ export class AppDialogsManager {
public setListClickListener(list: HTMLUListElement, onFound?: () => void) {
list.addEventListener('click', (e: Event) => {
let target = e.target as HTMLElement;
let elem = target.tagName != 'LI' ? findUpTag(target, 'LI') : target;
let elem = target.classList.contains('rp') ? target : findUpClassName(target, 'rp');
if(!elem) {
return;
}
/* if(this.lastActiveListElement) {
elem = elem.parentElement;
if(this.lastActiveListElement) {
this.lastActiveListElement.classList.remove('active');
} */
}
if(elem) {
/* if(chatClosedDiv) {
@ -105,11 +109,23 @@ export class AppDialogsManager { @@ -105,11 +109,23 @@ export class AppDialogsManager {
}
div.style.backgroundColor = '';
div.style.fontSize = '';
div.classList.add('tgico-savedmessages');
return true;
}
if(peerID) {
let user = appUsersManager.getUser(peerID);
if(user && user.pFlags && user.pFlags.deleted) {
if(div.firstChild) {
div.firstChild.remove();
}
div.style.backgroundColor = '';
div.classList.add('tgico-avatar_deletedaccount');
return true;
}
}
//if(!location || location.empty || !location.photo_small) {
if(div.firstChild) {
div.firstChild.remove();
@ -120,8 +136,7 @@ export class AppDialogsManager { @@ -120,8 +136,7 @@ export class AppDialogsManager {
color = appPeersManager.getPeerColorByID(peerID);
}
div.classList.remove('tgico-savedmessages');
div.style.fontSize = '';
div.classList.remove('tgico-savedmessages', 'tgico-avatar_deletedaccount');
div.style.backgroundColor = color;
let abbrSplitted = (!title && peerID ? appPeersManager.getPeerTitle(peerID, true) : title).split(' ');
@ -154,19 +169,22 @@ export class AppDialogsManager { @@ -154,19 +169,22 @@ export class AppDialogsManager {
let img = new Image();
img.src = this.savedAvatarURLs[peerID];
div.innerHTML = '';
div.style.fontSize = '0'; // need
//div.style.fontSize = '0'; // need
//div.style.backgroundColor = '';
div.append(img);
return true;
}
public sortDom(archived = false) {
// return;
//return;
//if(archived) return;
let dialogs = appMessagesManager.dialogsStorage.dialogs.slice();
let inUpper: {element: HTMLLIElement, height: number}[] = [];
let inBottom: {element: HTMLLIElement, height: number}[] = [];
let inUpper: Scrollable['hiddenElements']['up'] = [];
let inBottom: Scrollable['hiddenElements']['down'] = [];
let inVisible: Scrollable['visibleElements'] = [];
let pinnedDialogs = [];
@ -210,10 +228,11 @@ export class AppDialogsManager { @@ -210,10 +228,11 @@ export class AppDialogsManager {
let chatList = archived ? this.chatListArchived : this.chatList;
let chatsHidden = archived ? this.chatsArchivedHidden : this.chatsHidden;
let chatsVisible = archived ? this.chatsArchivedVisible : this.chatsVisible;
let hiddenLength: number = chatsHidden.up.length;
let inViewportLength = chatList.childElementCount;
let hiddenConcated = chatsHidden.up.concat(chatsHidden.down);
let concated = chatsHidden.up.concat(chatsVisible, chatsHidden.down);
//console.log('sortDom clearing innerHTML', archived, hiddenLength, inViewportLength);
@ -224,21 +243,27 @@ export class AppDialogsManager { @@ -224,21 +243,27 @@ export class AppDialogsManager {
let dom = this.getDialogDom(d.peerID);
if(!dom) return;
let child = concated.find(obj => obj.element == dom.listEl);
if(!child) {
return console.error('no child by listEl:', dom.listEl, archived, concated);
}
if(inUpper.length < hiddenLength) {
let child = hiddenConcated.find(obj => obj.element == dom.listEl);
inUpper.push({element: dom.listEl, height: child ? child.height : 0});
inUpper.push(child);
} else if(inViewportIndex <= inViewportLength - 1) {
chatList.append(dom.listEl);
inVisible.push(child);
++inViewportIndex;
} else {
let child = hiddenConcated.find(obj => obj.element == dom.listEl);
inBottom.push({element: dom.listEl, height: child ? child.height : 0});
inBottom.push(child);
}
});
//console.log('sortDom', sorted.length, inUpper.length, chatList.childElementCount, inBottom.length);
chatsHidden.up = inUpper;
chatsVisible.length = 0;
chatsVisible.push(...inVisible);
chatsHidden.down = inBottom;
}
@ -368,7 +393,7 @@ export class AppDialogsManager { @@ -368,7 +393,7 @@ export class AppDialogsManager {
dom.listEl.setAttribute('data-mid', lastMessage.mid);
if(this.doms[peerID]) {
if(this.doms[peerID] || this.domsArchived[peerID]) {
this.setUnreadMessages(dialog);
}
}
@ -490,15 +515,6 @@ export class AppDialogsManager { @@ -490,15 +515,6 @@ export class AppDialogsManager {
let li = document.createElement('li');
li.append(paddingDiv);
li.setAttribute('data-peerID', '' + peerID);
/* let li = document.createElement('li');
li.classList.add('rp');
li.append(avatarDiv, captionDiv);
li.setAttribute('data-peerID', '' + peerID);
ripple(li); */
/* let detailsDiv = document.createElement('div');
detailsDiv.classList.add('dialog-details'); */
let statusSpan = document.createElement('span');
statusSpan.classList.add('message-status');
@ -533,15 +549,12 @@ export class AppDialogsManager { @@ -533,15 +549,12 @@ export class AppDialogsManager {
if(!container) {
if(dialog.folder_id && dialog.folder_id == 1) {
this.chatListArchived.append(li);
appSidebarLeft.scrollArchived.append(li);
this.domsArchived[dialog.peerID] = dom;
} else {
//this.chatList.append(li);
appSidebarLeft.scroll.append(li);
this.doms[dialog.peerID] = dom;
}
//this.appendTo.push(li);
if(dialog.pFlags.pinned) {
li.classList.add('dialog-pinned');

3
src/lib/appManagers/appDocsManager.ts

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import apiFileManager, { CancellablePromise } from '../mtproto/apiFileManager';
import apiFileManager from '../mtproto/apiFileManager';
import FileManager from '../filemanager';
import {RichTextProcessor} from '../richtextprocessor';
import { CancellablePromise } from '../polyfill';
//import { MTDocument } from '../../components/misc';
class AppDocsManager {

344
src/lib/appManagers/appImManager.ts

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import apiManager from '../mtproto/apiManager';
import { $rootScope, isElementInViewport, numberWithCommas, findUpClassName, formatNumber, placeCaretAtEnd, calcImageInBox, findUpTag, langPack } from "../utils";
import { $rootScope, isElementInViewport, numberWithCommas, findUpClassName, formatNumber, placeCaretAtEnd, calcImageInBox, findUpTag, langPack, generatePathData } from "../utils";
import appUsersManager from "./appUsersManager";
import appMessagesManager from "./appMessagesManager";
import appPeersManager from "./appPeersManager";
@ -32,17 +32,18 @@ class ScrollPosition { @@ -32,17 +32,18 @@ class ScrollPosition {
container: HTMLElement;
rAF: number;
debug = true;
prepared = false;
constructor(node: HTMLElement) {
this.container = node.parentElement;
}
restore() {
restore(callback?: () => void) {
let setScrollTop = this.container.scrollHeight - this.previousScrollHeightMinusTop;
if(this.debug) appImManager.log('scrollPosition restore', this.readyFor, this.container.scrollHeight,
setScrollTop, this.container, this.container.parentElement.classList.contains('scrolled-down'));
if(this.readyFor === 'up'/* || this.container.parentElement.classList.contains('scrolled-down') */) {
if(this.readyFor === 'up' || this.container.parentElement.classList.contains('scrolled-down')) {
if(this.debug) appImManager.log('scrollPosition restore 2', this.readyFor, this.container.scrollHeight,
setScrollTop, this.container);
@ -50,13 +51,17 @@ class ScrollPosition { @@ -50,13 +51,17 @@ class ScrollPosition {
this.rAF = window.requestAnimationFrame(() => {
this.container.scrollTop = this.container.scrollHeight - this.previousScrollHeightMinusTop;
this.rAF = 0;
this.prepared = false;
callback && callback();
});
} else if(this.container.parentElement.classList.contains('scrolled-down')) {
}/* else if(this.container.parentElement.classList.contains('scrolled-down')) {
if(this.debug) appImManager.log('scrollPosition restore 2', this.readyFor, this.container.scrollHeight,
setScrollTop, this.container);
this.container.scrollTop = setScrollTop;
}
this.prepared = false;
} */
// 'down' doesn't need to be special cased unless the
// content was flowing upwards, which would only happen
@ -65,6 +70,8 @@ class ScrollPosition { @@ -65,6 +70,8 @@ class ScrollPosition {
}
prepareFor(direction = 'up') {
//if(this.prepared) return;
if(this.rAF) {
window.cancelAnimationFrame(this.rAF);
this.rAF = 0;
@ -81,7 +88,7 @@ class ScrollPosition { @@ -81,7 +88,7 @@ class ScrollPosition {
//let scrollTop = this.container.scrollTop;
//this.previousScrollHeightMinusTop = scrollTop > 0 || this.readyFor == 'up' ? this.container.scrollHeight - this.container.scrollTop : 0;
if(this.debug) appImManager.log.trace('scrollPosition prepareFor', direction, this.container.scrollHeight,
if(this.debug) appImManager.log.warn('scrollPosition prepareFor', direction, this.container.scrollHeight,
this.container.scrollTop, this.previousScrollHeightMinusTop);
}
}
@ -135,17 +142,63 @@ class BubbleGroups { @@ -135,17 +142,63 @@ class BubbleGroups {
//console.log('addBubble', bubble, message.mid, fromID, reverse, group);
this.bubblesByGroups[reverse ? 'unshift' : 'push']({timestamp, fromID, mid: message.mid, group});
this.updateGroup(group);
this.updateGroup(group, reverse);
}
setClipIfNeeded(bubble: HTMLDivElement, remove = false) {
if(bubble.classList.contains('is-message-empty')/* && !bubble.classList.contains('is-reply') */
&& (bubble.classList.contains('photo') || bubble.classList.contains('video'))) {
let container = bubble.querySelector('.bubble__media-container') as SVGSVGElement;
if(!container) return;
Array.from(container.children).forEach(object => {
if(object instanceof SVGDefsElement) return;
if(remove) {
object.removeAttributeNS(null, 'clip-path');
} else {
let clipID = container.dataset.clipID;
let path = container.firstElementChild.firstElementChild.lastElementChild as SVGPathElement;
let width = +object.getAttributeNS(null, 'width');
let height = +object.getAttributeNS(null, 'height');
let isOut = bubble.classList.contains('is-out');
let isReply = bubble.classList.contains('is-reply');
let d = '';
console.log('setClipIfNeeded', object, width, height, isOut);
let tr: number, tl: number;
if(bubble.classList.contains('forwarded') || isReply) {
tr = tl = 0;
} else if(isOut) {
tr = bubble.classList.contains('is-group-first') ? 12 : 6;
tl = 12;
} else {
tr = 12;
tl = bubble.classList.contains('is-group-first') ? 12 : 6;
}
if(isOut) {
d = generatePathData(0, 0, width - 9, height, tl, tr, 0, 12);
} else {
d = generatePathData(9, 0, width - 9, height, tl, tr, 12, 0);
}
path.setAttributeNS(null, 'd', d);
object.setAttributeNS(null, 'clip-path', 'url(#' + clipID + ')');
}
});
}
}
updateGroup(group: HTMLDivElement[]) {
updateGroup(group: HTMLDivElement[], reverse = false) {
if(this.updateRAFs.has(group)) {
window.cancelAnimationFrame(this.updateRAFs.get(group));
this.updateRAFs.delete(group);
}
this.updateRAFs.set(group, window.requestAnimationFrame(() => {
this.updateRAFs.delete(group);
//this.updateRAFs.set(group, window.requestAnimationFrame(() => {
//this.updateRAFs.delete(group);
if(!group.length) {
return;
@ -153,26 +206,34 @@ class BubbleGroups { @@ -153,26 +206,34 @@ class BubbleGroups {
let first = group[0];
//appImManager.scrollPosition.prepareFor(reverse ? 'up' : 'down');
//console.log('updateGroup', group, first);
if(group.length == 1) {
first.classList.add('is-group-first', 'is-group-last');
this.setClipIfNeeded(first);
return;
} else {
first.classList.remove('is-group-last');
first.classList.add('is-group-first');
this.setClipIfNeeded(first, true);
}
let length = group.length - 1;
for(let i = 1; i < length; ++i) {
let bubble = group[i];
bubble.classList.remove('is-group-last', 'is-group-first');
this.setClipIfNeeded(bubble, true);
}
let last = group[group.length - 1];
last.classList.remove('is-group-first');
last.classList.add('is-group-last');
}));
this.setClipIfNeeded(last);
//appImManager.scrollPosition.restore();
//}));
}
updateGroupByMessageID(mid: number) {
@ -202,8 +263,8 @@ export class AppImManager { @@ -202,8 +263,8 @@ export class AppImManager {
public chatInner = document.getElementById('bubbles-inner') as HTMLDivElement;
public searchBtn = this.pageEl.querySelector('.chat-search-button') as HTMLButtonElement;
public goDownBtn = this.pageEl.querySelector('#bubbles-go-down') as HTMLButtonElement;
private getHistoryPromise: Promise<boolean>;
private getHistoryTimeout = 0;
private getHistoryTopPromise: Promise<boolean>;
private getHistoryBottomPromise: Promise<boolean>;
private chatInputC: ChatInput = null;
@ -212,7 +273,12 @@ export class AppImManager { @@ -212,7 +273,12 @@ export class AppImManager {
public muted = false;
public bubbles: {[mid: number]: HTMLDivElement} = {};
public dateMessages: {[timestamp: number]: { div: HTMLDivElement, firstTimestamp: number }} = {};
public dateMessages: {[timestamp: number]: {
div: HTMLDivElement,
firstTimestamp: number,
bubble: HTMLDivElement,
timeout?: number
}} = {};
public unreaded: number[] = [];
public unreadOut: number[] = [];
public needUpdate: {replyMid: number, mid: number}[] = []; // if need wrapSingleMessage
@ -227,7 +293,6 @@ export class AppImManager { @@ -227,7 +293,6 @@ export class AppImManager {
private firstTopMsgID = 0;
public loadMediaQueue: Array<() => Promise<void>> = [];
private loadMediaQueuePromise: Promise<void[]> = null;
private loadingMedia = 0;
public scroll: HTMLDivElement = null;
@ -348,11 +413,6 @@ export class AppImManager { @@ -348,11 +413,6 @@ export class AppImManager {
/////this.log('message_sent', bubble);
let media = bubble.querySelector('img, video');
if(media) {
media.setAttribute('message-id', mid);
}
bubble.classList.remove('is-sending');
bubble.classList.add('is-sent');
@ -380,7 +440,7 @@ export class AppImManager { @@ -380,7 +440,7 @@ export class AppImManager {
if(!bubble) return;
let message = appMessagesManager.getMessage(mid);
this.renderMessage(message, false, false, bubble, false);
this.renderMessage(message, true, false, bubble, false);
});
$rootScope.$on('messages_downloaded', (e: CustomEvent) => {
@ -410,7 +470,8 @@ export class AppImManager { @@ -410,7 +470,8 @@ export class AppImManager {
delete message.reply_to_mid; // WARNING!
}
this.renderMessage(message, false, false, bubble, false);
this.renderMessage(message, true, false, bubble, false);
//this.renderMessage(message, true, true, bubble, false);
}
});
});
@ -451,7 +512,7 @@ export class AppImManager { @@ -451,7 +512,7 @@ export class AppImManager {
if(!bubble) return;
if(['IMG', 'VIDEO', 'SVG', 'DIV'].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV');
if(['IMG', 'VIDEO', 'SVG', 'DIV', 'image'].indexOf(target.tagName) === -1) target = findUpTag(target, 'DIV');
if(target.tagName == 'DIV') {
if(target.classList.contains('forward')) {
@ -490,31 +551,39 @@ export class AppImManager { @@ -490,31 +551,39 @@ export class AppImManager {
if(!isNaN(peerID)) {
this.setPeer(peerID);
}
} else if((target.tagName == 'IMG' && !target.classList.contains('emoji')) || target.tagName == 'VIDEO') {
let messageID = +target.getAttribute('message-id');
} else if(((target.tagName == 'IMG' || target.tagName == 'image') && !target.classList.contains('emoji')) || target.tagName == 'VIDEO') {
let messageID = 0;
for(let mid in this.bubbles) {
if(this.bubbles[mid] == bubble) {
messageID = +mid;
break;
}
}
let message = appMessagesManager.getMessage(messageID);
if(!message) {
this.log.warn('no message by messageID:', messageID);
return;
}
let ids = Object.keys(this.bubbles).map(k => +k).filter(id => {
if(!this.scrollable.visibleElements.find(e => e.element == this.bubbles[id])) return false;
let message = appMessagesManager.getMessage(id);
return message.media && (message.media.photo || (message.media.document && (message.media.document.type == 'video' || message.media.document.type == 'gif')) || (message.media.webpage && (message.media.webpage.document || message.media.webpage.photo)));
}).sort();
let idx = ids.findIndex(i => i == messageID);
let prev = ids[idx + 1] || null;
let next = ids[idx - 1] || null;
let prevTarget = this.bubbles[prev] ? this.bubbles[prev].querySelector('img, video') as HTMLElement : null;
let nextTarget = this.bubbles[next] ? this.bubbles[next].querySelector('img, video') as HTMLElement : null;
let targets = ids.map(id => ({
//element: (this.bubbles[id].querySelector('img, video') || this.bubbles[id].querySelector('image')) as HTMLElement,
element: this.bubbles[id].querySelector('img, video, .bubble__media-container') as HTMLElement,
mid: id
}));
/////this.log('ids', ids, idx, this.bubbles[prev], this.bubbles[next]);
appMediaViewer.openMedia(message, target, nextTarget, prevTarget);
appMediaViewer.openMedia(message, targets[idx].element, true,
this.scroll.parentElement, targets.slice(0, idx), targets.slice(idx + 1));
//appMediaViewer.openMedia(message, target as HTMLImageElement);
}
@ -578,10 +647,12 @@ export class AppImManager { @@ -578,10 +647,12 @@ export class AppImManager {
let bubble: HTMLDivElement = null;
try {
bubble = findUpClassName(e.target, 'bubble');
bubble = findUpClassName(e.target, 'bubble__container');
} catch(e) {}
if(bubble) {
bubble = bubble.parentElement as HTMLDivElement; // bc container
e.preventDefault();
e.cancelBubble = true;
@ -601,7 +672,7 @@ export class AppImManager { @@ -601,7 +672,7 @@ export class AppImManager {
this.contextMenuMsgID = msgID;
let side = bubble.parentElement.classList.contains('in') ? 'left' : 'right';
let side = bubble.classList.contains('is-in') ? 'left' : 'right';
this.contextMenuEdit.style.display = side == 'right' ? '' : 'none';
@ -815,54 +886,52 @@ export class AppImManager { @@ -815,54 +886,52 @@ export class AppImManager {
}
public loadMoreHistory(top: boolean) {
// load more history
// возможно нужно добавить разные таймауты для верха и низа
if(!this.getHistoryPromise && !this.getHistoryTimeout && this.peerID && !testScroll) {
this.getHistoryTimeout = setTimeout(() => { // must be
let history = Object.keys(this.bubbles).map(id => +id).sort();
/* let history = appMessagesManager.historiesStorage[this.peerID].history;
let length = history.length; */
// filter negative ids
let lastBadIndex = -1;
for(let i = 0; i < history.length; ++i) {
if(history[i] <= 0) lastBadIndex = i;
else break;
}
if(lastBadIndex != -1) {
history = history.slice(lastBadIndex + 1);
}
this.getHistoryTimeout = 0;
if(!this.scrolledAll && top) {
this.log('Will load more (up) history by id:', history[0], 'maxID:', history[history.length - 1], history);
/* false && */!testScroll && this.getHistory(history[0], true).then(() => { // uncomment
this.onScroll();
}).catch(err => {
this.log.warn('Could not load more history, err:', err);
});
}
if(this.scrolledAllDown) return;
let dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0];
/* if(!dialog) {
this.log.warn('no dialog for load history');
return;
} */
// if scroll down after search
if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)) {
this.log('Will load more (down) history by maxID:', history[history.length - 1], history);
/* false && */!testScroll && this.getHistory(history[history.length - 1], false, true).then(() => { // uncomment
this.onScroll();
}).catch(err => {
this.log.warn('Could not load more history, err:', err);
});
}
}, 0);
this.log('loadMoreHistory', top);
if(!this.peerID || testScroll || (top && this.getHistoryTopPromise) || (!top && this.getHistoryBottomPromise)) return;
let history = Object.keys(this.bubbles).map(id => +id).sort();
if(!history.length) return;
/* let history = appMessagesManager.historiesStorage[this.peerID].history;
let length = history.length; */
// filter negative ids
let lastBadIndex = -1;
for(let i = 0; i < history.length; ++i) {
if(history[i] <= 0) lastBadIndex = i;
else break;
}
if(lastBadIndex != -1) {
history = history.slice(lastBadIndex + 1);
}
if(top && !this.scrolledAll) {
this.scrollable.lock('both');
this.log('Will load more (up) history by id:', history[0], 'maxID:', history[history.length - 1], history);
/* false && */this.getHistory(history[0], true).then(() => {
this.onScroll();
}).catch(err => {
this.log.warn('Could not load more history, err:', err);
});
}
if(this.scrolledAllDown) return;
let dialog = appMessagesManager.getDialogByPeerID(this.peerID)[0];
/* if(!dialog) {
this.log.warn('no dialog for load history');
return;
} */
// if scroll down after search
if(!top && (!dialog || history.indexOf(dialog.top_message) === -1)) {
this.scrollable.lock('both');
this.log('Will load more (down) history by maxID:', history[history.length - 1], history);
/* false && */this.getHistory(history[history.length - 1], false, true).then(() => {
this.onScroll();
}).catch(err => {
this.log.warn('Could not load more history, err:', err);
});
}
}
@ -904,7 +973,7 @@ export class AppImManager { @@ -904,7 +973,7 @@ export class AppImManager {
}
public setScroll() {
this.scrollable = new Scrollable(this.bubblesContainer, false, true, 750, 'IM', this.chatInner/* 1500 */, 450);
this.scrollable = new Scrollable(this.bubblesContainer, 'y', 750, 'IM', this.chatInner/* 1500 */, 300);
this.scroll = this.scrollable.container;
this.bubblesContainer.append(this.goDownBtn);
@ -1036,8 +1105,6 @@ export class AppImManager { @@ -1036,8 +1105,6 @@ export class AppImManager {
this.chatInner.innerHTML = '';
this.scrollable.setVirtualContainer(this.chatInner);
//appSidebarRight.minMediaID = {};
}
public setPeer(peerID: number, lastMsgID = 0, forwarding = false) {
@ -1099,18 +1166,15 @@ export class AppImManager { @@ -1099,18 +1166,15 @@ export class AppImManager {
if(!samePeer && appDialogsManager.lastActiveListElement) {
appDialogsManager.lastActiveListElement.classList.remove('active');
}
let dom = appDialogsManager.getDialogDom(this.peerID);
if(dom) {
appDialogsManager.lastActiveListElement = dom.listEl;
dom.listEl.classList.add('active');
}
this.firstTopMsgID = dialog ? dialog.top_message : 0;
/* let dom = appDialogsManager.getDialogDom(this.peerID);
if(!dom) {
this.log.warn('No rendered dialog by peerID:', this.peerID);
appDialogsManager.addDialog(dialog);
dom = appDialogsManager.getDialogDom(this.peerID);
}
// warning need check
dom.listEl.classList.add('active'); */
this.setPeerStatus();
let title = '';
@ -1136,14 +1200,14 @@ export class AppImManager { @@ -1136,14 +1200,14 @@ export class AppImManager {
//this.scroll.scrollTop = this.scroll.scrollHeight;
return this.setPeerPromise = Promise.all([
this.getHistory(forwarding ? lastMsgID + 1 : lastMsgID).then(() => {
this.getHistory(forwarding ? lastMsgID + 1 : lastMsgID, true).then(() => {
////this.log('setPeer removing preloader');
if(lastMsgID) {
if(!forwarding) {
let message = appMessagesManager.getMessage(lastMsgID);
//////this.log('setPeer render last message:', message, lastMsgID);
this.renderMessage(message);
this.renderMessage(message, false, true);
}
if(!dialog || lastMsgID != dialog.top_message) {
@ -1260,6 +1324,7 @@ export class AppImManager { @@ -1260,6 +1324,7 @@ export class AppImManager {
});
}
// reverse means top, will save scrollPosition if bubble will be higher
public renderMessage(message: any, reverse = false, multipleRender?: boolean, bubble: HTMLDivElement = null, updatePosition = true) {
/////this.log('message to render:', message);
if(message.deleted) return;
@ -1357,8 +1422,7 @@ export class AppImManager { @@ -1357,8 +1422,7 @@ export class AppImManager {
attachmentDiv.innerHTML = richText;
messageDiv.classList.add('message-empty');
bubble.classList.add('emoji-' + emojiEntities.length + 'x', 'emoji-big');
bubble.classList.add('is-message-empty', 'emoji-' + emojiEntities.length + 'x', 'emoji-big');
bubbleContainer.append(attachmentDiv);
} else {
@ -1397,7 +1461,7 @@ export class AppImManager { @@ -1397,7 +1461,7 @@ export class AppImManager {
attachmentDiv.classList.add('attachment');
if(!message.message) {
messageDiv.classList.add('message-empty');
bubble.classList.add('is-message-empty');
}
let processingWebPage = false;
@ -1432,7 +1496,7 @@ export class AppImManager { @@ -1432,7 +1496,7 @@ export class AppImManager {
let icoDiv = docDiv.querySelector('.document-ico');
preloader.attach(icoDiv, false);
messageDiv.classList.remove('message-empty');
bubble.classList.remove('is-message-empty');
messageDiv.append(docDiv);
processingWebPage = true;
break;
@ -1449,7 +1513,7 @@ export class AppImManager { @@ -1449,7 +1513,7 @@ export class AppImManager {
bubble.classList.add('hide-name', 'photo');
wrapPhoto.call(this, photo, message, attachmentDiv);
wrapPhoto.call(this, photo, message, attachmentDiv, undefined, undefined, true, our);
break;
}
@ -1492,7 +1556,7 @@ export class AppImManager { @@ -1492,7 +1556,7 @@ export class AppImManager {
if(doc.type == 'gif' || doc.type == 'video') {
//if(doc.size <= 20e6) {
bubble.classList.add('video');
wrapVideo.call(this, doc, preview, message);
wrapVideo.call(this, doc, preview, message, true, null, false, false, 380, 300);
//}
} else {
doc = null;
@ -1503,7 +1567,7 @@ export class AppImManager { @@ -1503,7 +1567,7 @@ export class AppImManager {
bubble.classList.add('photo');
//appPhotosManager.savePhoto(webpage.photo); // hot-fix because no webpage manager
wrapPhoto.call(this, webpage.photo, message, preview, 380, 300);
wrapPhoto.call(this, webpage.photo, message, preview, 380, 300, false);
}
if(preview) {
@ -1576,14 +1640,14 @@ export class AppImManager { @@ -1576,14 +1640,14 @@ export class AppImManager {
bubble.classList.add('round');
}
bubble.classList.add('video');
wrapVideo.call(this, doc, attachmentDiv, message, true, null, false, doc.type == 'round');
bubble.classList.add('hide-name', 'video');
wrapVideo.call(this, doc, attachmentDiv, message, true, null, false, doc.type == 'round', 380, 380, doc.type != 'round', our);
break;
} else if(doc.mime_type == 'audio/ogg') {
let docDiv = wrapDocument(doc);
messageDiv.classList.remove('message-empty');
bubble.classList.remove('is-message-empty');
bubble.classList.add('bubble-audio');
messageDiv.append(docDiv);
@ -1593,7 +1657,7 @@ export class AppImManager { @@ -1593,7 +1657,7 @@ export class AppImManager {
} else {
let docDiv = wrapDocument(doc);
messageDiv.classList.remove('message-empty');
bubble.classList.remove('is-message-empty');
messageDiv.append(docDiv);
processingWebPage = true;
@ -1602,7 +1666,7 @@ export class AppImManager { @@ -1602,7 +1666,7 @@ export class AppImManager {
}
default:
messageDiv.classList.remove('message-empty');
bubble.classList.remove('is-message-empty');
messageDiv.innerHTML = 'unrecognized media type: ' + message.media._;
messageDiv.append(timeSpan);
this.log.warn('unrecognized media type:', message.media._, message);
@ -1705,18 +1769,20 @@ export class AppImManager { @@ -1705,18 +1769,20 @@ export class AppImManager {
bubble.classList.add('hide-name');
}
bubble.dataset.mid = message.mid;
bubble.classList.add(our ? 'is-out' : 'is-in');
if(updatePosition) {
this.bubbleGroups.addBubble(bubble, message, reverse);
if(reverse) {
this.scrollable.prepend(bubble);
} else {
this.scrollable.append(bubble);
}
this.bubbleGroups.addBubble(bubble, message, reverse);
let justDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
let dateTimestamp = justDate.getTime();
let needUpdatePos = false;
if(!(dateTimestamp in this.dateMessages)) {
let str = '';
@ -1740,17 +1806,28 @@ export class AppImManager { @@ -1740,17 +1806,28 @@ export class AppImManager {
////////this.log('need to render date message', dateTimestamp, str);
this.dateMessages[dateTimestamp] = {
div,
div,
bubble,
firstTimestamp: date.getTime()
};
this.scrollable.insertBefore(div, bubble);
needUpdatePos = true;
} else {
let dateMessage = this.dateMessages[dateTimestamp];
if(dateMessage.firstTimestamp > date.getTime()) {
this.scrollable.insertBefore(dateMessage.div, bubble);
dateMessage.bubble = bubble;
needUpdatePos = true;
}
}
let dateMessage = this.dateMessages[dateTimestamp];
if(needUpdatePos && !dateMessage.timeout) {
dateMessage.timeout = setTimeout(() => {
delete dateMessage.timeout;
this.scrollable.insertBefore(dateMessage.div, dateMessage.bubble);
}, 0);
}
} else {
this.bubbleGroups.updateGroupByMessageID(message.mid);
}
@ -1792,10 +1869,12 @@ export class AppImManager { @@ -1792,10 +1869,12 @@ export class AppImManager {
loadCount = 0;
maxID += 1;
}
return this.getHistoryPromise = appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit)
.then((result: any) => {
let promise = appMessagesManager.getHistory(this.peerID, maxID, loadCount, backLimit)
.then((result) => {
this.log('getHistory result by maxID:', maxID, reverse, isBackLimit, result);
(reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined);
//console.timeEnd('render getHistory');
@ -1821,24 +1900,26 @@ export class AppImManager { @@ -1821,24 +1900,26 @@ export class AppImManager {
//this.chatInner.innerHTML = '';
let history = result.history.slice();
if(reverse) history.reverse();
//let method = reverse ? result.history.forEach : result.history.forEachReverse;
let method = reverse ? Array.prototype.forEach : Array.prototype.forEachReverse;
method = method.bind(result.history);
//console.time('render history');
this.log('getHistory method', method);
if(!isBackLimit) {
this.scrollPosition.prepareFor(reverse ? 'up' : 'down');
}
if(testScroll) {
for(let i = 0; i < 25; ++i) history.forEachReverse((msgID: number) => {
for(let i = 0; i < 25; ++i) method((msgID) => {
let message = appMessagesManager.getMessage(msgID);
this.renderMessage(message, reverse, true);
});
} else {
history.forEachReverse((msgID: number) => {
method((msgID) => {
let message = appMessagesManager.getMessage(msgID);
this.renderMessage(message, reverse, true);
@ -1846,17 +1927,28 @@ export class AppImManager { @@ -1846,17 +1927,28 @@ export class AppImManager {
}
if(!isBackLimit) {
this.scrollPosition.restore();
this.scrollPosition.restore(() => {
this.scrollable.unlock('both');
});
} else {
this.scrollable.unlock('both');
}
//console.timeEnd('render history');
this.getHistoryPromise = undefined;
//console.timeEnd('render history total');
return true;
});
}, () => {
(reverse ? this.getHistoryTopPromise = undefined : this.getHistoryBottomPromise = undefined);
this.scrollable.unlock('both');
return false;
})/* .then(res => {
this.scrollable.unlock(reverse);
return res;
}) */;
return (reverse ? this.getHistoryTopPromise = promise : this.getHistoryBottomPromise = promise);
}
public setMutedState(muted = false) {

330
src/lib/appManagers/appMediaViewer copy.ts

@ -1,330 +0,0 @@ @@ -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();

389
src/lib/appManagers/appMediaViewer.ts

@ -6,8 +6,9 @@ import { RichTextProcessor } from "../richtextprocessor"; @@ -6,8 +6,9 @@ import { RichTextProcessor } from "../richtextprocessor";
import { logger } from "../polyfill";
import ProgressivePreloader from "../../components/preloader";
import { wrapVideo } from "../../components/wrappers";
import { findUpClassName } from "../utils";
import { findUpClassName, $rootScope, generatePathData } from "../utils";
import appDocsManager from "./appDocsManager";
import { wrapPlayer } from "../ckin";
export class AppMediaViewer {
private overlaysDiv = document.querySelector('.overlays') as HTMLDivElement;
@ -36,12 +37,25 @@ export class AppMediaViewer { @@ -36,12 +37,25 @@ export class AppMediaViewer {
private preloader: ProgressivePreloader = null;
private lastTarget: HTMLElement = null;
private prevTarget: HTMLElement = null;
private nextTarget: HTMLElement = null;
private prevTargets: {
element: HTMLElement,
mid: number
}[] = [];
private nextTargets: AppMediaViewer['prevTargets'] = [];
private targetContainer: HTMLElement = null;
private loadMore: () => void = null;
public log: ReturnType<typeof logger>;
public onKeyDownBinded: any;
public onClickBinded: any;
private peerID = 0;
private loadMediaPromiseUp: Promise<void> = null;
private loadMediaPromiseDown: Promise<void> = null;
private loadedAllMediaUp = false;
private loadedAllMediaDown = false;
private reverse = false; // reverse means next = higher msgid
constructor() {
this.log = logger('AMV');
@ -56,30 +70,35 @@ export class AppMediaViewer { @@ -56,30 +70,35 @@ export class AppMediaViewer {
URL.revokeObjectURL((this.content.container.firstElementChild as HTMLImageElement).src);
}
this.peerID = 0;
this.currentMessageID = 0;
this.setMoverToTarget(this.lastTarget, true);
this.lastTarget = null;
this.prevTarget = null;
this.nextTarget = null;
this.prevTargets = [];
this.nextTargets = [];
this.loadedAllMediaUp = this.loadedAllMediaDown = false;
this.loadMediaPromiseUp = this.loadMediaPromiseDown = null;
window.removeEventListener('keydown', this.onKeyDownBinded);
});
this.buttons.prev.addEventListener('click', () => {
let target = this.prevTarget;
let target = this.prevTargets.pop();
if(target) {
target.click();
this.nextTargets.unshift({element: this.lastTarget, mid: this.currentMessageID});
this.openMedia(appMessagesManager.getMessage(target.mid), target.element);
} else {
this.buttons.prev.style.display = 'none';
}
});
this.buttons.next.addEventListener('click', () => {
let target = this.nextTarget;
let target = this.nextTargets.shift();
if(target) {
target.click();
this.prevTargets.push({element: this.lastTarget, mid: this.currentMessageID});
this.openMedia(appMessagesManager.getMessage(target.mid), target.element);
} else {
this.buttons.next.style.display = 'none';
}
@ -113,7 +132,7 @@ export class AppMediaViewer { @@ -113,7 +132,7 @@ export class AppMediaViewer {
} catch(err) {return false;}
});
if(/* target == this.mediaViewerDiv */!mover || target.tagName == 'IMG') {
if(/* target == this.mediaViewerDiv */!mover || target.tagName == 'IMG' || target.tagName == 'image') {
this.buttons.close.click();
}
};
@ -144,6 +163,7 @@ export class AppMediaViewer { @@ -144,6 +163,7 @@ export class AppMediaViewer {
let wasActive = fromRight !== 0;
let delay = wasActive ? 350 : 200;
//let delay = wasActive ? 350 : 10000;
/* if(wasActive) {
this.moveTheMover(mover);
@ -152,7 +172,19 @@ export class AppMediaViewer { @@ -152,7 +172,19 @@ export class AppMediaViewer {
///////this.log('setMoverToTarget', target, closing, wasActive, fromRight);
let rect = target.getBoundingClientRect();
let realParent: HTMLDivElement;
let rect: DOMRect;
if(target) {
if(target instanceof SVGImageElement || target.parentElement instanceof SVGForeignObjectElement) {
realParent = findUpClassName(target, 'attachment');
rect = realParent.getBoundingClientRect();
} else {
realParent = target.parentElement as HTMLDivElement;
rect = target.getBoundingClientRect();
}
}
let containerRect = this.content.container.getBoundingClientRect();
let transform = '';
@ -174,13 +206,22 @@ export class AppMediaViewer { @@ -174,13 +206,22 @@ export class AppMediaViewer {
mover.classList.remove('cover');
let borderRadius = '';
let scaleX = rect.width / containerRect.width;
let scaleY = rect.height / containerRect.height;
if(!wasActive) {
let scaleX = rect.width / containerRect.width;
let scaleY = rect.height / containerRect.height;
transform += `scale(${scaleX},${scaleY}) `;
}
borderRadius = window.getComputedStyle(target.parentElement).getPropertyValue('border-radius');
let borderRadius = window.getComputedStyle(realParent).getPropertyValue('border-radius');
let brSplitted = borderRadius.split(' ');
if(brSplitted.length != 4) {
if(!brSplitted[0]) brSplitted[0] = '0px';
for(let i = brSplitted.length; i < 4; ++i) {
brSplitted[i] = brSplitted[i % 2] || brSplitted[0] || '0px';
}
}
borderRadius = brSplitted.map(r => (parseInt(r) / scaleX) + 'px').join(' ');
if(!wasActive) {
mover.style.borderRadius = borderRadius;
}
@ -190,6 +231,9 @@ export class AppMediaViewer { @@ -190,6 +231,9 @@ export class AppMediaViewer {
this.log('setMoverToTarget', mover.style.transform);
} */
let path: SVGPathElement;
let isOut = target.classList.contains('is-out');
if(!closing) {
let img: HTMLImageElement;
let video: HTMLVideoElement;
@ -200,15 +244,63 @@ export class AppMediaViewer { @@ -200,15 +244,63 @@ export class AppMediaViewer {
img.src = target.style.backgroundImage.slice(5, -2);
//mover.classList.add('cover');
//mover.style.backgroundImage = target.style.backgroundImage;
} else if(target.tagName == 'IMG') {
} else if(target.tagName == 'IMG' || target.tagName == 'image') {
img = new Image();
img.src = (target as HTMLImageElement).src;
img.src = target instanceof SVGImageElement ? target.getAttributeNS(null, 'href') : (target as HTMLImageElement).src;
img.style.objectFit = 'contain';
} else if(target.tagName == 'VIDEO') {
video = document.createElement('video');
let source = document.createElement('source');
source.src = target.querySelector('source').src;
video.append(source);
} else if(target instanceof SVGSVGElement) {
let clipID = target.dataset.clipID;
let newClipID = clipID + '-mv';
let {width, height} = containerRect;
let newSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
newSvg.setAttributeNS(null, 'width', '' + width);
newSvg.setAttributeNS(null, 'height', '' + height);
newSvg.insertAdjacentHTML('beforeend', target.firstElementChild.outerHTML.replace(clipID, newClipID));
newSvg.insertAdjacentHTML('beforeend', target.lastElementChild.outerHTML.replace(clipID, newClipID));
// теперь надо выставить новую позицию для хвостика
let defs = newSvg.firstElementChild;
let use = defs.firstElementChild.firstElementChild as SVGUseElement;
if(use instanceof SVGUseElement) {
let transform = use.getAttributeNS(null, 'transform');
transform = transform.replace(/translate\((.+?), (.+?)\) scale\((.+?), (.+?)\)/, (match, x, y, sX, sY) => {
x = +x;
if(x != 2) {
x = width - (2 / scaleX);
} else {
x = 2 / scaleX;
}
y = height;
return `translate(${x}, ${y}) scale(${+sX / scaleX}, ${+sY / scaleY})`;
});
use.setAttributeNS(null, 'transform', transform);
// и новый RECT
path = defs.firstElementChild.lastElementChild as SVGPathElement;
// код ниже нужен только чтобы скрыть моргание до момента как сработает таймаут
let d: string;
let br = borderRadius.split(' ').map(v => parseInt(v));
if(isOut) d = generatePathData(0, 0, width - 9 / scaleX, height, ...br);
else d = generatePathData(9 / scaleX, 0, width - 9 / scaleX, height, ...br);
path.setAttributeNS(null, 'd', d);
}
let mediaEl = newSvg.lastElementChild;
mediaEl.setAttributeNS(null, 'width', '' + containerRect.width);
mediaEl.setAttributeNS(null, 'height', '' + containerRect.height);
mover.prepend(newSvg);
}
if(img) {
@ -223,8 +315,16 @@ export class AppMediaViewer { @@ -223,8 +315,16 @@ export class AppMediaViewer {
setTimeout(() => {
mover.classList.add(wasActive ? 'moving' : 'active');
}, 0);
}, 0);
} else {
if(target instanceof SVGSVGElement) {
path = mover.querySelector('path');
if(path) {
this.sizeTailPath(path, containerRect, scaleX, delay, false, isOut, borderRadius);
}
}
setTimeout(() => {
this.overlaysDiv.classList.remove('active');
}, 0);
@ -260,18 +360,49 @@ export class AppMediaViewer { @@ -260,18 +360,49 @@ export class AppMediaViewer {
mover.classList.remove('cover');
}, delay / 2);
if(path) {
this.sizeTailPath(path, containerRect, scaleX, delay, true, isOut, borderRadius);
}
};
}
public sizeTailPath(path: SVGPathElement, rect: DOMRect, scaleX: number, delay: number, upscale: boolean, isOut: boolean, borderRadius: string) {
let start = Date.now();
let {width, height} = rect;
delay = delay / 2;
let br = borderRadius.split(' ').map(v => parseInt(v));
let step = () => {
let diff = Date.now() - start;
let progress = diff / delay;
if(progress > 1) progress = 1;
if(upscale) progress = 1 - progress;
let _br = br.map(v => v * progress);
let d: string;
if(isOut) d = generatePathData(0, 0, width - (9 / scaleX * progress), height, ..._br);
else d = generatePathData(9 / scaleX * progress, 0, width/* width - (9 / scaleX * progress) */, height, ..._br);
path.setAttributeNS(null, 'd', d);
if(diff < delay) window.requestAnimationFrame(step);
};
//window.requestAnimationFrame(step);
step();
}
public moveTheMover(mover: HTMLDivElement, toLeft = true) {
let windowW = appPhotosManager.windowW;
let windowH = appPhotosManager.windowH;
mover.classList.add('moving');
let rect = mover.getBoundingClientRect();
let newTransform = mover.style.transform.replace(/translate\((.+?),/, /* 'translate(-' + windowW + 'px,', */ (match, p1) => {
let newTransform = mover.style.transform.replace(/translate\((.+?),/, (match, p1) => {
/////////this.log('replace func', match, p1);
let x = +p1.slice(0, -2);
x = toLeft ? -rect.width : windowW;
@ -298,28 +429,148 @@ export class AppMediaViewer { @@ -298,28 +429,148 @@ export class AppMediaViewer {
return this.content.mover = newMover;
}
public isElementVisible(container: HTMLElement, target: HTMLElement) {
let rect = container.getBoundingClientRect();
let targetRect = target.getBoundingClientRect();
return targetRect.bottom > rect.top && targetRect.top < rect.bottom;
}
// нет смысла делать проверку для reverse и loadMediaPromise
public loadMoreMedia(older = true) {
//if(!older && this.reverse) return;
if(older && this.loadedAllMediaDown) return;
else if(!older && this.loadedAllMediaUp) return;
if(older && this.loadMediaPromiseDown) return this.loadMediaPromiseDown;
else if(!older && this.loadMediaPromiseUp) return this.loadMediaPromiseUp;
let loadCount = 50;
let backLimit = older ? 0 : loadCount;
let maxID = this.currentMessageID;
public openMedia(message: any, target?: HTMLElement, prevTarget?: HTMLElement, nextTarget?: HTMLElement) {
let anchor: {element: HTMLElement, mid: number};
if(older) {
anchor = this.reverse ? this.prevTargets[0] : this.nextTargets[this.nextTargets.length - 1];
} else {
anchor = this.reverse ? this.nextTargets[this.nextTargets.length - 1] : this.prevTargets[0];
}
if(anchor) maxID = anchor.mid;
if(!older) maxID += 1;
let peerID = this.peerID;
let promise = appMessagesManager.getSearch(peerID, '',
{_: 'inputMessagesFilterPhotoVideo'}, maxID, loadCount/* older ? loadCount : 0 */, 0, backLimit).then(value => {
if(this.peerID != peerID) {
this.log.warn('peer changed');
return;
}
this.log('loaded more media by maxID:', maxID, value, older, this.reverse);
if(value.history.length < loadCount) {
/* if(this.reverse) {
if(older) this.loadedAllMediaUp = true;
else this.loadedAllMediaDown = true;
} else { */
if(older) this.loadedAllMediaDown = true;
else this.loadedAllMediaUp = true;
//}
}
let method = older ? value.history.forEach : value.history.forEachReverse;
method.call(value.history, mid => {
let message = appMessagesManager.getMessage(mid);
let media = message.media;
if(!media || !(media.photo || media.document || (media.webpage && media.webpage.document))) return;
if(media._ == 'document' && media.type != 'video') return;
let t = {element: null as HTMLElement, mid: mid};
if(older) {
if(this.reverse) this.prevTargets.unshift(t);
else this.nextTargets.push(t);
} else {
if(this.reverse) this.nextTargets.push(t);
else this.prevTargets.unshift(t);
}
});
this.buttons.prev.style.display = this.prevTargets.length ? '' : 'none';
this.buttons.next.style.display = this.nextTargets.length ? '' : 'none';
}, () => {}).then(() => {
if(older) this.loadMediaPromiseDown = null;
else this.loadMediaPromiseUp = null;
});
if(older) this.loadMediaPromiseDown = promise;
else this.loadMediaPromiseUp = promise;
return promise;
}
public updateMediaSource(target: HTMLElement, url: string, tagName: 'source' | 'image') {
//if(target instanceof SVGSVGElement) {
let el = target.querySelector(tagName);
if(tagName == 'source') (el as HTMLSourceElement).src = url;
else el.setAttributeNS(null, 'href', url);
/* } else {
} */
}
public openMedia(message: any, target?: HTMLElement, reverse = false, targetContainer?: HTMLElement,
prevTargets: AppMediaViewer['prevTargets'] = [], nextTargets: AppMediaViewer['prevTargets'] = [], loadMore: () => void = null) {
////////this.log('openMedia doc:', message, prevTarget, nextTarget);
let media = message.media.photo || message.media.document || message.media.webpage.document || message.media.webpage.photo;
let isVideo = media.mime_type == 'video/mp4';
let isFirstOpen = !this.peerID;
if(isFirstOpen) {
this.peerID = $rootScope.selectedPeerID;
this.targetContainer = targetContainer;
this.prevTargets = prevTargets;
this.nextTargets = nextTargets;
this.reverse = reverse;
//this.loadMore = loadMore;
}
/* if(this.nextTargets.length < 10 && this.loadMore) {
this.loadMore();
} */
let fromRight = 0;
if(this.lastTarget !== null) {
if(this.lastTarget === prevTarget) {
fromRight = 1;
} else if(this.lastTarget === nextTarget) {
fromRight = -1;
}
if(!isFirstOpen) {
//if(this.lastTarget === prevTarget) {
if(this.reverse) fromRight = this.currentMessageID < message.mid ? 1 : -1;
else fromRight = this.currentMessageID > message.mid ? 1 : -1;
}
//if(prevTarget && (!prevTarget.parentElement || !this.isElementVisible(this.targetContainer, prevTarget))) prevTarget = null;
//if(nextTarget && (!nextTarget.parentElement || !this.isElementVisible(this.targetContainer, nextTarget))) nextTarget = null;
this.buttons.prev.style.display = this.prevTargets.length ? '' : 'none';
this.buttons.next.style.display = this.nextTargets.length ? '' : 'none';
let container = this.content.container;
let useContainerAsTarget = !target;
if(useContainerAsTarget) target = container;
this.currentMessageID = message.mid;
this.prevTarget = prevTarget || null;
this.nextTarget = nextTarget || null;
this.lastTarget = target;
let container = this.content.container;
if(this.nextTargets.length < 20) {
this.loadMoreMedia(!this.reverse);
}
if(this.prevTargets.length < 20) {
this.loadMoreMedia(this.reverse);
}
if(container.firstElementChild) {
container.innerHTML = '';
@ -366,25 +617,82 @@ export class AppMediaViewer { @@ -366,25 +617,82 @@ export class AppMediaViewer {
////////this.log('will wrap video', media, size);
if(useContainerAsTarget) target = target.querySelector('img, video') || target;
let afterTimeout = this.setMoverToTarget(target, false, fromRight);
//if(wasActive) return;
//return;
setTimeout(() => {
afterTimeout();
//return;
wrapVideo.call(this, media, mover, message, false, this.preloader).then(() => {
let video = mover.querySelector('video') || document.createElement('video');
let source: HTMLSourceElement;
if(video.firstElementChild) {
source = video.firstElementChild as HTMLSourceElement;
}
video.dataset.ckin = 'default';
video.dataset.overlay = '1';
if(!source || !source.src) {
let promise = appDocsManager.downloadDoc(media);
this.preloader.attach(mover, true, promise);
promise.then(blob => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed video');
return;
}
let url = URL.createObjectURL(blob);
if(target instanceof SVGSVGElement) {
this.updateMediaSource(mover, url, 'source');
this.updateMediaSource(target, url, 'source');
} else {
let img = mover.firstElementChild;
if(img instanceof Image) {
mover.removeChild(img);
}
source = document.createElement('source');
//source.src = doc.url;
source.src = url;
source.type = media.mime_type;
mover.prepend(video);
video.append(source);
}
let wrapper = wrapPlayer(video);
(wrapper.querySelector('.toggle') as HTMLButtonElement).click();
});
} else {
let wrapper = wrapPlayer(video);
(wrapper.querySelector('.toggle') as HTMLButtonElement).click();
}
/* wrapVideo.call(this, media, mover, message, false, this.preloader).then(() => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed video');
return;
}
});
}); */
}, 0);
} else {
let size = appPhotosManager.setAttachmentSize(media.id, container, maxWidth, maxHeight);
if(useContainerAsTarget) target = target.querySelector('img, video') || target;
let afterTimeout = this.setMoverToTarget(target, false, fromRight);
//return;
//if(wasActive) return;
setTimeout(() => {
afterTimeout();
//return;
this.preloader.attach(mover);
let cancellablePromise = appPhotosManager.preloadPhoto(media.id, size);
@ -396,9 +704,15 @@ export class AppMediaViewer { @@ -396,9 +704,15 @@ export class AppMediaViewer {
///////this.log('indochina', blob);
let image = mover.firstElementChild as HTMLImageElement || new Image();
image.src = URL.createObjectURL(blob);
mover.prepend(image);
let url = URL.createObjectURL(blob);
if(target instanceof SVGSVGElement) {
this.updateMediaSource(target, url, 'image');
this.updateMediaSource(mover, url, 'image');
} else {
let image = mover.firstElementChild as HTMLImageElement || new Image();
image.src = url;
mover.prepend(image);
}
this.preloader.detach();
}).catch(err => {
@ -406,9 +720,6 @@ export class AppMediaViewer { @@ -406,9 +720,6 @@ export class AppMediaViewer {
});
}, 0);
}
this.buttons.prev.style.display = this.prevTarget ? '' : 'none';
this.buttons.next.style.display = this.nextTarget ? '' : 'none';
}
}

26
src/lib/appManagers/appMessagesManager.ts

@ -11,7 +11,7 @@ import appPhotosManager from "./appPhotosManager"; @@ -11,7 +11,7 @@ import appPhotosManager from "./appPhotosManager";
import AppStorage from '../storage';
import AppPeersManager from "./appPeersManager";
import ServerTimeManager from "../mtproto/serverTimeManager";
import apiFileManager, { CancellablePromise } from "../mtproto/apiFileManager";
import apiFileManager from "../mtproto/apiFileManager";
import appDocsManager from "./appDocsManager";
import appImManager from "./appImManager";
import { MTDocument } from "../../components/wrappers";
@ -19,6 +19,7 @@ import ProgressivePreloader from "../../components/preloader"; @@ -19,6 +19,7 @@ import ProgressivePreloader from "../../components/preloader";
import serverTimeManager from "../mtproto/serverTimeManager";
import apiManager from "../mtproto/apiManager";
import appWebPagesManager from "./appWebPagesManager";
import { CancellablePromise, deferredPromise } from "../polyfill";
type HistoryStorage = {
count: number | null,
@ -660,15 +661,7 @@ export class AppMessagesManager { @@ -660,15 +661,7 @@ export class AppMessagesManager {
invoke(flags, inputMedia);
} else if(file instanceof File || file instanceof Blob) {
let deferredHelper: {
resolve?: () => void,
reject?: (error: any) => void
} = {};
let deferred: CancellablePromise<void> = new Promise((resolve, reject) => {
deferredHelper.resolve = resolve;
deferredHelper.reject = reject;
});
Object.assign(deferred, deferredHelper);
let deferred = deferredPromise<void>();
this.sendFilePromise.then(() => {
if(!uploaded || message.error) {
@ -1816,7 +1809,7 @@ export class AppMessagesManager { @@ -1816,7 +1809,7 @@ export class AppMessagesManager {
public getSearch(peerID = 0, query: string = '', inputFilter: {
_?: string
} = {_: 'inputMessagesFilterEmpty'}, maxID: number, limit: number, offsetRate = 0): Promise<{
} = {_: 'inputMessagesFilterEmpty'}, maxID: number, limit: number, offsetRate = 0, backLimit = 0): Promise<{
count: number,
next_rate: number,
history: number[]
@ -1953,9 +1946,9 @@ export class AppMessagesManager { @@ -1953,9 +1946,9 @@ export class AppMessagesManager {
filter: inputFilter || {_: 'inputMessagesFilterEmpty'},
min_date: 0,
max_date: 0,
limit: limit || 20,
limit: limit,
offset_id: appMessagesIDsManager.getMessageLocalID(maxID) || 0,
add_offset: 0,
add_offset: backLimit ? -backLimit : 0,
max_id: 0,
min_id: 0
}, {
@ -3024,7 +3017,12 @@ export class AppMessagesManager { @@ -3024,7 +3017,12 @@ export class AppMessagesManager {
});
}
public getHistory(peerID: number, maxID = 0, limit = 0, backLimit?: number, prerendered?: number) {
public getHistory(peerID: number, maxID = 0, limit = 0, backLimit?: number, prerendered?: number): Promise<{
count: number,
history: number[],
unreadOffset: number,
unreadSkip: boolean
}> {
if(this.migratedFromTo[peerID]) {
peerID = this.migratedFromTo[peerID];
}

102
src/lib/appManagers/appPhotosManager.ts

@ -36,16 +36,16 @@ export class AppPhotosManager { @@ -36,16 +36,16 @@ export class AppPhotosManager {
constructor() {
window.addEventListener('resize', (e) => {
//fastdom.measure(() => {
this.windowW = document.body.scrollWidth;
this.windowH = document.body.scrollHeight;
this.windowW = document.body.scrollWidth;
this.windowH = document.body.scrollHeight;
//});
//console.log(`Set windowW, windowH: ${this.windowW}x${this.windowH}`);
});
//fastdom.measure(() => {
console.log('measure works');
this.windowW = document.body.scrollWidth;
this.windowH = document.body.scrollHeight;
console.log('measure works');
this.windowW = document.body.scrollWidth;
this.windowH = document.body.scrollHeight;
//});
}
@ -79,7 +79,7 @@ export class AppPhotosManager { @@ -79,7 +79,7 @@ export class AppPhotosManager {
width *= 2;
height *= 2;
}
/*
s box 100x100
m box 320x320
@ -93,21 +93,21 @@ export class AppPhotosManager { @@ -93,21 +93,21 @@ export class AppPhotosManager {
let bestPhotoSize: MTPhotoSize = {_: 'photoSizeEmpty'};
let bestDiff = 0xFFFFFF;
//console.log('choosePhotoSize', photo);
let sizes = photo.sizes || photo.thumbs;
if(!sizes) return bestPhotoSize;
sizes.forEach((photoSize: typeof bestPhotoSize) => {
if(!photoSize.w || !photoSize.h) return;
let diff = Math.abs(photoSize.w * photoSize.h - width * height);
if(diff < bestDiff) {
bestPhotoSize = photoSize;
bestDiff = diff;
}
//console.log('diff', diff, photoSize, bestPhotoSize);
});
@ -139,7 +139,7 @@ export class AppPhotosManager { @@ -139,7 +139,7 @@ export class AppPhotosManager {
});
}
public setAttachmentPreview(bytes: Uint8Array, div: HTMLElement, isSticker = false, background = false) {
public setAttachmentPreview(bytes: Uint8Array, element: HTMLElement | SVGSVGElement, isSticker = false, background = false) {
//image.src = "data:image/jpeg;base64," + bytesToBase64(photo.sizes[0].bytes);
//photo.sizes[0].bytes = new Uint8Array([...photo.sizes[0].bytes].reverse());
@ -154,23 +154,30 @@ export class AppPhotosManager { @@ -154,23 +154,30 @@ export class AppPhotosManager {
//console.log('setAttachmentPreview', bytes, arr, div, isSticker);
let blob = new Blob([arr], { type: "image/jpeg" } );
let blob = new Blob([arr], {type: "image/jpeg"});
if(background) {
div.style.backgroundImage = 'url(' + URL.createObjectURL(blob) + ')';
element.style.backgroundImage = 'url(' + URL.createObjectURL(blob) + ')';
} else {
let image = new Image();
image.src = URL.createObjectURL(blob);
image.style.width = '100%';
image.style.height = '100%';
div.append(image);
if(element instanceof SVGSVGElement) {
let image = document.createElementNS("http://www.w3.org/2000/svg", "image");
image.setAttributeNS(null, 'href', URL.createObjectURL(blob));
//image.setAttributeNS(null, 'preserveAspectRatio', 'xMinYMin slice');
element.append(image);
} else {
let image = new Image();
image.src = URL.createObjectURL(blob);
image.style.width = '100%';
image.style.height = '100%';
element.append(image);
}
}
}
public setAttachmentSize(photoID: any, div: HTMLDivElement, boxWidth = 380, boxHeight = 380, isSticker = false) {
public setAttachmentSize(photoID: any, element: HTMLElement | SVGSVGElement, boxWidth = 380, boxHeight = 380, isSticker = false) {
let photo: /* MTDocument | MTPhoto */any = null;
if(typeof(photoID) === 'string') {
photo = this.photos[photoID];
if(!photo) return {_: 'photoEmpty'};
@ -183,17 +190,32 @@ export class AppPhotosManager { @@ -183,17 +190,32 @@ export class AppPhotosManager {
let sizes = photo.sizes || photo.thumbs;
if(sizes && sizes[0].bytes) {
this.setAttachmentPreview(sizes[0].bytes, div, isSticker);
this.setAttachmentPreview(sizes[0].bytes, element, isSticker);
}
if(photo._ == 'document' /* && photo.type != 'video' *//* && photo.type != 'gif' */) {
let {w, h} = calcImageInBox(photo.w || 512, photo.h || 512, boxWidth, boxHeight);
div.style.width = w + 'px';
div.style.height = h + 'px';
let width: number;
let height: number;
if(photo._ == 'document') {
width = photo.w || 512;
height = photo.h || 512;
} else {
let {w, h} = calcImageInBox(photoSize.w || 100, photoSize.h || 100, boxWidth, boxHeight);
div.style.width = w + 'px';
div.style.height = h + 'px';
width = photoSize.w || 100;
height = photoSize.h || 100;
}
let {w, h} = calcImageInBox(width, height, boxWidth, boxHeight);
if(element instanceof SVGSVGElement) {
element.setAttributeNS(null, 'width', '' + w);
element.setAttributeNS(null, 'height', '' + h);
if(element.firstElementChild) {
let imageSvg = element.firstElementChild as SVGImageElement;
imageSvg.setAttributeNS(null, 'width', '' + w);
imageSvg.setAttributeNS(null, 'height', '' + h);
}
} else {
element.style.width = w + 'px';
element.style.height = h + 'px';
}
return photoSize;
@ -201,7 +223,7 @@ export class AppPhotosManager { @@ -201,7 +223,7 @@ export class AppPhotosManager {
public async preloadPhoto(photoID: any, photoSize?: MTPhotoSize): Promise<Blob> {
let photo: any = null;
if(typeof(photoID) === 'string') {
photo = this.photos[photoID];
if(!photo) return Promise.reject();
@ -212,13 +234,13 @@ export class AppPhotosManager { @@ -212,13 +234,13 @@ export class AppPhotosManager {
if(!photoSize) {
let fullWidth = this.windowW/* - (Config.Mobile ? 20 : 32) */;
let fullHeight = this.windowH/* - (Config.Mobile ? 150 : 116) */;
photoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
}
if(photoSize && photoSize._ != 'photoSizeEmpty') {
photoSize.preloaded = true;
// maybe it's a thumb
let isPhoto = photoSize.size && photo.access_hash && photo.file_reference;
let location = isPhoto ? {
@ -228,23 +250,23 @@ export class AppPhotosManager { @@ -228,23 +250,23 @@ export class AppPhotosManager {
file_reference: photo.file_reference,
thumb_size: photoSize.type
} : photoSize.location;
/* if(overwrite) {
await apiFileManager.deleteFile(location);
console.log('Photos deleted file!');
} */
if(isPhoto/* && photoSize.size >= 1e6 */) {
//console.log('Photos downloadFile exec', photo);
/* let promise = apiFileManager.downloadFile(photo.dc_id, location, photoSize.size);
let blob = await promise;
if(blob.size < photoSize.size && overwrite) {
await apiFileManager.deleteFile(location);
console.log('Photos deleted file!');
return apiFileManager.downloadFile(photo.dc_id, location, photoSize.size);
}
return blob; */
return apiFileManager.downloadFile(photo.dc_id, location, photoSize.size);
} else {
@ -353,7 +375,7 @@ export class AppPhotosManager { @@ -353,7 +375,7 @@ export class AppPhotosManager {
});
} catch(err) {
console.error('err', err);
var cachedBlob = apiFileManager.getCachedFile(inputFileLocation)
if (cachedBlob) {
return fileManager.download(cachedBlob, mimeType, fileName);

60
src/lib/appManagers/appSidebarLeft.ts

@ -27,7 +27,7 @@ class SearchGroup { @@ -27,7 +27,7 @@ class SearchGroup {
this.container.append(this.nameEl, this.list);
this.container.style.display = 'none';
//appDialogsManager.setListClickListener(this.list);
appDialogsManager.setListClickListener(this.list);
}
clear() {
@ -62,6 +62,9 @@ class AppSidebarLeft { @@ -62,6 +62,9 @@ class AppSidebarLeft {
//private chatsLoadCount = 0;
//private loadDialogsPromise: Promise<any>;
private loadDialogsPromise: ReturnType<AppMessagesManager["getConversations"]>;
private loadedAll = false;
private loadedArchivedAll = false;
private log = logger('SL');
@ -91,15 +94,20 @@ class AppSidebarLeft { @@ -91,15 +94,20 @@ class AppSidebarLeft {
//this.chatsContainer.append(this.chatsPreloader);
//this.chatsLoadCount = Math.round(document.body.scrollHeight / 70 * 1.5);
let splitOffset = 1110;
this.scroll = new Scrollable(this.chatsContainer as HTMLDivElement, false, true, 300, 'CL');
this.scroll = new Scrollable(this.chatsContainer, 'y', splitOffset, 'CL', appDialogsManager.chatList, 500);
this.scroll.setVirtualContainer(appDialogsManager.chatList);
this.scroll.onScrolledBottom = this.onChatsScroll.bind(this);
appDialogsManager.chatsHidden = this.scroll.hiddenElements;
appDialogsManager.chatsVisible = this.scroll.visibleElements;
this.scrollArchived = new Scrollable(this.chatsArchivedContainer as HTMLDivElement, false, true, 300, 'CLA');
this.scrollArchived = new Scrollable(this.chatsArchivedContainer, 'y', splitOffset, 'CLA', appDialogsManager.chatListArchived, 500);
this.scrollArchived.setVirtualContainer(appDialogsManager.chatListArchived);
this.scrollArchived.onScrolledBottom = this.onChatsArchivedScroll.bind(this);
appDialogsManager.chatsArchivedHidden = this.scrollArchived.hiddenElements;
appDialogsManager.chatsArchivedVisible = this.scrollArchived.visibleElements;
//this.scrollArchived.container.addEventListener('scroll', this.onChatsArchivedScroll.bind(this));
this.listsContainer = new Scrollable(this.searchContainer).container;
@ -111,11 +119,7 @@ class AppSidebarLeft { @@ -111,11 +119,7 @@ class AppSidebarLeft {
///////this.log('savedbtn click');
setTimeout(() => { // menu doesn't close if no timeout (lol)
let dom = appDialogsManager.getDialogDom(appImManager.myID);
if(dom) {
dom.listEl.click();
} else {
appImManager.setPeer(appImManager.myID);
}
appImManager.setPeer(appImManager.myID);
}, 0);
});
@ -225,11 +229,15 @@ class AppSidebarLeft { @@ -225,11 +229,15 @@ class AppSidebarLeft {
let offset = archived ? this.chatsArchivedOffsetIndex : this.chatsOffsetIndex;
//let offset = 0;
let scroll = archived ? this.scrollArchived : this.scroll;
scroll.lock();
try {
console.time('getDialogs time');
this.loadDialogsPromise = appMessagesManager.getConversations('', offset, 50/*this.chatsLoadCount */, +archived);
let loadCount = 50/*this.chatsLoadCount */;
this.loadDialogsPromise = appMessagesManager.getConversations('', offset, loadCount, +archived);
let result = await this.loadDialogsPromise;
@ -246,12 +254,17 @@ class AppSidebarLeft { @@ -246,12 +254,17 @@ class AppSidebarLeft {
});
}
if(!result.dialogs.length || (archived ? this.scrollArchived.length == result.count : this.scroll.length == result.count)) { // loaded all
if(archived) this.loadedArchivedAll = true;
else this.loadedAll = true;
}
/* if(archived) {
let count = result.count;
this.archivedCount.innerText = '' + count;
} */
//this.log('getDialogs ' + this.chatsLoadCount + ' dialogs by offset:', offset, result, this.scroll.hiddenElements);
this.log('getDialogs ' + loadCount + ' dialogs by offset:', offset, result, this.scroll.length);
this.scroll.onScroll();
} catch(err) {
this.log.error(err);
@ -259,30 +272,19 @@ class AppSidebarLeft { @@ -259,30 +272,19 @@ class AppSidebarLeft {
this.chatsPreloader.remove();
this.loadDialogsPromise = undefined;
scroll.unlock();
}
public onChatsScroll() {
//this.log(this.scroll.hiddenElements.down.length, this.loadDialogsPromise, appDialogsManager.chatList.childNodes);
if(this.scroll.hiddenElements.down.length > 0 || this.loadDialogsPromise/* || 1 == 1 */) return;
if(this.loadedAll || this.scroll.hiddenElements.down.length > 0 || this.loadDialogsPromise/* || 1 == 1 */) return;
this.loadDialogs();
}
public onChatsArchivedScroll() {
//this.log(this.scrollArchived.hiddenElements.down.length, this.loadDialogsPromise, appDialogsManager.chatListArchived.childNodes);
if(this.scrollArchived.hiddenElements.down.length > 0/* || 1 == 1 */) return;
if(this.loadedArchivedAll || this.scrollArchived.hiddenElements.down.length > 0 || this.loadDialogsPromise/* || 1 == 1 */) return;
if(!this.loadDialogsPromise) {
let d = Array.from(appDialogsManager.chatListArchived.childNodes).slice(-5);
for(let node of d) {
if(isElementInViewport(node)) {
this.loadDialogs(true);
break;
}
}
//console.log('last 5 dialogs:', d);
}
this.loadDialogs(true);
}
public onSidebarScroll() {
@ -434,4 +436,8 @@ class AppSidebarLeft { @@ -434,4 +436,8 @@ class AppSidebarLeft {
}
}
export default new AppSidebarLeft();
const appSidebarLeft = new AppSidebarLeft();
(window as any).appSidebarLeft = appSidebarLeft;
export default appSidebarLeft;

107
src/lib/appManagers/appSidebarRight.ts

@ -41,9 +41,8 @@ class AppSidebarRight { @@ -41,9 +41,8 @@ class AppSidebarRight {
public lastSharedMediaDiv: HTMLDivElement = null;
private loadSidebarMediaPromises: {
[type: string]: Promise<void>
} = {};
private loadSidebarMediaPromises: {[type: string]: Promise<void>} = {};
private loadedAllMedia: {[type: string]: boolean} = {};
public sharedMediaTypes = [
'inputMessagesFilterContacts',
@ -73,12 +72,9 @@ class AppSidebarRight { @@ -73,12 +72,9 @@ class AppSidebarRight {
private peerID = 0;
public sidebarScroll: Scrollable = null;
public scroll: Scrollable = null;
private savedVirtualStates: {
[id: number]: {
hiddenElements: any,
paddings: any
}
[id: number]: Scrollable['state']
} = {};
private profileTabs: HTMLUListElement;
@ -94,10 +90,10 @@ class AppSidebarRight { @@ -94,10 +90,10 @@ class AppSidebarRight {
let container = this.profileContentEl.querySelector('.profile-tabs-content') as HTMLDivElement;
this.profileTabs = this.profileContentEl.querySelector('.profile-tabs') as HTMLUListElement;
this.sidebarScroll = new Scrollable(this.sidebarEl, false, true, 500, 'SR');
this.sidebarScroll.container.addEventListener('scroll', this.onSidebarScroll.bind(this));
this.sidebarScroll.onScrolledBottom = () => {
if(this.sharedMediaSelected && !this.sidebarScroll.hiddenElements.down.length
this.scroll = new Scrollable(this.sidebarEl, 'y', 1200, 'SR');
this.scroll.container.addEventListener('scroll', this.onSidebarScroll.bind(this));
this.scroll.onScrolledBottom = () => {
if(this.sharedMediaSelected && !this.scroll.hiddenElements.down.length
&& this.sharedMediaSelected.childElementCount/* && false */) {
this.loadSidebarMedia(true);
}
@ -114,27 +110,17 @@ class AppSidebarRight { @@ -114,27 +110,17 @@ class AppSidebarRight {
}
if(this.prevTabID != -1) {
this.savedVirtualStates[this.prevTabID] = {
hiddenElements: {
up: this.sidebarScroll.hiddenElements.up.slice(),
down: this.sidebarScroll.hiddenElements.down.slice(),
},
paddings: {
up: this.sidebarScroll.paddings.up,
down: this.sidebarScroll.paddings.down
}
};
this.savedVirtualStates[this.prevTabID] = this.scroll.state;
}
this.prevTabID = id;
this.log('setVirtualContainer', id, this.sharedMediaSelected);
this.sidebarScroll.setVirtualContainer(this.sharedMediaSelected);
this.scroll.setVirtualContainer(this.sharedMediaSelected);
if(this.savedVirtualStates[id]) {
this.log(this.savedVirtualStates[id]);
this.sidebarScroll.hiddenElements = this.savedVirtualStates[id].hiddenElements;
this.sidebarScroll.paddings = this.savedVirtualStates[id].paddings;
this.scroll.state = this.savedVirtualStates[id];
}
}, this.onSidebarScroll.bind(this));
@ -146,7 +132,7 @@ class AppSidebarRight { @@ -146,7 +132,7 @@ class AppSidebarRight {
this.sharedMedia.contentMedia.addEventListener('click', (e) => {
let target = e.target as HTMLDivElement;
let messageID = +target.getAttribute('message-id');
let messageID = +target.dataset.mid;
if(!messageID) {
this.log.warn('no messageID by click on target:', target);
return;
@ -156,11 +142,11 @@ class AppSidebarRight { @@ -156,11 +142,11 @@ class AppSidebarRight {
let ids = Object.keys(this.mediaDivsByIDs).map(k => +k).sort();
let idx = ids.findIndex(i => i == messageID);
let targets = ids.map(id => ({element: this.mediaDivsByIDs[id], mid: id}));
let prev = ids[idx + 1] || null;
let next = ids[idx - 1] || null;
appMediaViewer.openMedia(message, target, this.mediaDivsByIDs[prev] || null, this.mediaDivsByIDs[next] || null);
appMediaViewer.openMedia(message, target, false, this.sidebarEl,
targets.slice(idx + 1).reverse(), targets.slice(0, idx).reverse(), () => this.loadSidebarMedia(true));
});
this.profileElements.notificationsCheckbox.addEventListener('change', () => {
@ -170,7 +156,7 @@ class AppSidebarRight { @@ -170,7 +156,7 @@ class AppSidebarRight {
window.addEventListener('resize', () => {
setTimeout(() => {
this.sidebarScroll.onScroll();
this.scroll.onScroll();
this.onSidebarScroll();
}, 0);
});
@ -178,7 +164,7 @@ class AppSidebarRight { @@ -178,7 +164,7 @@ class AppSidebarRight {
if(testScroll) {
let div = document.createElement('div');
for(let i = 0; i < 500; ++i) {
//div.insertAdjacentHTML('beforeend', `<div message-id="0" style="background-image: url(assets/img/camomile.jpg);"></div>`);
//div.insertAdjacentHTML('beforeend', `<div style="background-image: url(assets/img/camomile.jpg);"></div>`);
div.insertAdjacentHTML('beforeend', `<div data-id="${i / 3 | 0}">${i / 3 | 0}</div>`);
if((i + 1) % 3 == 0) {
@ -201,8 +187,10 @@ class AppSidebarRight { @@ -201,8 +187,10 @@ class AppSidebarRight {
/////this.log('sidebarEl', this.sidebarEl, enable, isElementInViewport(this.sidebarEl));
if(enable !== undefined) {
if(enable) this.sidebarEl.classList.add('active');
else this.sidebarEl.classList.remove('active');
if(enable) {
setTimeout(() => this.lazyLoadQueueSidebar.check(), 200);
this.sidebarEl.classList.add('active');
} else this.sidebarEl.classList.remove('active');
return;
}
@ -223,9 +211,13 @@ class AppSidebarRight { @@ -223,9 +211,13 @@ class AppSidebarRight {
let peerID = this.peerID;
let typesToLoad = single ? [this.sharedMediaType] : this.sharedMediaTypes;
typesToLoad = typesToLoad.filter(type => !this.loadedAllMedia[type]);
if(!typesToLoad.length) return;
if(!this.historiesStorage[peerID]) this.historiesStorage[peerID] = {};
let historyStorage = this.historiesStorage[peerID];
this.scroll.lock();
let promises = typesToLoad.map(type => {
if(this.loadSidebarMediaPromises[type]) return this.loadSidebarMediaPromises[type];
@ -243,12 +235,17 @@ class AppSidebarRight { @@ -243,12 +235,17 @@ class AppSidebarRight {
maxID = !maxID && ids.length ? ids[ids.length - 1] : maxID;
//this.log('search house of glass pre', type, ids, maxID);
return this.loadSidebarMediaPromises[type] = appMessagesManager.getSearch(peerID, '', {_: type}, maxID, history.length ? 50 : 15)
let loadCount = history.length ? 50 : 15;
return this.loadSidebarMediaPromises[type] = appMessagesManager.getSearch(peerID, '', {_: type}, maxID, loadCount)
.then(value => {
ids = ids.concat(value.history);
history.push(...ids);
//this.log('search house of glass', type, value, ids, this.cleared);
this.log('search house of glass', type, value, ids, this.cleared);
if(value.history.length < loadCount) {
this.loadedAllMedia[type] = true;
}
if($rootScope.selectedPeerID != peerID) {
this.log.warn('peer changed');
@ -266,6 +263,8 @@ class AppSidebarRight { @@ -266,6 +263,8 @@ class AppSidebarRight {
let message = appMessagesManager.getMessage(mid);
if(message.media) messages.push(message);
}
let elemsToAppend: HTMLElement[] = [];
/*'inputMessagesFilterContacts',
'inputMessagesFilterPhotoVideo',
@ -280,7 +279,7 @@ class AppSidebarRight { @@ -280,7 +279,7 @@ class AppSidebarRight {
let media = message.media.photo || message.media.document || (message.media.webpage && message.media.webpage.document);
if(!media) {
//this.log('no media!', message);
continue;;
continue;
}
if(media._ == 'document' && media.type != 'video'/* && media.type != 'gif' */) {
@ -313,13 +312,16 @@ class AppSidebarRight { @@ -313,13 +312,16 @@ class AppSidebarRight {
div.style.backgroundImage = 'url(' + url + ')';
});
div.setAttribute('message-id', '' + message.mid);
div.dataset.mid = '' + message.mid;
this.lazyLoadQueueSidebar.push({div, load});
this.lastSharedMediaDiv.append(div);
if(this.lastSharedMediaDiv.childElementCount == 3) {
this.sidebarScroll.append(this.lastSharedMediaDiv);
if(!this.scroll.contains(this.lastSharedMediaDiv)) {
elemsToAppend.push(this.lastSharedMediaDiv);
}
this.lastSharedMediaDiv = document.createElement('div');
}
@ -349,7 +351,7 @@ class AppSidebarRight { @@ -349,7 +351,7 @@ class AppSidebarRight {
//this.log('come back down to my knees', message);
let div = wrapDocument(message.media.document, true);
this.sidebarScroll.append(div);
elemsToAppend.push(div);
}
break;
}
@ -407,7 +409,7 @@ class AppSidebarRight { @@ -407,7 +409,7 @@ class AppSidebarRight {
`);
if(div.innerText.trim().length) {
this.sidebarScroll.append(div);
elemsToAppend.push(div);
}
}
@ -437,6 +439,16 @@ class AppSidebarRight { @@ -437,6 +439,16 @@ class AppSidebarRight {
break;
}
if(this.lastSharedMediaDiv.childElementCount && !this.scroll.contains(this.lastSharedMediaDiv)) {
elemsToAppend.push(this.lastSharedMediaDiv);
}
if(elemsToAppend.length) {
window.requestAnimationFrame(() => {
elemsToAppend.forEach(el => this.scroll.append(el));
});
}
if(sharedMediaDiv) {
let parent = sharedMediaDiv.parentElement;
if(parent.lastElementChild.classList.contains('preloader')) {
@ -453,12 +465,15 @@ class AppSidebarRight { @@ -453,12 +465,15 @@ class AppSidebarRight {
});
});
return promises;
return Promise.all(promises).then(() => {
this.scroll.unlock();
});
}
public fillProfileElements() {
let peerID = this.peerID = $rootScope.selectedPeerID;
this.loadSidebarMediaPromises = {};
this.loadedAllMedia = {};
this.lastSharedMediaDiv = document.createElement('div');
//this.log('fillProfileElements');
@ -496,7 +511,7 @@ class AppSidebarRight { @@ -496,7 +511,7 @@ class AppSidebarRight {
this.savedVirtualStates = {};
this.prevTabID = -1;
this.sidebarScroll.setVirtualContainer(null);
this.scroll.setVirtualContainer(null);
(this.profileTabs.children[1] as HTMLLIElement).click(); // set media
let setText = (text: string, el: HTMLDivElement) => {
@ -554,7 +569,7 @@ class AppSidebarRight { @@ -554,7 +569,7 @@ class AppSidebarRight {
appMessagesManager.wrapSingleMessage(userFull.pinned_msg_id);
}
this.sidebarScroll.getScrollTopOffset();
this.scroll.getScrollTopOffset();
});
} else {
let chat = appPeersManager.getPeer(peerID);
@ -571,11 +586,11 @@ class AppSidebarRight { @@ -571,11 +586,11 @@ class AppSidebarRight {
setText(RichTextProcessor.wrapRichText(chatFull.about), this.profileElements.bio);
}
this.sidebarScroll.getScrollTopOffset();
this.scroll.getScrollTopOffset();
});
}
this.sidebarScroll.getScrollTopOffset();
this.scroll.getScrollTopOffset();
//this.loadSidebarMedia();
}
}

16
src/lib/mtproto/apiFileManager.ts

@ -3,14 +3,7 @@ import { nextRandomInt } from "../bin_utils"; @@ -3,14 +3,7 @@ import { nextRandomInt } from "../bin_utils";
import IdbFileStorage from "../idb";
import FileManager from "../filemanager";
import apiManager from "./apiManager";
import { logger } from "../polyfill";
export interface CancellablePromise<T> extends Promise<T> {
resolve?: (...args: any[]) => void,
reject?: (...args: any[]) => void,
cancel?: () => void,
notify?: (...args: any[]) => void
}
import { logger, deferredPromise, CancellablePromise } from "../polyfill";
export class ApiFileManager {
public cachedFs = false;
@ -319,12 +312,7 @@ export class ApiFileManager { @@ -319,12 +312,7 @@ export class ApiFileManager {
//this.log('arriba');
//var deferred = $q.defer()
let deferredHelper: any = {notify: () => {}};
let deferred: CancellablePromise<Blob> = new Promise((resolve, reject) => {
deferredHelper.resolve = resolve;
deferredHelper.reject = reject;
});
Object.assign(deferred, deferredHelper);
let deferred = deferredPromise<Blob>();
//return;

47
src/lib/polyfill.ts

@ -5,31 +5,62 @@ export function logger(prefix: string) { @@ -5,31 +5,62 @@ export function logger(prefix: string) {
function Log(...args: any[]) {
return console.log(dT(), '[' + prefix + ']:', ...args);
}
Log.warn = function(...args: any[]) {
return console.warn(dT(), '[' + prefix + ']:', ...args);
};
Log.info = function(...args: any[]) {
return console.info(dT(), '[' + prefix + ']:', ...args);
};
Log.error = function(...args: any[]) {
return console.error(dT(), '[' + prefix + ']:', ...args);
};
Log.trace = function(...args: any[]) {
return console.trace(dT(), '[' + prefix + ']:', ...args);
}
return Log;
};
export interface CancellablePromise<T> extends Promise<T> {
resolve?: (...args: any[]) => void,
reject?: (...args: any[]) => void,
cancel?: () => void,
notify?: (...args: any[]) => void,
isFulfilled?: boolean,
isRejected?: boolean
}
export function deferredPromise<T>() {
let deferredHelper: any = {notify: () => {}, isFulfilled: false, isRejected: false};
let deferred: CancellablePromise<T> = new Promise<T>((resolve, reject) => {
deferredHelper.resolve = (value: T) => {
if(deferred.isFulfilled) return;
deferred.isFulfilled = true;
resolve(value);
};
deferredHelper.reject = (...args: any[]) => {
if(deferred.isRejected) return;
deferred.isRejected = true;
reject(...args);
};
});
Object.assign(deferred, deferredHelper);
return deferred;
}
Object.defineProperty(Uint8Array.prototype, 'hex', {
get: function(): string {
return bytesToHex([...this]);
},
set: function(str: string) {
this.set(bytesFromHex(str));
},
@ -76,12 +107,12 @@ declare global { @@ -76,12 +107,12 @@ declare global {
randomize: () => Uint8Array,
concat: (...args: Array<Uint8Array | ArrayBuffer | number[]>) => Uint8Array
}
interface Array<T> {
forEachReverse(callback: (value: T, index?: number, array?: Array<T>) => void): void;
findAndSplice(verify: (value: T, index?: number, array?: Array<T>) => boolean): T;
}
interface String {
toHHMMSS(leadZero?: boolean): string;
}

5
src/lib/services.ts

@ -14,6 +14,7 @@ import AppStickersManager from './appManagers/appStickersManager'; @@ -14,6 +14,7 @@ import AppStickersManager from './appManagers/appStickersManager';
import AppDocsManager from './appManagers/appDocsManager';
import AppSidebarRight from './appManagers/appSidebarRight';
import AppSidebarLeft from './appManagers/appSidebarLeft';
import AppMediaViewer from './appManagers/appMediaViewer';
//import AppSharedMediaManager from './appManagers/appSharedMediaManager';
export const appUsersManager = AppUsersManager;
@ -31,6 +32,7 @@ export const appDocsManager = AppDocsManager; @@ -31,6 +32,7 @@ export const appDocsManager = AppDocsManager;
//export const appSharedMediaManager = AppSharedMediaManager;
export const appSidebarRight = AppSidebarRight;
export const appSidebarLeft = AppSidebarLeft;
export const appMediaViewer = AppMediaViewer;
(window as any).Services = {
appUsersManager,
@ -46,6 +48,7 @@ export const appSidebarLeft = AppSidebarLeft; @@ -46,6 +48,7 @@ export const appSidebarLeft = AppSidebarLeft;
appImManager,
appStickersManager,
appSidebarRight,
appSidebarLeft
appSidebarLeft,
appMediaViewer
//appSharedMediaManager
};

62
src/lib/utils.js

@ -312,6 +312,60 @@ export const $rootScope = { @@ -312,6 +312,60 @@ export const $rootScope = {
}
};
// generate a path's arc data parameter
// http://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands
var arcParameter = function(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y) {
return [rx, ',', ry, ' ',
xAxisRotation, ' ',
largeArcFlag, ',',
sweepFlag, ' ',
x, ',', y ].join('');
};
export function generatePathData( x, y, width, height, tl, tr, br, bl ) {
var data = [];
// start point in top-middle of the rectangle
data.push('M' + (x + width / 2) + ',' + y);
// next we go to the right
data.push('H' + (x + width - tr));
if (tr > 0) {
// now we draw the arc in the top-right corner
data.push('A' + arcParameter(tr, tr, 0, 0, 1, (x + width), (y + tr)));
}
// next we go down
data.push('V' + (y + height - br));
if (br > 0) {
// now we draw the arc in the lower-right corner
data.push('A' + arcParameter(br, br, 0, 0, 1, (x + width - br), (y + height)));
}
// now we go to the left
data.push('H' + (x + bl));
if (bl > 0) {
// now we draw the arc in the lower-left corner
data.push('A' + arcParameter(bl, bl, 0, 0, 1, (x + 0), (y + height - bl)));
}
// next we go up
data.push('V' + (y + tl));
if (tl > 0) {
// now we draw the arc in the top-left corner
data.push('A' + arcParameter(tl, tl, 0, 0, 1, (x + tl), (y + 0)));
}
// and we close the path
data.push('Z');
return data.join(' ');
};
export const langPack = {
"messageActionChatCreate": "created the group",
"messageActionChatEditTitle": "changed group name",
@ -374,8 +428,8 @@ export function numberWithCommas(x) { @@ -374,8 +428,8 @@ export function numberWithCommas(x) {
export function findUpClassName(el, className) {
if(el.classList.contains(className)) return el; // 03.02.2020
while(el.parentNode) {
el = el.parentNode;
while(el.parentElement) {
el = el.parentElement;
if(el.classList.contains(className))
return el;
}
@ -385,8 +439,8 @@ export function findUpClassName(el, className) { @@ -385,8 +439,8 @@ export function findUpClassName(el, className) {
export function findUpTag(el, tag) {
if(el.tagName == tag) return el; // 03.02.2020
while(el.parentNode) {
el = el.parentNode;
while(el.parentElement) {
el = el.parentElement;
if(el.tagName === tag)
return el;
}

145
src/scss/partials/_chat.scss

@ -264,6 +264,26 @@ @@ -264,6 +264,26 @@
}
}
&.is-group-last {
padding-bottom: 2.5px;
}
&:not(.forwarded) {
&:not(.is-group-first) {
.bubble__container > .name {
display: none;
}
&:not(.is-message-empty):not(.is-reply) .message {
padding-top: 6px;
}
}
}
&:not(.is-group-last) .user-avatar {
display: none;
}
&:not(.hide-name) {
.audio {
margin: 4px 0;
@ -387,16 +407,20 @@ @@ -387,16 +407,20 @@
object-fit: contain;
}
.message.message-empty {
&.is-message-empty .message {
display: none;
}
&:hover .message.message-empty {
&.is-message-empty:hover .message {
display: block;
}
}
&.sticker {
.attachment {
border-radius: 0;
}
.bubble__container {
max-width: 200px;
max-height: 200px;
@ -410,8 +434,8 @@ @@ -410,8 +434,8 @@
}
}
.message:not(.message-empty) + .attachment,
&.is-reply .message:not(.message-empty) + .attachment {
&:not(.is-message-empty) .attachment/* ,
&:not(.is-message-empty).is-reply .attachment */ {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
@ -613,8 +637,14 @@ @@ -613,8 +637,14 @@
//padding-top: .2675rem;
padding-top: 6px;
}
&.message-empty {
.emoji {
font-size: 1.2rem;
}
}
&.is-message-empty {
.message {
position: absolute;
bottom: .1rem;
right: .1rem;
@ -625,12 +655,19 @@ @@ -625,12 +655,19 @@
.time {
color: #fff;
padding: 0;
display: flex;
align-items: center;
width: auto;
.inner {
margin-bottom: 0;
position: relative;
padding: 0 2.5px;
bottom: 0;
}
}
}
.emoji {
font-size: 1.2rem;
}
}
.time {
@ -667,7 +704,7 @@ @@ -667,7 +704,7 @@
}
&.is-edited.channel-post .time {
width: calc(5rem + 42px);
min-width: calc(5rem + 46px);
}
&.channel-post .time {
@ -677,20 +714,6 @@ @@ -677,20 +714,6 @@
&.is-edited .time {
width: 90px;
}
.message.message-empty .time {
padding: 0;
display: flex;
align-items: center;
width: auto;
.inner {
margin-bottom: 0;
position: relative;
padding: 0 2.5px;
bottom: 0;
}
}
.user-avatar {
position: absolute;
@ -734,20 +757,24 @@ @@ -734,20 +757,24 @@
padding-bottom: 6px;
}
.message:not(.message-empty) {
&:not(.is-message-empty) .message {
//padding-top: .2675rem;
padding-top: 6px;
}
}
}
&.hide-name:not(.is-reply) .message:not(.message-empty) {
&.hide-name:not(.is-reply):not(.is-message-empty) .message {
//padding-top: .2675rem;
padding-top: 6px;
}
&.hide-name:not(.sticker):not(.emoji-big) .reply {
margin-top: 6px;
&:not(.sticker):not(.emoji-big) {
&.hide-name, &:not(.is-group-first) {
.reply {
margin-top: 6px;
}
}
}
&:not(.sticker):not(.emoji-big):not(.round).is-group-last .bubble__container:after {
@ -760,6 +787,22 @@ @@ -760,6 +787,22 @@
background-size: 11px 20px;
background-position-y: 1px;
}
&.photo, &.video:not(.round) {
&.is-message-empty.is-group-last {
&.is-group-last .bubble__container:after {
display: none;
}
.attachment {
overflow: visible;
}
}
}
&__media-container {
cursor: pointer;
}
}
.bubble-audio.is-in .time {
@ -769,18 +812,12 @@ @@ -769,18 +812,12 @@
.bubble-audio.is-out .time {
width: inherit;
}
/* .bubble + .bubble {
margin-top: 5px;
/* .is-group-last.is-in + .is-out,
.is-group-last.is-out + .is-in {
padding-top: 10px;
} */
.in,
.out {
display: flex;
flex-direction: column;
padding: 5px 0;
}
.is-in {
.bubble__container {
margin-right: auto;
@ -808,7 +845,7 @@ @@ -808,7 +845,7 @@
&.forwarded .attachment,
&.is-reply .attachment,
&:not(.hide-name) .message-empty + .attachment/* ,
&:not(.hide-name).is-message-empty .attachment/* ,
&:not(.hide-name):not(.sticker) .attachment */ {
border-top-left-radius: 0;
border-top-right-radius: 0;
@ -853,6 +890,18 @@ @@ -853,6 +890,18 @@
margin-bottom: 4px;
}
}
&.photo, &.video:not(.round) {
&.is-message-empty.is-group-last {
.attachment {
border-bottom-left-radius: 0;
.bubble__media-container {
margin-left: -9px;
}
}
}
}
}
.is-out {
@ -984,6 +1033,18 @@ @@ -984,6 +1033,18 @@
background-color: #4FAE4E;
}
}
&.photo, &.video:not(.round) {
&.is-message-empty.is-group-last {
.attachment {
border-bottom-right-radius: 0;
.bubble__media-container {
margin-right: -9px;
}
}
}
}
}
}
@ -1118,6 +1179,10 @@ @@ -1118,6 +1179,10 @@
flex: 1 1 auto;
position: relative;
overflow: hidden;
> .scrollable {
position: relative;
}
}
.btn-icon {

58
src/scss/partials/_chatlist.scss

@ -53,7 +53,7 @@ @@ -53,7 +53,7 @@
li {
padding: 2px 0;
overflow: hidden;
//overflow: hidden;
background-color: #fff;
}
@ -61,12 +61,15 @@ @@ -61,12 +61,15 @@
height: 70px;
max-height: 70px;
border-radius: $border-radius;
display: grid;
align-items: center;
grid-template-columns: 64px calc(100% - 64px - 6.5px);
//align-items: center;
/* display: grid;
grid-template-columns: 64px calc(100% - 64px - 6.5px); */
display: flex;
align-items: flex-start;
flex-direction: row;
position: relative;
cursor: pointer;
padding: 0 8.5px;
padding: 8px 8.5px;
margin: 0 8.5px 0 8px;
overflow: hidden;
@ -99,8 +102,11 @@ @@ -99,8 +102,11 @@
margin: 0;
display: flex;
justify-content: space-between;
align-items: center;
height: 1.7rem; // hot-fix
flex-direction: row;
//align-items: center;
align-items: flex-start;
//height: 1.7rem; // hot-fix
height: 27px; // maybe new hot-fix
/* span:not(.tgico-pinnedchat):not(.emoji):last-child { */
.user-title + span {
@ -119,18 +125,26 @@ @@ -119,18 +125,26 @@
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
margin: .1rem 0;
//margin: .1rem 0;
line-height: 27px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.user-avatar {
flex: 0 0 auto;
}
.user-caption {
max-width: 100%;
max-height: 100%;
overflow: hidden;
color: $color-gray;
flex: 1 1 auto;
padding-right: 6.5px;
padding-left: 10px;
}
.user-title {
@ -147,7 +161,8 @@ @@ -147,7 +161,8 @@
margin: 0;
width: auto;
font-size: 14px;
vertical-align: unset;
//vertical-align: unset;
margin-top: -1.5px;
}
}
@ -168,38 +183,43 @@ @@ -168,38 +183,43 @@
font-size: 1.2rem;
margin: 0 .125rem;
overflow: visible;
margin-top: -1.5px;
}
}
.message-status {
margin-right: .1rem;
margin-top: .3rem;
//margin-top: .3rem;
margin-top: -.3rem;
&[class*=" tgico-"] {
color: $success-color;
font-size: 1.25rem;
}
&:before {
vertical-align: middle;
}
}
.unread, .unread-muted {
border-radius: 12px;
min-width: 24px;
padding: 0 8px;
.unread, .unread-muted, .tgico-pinnedchat {
height: 24px;
text-align: center;
line-height: 24px;
color: #fff;
border-radius: 12px;
margin-top: 1.5px;
}
.unread, .unread-muted {
min-width: 24px;
padding: 0 8px;
font-weight: 500;
}
.tgico-pinnedchat {
border-radius: 50%;
width: 24px;
height: 24px;
line-height: 24px;
font-size: 1.5rem;
color: #fff;
text-align: center;
}
.unread:empty, .unread-muted:empty {

1
src/scss/partials/_mediaViewer.scss

@ -181,6 +181,7 @@ @@ -181,6 +181,7 @@
z-index: 4;
display: flex;
justify-content: center;
align-items: center; // mb net
min-height: auto!important;
//transition: .5s all;
left: 0;

8
src/scss/partials/_rightSIdebar.scss

@ -95,11 +95,15 @@ @@ -95,11 +95,15 @@
margin-top: 1px;
}
.user-avatar {
.profile-avatar.user-avatar {
width: 120px;
height: 120px;
margin: 0 auto 20px;
font-size: 4rem!important;
font-size: 4rem;
&.tgico-avatar_deletedaccount {
font-size: 6rem;
}
}
[type="checkbox"] + span {

39
src/scss/style.scss

@ -77,6 +77,11 @@ button, input, optgroup, select, textarea, html { @@ -77,6 +77,11 @@ button, input, optgroup, select, textarea, html {
}
}
.disable-hover,
.disable-hover * {
pointer-events: none !important;
}
.container {
margin: 0 auto;
}
@ -304,6 +309,10 @@ input { @@ -304,6 +309,10 @@ input {
width: 12px;
height: 12px;
}
&.tgico-avatar_deletedaccount {
font-size: 3rem;
}
}
/* .user-title, b {
@ -1304,7 +1313,7 @@ span.popup-close { @@ -1304,7 +1313,7 @@ span.popup-close {
}
}
div.scrollable::-webkit-scrollbar {
/* div.scrollable::-webkit-scrollbar {
width: 0;
height: 0;
}
@ -1321,7 +1330,7 @@ div.scrollable::-webkit-scrollbar-thumb { @@ -1321,7 +1330,7 @@ div.scrollable::-webkit-scrollbar-thumb {
}
::-webkit-scrollbar-corner {
background-color: transparent;
}
} */
.scrollable {
width: 100%;
@ -1329,7 +1338,19 @@ div.scrollable::-webkit-scrollbar-thumb { @@ -1329,7 +1338,19 @@ div.scrollable::-webkit-scrollbar-thumb {
overflow-y: hidden;
overflow-x: hidden;
max-height: 100%;
position: relative;
//position: relative;
will-change: transform;
transform: translateZ(0);
-webkit-transform: translateZ(0);
position: absolute;
top: 0px;
left: 0px;
bottom: 0px;
right: 0px;
display: flex;
flex-direction: column;
&.scrollable-x {
overflow-x: auto;
@ -1350,6 +1371,14 @@ div.scrollable::-webkit-scrollbar-thumb { @@ -1350,6 +1371,14 @@ div.scrollable::-webkit-scrollbar-thumb {
height: 4px;
bottom: 0px;
}
.scroll-padding {
flex: 0 0 auto;
&:first-child + * {
flex: 1 1 auto;
}
}
}
.scrollbar-thumb {
@ -1362,10 +1391,12 @@ div.scrollable::-webkit-scrollbar-thumb { @@ -1362,10 +1391,12 @@ div.scrollable::-webkit-scrollbar-thumb {
//cursor: grab;
cursor: default;
opacity: 0;
transition-property: opacity,width,right;
transition-property: opacity;
transition-duration: .2s;
transition-timing-function: ease-in-out;
display: none;
border-radius: $border-radius;
z-index: 2;
}

Loading…
Cancel
Save