/** * virtual-scroll.js — Lightweight vanilla-JS virtual scroll helper. * Supports
containers and tables. * * Usage: * var vs = new VirtualScroll({ * container: document.getElementById('myTableBody'), * rowHeight: 48, * buffer: 5, * renderRow: function(item, index) { return '...'; } * }); * vs.setData(arrayOfItems); */ (function(window) { 'use strict'; function VirtualScroll(opts) { this.container = opts.container; this.rowHeight = opts.rowHeight || 48; this.buffer = opts.buffer || 5; this.renderRow = opts.renderRow || function() { return ''; }; this.emptyHtml = opts.emptyHtml || ''; this.data = []; this._scrollHandler = this._onScroll.bind(this); this._resizeHandler = this._onResize.bind(this); this._isTbody = this.container.tagName === 'TBODY'; this._init(); } VirtualScroll.prototype._init = function() { var c = this.container; if (!this._isTbody) { c.style.overflowY = 'auto'; c.style.position = 'relative'; if (!c.style.maxHeight && !c.style.height) { c.style.maxHeight = '60vh'; } } else { // For tbody, scroll is on the parent element (table wrapper) var table = c.closest('table'); if (table) { var wrapper = table.parentElement; if (wrapper && wrapper.classList.contains('vs-container')) { wrapper.addEventListener('scroll', this._scrollHandler, { passive: true }); } } } window.addEventListener('resize', this._resizeHandler, { passive: true }); }; VirtualScroll.prototype.setData = function(data) { this.data = data || []; this._render(); }; VirtualScroll.prototype.refresh = function() { this._render(); }; VirtualScroll.prototype._onScroll = function() { this._render(); }; VirtualScroll.prototype._onResize = function() { this._render(); }; VirtualScroll.prototype._getScrollTop = function() { if (this._isTbody) { var table = this.container.closest('table'); if (table) { var wrapper = table.parentElement; if (wrapper && wrapper.classList.contains('vs-container')) { return wrapper.scrollTop; } } return 0; } return this.container.scrollTop; }; VirtualScroll.prototype._getContainerHeight = function() { if (this._isTbody) { var table = this.container.closest('table'); if (table) { var wrapper = table.parentElement; if (wrapper && wrapper.classList.contains('vs-container')) { return wrapper.clientHeight; } } return 600; } return this.container.clientHeight; }; VirtualScroll.prototype._render = function() { var data = this.data; var rowH = this.rowHeight; var buffer = this.buffer; if (!data.length) { if (this._isTbody) { this.container.innerHTML = this.emptyHtml; } else { this.container.innerHTML = this.emptyHtml; } return; } var scrollTop = this._getScrollTop(); var containerHeight = this._getContainerHeight(); var startIdx = Math.max(0, Math.floor(scrollTop / rowH) - buffer); var endIdx = Math.min(data.length, Math.ceil((scrollTop + containerHeight) / rowH) + buffer); var html = ''; if (this._isTbody) { // Top spacer row var topSpacerHeight = startIdx * rowH; if (topSpacerHeight > 0) { html += ''; } for (var i = startIdx; i < endIdx; i++) { html += this.renderRow(data[i], i); } // Bottom spacer row var bottomSpacerHeight = (data.length - endIdx) * rowH; if (bottomSpacerHeight > 0) { html += ''; } } else { for (var j = startIdx; j < endIdx; j++) { html += this.renderRow(data[j], j); } } this.container.innerHTML = html; }; VirtualScroll.prototype.destroy = function() { if (this._isTbody) { var table = this.container.closest('table'); if (table) { var wrapper = table.parentElement; if (wrapper && wrapper.classList.contains('vs-container')) { wrapper.removeEventListener('scroll', this._scrollHandler); } } } else { this.container.removeEventListener('scroll', this._scrollHandler); } window.removeEventListener('resize', this._resizeHandler); }; window.VirtualScroll = VirtualScroll; })(window);