Support all modes: sticky row+column, sticky row, sticky column, sticky nothing

master
Vitaliy Filippov 2017-07-07 01:26:49 +03:00
parent afedd70edd
commit 89084fa045
2 changed files with 200 additions and 135 deletions

View File

@ -159,6 +159,16 @@ table.grid .celleditor input
z-index: 1;
}
.grid-no-sticky-col .grid-body-wrapper
{
padding-left: 0;
}
.grid-no-sticky-col .grid-fixed-row-wrapper
{
padding-left: 0;
}
.grid-fixed-cell
{
position: absolute;

View File

@ -1,11 +1,12 @@
/**
* Very simple and fast tree grid/table, with support for:
* - fixed header and column
* - sticky(fixed) row
* - sticky(fixed) column
* - virtual scrolling
* - dynamic loading of child nodes
*
* License: MPL 2.0+, (c) Vitaliy Filippov 2016+
* Version: 2017-07-06
* Version: 2017-07-07
*/
/**
@ -81,6 +82,7 @@
*/
function TreeGrid(options)
{
var self = this;
this.renderer = options.renderer;
this.bindProps = { 'style': 1, 'className': 1, 'title': 1, 'innerHTML': 1 };
if (options.bind)
@ -95,58 +97,72 @@ function TreeGrid(options)
this.table.appendChild(this.thead);
this.table.appendChild(this.tbody);
this.thead.appendChild(document.createElement('tr'));
var self = this;
if (options.stickyHeaders)
this.wrapper = document.createElement('div');
this.stickyRow = options.stickyHeaders||options.stickyRow;
this.stickyColumn = options.stickyHeaders||options.stickyColumn;
this.minItemHeight = 21;
if (this.stickyRow || this.stickyColumn)
{
this.stickyHeaders = true;
this.minItemHeight = 24;
this.toSync = [];
this.wrapper = document.createElement('div');
// table for fixed cell
this.fixedCell = document.createElement('table');
this.fixedCell.className = 'grid grid-fixed-cell';
this.fixedCell.appendChild(document.createElement('thead'));
this.fixedCell.firstChild.appendChild(document.createElement('tr'));
this.wrapper.appendChild(this.fixedCell);
// table for fixed row
this.fixedRow = document.createElement('table');
this.fixedRow.className = 'grid grid-fixed-row';
this.fixedRow.appendChild(this.thead);
this.table.insertBefore(document.createElement('thead'), this.tbody);
this.sizeRow = document.createElement('tr');
this.table.firstChild.appendChild(this.sizeRow);
this.fixedRowWrapper = document.createElement('div');
this.fixedRowWrapper.className = 'grid-fixed-row-wrapper';
this.fixedRowWrapper.appendChild(this.fixedRow);
this.wrapper.appendChild(this.fixedRowWrapper);
// table for fixed column
this.fixedCol = document.createElement('table');
this.fixedCol.className = 'grid grid-fixed-col';
this.fixedColBody = document.createElement('tbody');
this.fixedCol.appendChild(this.fixedColBody);
this.fixedColWrapper = document.createElement('div');
this.fixedColWrapper.className = 'grid-fixed-col-wrapper';
this.fixedColSizer = document.createElement('div');
this.fixedColSizer.className = 'grid-fixed-col-sizer';
this.fixedColWrapper.appendChild(this.fixedColSizer);
this.fixedColWrapper.appendChild(this.fixedCol);
this.fixedColWrapper2 = document.createElement('div');
this.fixedColWrapper2.className = 'grid-fixed-col-wrapper2';
this.fixedColWrapper2.appendChild(this.fixedColWrapper);
this.wrapper.appendChild(this.fixedColWrapper2);
// table body wrapper
this.wrapper.className = 'grid-wrapper';
this.tableWrapper = document.createElement('div');
this.tableWrapper.className = 'grid-body-wrapper';
this.tableSizer = document.createElement('div');
this.tableSizer.className = 'grid-fixed-col-sizer';
this.tableWrapper.appendChild(this.tableSizer);
this.tableWrapper.appendChild(this.table);
this.wrapper.appendChild(this.tableWrapper);
// sticky column width
this.stickyBorderWidth = options.stickyBorderWidth || 1;
if (options.stickyColumnWidth)
this.setStickyColumnWidth(options.stickyColumnWidth);
if (this.stickyRow && this.stickyColumn)
{
// table for fixed cell
this.fixedCell = document.createElement('table');
this.fixedCell.className = 'grid grid-fixed-cell';
this.fixedCell.appendChild(document.createElement('thead'));
this.fixedCell.firstChild.appendChild(document.createElement('tr'));
this.wrapper.appendChild(this.fixedCell);
}
if (this.stickyRow)
{
// table for fixed row
this.fixedRow = document.createElement('table');
this.fixedRow.className = 'grid grid-fixed-row';
this.fixedRow.appendChild(this.thead);
this.table.insertBefore(document.createElement('thead'), this.tbody);
this.sizeRow = document.createElement('tr');
this.table.firstChild.appendChild(this.sizeRow);
this.fixedRowWrapper = document.createElement('div');
this.fixedRowWrapper.className = 'grid-fixed-row-wrapper';
this.fixedRowWrapper.appendChild(this.fixedRow);
this.wrapper.appendChild(this.fixedRowWrapper);
}
if (this.stickyColumn)
{
this.toSync = [];
// table for fixed column
this.fixedCol = document.createElement('table');
this.fixedCol.className = 'grid grid-fixed-col';
if (!this.stickyRow)
{
this.fixedColHeader = document.createElement('thead');
this.fixedColHeader.appendChild(document.createElement('tr'));
this.fixedCol.append(this.fixedColHeader);
}
this.fixedColBody = document.createElement('tbody');
this.fixedCol.appendChild(this.fixedColBody);
this.fixedColWrapper = document.createElement('div');
this.fixedColWrapper.className = 'grid-fixed-col-wrapper';
this.fixedColSizer = document.createElement('div');
this.fixedColSizer.className = 'grid-fixed-col-sizer';
this.fixedColWrapper.appendChild(this.fixedColSizer);
this.fixedColWrapper.appendChild(this.fixedCol);
this.fixedColWrapper2 = document.createElement('div');
this.fixedColWrapper2.className = 'grid-fixed-col-wrapper2';
this.fixedColWrapper2.appendChild(this.fixedColWrapper);
this.wrapper.appendChild(this.fixedColWrapper2);
// sticky column width
this.stickyBorderWidth = options.stickyBorderWidth || 1;
addListener(this.fixedColWrapper, 'scroll', function()
{
if (self.scrolling != 1)
{
self.tableWrapper.scrollTop = self.fixedColWrapper.scrollTop;
self.scrolling = 2;
}
else
self.scrolling = 0;
});
}
// scroll and resize handlers
if (!options.noStickyResizeHandler)
{
@ -155,33 +171,42 @@ function TreeGrid(options)
self.syncStickyHeaders(true);
});
}
var syncTimer;
addListener(this.tableWrapper, 'scroll', function()
{
if (self.scrolling != 2)
{
self.fixedRowWrapper.scrollLeft = self.tableWrapper.scrollLeft;
self.fixedColWrapper.scrollTop = self.tableWrapper.scrollTop;
self.scrolling = 1;
}
else
self.scrolling = 0;
if (!syncTimer)
{
syncTimer = setTimeout(function() { self.syncView(); syncTimer = null; }, 100);
}
});
addListener(this.fixedColWrapper, 'scroll', function()
{
if (self.scrolling != 1)
{
self.tableWrapper.scrollTop = self.fixedColWrapper.scrollTop;
self.scrolling = 2;
}
else
self.scrolling = 0;
});
}
addListener(window, 'resize', function()
{
self.syncView(true);
});
// table body wrapper
this.wrapper.className = 'grid-wrapper';
this.tableWrapper = document.createElement('div');
this.tableWrapper.className = 'grid-body-wrapper';
this.tableSizer = document.createElement('div');
this.tableSizer.className = 'grid-fixed-col-sizer';
this.tableWrapper.appendChild(this.tableSizer);
this.tableWrapper.appendChild(this.table);
this.wrapper.appendChild(this.tableWrapper);
if (!this.stickyColumn)
{
this.wrapper.className += ' grid-no-sticky-col';
}
var syncTimer;
addListener(this.tableWrapper, 'scroll', function()
{
if (self.scrolling != 2)
{
if (self.stickyRow)
self.fixedRowWrapper.scrollLeft = self.tableWrapper.scrollLeft;
if (self.stickyColumn)
self.fixedColWrapper.scrollTop = self.tableWrapper.scrollTop;
self.scrolling = 1;
}
else
self.scrolling = 0;
if (!syncTimer)
{
syncTimer = setTimeout(function() { self.syncView(); syncTimer = null; }, 100);
}
});
addListener(this.wrapper||this.table, 'mouseover', function(ev) { self.nodeOnHover(true, ev); });
addListener(this.wrapper||this.table, 'mouseout', function(ev) { self.nodeOnHover(false, ev); });
this.setHeader(options.header);
@ -280,7 +305,7 @@ TreeGrid.prototype._renderNodes = function(nodes)
tr._node = nodes[i];
nodes[i].tr.push(tr);
}
if (this.stickyHeaders)
if (this.stickyColumn)
{
nodes[i].col_tr = [];
for (var j = 0; j < (nodes[i].rows||1); j++)
@ -477,7 +502,7 @@ TreeGrid.prototype.syncView = function()
{
if (!this.table.offsetParent)
return;
var offsetHeight = this.wrapper.offsetHeight - this.thead.offsetHeight;
var offsetHeight = this.wrapper.offsetHeight - (this.stickyRow ? this.thead.offsetHeight : 0);
var visibleItems = Math.ceil(offsetHeight/this.minItemHeight);
var endStart = this.nodeCount - visibleItems;
if (endStart < 0)
@ -493,7 +518,8 @@ TreeGrid.prototype.syncView = function()
var lastFirst = endStart+this._findVisibleNodeOffset(lastNodes, offsetHeight);
var avgItemHeight = this.endItemHeight / lastNodes.length;
avgItemHeight = (avgItemHeight < this.minItemHeight ? this.minItemHeight : avgItemHeight);
this.fixedColSizer.style.height = (this.nodeCount*avgItemHeight)+'px';
if (this.stickyColumn)
this.fixedColSizer.style.height = (this.nodeCount*avgItemHeight)+'px';
this.tableSizer.style.height = (this.nodeCount*avgItemHeight)+'px';
var scrollPos = this.nodeCount*avgItemHeight > this.tableWrapper.offsetHeight ?
this.tableWrapper.scrollTop / (this.nodeCount*avgItemHeight - this.tableWrapper.offsetHeight) : 0;
@ -528,7 +554,7 @@ TreeGrid.prototype.syncView = function()
}
this._setPlaceholder('top', this.tableWrapper.scrollTop-firstItemHeight);
}
else if (this.stickyHeaders)
else if (this.stickyColumn)
{
this.fixedColSizer.style.height = 0;
this.tableSizer.style.height = 0;
@ -536,12 +562,13 @@ TreeGrid.prototype.syncView = function()
this._finishSync();
// Then sync row sizes for fixed column to work properly
var total = 0;
if (this.stickyHeaders)
if (this.stickyColumn)
{
total = this.syncStickyHeaders(true);
}
else
{
this.syncStickyRow();
for (var i = 0; i < this.tbody.rows.length; i++)
total += (this.tbody.rows[i].offsetHeight||0);
}
@ -586,59 +613,66 @@ TreeGrid.prototype.setStickyColumnWidth = function(w)
w = this.fixedColWrapper2.offsetWidth;
this.fixedColWrapper.style.width = (w+this.fixedColWrapper.offsetWidth-this.fixedColWrapper.clientWidth)+'px';
this.tableWrapper.style.paddingLeft = (w-this.stickyBorderWidth)+'px';
this.fixedRowWrapper.style.paddingLeft = (w-this.stickyBorderWidth)+'px';
this.fixedCell.style.width = w+'px';
if (this.stickyRow)
{
this.fixedRowWrapper.style.paddingLeft = (w-this.stickyBorderWidth)+'px';
this.fixedCell.style.width = w+'px';
}
}
TreeGrid.prototype.syncStickyHeaders = function(sync_all)
{
if (!this.stickyHeaders || !this.wrapper.offsetParent)
if (!this.wrapper.offsetParent)
return 0;
var total = 0;
if (this.fixedCol.offsetWidth)
this.setStickyColumnWidth(this.fixedCol.offsetWidth+'px');
var h1, h2, h = [];
if (sync_all)
if (this.stickyColumn)
{
for (var i = 0; i < this.tbody.rows.length; i++)
if (this.fixedCol.offsetWidth)
this.setStickyColumnWidth(this.fixedCol.offsetWidth+'px');
var h1, h2, h = [];
if (sync_all)
{
h1 = this.tbody.rows[i].offsetHeight||0;
h2 = this.fixedColBody.rows[i].offsetHeight||0;
h[i] = h1 < h2 ? h2 : h1;
}
for (var i = 0; i < this.tbody.rows.length; i++)
{
if (h[i] && !this.tbody.rows[i].is_placeholder)
for (var i = 0; i < this.tbody.rows.length; i++)
{
total += h[i];
this.tbody.rows[i].style.height = h[i]+'px';
this.fixedColBody.rows[i].style.height = h[i]+'px';
h1 = this.tbody.rows[i].offsetHeight||0;
h2 = this.fixedColBody.rows[i].offsetHeight||0;
h[i] = h1 < h2 ? h2 : h1;
}
}
}
else
{
for (var i = 0; i < this.toSync.length; i++)
{
for (var k = 0; k < this.toSync[i].tr.length; k++)
for (var i = 0; i < this.tbody.rows.length; i++)
{
h1 = this.toSync[i].tr[k].offsetHeight;
h2 = this.toSync[i].col_tr[k].offsetHeight;
h.push(h1 && h2 && (h1 < h2 ? h2 : h1));
}
}
for (var i = 0, j = 0; i < this.toSync.length; i++)
{
for (var k = 0; k < this.toSync[i].tr.length; k++, j++)
{
if (h[j])
if (h[i] && !this.tbody.rows[i].is_placeholder)
{
this.toSync[i].tr[k].style.height = h[j]+'px';
this.toSync[i].col_tr[k].style.height = h[j]+'px';
total += h[j];
total += h[i];
this.tbody.rows[i].style.height = h[i]+'px';
this.fixedColBody.rows[i].style.height = h[i]+'px';
}
}
}
else
{
for (var i = 0; i < this.toSync.length; i++)
{
for (var k = 0; k < this.toSync[i].tr.length; k++)
{
h1 = this.toSync[i].tr[k].offsetHeight;
h2 = this.toSync[i].col_tr[k].offsetHeight;
h.push(h1 && h2 && (h1 < h2 ? h2 : h1));
}
}
for (var i = 0, j = 0; i < this.toSync.length; i++)
{
for (var k = 0; k < this.toSync[i].tr.length; k++, j++)
{
if (h[j])
{
this.toSync[i].tr[k].style.height = h[j]+'px';
this.toSync[i].col_tr[k].style.height = h[j]+'px';
total += h[j];
}
}
}
}
this.toSync = [];
}
this.syncStickyRow();
return total;
@ -646,6 +680,19 @@ TreeGrid.prototype.syncStickyHeaders = function(sync_all)
TreeGrid.prototype.syncStickyRow = function()
{
if (!this.stickyRow)
{
if (this.stickyColumn)
{
this.fixedColWrapper2.style.bottom = (this.tableWrapper.offsetHeight-this.tableWrapper.clientHeight)+'px';
var hh = this.thead.offsetHeight;
if (this.fixedColHeader.rows[0].offsetHeight > hh)
hh = this.fixedColHeader.rows[0].offsetHeight;
this.thead.rows[0].style.height = hh+'px';
this.fixedColHeader.rows[0].style.height = hh+'px';
}
return;
}
var trh = this.fixedRow.rows[0], w = [];
for (var i = 0; i < trh.cells.length; i++)
w[i] = this.sizeRow.cells[i] && this.sizeRow.cells[i].offsetWidth || 0;
@ -653,32 +700,40 @@ TreeGrid.prototype.syncStickyRow = function()
for (var i = 0; i < trh.cells.length; i++)
trh.cells[i].style.width = w[i]+'px';
this.tableWrapper.style.paddingTop = (this.fixedRow.offsetHeight-this.sizeRow.offsetHeight)+'px';
this.fixedColWrapper2.style.bottom = (this.tableWrapper.offsetHeight-this.tableWrapper.clientHeight)+'px';
this.fixedRowWrapper.style.right = (this.tableWrapper.offsetWidth-this.tableWrapper.clientWidth)+'px';
this.fixedColWrapper.style.paddingTop = this.fixedRow.offsetHeight+'px';
if (this.fixedCell.rows[0].cells[0])
this.fixedCell.rows[0].cells[0].style.height = (this.fixedRow.offsetHeight)+'px';
if (this.stickyColumn)
{
this.fixedColWrapper2.style.bottom = (this.tableWrapper.offsetHeight-this.tableWrapper.clientHeight)+'px';
this.fixedColWrapper.style.paddingTop = this.fixedRow.offsetHeight+'px';
if (this.fixedCell.rows[0].cells[0])
this.fixedCell.rows[0].cells[0].style.height = (this.fixedRow.offsetHeight)+'px';
}
}
TreeGrid.prototype.setHeader = function(newHeader)
{
var tr = this.thead.rows[0];
tr.innerHTML = '';
if (this.stickyHeaders)
if (this.stickyRow)
{
this.sizeRow.innerHTML = '';
this.fixedCell.rows[0].innerHTML = '';
}
if (this.stickyColumn)
{
(this.fixedCell || this.fixedColHeader).rows[0].innerHTML = '';
}
for (var i = 0; i < newHeader.length; i++)
{
var th = document.createElement('th');
this._setProps(th, newHeader[i]);
if (this.stickyHeaders && i == 0)
this.fixedCell.rows[0].appendChild(th);
if (this.stickyColumn && i == 0)
{
(this.fixedCell || this.fixedColHeader).rows[0].appendChild(th);
}
else
{
tr.appendChild(th);
if (this.stickyHeaders)
if (this.stickyRow)
this.sizeRow.appendChild(th.cloneNode(true));
}
}
@ -730,7 +785,7 @@ TreeGrid.prototype._getCellIndex = function(n, sticky)
var i = 0, p = n;
while ((p = p.previousSibling))
i += (p.nodeType != 3 ? 1 : 0);
if (sticky && this.stickyHeaders)
if (sticky && this.stickyRow)
{
p = n.parentNode.parentNode.parentNode;
if (p == this.table || p == this.fixedRow)
@ -1060,8 +1115,8 @@ TreeGridNode.prototype._hashEqual = function(a, b)
TreeGridNode.prototype._renderCell = function(rowIndex, colIndex, cell, force)
{
var tr = (this.grid.stickyHeaders && colIndex == 0 ? this.col_tr : this.tr)[rowIndex];
var ri = (this.grid.stickyHeaders && colIndex > 0 ? colIndex-1 : 0);
var tr = (this.grid.stickyColumn && colIndex == 0 ? this.col_tr : this.tr)[rowIndex];
var ri = this.grid.stickyColumn ? (colIndex > 0 ? colIndex-1 : 0) : colIndex;
while (!tr.cells[ri])
tr.appendChild(document.createElement('td'));
// virtualDOM-like approach: compare old HTML properties
@ -1133,7 +1188,7 @@ TreeGridNode.prototype.render = function(colidx, rowidx, force, skipSync)
modified = this._renderCell(i, j, cells[i] && cells[i][j], force) || modified;
this._oldCells = cells;
}
if (modified && this.grid.stickyHeaders && skipSync !== 2)
if (modified && this.grid.stickyColumn && skipSync !== 2)
{
this.grid.toSync.push(this);
if (!skipSync)