Incorporate lazyrender code from element

Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
This commit is contained in:
RMidhunSuresh 2021-07-11 18:12:27 +05:30
parent ee072343f5
commit 452eee6767
2 changed files with 103 additions and 56 deletions

View file

@ -1,5 +1,12 @@
.RightPanelView{ .RightPanelView{
grid-area: right; grid-area: right;
min-height: 0;
}
.LazyListParent {
overflow:scroll;
overflow-x:hidden;
height: 100vh;
} }
.RoomDetailsView { .RoomDetailsView {
@ -34,11 +41,6 @@
width: 100%; width: 100%;
} }
ul.MemberListView {
overflow: scroll;
height: 100vh;
}
.MemberTileView { .MemberTileView {
display: flex; display: flex;
} }

View file

@ -1,55 +1,88 @@
import {el} from "./html.js";
import {mountView} from "./utils.js"; import {mountView} from "./utils.js";
import {insertAt, ListView} from "./ListView.js"; import {insertAt, ListView} from "./ListView.js";
class Range { class ItemRange {
constructor(start = 0, end = 0) { constructor(topCount, renderCount, bottomCount) {
this.start = start; this.topCount = topCount;
this.end = end; this.renderCount = renderCount;
this._expanded = false; this.bottomCount = bottomCount;
} }
_onInitialExpand() { contains(range) {
if (this._expanded) { return; } // don't contain empty ranges
this._initialStart = this.start; // as it will prevent clearing the list
this._expanded = true; // once it is scrolled far enough out of view
if (!range.renderCount && this.renderCount) {
return false;
}
return range.topCount >= this.topCount &&
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
} }
expandFromEnd(units) { containsIndex(idx) {
this._onInitialExpand(); return idx >= this.topCount && idx <= (this.topCount + this.renderCount);
this.start = this.end;
this.end += units;
this._expanded = true;
} }
contains(idx) { expand(amount) {
const start = this._expanded ? this._initialStart : this.start; // don't expand ranges that won't render anything
return idx >= start && idx <= this.end; if (this.renderCount === 0) {
return this;
}
const topGrow = Math.min(amount, this.topCount);
const bottomGrow = Math.min(amount, this.bottomCount);
return new ItemRange(
this.topCount - topGrow,
this.renderCount + topGrow + bottomGrow,
this.bottomCount - bottomGrow,
);
}
totalSize() {
return this.topCount + this.renderCount + this.bottomCount;
} }
} }
export class LazyListView extends ListView { export class LazyListView extends ListView {
constructor({itemHeight, height, appendCount = 5, ...options}, childCreator) { constructor({itemHeight, height, ...options}, childCreator) {
super(options, childCreator); super(options, childCreator);
this._itemHeight = itemHeight; this._itemHeight = itemHeight;
this._height = height; this._height = height;
this._appendCount = appendCount; this._overflowMargin = 5;
this._range = new Range(); this._overflowItems = 20;
} }
_isFullyScrolled() { _getVisibleRange() {
return this._root.scrollHeight - Math.abs(this._root.scrollTop) === this._root.clientHeight; const length = this._list ? this._list.length : 0;
const scrollTop = this._parent.scrollTop;
const topCount = Math.min(Math.max(0, Math.floor(scrollTop / this._itemHeight)), length);
const itemsAfterTop = length - topCount;
const visibleItems = this._height !== 0 ? Math.ceil(this._height / this._itemHeight) : 0;
const renderCount = Math.min(visibleItems, itemsAfterTop);
const bottomCount = itemsAfterTop - renderCount;
return new ItemRange(topCount, renderCount, bottomCount);
} }
_renderMoreIfNeeded() { _renderMoreIfNeeded() {
if (!this._isFullyScrolled()) { const range = this._getVisibleRange();
return; const intersectRange = range.expand(this._overflowMargin);
const renderRange = range.expand(this._overflowItems);
const listHasChangedSize = !!this._renderRange && this._list.length !== this._renderRange.totalSize();
console.log("currentRange", range);
console.log("renderRange", renderRange);
console.log("intersectRange", intersectRange);
console.log("LastRenderedRange", this._renderRange);
// only update render Range if the list has shrunk/grown and we need to adjust padding OR
// if the new range + overflowMargin isn't contained by the old anymore
if (listHasChangedSize || !this._renderRange || !this._renderRange.contains(intersectRange)) {
console.log("New render change");
this._renderRange = renderRange;
this._renderElementsInRange();
} }
this._range.expandFromEnd(this._appendCount);
this._renderElementsInRange();
} }
_renderElementsInRange() { _renderItems(items) {
const items = this._list.slice(this._range.start, this._range.end);
const fragment = document.createDocumentFragment(); const fragment = document.createDocumentFragment();
for (const item of items) { for (const item of items) {
const view = this._childCreator(item.value); const view = this._childCreator(item.value);
@ -59,54 +92,66 @@ export class LazyListView extends ListView {
this._root.appendChild(fragment); this._root.appendChild(fragment);
} }
_calculateInitialRenderCount() { _renderElementsInRange() {
return Math.ceil(this._height / this._itemHeight); const { topCount, renderCount, bottomCount } = this._renderRange;
const paddingTop = topCount * this._itemHeight;
const paddingBottom = bottomCount * this._itemHeight;
const renderedItems = (this._list || []).slice(
topCount,
topCount + renderCount,
);
this._root.style.paddingTop = `${paddingTop}px`;
this._root.style.paddingBottom = `${paddingBottom}px`;
this._root.innerHTML = "";
this._renderItems(renderedItems);
} }
loadList() { mount() {
if (!this._list) { const root = super.mount();
return; this._parent = el("div", {className: "LazyListParent"}, root);
}
this._subscription = this._list.subscribe(this);
this._range.end = this._calculateInitialRenderCount() + this._appendCount;
this._childInstances = [];
this._renderElementsInRange();
/* /*
Hooking to scroll events can be expensive. Hooking to scroll events can be expensive.
But in most of these scroll events, we return early. But in most of these scroll events, we return early.
Do we need to do more (like event throttling)? Do we need to do more (like event throttling)?
*/ */
this._root.addEventListener("scroll", () => this._renderMoreIfNeeded()); this._parent.addEventListener("scroll", () => this._renderMoreIfNeeded());
this._renderMoreIfNeeded();
return this._parent;
}
loadList() {
if (!this._list) { return; }
this._subscription = this._list.subscribe(this);
this._childInstances = [];
} }
// onAdd, onRemove, ... should be called only if the element is already rendered // onAdd, onRemove, ... should be called only if the element is already rendered
onAdd(idx, value) { onAdd() {
if (this._range.contains(idx)) { this._renderMoreIfNeeded();
super.onAdd(idx, value);
}
} }
onRemove(idx, value) { onRemove() {
if (this._range.contains(idx)) { this._renderMoreIfNeeded();
super.onRemove(idx, value);
}
} }
onUpdate(idx, value, params) { onUpdate(idx, value, params) {
if (this._range.contains(idx)) { console.log("onUpdate");
if (this._renderRange.containsIndex(idx)) {
super.onUpdate(idx, value, params); super.onUpdate(idx, value, params);
} }
} }
recreateItem(idx, value) { recreateItem(idx, value) {
if (this._range.contains(idx)) { console.log("recreateItem");
if (this._renderRange.containsIndex(idx)) {
super.recreateItem(idx, value) super.recreateItem(idx, value)
} }
} }
onMove(fromIdx, toIdx, value) { onMove(fromIdx, toIdx, value) {
const fromInRange = this._range.contains(fromIdx); console.log("onMove");
const toInRange = this._range.contains(toIdx); const fromInRange = this._renderRange.containsIndex(fromIdx);
const toInRange = this._renderRange.containsIndex(toIdx);
if (fromInRange && toInRange) { if (fromInRange && toInRange) {
super.onMove(fromIdx, toIdx, value); super.onMove(fromIdx, toIdx, value);
} }