cinny/src/app/organisms/room/TimelineScroll.js

137 lines
4.2 KiB
JavaScript
Raw Normal View History

import { getScrollInfo } from '../../../util/common';
class TimelineScroll {
constructor(target) {
if (target === null) {
throw new Error('Can not initialize TimelineScroll, target HTMLElement in null');
}
this.scroll = target;
this.backwards = false;
this.inTopHalf = false;
this.isScrollable = false;
this.top = 0;
this.bottom = 0;
this.height = 0;
this.viewHeight = 0;
this.topMsg = null;
this.bottomMsg = null;
this.diff = 0;
}
scrollToBottom() {
const scrollInfo = getScrollInfo(this.scroll);
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
this._scrollTo(scrollInfo, maxScrollTop);
}
// use previous calc by this._updateTopBottomMsg() & this._calcDiff.
tryRestoringScroll() {
const scrollInfo = getScrollInfo(this.scroll);
let scrollTop = 0;
const ot = this.inTopHalf ? this.topMsg?.offsetTop : this.bottomMsg?.offsetTop;
if (!ot) scrollTop = Math.round(this.height - this.viewHeight);
else scrollTop = ot - this.diff;
this._scrollTo(scrollInfo, scrollTop);
}
scrollToIndex(index, offset = 0) {
const scrollInfo = getScrollInfo(this.scroll);
const msgs = this.scroll.lastElementChild.lastElementChild.children;
const offsetTop = msgs[index]?.offsetTop;
if (offsetTop === undefined) return;
// if msg is already in visible are we don't need to scroll to that
if (offsetTop > scrollInfo.top && offsetTop < (scrollInfo.top + scrollInfo.viewHeight)) return;
const to = offsetTop - offset;
this._scrollTo(scrollInfo, to);
}
_scrollTo(scrollInfo, scrollTop) {
this.scroll.scrollTop = scrollTop;
// browser emit 'onscroll' event only if the 'element.scrollTop' value changes.
// so here we flag that the upcoming 'onscroll' event is
// emitted as side effect of assigning 'this.scroll.scrollTop' above
// only if it's changes.
// by doing so we prevent this._updateCalc() from calc again.
if (scrollTop !== this.top) {
this.scrolledByCode = true;
}
const sInfo = { ...scrollInfo };
const maxScrollTop = scrollInfo.height - scrollInfo.viewHeight;
sInfo.top = (scrollTop > maxScrollTop) ? maxScrollTop : scrollTop;
this._updateCalc(sInfo);
}
// we maintain reference of top and bottom messages
// to restore the scroll position when
// messages gets removed from either end and added to other.
_updateTopBottomMsg() {
const msgs = this.scroll.lastElementChild.lastElementChild.children;
const lMsgIndex = msgs.length - 1;
// TODO: classname 'ph-msg' prevent this class from being used
const PLACEHOLDER_COUNT = 2;
this.topMsg = msgs[0]?.className === 'ph-msg'
? msgs[PLACEHOLDER_COUNT]
: msgs[0];
this.bottomMsg = msgs[lMsgIndex]?.className === 'ph-msg'
? msgs[lMsgIndex - PLACEHOLDER_COUNT]
: msgs[lMsgIndex];
}
// we calculate the difference between first/last message and current scrollTop.
// if we are going above we calc diff between first and scrollTop
// else otherwise.
// NOTE: This will help to restore the scroll when msgs get's removed
// from one end and added to other end
_calcDiff(scrollInfo) {
if (!this.topMsg || !this.bottomMsg) return 0;
if (this.inTopHalf) {
return this.topMsg.offsetTop - scrollInfo.top;
}
return this.bottomMsg.offsetTop - scrollInfo.top;
}
_updateCalc(scrollInfo) {
const halfViewHeight = Math.round(scrollInfo.viewHeight / 2);
const scrollMiddle = scrollInfo.top + halfViewHeight;
const lastMiddle = this.top + halfViewHeight;
this.backwards = scrollMiddle < lastMiddle;
this.inTopHalf = scrollMiddle < scrollInfo.height / 2;
this.isScrollable = scrollInfo.isScrollable;
this.top = scrollInfo.top;
this.bottom = scrollInfo.height - (scrollInfo.top + scrollInfo.viewHeight);
this.height = scrollInfo.height;
this.viewHeight = scrollInfo.viewHeight;
this._updateTopBottomMsg();
this.diff = this._calcDiff(scrollInfo);
}
calcScroll() {
if (this.scrolledByCode) {
this.scrolledByCode = false;
return undefined;
}
const scrollInfo = getScrollInfo(this.scroll);
this._updateCalc(scrollInfo);
return this.backwards;
}
}
export default TimelineScroll;