summaryrefslogtreecommitdiffstats
path: root/toolkit/components/satchel/megalist/content/VirtualizedList.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/satchel/megalist/content/VirtualizedList.mjs')
-rw-r--r--toolkit/components/satchel/megalist/content/VirtualizedList.mjs136
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);