diff --git a/treegrid.css b/treegrid.css index c38914a..4a31a31 100644 --- a/treegrid.css +++ b/treegrid.css @@ -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; diff --git a/treegrid.js b/treegrid.js index f29f4e9..06abaeb 100644 --- a/treegrid.js +++ b/treegrid.js @@ -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)