/* 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);