// Virtual Scroller Component // Implements efficient scrolling for large lists by only rendering visible items class VirtualScroller { constructor(options) { this.container = options.container; this.itemHeight = options.itemHeight; this.bufferSize = options.bufferSize || 5; this.renderItem = options.renderItem; this.items = []; this.scrollTop = 0; this.visibleItems = new Map(); this.observer = new IntersectionObserver( this._handleIntersection.bind(this), { threshold: 0.1 } ); this._setupContainer(); this._bindEvents(); } _setupContainer() { if (!this.container) { throw new Error('Container element is required'); } this.container.style.position = 'relative'; this.container.style.height = '600px'; // Default height this.container.style.overflowY = 'auto'; // Create spacer element to maintain scroll height this.spacer = document.createElement('div'); this.spacer.style.position = 'absolute'; this.spacer.style.top = '0'; this.spacer.style.left = '0'; this.spacer.style.width = '1px'; this.container.appendChild(this.spacer); } _bindEvents() { this.container.addEventListener( 'scroll', this._debounce(this._handleScroll.bind(this), 16) ); // Handle container resize if (window.ResizeObserver) { const resizeObserver = new ResizeObserver(this._debounce(() => { this._render(); }, 16)); resizeObserver.observe(this.container); } } setItems(items) { this.items = items; this.spacer.style.height = `${items.length * this.itemHeight}px`; this._render(); } _handleScroll() { this.scrollTop = this.container.scrollTop; this._render(); } _handleIntersection(entries) { entries.forEach(entry => { const itemId = entry.target.dataset.itemId; if (!entry.isIntersecting) { this.visibleItems.delete(itemId); entry.target.remove(); } }); } _render() { const visibleRange = this._getVisibleRange(); const itemsToRender = new Set(); // Calculate which items should be visible for (let i = visibleRange.start; i <= visibleRange.end; i++) { if (i >= 0 && i < this.items.length) { itemsToRender.add(i); } } // Remove items that are no longer visible for (const [itemId] of this.visibleItems) { const index = parseInt(itemId); if (!itemsToRender.has(index)) { const element = this.container.querySelector(`[data-item-id="${itemId}"]`); if (element) { this.observer.unobserve(element); element.remove(); } this.visibleItems.delete(itemId); } } // Add new visible items for (const index of itemsToRender) { if (!this.visibleItems.has(index.toString())) { this._renderItem(index); } } // Update performance metrics this._updateMetrics(itemsToRender.size); } _renderItem(index) { const item = this.items[index]; const element = document.createElement('div'); element.style.position = 'absolute'; element.style.top = `${index * this.itemHeight}px`; element.style.left = '0'; element.style.width = '100%'; element.dataset.itemId = index.toString(); // Render content element.innerHTML = this.renderItem(item); // Add to container and observe this.container.appendChild(element); this.observer.observe(element); this.visibleItems.set(index.toString(), element); // Adjust actual height if needed const actualHeight = element.offsetHeight; if (actualHeight !== this.itemHeight) { element.style.height = `${this.itemHeight}px`; } } _getVisibleRange() { const start = Math.floor(this.scrollTop / this.itemHeight) - this.bufferSize; const visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight); const end = start + visibleCount + 2 * this.bufferSize; return { start, end }; } _updateMetrics(visibleCount) { const metrics = { totalItems: this.items.length, visibleItems: visibleCount, scrollPosition: this.scrollTop, containerHeight: this.container.clientHeight, renderTime: performance.now() // You can use this with the previous render time }; // Dispatch metrics event this.container.dispatchEvent(new CustomEvent('virtualScroller:metrics', { detail: metrics })); } _debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Public methods scrollToIndex(index) { if (index >= 0 && index < this.items.length) { this.container.scrollTop = index * this.itemHeight; } } refresh() { this._render(); } destroy() { this.observer.disconnect(); this.container.innerHTML = ''; this.items = []; this.visibleItems.clear(); } } export default VirtualScroller;