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