diff --git a/src/platform/web/ui/css/right-panel.css b/src/platform/web/ui/css/right-panel.css index c083bd20..aa568068 100644 --- a/src/platform/web/ui/css/right-panel.css +++ b/src/platform/web/ui/css/right-panel.css @@ -1,5 +1,12 @@ .RightPanelView{ grid-area: right; + min-height: 0; +} + +.LazyListParent { + overflow:scroll; + overflow-x:hidden; + height: 100vh; } .RoomDetailsView { @@ -34,11 +41,6 @@ width: 100%; } -ul.MemberListView { - overflow: scroll; - height: 100vh; -} - .MemberTileView { display: flex; } diff --git a/src/platform/web/ui/general/LazyListView.js b/src/platform/web/ui/general/LazyListView.js index 1ea10d70..53c7b12f 100644 --- a/src/platform/web/ui/general/LazyListView.js +++ b/src/platform/web/ui/general/LazyListView.js @@ -1,55 +1,88 @@ +import {el} from "./html.js"; import {mountView} from "./utils.js"; import {insertAt, ListView} from "./ListView.js"; -class Range { - constructor(start = 0, end = 0) { - this.start = start; - this.end = end; - this._expanded = false; +class ItemRange { + constructor(topCount, renderCount, bottomCount) { + this.topCount = topCount; + this.renderCount = renderCount; + this.bottomCount = bottomCount; } - _onInitialExpand() { - if (this._expanded) { return; } - this._initialStart = this.start; - this._expanded = true; + 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); } - expandFromEnd(units) { - this._onInitialExpand(); - this.start = this.end; - this.end += units; - this._expanded = true; + containsIndex(idx) { + return idx >= this.topCount && idx <= (this.topCount + this.renderCount); } - contains(idx) { - const start = this._expanded ? this._initialStart : this.start; - return idx >= start && idx <= this.end; + 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, appendCount = 5, ...options}, childCreator) { + constructor({itemHeight, height, ...options}, childCreator) { super(options, childCreator); this._itemHeight = itemHeight; this._height = height; - this._appendCount = appendCount; - this._range = new Range(); + this._overflowMargin = 5; + this._overflowItems = 20; } - _isFullyScrolled() { - return this._root.scrollHeight - Math.abs(this._root.scrollTop) === this._root.clientHeight; + _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() { - if (!this._isFullyScrolled()) { - return; + 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(); } - this._range.expandFromEnd(this._appendCount); - this._renderElementsInRange(); } - _renderElementsInRange() { - const items = this._list.slice(this._range.start, this._range.end); + _renderItems(items) { const fragment = document.createDocumentFragment(); for (const item of items) { const view = this._childCreator(item.value); @@ -59,54 +92,66 @@ export class LazyListView extends ListView { this._root.appendChild(fragment); } - _calculateInitialRenderCount() { - return Math.ceil(this._height / this._itemHeight); + _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); } - loadList() { - if (!this._list) { - return; - } - this._subscription = this._list.subscribe(this); - this._range.end = this._calculateInitialRenderCount() + this._appendCount; - this._childInstances = []; - this._renderElementsInRange(); + 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._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(idx, value) { - if (this._range.contains(idx)) { - super.onAdd(idx, value); - } + onAdd() { + this._renderMoreIfNeeded(); } - onRemove(idx, value) { - if (this._range.contains(idx)) { - super.onRemove(idx, value); - } + onRemove() { + this._renderMoreIfNeeded(); } onUpdate(idx, value, params) { - if (this._range.contains(idx)) { + console.log("onUpdate"); + if (this._renderRange.containsIndex(idx)) { super.onUpdate(idx, value, params); } } recreateItem(idx, value) { - if (this._range.contains(idx)) { + console.log("recreateItem"); + if (this._renderRange.containsIndex(idx)) { super.recreateItem(idx, value) } } onMove(fromIdx, toIdx, value) { - const fromInRange = this._range.contains(fromIdx); - const toInRange = this._range.contains(toIdx); + console.log("onMove"); + const fromInRange = this._renderRange.containsIndex(fromIdx); + const toInRange = this._renderRange.containsIndex(toIdx); if (fromInRange && toInRange) { super.onMove(fromIdx, toIdx, value); }