137 lines
4.2 KiB
JavaScript
137 lines
4.2 KiB
JavaScript
|
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;
|