// virtual scrolling for efficient rendering of massive queues class QueueVirtualScroller { // initialize virtual scroller with container, tbody, and row factory constructor( container, tbody, itemCount, createRow, { buffer = 32, onScroll } = {}, ) { this.container = container; this.tbody = tbody; this.itemCount = itemCount; this.createRow = createRow; this.buffer = buffer; this.onScroll = onScroll; this.visibleStart = this.visibleEnd = 0; this.rafId = null; this.firstRender = true; this.rowHeight = 0; this.handleScheduleRender = () => this.scheduleRender(); this.container.addEventListener("scroll", this.handleScheduleRender, { passive: true, }); window.addEventListener("resize", this.handleScheduleRender); this.render(); } // batch scroll and resize events into a single render call via requestAnimationFrame scheduleRender() { if (this.rafId) return; this.rafId = requestAnimationFrame(() => { this.rafId = null; this.render(); }); } // render visible rows with spacer rows for scroll height render(forceRedraw = false) { // measure row height on first render or when settings change if (this.firstRender || forceRedraw) { if (this.itemCount > 0) { const sample = this.createRow(0); this.tbody.appendChild(sample); this.rowHeight = sample.offsetHeight; sample.remove(); } this.firstRender = false; } if (!this.itemCount) { this.tbody.replaceChildren(); return; } // calculate visible range based on scroll position const scrollTop = this.container.scrollTop; const start = Math.max( 0, Math.floor(scrollTop / this.rowHeight) - this.buffer, ); const end = Math.min( this.itemCount, Math.ceil((scrollTop + this.container.clientHeight) / this.rowHeight) + this.buffer, ); // skip render if visible range hasn't changed if (!forceRedraw && start === this.visibleStart && end === this.visibleEnd) return; this.visibleStart = start; this.visibleEnd = end; const frag = document.createDocumentFragment(); // spacer for hidden top rows if (start > 0) { const tr = document.createElement("tr"); tr.style.height = `${start * this.rowHeight}px`; frag.appendChild(tr); } // visible rows with stripe pattern for (let i = start; i < end; i++) { const row = this.createRow(i); if (i % 2 === 1) row.classList.add(CLASSES.STRIPE); frag.appendChild(row); } // spacer for hidden bottom rows if (end < this.itemCount) { const tr = document.createElement("tr"); tr.style.height = `${(this.itemCount - end) * this.rowHeight}px`; frag.appendChild(tr); } this.tbody.replaceChildren(); this.tbody.appendChild(frag); this.onScroll?.(); } // update item count and re-render with fresh data updateItemCount(itemCount) { this.itemCount = itemCount; this.render(true); } // clean up event listeners and cancel pending renders destroy() { cancelAnimationFrame(this.rafId); this.container.removeEventListener("scroll", this.handleScheduleRender); window.removeEventListener("resize", this.handleScheduleRender); } }