diff options
Diffstat (limited to 'toolkit/components/satchel/megalist/content/VirtualizedList.mjs')
-rw-r--r-- | toolkit/components/satchel/megalist/content/VirtualizedList.mjs | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/toolkit/components/satchel/megalist/content/VirtualizedList.mjs b/toolkit/components/satchel/megalist/content/VirtualizedList.mjs new file mode 100644 index 0000000000..7903a189eb --- /dev/null +++ b/toolkit/components/satchel/megalist/content/VirtualizedList.mjs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Virtualized List can efficiently show billions of lines provided + * that all of them have the same height. + * + * Caller is responsible for setting createLineElement(index) function to + * create elements as they are scrolled into the view. + */ +class VirtualizedList extends HTMLElement { + lineHeight = 64; + #lineCount = 0; + + get lineCount() { + return this.#lineCount; + } + + set lineCount(value) { + this.#lineCount = value; + this.#rebuildVisibleLines(); + } + + #selectedIndex = 0; + + get selectedIndex() { + return this.#selectedIndex; + } + + set selectedIndex(value) { + this.#selectedIndex = value; + if (this.#container) { + this.updateLineSelection(true); + } + } + + #container; + + connectedCallback() { + this.#container = this.ownerDocument.createElement("ul"); + this.#container.classList.add("lines-container"); + this.appendChild(this.#container); + + this.#rebuildVisibleLines(); + this.addEventListener("scroll", () => this.#rebuildVisibleLines()); + } + + requestRefresh() { + this.#container.replaceChildren(); + this.#rebuildVisibleLines(); + } + + updateLineSelection(scrollIntoView) { + const lineElements = this.#container.querySelectorAll(".line"); + let selectedElement; + + for (let lineElement of lineElements) { + let isSelected = Number(lineElement.dataset.index) === this.selectedIndex; + if (isSelected) { + selectedElement = lineElement; + } + lineElement.classList.toggle("selected", isSelected); + } + + if (scrollIntoView) { + if (selectedElement) { + selectedElement.scrollIntoView({ block: "nearest" }); + } else { + let selectedTop = this.selectedIndex * this.lineHeight; + if (this.scrollTop > selectedTop) { + this.scrollTop = selectedTop; + } else { + this.scrollTop = selectedTop - this.clientHeight + this.lineHeight; + } + } + } + } + + #rebuildVisibleLines() { + if (!this.isConnected || !this.createLineElement) { + return; + } + + this.#container.style.height = `${this.lineHeight * this.lineCount}px`; + + let firstLineIndex = Math.floor(this.scrollTop / this.lineHeight); + let visibleLineCount = Math.ceil(this.clientHeight / this.lineHeight); + let lastLineIndex = firstLineIndex + visibleLineCount; + let extraLines = Math.ceil(visibleLineCount / 2); // They are present in DOM, but not visible + + firstLineIndex = Math.max(0, firstLineIndex - extraLines); + lastLineIndex = Math.min(this.lineCount, lastLineIndex + extraLines); + + let previousChild = null; + let visibleLines = new Map(); + + for (let child of Array.from(this.#container.children)) { + let index = Number(child.dataset.index); + if (index < firstLineIndex || index > lastLineIndex) { + child.remove(); + } else { + visibleLines.set(index, child); + } + } + + for (let index = firstLineIndex; index <= lastLineIndex; index++) { + let child = visibleLines.get(index); + if (!child) { + child = this.createLineElement(index); + + if (!child) { + // Friday fix :-) + //todo: figure out what was on that Friday and how can we fix it + continue; + } + + child.style.top = `${index * this.lineHeight}px`; + child.dataset.index = index; + + if (previousChild) { + previousChild.after(child); + } else if (this.#container.firstElementChild?.offsetTop > top) { + this.#container.firstElementChild.before(child); + } else { + this.#container.appendChild(child); + } + } + previousChild = child; + } + + this.updateLineSelection(false); + } +} + +customElements.define("virtualized-list", VirtualizedList); |