This repository has been archived on 2022-08-19. You can view files and clone it, but cannot push or open issues or pull requests.
hydrogen-web/src/platform/web/ui/general/LazyListView.js
RMidhunSuresh 452eee6767 Incorporate lazyrender code from element
Signed-off-by: RMidhunSuresh <rmidhunsuresh@gmail.com>
2021-07-16 17:08:18 +05:30

173 lines
6 KiB
JavaScript

import {el} from "./html.js";
import {mountView} from "./utils.js";
import {insertAt, ListView} from "./ListView.js";
class ItemRange {
constructor(topCount, renderCount, bottomCount) {
this.topCount = topCount;
this.renderCount = renderCount;
this.bottomCount = bottomCount;
}
contains(range) {
// don't contain empty ranges
// as it will prevent clearing the list
// 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);
}
containsIndex(idx) {
return idx >= this.topCount && idx <= (this.topCount + this.renderCount);
}
expand(amount) {
// don't expand ranges that won't render anything
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 {
constructor({itemHeight, height, ...options}, childCreator) {
super(options, childCreator);
this._itemHeight = itemHeight;
this._height = height;
this._overflowMargin = 5;
this._overflowItems = 20;
}
_getVisibleRange() {
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() {
const range = this._getVisibleRange();
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();
}
}
_renderItems(items) {
const fragment = document.createDocumentFragment();
for (const item of items) {
const view = this._childCreator(item.value);
this._childInstances.push(view);
fragment.appendChild(mountView(view, this._mountArgs));
}
this._root.appendChild(fragment);
}
_renderElementsInRange() {
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);
}
mount() {
const root = super.mount();
this._parent = el("div", {className: "LazyListParent"}, root);
/*
Hooking to scroll events can be expensive.
But in most of these scroll events, we return early.
Do we need to do more (like event throttling)?
*/
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() {
this._renderMoreIfNeeded();
}
onRemove() {
this._renderMoreIfNeeded();
}
onUpdate(idx, value, params) {
console.log("onUpdate");
if (this._renderRange.containsIndex(idx)) {
super.onUpdate(idx, value, params);
}
}
recreateItem(idx, value) {
console.log("recreateItem");
if (this._renderRange.containsIndex(idx)) {
super.recreateItem(idx, value)
}
}
onMove(fromIdx, toIdx, value) {
console.log("onMove");
const fromInRange = this._renderRange.containsIndex(fromIdx);
const toInRange = this._renderRange.containsIndex(toIdx);
if (fromInRange && toInRange) {
super.onMove(fromIdx, toIdx, value);
}
else if (fromInRange && !toInRange) {
this.onBeforeListChanged();
const [child] = this._childInstances.splice(fromIdx, 1);
child.root().remove();
this.onListChanged();
}
else if (!fromInRange && toInRange) {
this.onBeforeListChanged();
const child = this._childCreator(value);
this._childInstances.splice(toIdx, 0, child);
insertAt(this._root, toIdx, mountView(child, this._mountArgs));
this.onListChanged();
}
}
}