From 12bac78cc206780d24b97c1a73f50cb32faa5f8f Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Tue, 24 Jan 2017 13:57:10 +0300 Subject: [PATCH] Use more standard sticky headers approach --- stickyheaders.js | 296 ----------------------------------- treegrid.css | 105 ++++++++++--- treegrid.js | 391 ++++++++++++++++++++++++++++++++++++----------- treegridtest.htm | 17 +-- 4 files changed, 390 insertions(+), 419 deletions(-) delete mode 100644 stickyheaders.js diff --git a/stickyheaders.js b/stickyheaders.js deleted file mode 100644 index 9746def..0000000 --- a/stickyheaders.js +++ /dev/null @@ -1,296 +0,0 @@ -// Simple Sticky Header and Column implementation for HTML tables -// (c) Vitaliy Filippov 2016 -// License: MPL 2.0+ -// Version: 2016-12-06 - -// USAGE: -// makeStickyHeaders(table): add sticky header and footer to table -// table.parentNode is assumed to be the scroll container -// checkStickyHeaders(table): call this after making table visible -// (if you've hidden it before) -// initStickyRows(table, trs): initialise table rows (trs = array[TableRow]) if not yet -// fixStickyRows(table, trs): fix table row (trs = array[TableRow]) after modifications -// fixStickyHeader(table): fix table header after modifications -// fixStickyColumnSizes(table, force): adjust column sizes if something is modified -// if force == true, reposition all fixed cells (header row and column) - -// WARNING 1: after using this plugin all columns and rows except 0th ones become shifted by 1. -// table.rows[1] and table.rows[*].cells[1] become special invisible rows/cells with copies of first -// row and column content inside. - -// WARNING 2: this plugin does NOT support border-collapse because it is implemented by non-integer border -// sizes O_o and slightly differently in different browsers which leads to problems calculating cell positions. - -// WARNING 3: for these hacks to work correctly, you need 'position: relative' on your table and table rows. - -// WARNING 4: all table cells should obviously have non-transparent background -// because fixed ones are positioned over normal. - -// WARNING 5: make sure you have no CSS rules that will differ on the original header row/column and cloned one, -// and that min/max width/height rules apply to the second row/column, not the first header one. - -;(function() { - -var _scri = 0; - -function getOffsetRect(e) -{ - var b = e.getBoundingClientRect(); - return { - left: Math.round(b.left*100)/100, - top: Math.round(b.top*100)/100, - width: Math.round((b.right-b.left)*100)/100, - height: Math.round((b.bottom-b.top)*100)/100 - }; -} - -window.makeStickyHeaders = function(table) -{ - if (!table._scrstyle) - { - table._scrstyle = document.createElement('style'); - (document.head||document.getElementsByTagName('head')[0]).appendChild(table._scrstyle); - table._scri = _scri++; - } - if (!table.rows.length || !table.rows[0].children.length) - return; - var w = [], l = [], h = table.rows[0].offsetHeight; - var sr = table.rows[0].cloneNode(); - sr.appendChild(document.createElement('td')); - sr.children[0].style.width = '0'; - sr.children[0].style.display = 'block'; - sr.children[0].style.position = 'absolute'; - sr.children[0].style.padding = '0'; - sr.style.visibility = 'hidden'; - table._widths = []; - table._sizerow = sr; - table.rows[0].style.height = '0'; - table.rows[0].style.display = 'block'; - table.rows[0].style.position = 'absolute'; - table.rows[0].parentNode.insertBefore(sr, table.rows[0].nextSibling); - fixStickyHeader(table, true); - table._fixPending = true; - checkStickyHeaders(table); - var row0top = getOffsetRect(table.rows[0]).top-getOffsetRect(table.rows[0].offsetParent).top; - addListener(table.parentNode, 'scroll', function(e) - { - var l = this.scrollLeft, t = this.scrollTop; - table.rows[0].style.top = (t+row0top)+'px'; - if (!table._scrlastleft || table._scrlastleft != l) - { - table._scrstyle.innerHTML = l > 0 ? '._scri'+table._scri+' { left: '+l+'px; }' : ''; - table._scrlastleft = l; - } - }); -} - -// should be called after showing table to recalculate fixed cell sizes -window.checkStickyHeaders = function(table) -{ - if (table._fixPending) - { - delete table._fixPending; - var rows = []; - for (var i = 2; i < table.rows.length; i++) - rows.push(table.rows[i]); - fixStickyRows(table, rows); - } -} - -// check if row(s) are not yet initialised and fix them -window.initStickyRows = function(table, trs, nocols) -{ - var ntrs = []; - for (var i = 0; i < trs.length; i++) - if (trs[i].children[0].className.indexOf(' _scri'+table._scri) < 0) - ntrs.push(trs[i]); - if (ntrs.length) - fixStickyRows(table, ntrs, nocols); -} - -// handle non-header row(s) change with O(1) reflow count -window.fixStickyRows = function(table, trs, nocols) -{ - // (1) make some changes - var e, d; - for (var i = 0; i < trs.length; i++) - { - e = trs[i].children[0]; - if (e.className.indexOf(' _scri'+table._scri) >= 0) - { - // refix - trs[i].removeChild(trs[i].children[1]); - d = e.cloneNode(true); - d.className = d.className.replace(' _scri'+table._scri, ''); - d.style.visibility = 'hidden'; - d.style.width = e._oldWidth; - d.style.height = ''; - d.style.position = ''; - d.style.display = ''; - d.style.zIndex = ''; - e.parentNode.insertBefore(d, e.nextSibling); - } - else - { - // fix new - d = e.cloneNode(true); - d.style.visibility = 'hidden'; - e.style.position = 'absolute'; - if (e.rowSpan > 1) - e.rowSpan = 1; - if (e.style.display == 'none') - e.style.visibility = 'hidden'; - e.style.display = 'block'; - e.style.zIndex = '1'; - trs[i].insertBefore(d, e.nextSibling); - e.className += ' _scri'+table._scri; - } - } - if (table.offsetParent) - { - // (2) caused by offsetWidth/offsetHeight - // (3) remember sizes - var w = [], h = [], b; - for (var i = 0; i < trs.length; i++) - { - e = trs[i].children[1]; - b = getOffsetRect(e); - w[i] = b.width; - h[i] = b.height; - } - // (4) apply sizes - for (var i = 0; i < trs.length; i++) - { - e = trs[i].children[0]; - e.style.width = w[i]+'px'; - e.style.height = h[i]+'px'; - } - } - else - { - // table is invisible - table._fixPending = true; - } - if (!nocols) - fixStickyColumnSizes(table); -} - -window.fixStickyColumnSizes = function(table, force) -{ - if (!table.offsetParent) - { - // table is invisible - table._fixPending = true; - return; - } - // check&fix column sizes - var changed = false, resizeFixed = false; - for (var i = 1; i < table._sizerow.children.length; i++) - { - var ne = table._sizerow.children[i]; - var nw = getOffsetRect(ne).width; - var cw = table._widths[i-1]; - if (nw != cw || force) - { - table._widths[i-1] = nw; - changed = true; - if (i == 1) - resizeFixed = true; - } - } - if (changed) - { - // reposition fixed header - var w = [], h = [], l = [], b, pb = getOffsetRect(table._sizerow); - for (var i = 0; i < table._sizerow.children.length; i++) - { - b = getOffsetRect(table._sizerow.children[i]); - w[i] = b.width; - h[i] = b.height; - l[i] = b.left-pb.left; - } - b = table.rows[0].children[0]; - b.style.width = w[1]+'px'; - b.style.height = h[1]+'px'; - for (var i = 1; i < table._sizerow.children.length; i++) - { - b = table.rows[0].children[i]; - b.style.left = l[i]+'px'; - b.style.width = w[i]+'px'; - b.style.height = h[i]+'px'; - } - if (resizeFixed) - { - // resize fixed column - var h = []; - for (var i = 0; i < table.rows.length; i++) - h[i] = getOffsetRect(table.rows[i].children[1]).height; - for (var i = 0; i < table.rows.length; i++) - { - if (i != 1) - { - table.rows[i].children[0].style.width = w[1]+'px'; - table.rows[i].children[0].style.height = h[i == 0 ? 1 : i]+'px'; - } - } - } - } -} - -// handle header row change -window.fixStickyHeader = function(table, nocols) -{ - var tr = table.rows[0]; - var sr = table._sizerow; - while (sr.children[1]) - sr.removeChild(sr.children[1]); - var b, e, renew = false; - for (var i = 0; i < tr.children.length; i++) - { - b = tr.children[i]; - e = b.cloneNode(true); - if (b._sticky) - { - if (i == 0) - e.className = e.className.replace(' _scri'+table._scri, ''); - e.style.position = ''; - e.style.width = b._oldWidth; - e.style.height = ''; - e.style.left = ''; - e.style.display = ''; - e.style.zIndex = ''; - if (i == 0) - i++; - } - else - renew = true; - sr.appendChild(e); - } - if (renew) - { - for (var i = 0; i < tr.children.length; i++) - { - var e = tr.children[i]; - if (!e._sticky) - { - e.style.position = 'absolute'; - e.style.display = 'block'; - e.style.zIndex = '1'; - if (i == 0) - { - var d = e.cloneNode(true); - d.style.visibility = 'hidden'; - tr.insertBefore(d, e.nextSibling); - i++; - e.className += ' _scri'+table._scri; - e.style.zIndex = '2'; - } - } - } - table._widths = []; - } - if (!nocols) - fixStickyColumnSizes(table); -} - -})(); diff --git a/treegrid.css b/treegrid.css index 4c1e100..2ac871f 100644 --- a/treegrid.css +++ b/treegrid.css @@ -6,30 +6,14 @@ img { vertical-align: middle; } body { -moz-text-size-adjust: none; } /* do not scale fonts in mobile firefox */ /*table { border-collapse: collapse; }*/ -/* scroll div styles */ -.scroller -{ - position: relative; - height: 100%; - width: 100%; -} - -.scroller > .inner -{ - position: absolute; - height: 100%; - width: 100%; - overflow: auto; -} - /* fixed header table styles */ -table.grid tbody tr:hover, table.grid tbody tr:hover td +table.grid tbody tr.hover, table.grid tbody tr.hover td { background-color: #e2eff8; } -table.grid tr.selected, table.grid td.selected, table.grid tr.selected:hover, -table.grid tr.selected:hover td, table.grid tr:hover td.selected +table.grid tr.selected, table.grid td.selected, table.grid tr.selected.hover, +table.grid tr.selected.hover td, table.grid tr.hover td.selected { background-color: #c1ddf1; box-shadow: inset 0 0 0 1px #3d91cf; @@ -50,7 +34,6 @@ table.grid { border-collapse: separate; border-spacing: 0; - border-left: 1px solid #ccc; } table.grid > *:first-child > tr:first-child > *, table.grid > tr:first-child > * @@ -58,6 +41,14 @@ table.grid > *:first-child > tr:first-child > *, table.grid > tr:first-child > * border-top: 1px solid #ccc; } +table.grid > * > tr > td:first-child, +table.grid > * > tr > th:first-child, +table.grid > tr > td:first-child, +table.grid > tr > th:first-child +{ + border-left: 1px solid #ccc; +} + table.grid, table.grid tr, table.grid td, table.grid th { box-sizing: border-box; @@ -124,6 +115,12 @@ table.grid .celleditor position: relative; } +table.grid .celleditor div +{ + height: 100%; + position: relative; +} + table.grid .celleditor input { box-shadow: inset 0 0 0 1px #3d91cf; @@ -141,3 +138,71 @@ table.grid .celleditor input -moz-box-sizing: border-box; display: block; } + +.grid-wrapper +{ + position: relative; + height: 100%; + width: 100%; +} + +.grid-body-wrapper +{ + position: absolute; + padding-left: 200px; + left: 0; + right: 0; + top: 0; + bottom: 0; + overflow: auto; + z-index: 1; +} + +.grid-fixed-cell +{ + position: absolute; + left: 0; + width: 200px; + top: 0; + bottom: 0; + overflow: hidden; + z-index: 4; +} + +.grid-fixed-col-wrapper2 +{ + position: absolute; + left: 0; + width: 200px; + top: 0; + bottom: 0; + overflow: hidden; + z-index: 3; +} + +.grid-fixed-col-wrapper +{ + position: absolute; + left: 0; + top: 0; + bottom: 0; + overflow-y: scroll; + overflow-x: hidden; + z-index: 3; +} + +.grid-fixed-row-wrapper +{ + position: absolute; + padding-left: 200px; + left: 0; + right: 0; + top: 0; + overflow: hidden; + z-index: 3; +} + +table.grid.grid-fixed-col > *:first-child > tr:first-child > *, table.grid.grid-fixed-col > tr:first-child > * +{ + border-top: 0; +} diff --git a/treegrid.js b/treegrid.js index 6bf99e5..be0dec5 100644 --- a/treegrid.js +++ b/treegrid.js @@ -1,13 +1,21 @@ /** - * Very simple and fast tree grid/table, compatible with dynamic loading and stickyheaders.js + * Very simple and fast tree grid/table, with support for fixed header and column, compatible with dynamic loading + * * License: MPL 2.0+, (c) Vitaliy Filippov 2016+ - * Version: 2016-12-06 + * Version: 2017-01-24 */ /** * USAGE: * - * var TG = new TreeGrid({ items: items, header: header, renderer: renderer }); + * var TG = new TreeGrid({ + * items: items, + * header: header, + * renderer: renderer, + * stickyHeaders: true|false, + * stickyColumnWidth: '25%'|'200px', + * stickyBorderWidth: 1 + * }); * document.body.appendChild(TG.table); * * // renderer is a function that generates cell HTML properties for a node @@ -83,11 +91,88 @@ 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.stickyHeaders = true; + 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.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.tableWrapper.appendChild(this.table); + this.wrapper.appendChild(this.tableWrapper); + // sticky column width + this.stickyBorderWidth = options.stickyBorderWidth || 1; + if (options.stickyColumnWidth) + this.setStickyColumnWidth(options.stickyColumnWidth); + // scroll and resize handlers + if (!options.noStickyResizeHandler) + { + addListener(window, 'resize', function() + { + self.syncStickyHeaders(true); + }); + } + 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; + }); + addListener(this.fixedColWrapper, 'scroll', function() + { + if (self.scrolling != 1) + { + self.tableWrapper.scrollTop = self.fixedColWrapper.scrollTop; + self.scrolling = 2; + } + else + self.scrolling = 0; + }); + } + 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); new TreeGridNode({ children: options.items }, this); } -function TreeGridNode(node, grid, level, insertBefore, startHidden) +function TreeGridNode(node, grid, level, insertBeforeNode, startHidden, skipSync) { this.grid = grid; this.level = level; @@ -101,6 +186,7 @@ function TreeGridNode(node, grid, level, insertBefore, startHidden) { this.leaf = node.leaf; this.collapsed = node.collapsed || !node.leaf && (!node.children || !node.children.length); + this._oldCells = []; this.tr = []; for (var i = 0; i < (node.rows || 1); i++) { @@ -110,87 +196,188 @@ function TreeGridNode(node, grid, level, insertBefore, startHidden) tr._node = this; this.tr.push(tr); } - this._oldCells = []; + if (grid.stickyHeaders) + { + this.col_tr = []; + for (var i = 0; i < (node.rows || 1); i++) + { + var tr = document.createElement('tr'); + if (startHidden) + tr.style.display = 'none'; + tr._node = this; + this.col_tr.push(tr); + } + } this.render(undefined, undefined, false, true); for (var i = 0; i < this.tr.length; i++) { - insertBefore ? grid.tbody.insertBefore(this.tr[i], insertBefore) : grid.tbody.appendChild(this.tr[i]); + insertBeforeNode ? grid.tbody.insertBefore(this.tr[i], insertBeforeNode.tr[0]) : grid.tbody.appendChild(this.tr[i]); + } + if (grid.stickyHeaders) + { + for (var i = 0; i < this.col_tr.length; i++) + { + insertBeforeNode ? grid.fixedColBody.insertBefore(this.col_tr[i], insertBeforeNode.col_tr[0]) : grid.fixedColBody.appendChild(this.col_tr[i]); + } + grid.toSync.push(this); } } this.children = []; this.childrenByKey = {}; if (node.children) { - var trs = []; for (var i = 0; i < node.children.length; i++) { - var child = new TreeGridNode(node.children[i], grid, this.level+1, insertBefore, this.collapsed); + var child = new TreeGridNode(node.children[i], grid, this.level+1, insertBeforeNode, this.collapsed, true); child._index = i; this.children.push(child); if (child.key !== undefined) this.childrenByKey[child.key] = child; - if (grid.stickyInit && !this.collapsed) - trs.push.apply(trs, child.tr); } - if (trs.length) - fixStickyRows(grid.table, trs); } + if (!skipSync) + this.grid.syncStickyHeaders(); } (function() { -TreeGrid.prototype.initStickyHeaders = function(options) +function htmlspecialchars(text) { - this.sticky = true; - this.noStickyResize = options.noWindowResize; - if (this.header && this.header.length) - this._makeStickyHeaders(); + return (''+text).replace(/&/g, '&') + .replace(/'/g, ''') // ' + .replace(/"/g, '"') // " + .replace(//g, '>'); } -TreeGrid.prototype.checkStickyHeaders = function() +TreeGrid.prototype.nodeOnHover = function(hover, ev) { - if (this.stickyInit) - checkStickyHeaders(this.table); -} - -TreeGrid.prototype._makeStickyHeaders = function() -{ - var self = this; - this.table.className += ' stickyheaders'; - makeStickyHeaders(this.table); - this.stickyInit = true; - if (!this.noStickyResize) + ev = ev||window.event; + var e = ev.target||ev.srcElement; + while (e.nodeName != 'TR' && e && e != this.wrapper && e != this.table) + e = e.parentNode; + if (!e._node) + return; + var subrow = 0; + while (e.previousSibling && e.previousSibling._node == e._node) { - addListener(window, 'resize', function() - { - if (self.stickyInit) - fixStickyColumnSizes(self.table); - }); + e = e.previousSibling; + subrow++; } + if (hover) + e._node.tr[subrow].className += ' hover'; + else + e._node.tr[subrow].className = e._node.tr[subrow].className.replace(/ hover/g, ''); + if (e._node.col_tr) + { + if (hover) + e._node.col_tr[subrow].className += ' hover'; + else + e._node.col_tr[subrow].className = e._node.col_tr[subrow].className.replace(/ hover/g, ''); + } +} + +TreeGrid.prototype.setStickyColumnWidth = function(w) +{ + this.fixedColWrapper2.style.width = w; + if (!this.fixedColWrapper2.offsetWidth) + return; + 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'; +} + +TreeGrid.prototype.syncStickyHeaders = function(sync_all) +{ + if (!this.stickyHeaders || !this.wrapper.offsetParent) + return; + if (this.fixedCol.offsetWidth) + this.setStickyColumnWidth(this.fixedCol.offsetWidth+'px'); + var h1, h2, h = []; + if (sync_all) + { + for (var i = 0; i < this.table.rows.length-1; i++) + { + h1 = this.table.rows[i+1].offsetHeight; + h2 = this.fixedCol.rows[i].offsetHeight; + if (h1 && h2) + h[i] = h1 < h2 ? h2 : h1; + } + for (var i = 0; i < this.table.rows.length-1; i++) + { + if (h[i]) + { + this.table.rows[i+1].style.height = h[i]+'px'; + this.fixedCol.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'; + } + } + } + } + this.toSync = []; + 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; + this.fixedRow.style.width = this.table.offsetWidth+'px'; + 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'; + 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) + { + this.sizeRow.innerHTML = ''; + this.fixedCell.rows[0].innerHTML = ''; + } for (var i = 0; i < newHeader.length; i++) { var th = document.createElement('th'); this._setProps(th, newHeader[i]); - tr.appendChild(th); + if (this.stickyHeaders && i == 0) + this.fixedCell.rows[0].appendChild(th); + else + { + tr.appendChild(th); + if (this.stickyHeaders) + this.sizeRow.appendChild(th.cloneNode(true)); + } } this.header = newHeader; this.editedCells = []; // header change clears the whole grid by now if (this.root) this.root.setChildren(false, []); - if (this.sticky) - { - if (!this.stickyInit) - this._makeStickyHeaders(); - else - fixStickyHeader(this.table); - } } TreeGrid.prototype.onExpand = function(node) @@ -231,11 +418,15 @@ TreeGrid.prototype._setProps = function(el, props) TreeGrid.prototype._getCellIndex = function(n, sticky) { - var i = 0; - while ((n = n.previousSibling)) - i += (n.nodeType != 3 ? 1 : 0); - if (sticky && this.stickyInit) - i = (i > 0 ? i-1 : i); + var i = 0, p = n; + while ((p = p.previousSibling)) + i += (p.nodeType != 3 ? 1 : 0); + if (sticky && this.stickyHeaders) + { + p = n.parentNode.parentNode.parentNode; + if (p == this.table || p == this.fixedRow) + i++; + } return i; } @@ -260,7 +451,7 @@ TreeGrid.prototype.initCellEditing = function() var td = evt.target||evt.srcElement; while (td.nodeName != 'TABLE' && td.nodeName != 'TD') td = td.parentNode; - if (td.nodeName == 'TD' && td.parentNode._node && td.previousSibling && td.className.indexOf('celleditor') < 0) + if (td.nodeName == 'TD' && td.parentNode._node && td.className.indexOf('celleditor') < 0) { var params = self.onStartCellEdit && self.onStartCellEdit(td.parentNode._node, self._getSubrow(td), self._getCellIndex(td, true), td) || {}; @@ -381,7 +572,7 @@ function initMultiCellSelection(self) self.table.className += ' disable-text-select'; - addListener(self.table, 'mousedown', function(evt) + addListener(self.wrapper||self.table, 'mousedown', function(evt) { evt = getEventCoord(evt); if (!evt.shiftKey) @@ -531,7 +722,7 @@ TreeGridNode.prototype._addCollapser = function() collapser.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; addListener(collapser, 'click', this._getToggleHandler()); } - var c0 = this.tr[0].cells[0]; + var c0 = (this.col_tr || this.tr)[0].cells[0]; c0.childNodes.length ? c0.insertBefore(collapser, c0.firstChild) : c0.appendChild(collapser); if (this.grid._initialPadding === undefined) { @@ -558,10 +749,8 @@ TreeGridNode.prototype._hashEqual = function(a, b) TreeGridNode.prototype._renderCell = function(rowIndex, colIndex, cell, force) { - // this may be the first render of a row inside a stickyheaders grid - var tr = this.tr[rowIndex]; - var isFix = tr.cells.length && tr.cells[0].className.indexOf(' _scri') >= 0; - var ri = isFix ? (colIndex > 0 ? colIndex+1 : 0) : colIndex; + var tr = (this.grid.stickyHeaders && colIndex == 0 ? this.col_tr : this.tr)[rowIndex]; + var ri = (this.grid.stickyHeaders && colIndex > 0 ? colIndex-1 : 0); while (!tr.cells[ri]) tr.appendChild(document.createElement('td')); // virtualDOM-like approach: compare old HTML properties @@ -569,7 +758,7 @@ TreeGridNode.prototype._renderCell = function(rowIndex, colIndex, cell, force) cell && !this._hashEqual(this._oldCells[rowIndex][colIndex], cell)) { var old; - if (isFix && colIndex == 0 && rowIndex == 0) + if (colIndex == 0 && rowIndex == 0) { old = { width: tr.cells[ri].style.width, @@ -583,17 +772,7 @@ TreeGridNode.prototype._renderCell = function(rowIndex, colIndex, cell, force) if (colIndex == 0) { if (rowIndex == 0) - { this._addCollapser(); - if (isFix) - { - // FIXME: this is rather ugly :-( we need to restore stickyheaders styles - tr.cells[ri]._oldWidth = tr.cells[ri].style.width; - for (var i in old) - tr.cells[ri].style[i] = old[i]; - tr.cells[ri].className += ' _scri'+this.grid.table._scri; - } - } else tr.cells[ri].style.paddingLeft = (this.grid._initialPadding + this.grid.levelIndent * this.level + 20) + 'px'; } @@ -602,7 +781,7 @@ TreeGridNode.prototype._renderCell = function(rowIndex, colIndex, cell, force) return false; } -TreeGridNode.prototype.render = function(colidx, rowidx, force, skipStickyFix) +TreeGridNode.prototype.render = function(colidx, rowidx, force, skipSync) { var cells = this.grid.renderer(this, colidx, rowidx); if (this.tr.length == 1) @@ -643,8 +822,12 @@ TreeGridNode.prototype.render = function(colidx, rowidx, force, skipStickyFix) modified = this._renderCell(i, j, cells[i] && cells[i][j], force) || modified; this._oldCells = cells; } - if (modified && this.grid.stickyInit && !skipStickyFix) - fixStickyColumnSizes(this.grid.table); + if (modified && this.grid.stickyHeaders) + { + this.grid.toSync.push(this); + if (!skipSync) + this.grid.syncStickyHeaders(); + } return modified; } @@ -668,7 +851,8 @@ TreeGridNode.prototype.toggle = function() if (this.leaf) return; this.collapsed = !this.collapsed; - this.tr[0].cells[0].firstChild.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; + (this.col_tr || this.tr)[0].cells[0].firstChild.className = + this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; if (this.collapsed) { // collapse all children @@ -678,6 +862,15 @@ TreeGridNode.prototype.toggle = function() c.style.display = 'none'; c = c.nextSibling; } + if (this.col_tr) + { + c = this.col_tr[this.col_tr.length-1].nextSibling; + while (c && c._node.level > this.level) + { + c.style.display = 'none'; + c = c.nextSibling; + } + } } else { @@ -690,17 +883,17 @@ TreeGridNode.prototype.toggle = function() for (var i = 0; i < e.tr.length; i++) { e.tr[i].style.display = ''; - if (this.grid.stickyInit) - trs.push(e.tr[i]); + if (this.grid.stickyHeaders) + { + e.col_tr[i].style.display = ''; + this.grid.toSync.push(e); + } } if (!e.collapsed) st = st.concat(e.children); } - if (trs.length && this.grid.stickyInit) - fixStickyRows(this.grid.table, trs); } - if (this.grid.stickyInit) - fixStickyColumnSizes(this.grid.table); + this.grid.syncStickyHeaders(); } TreeGridNode.prototype.setChildren = function(isLeaf, newChildren) @@ -709,23 +902,32 @@ TreeGridNode.prototype.setChildren = function(isLeaf, newChildren) { // root node this.grid.tbody.innerHTML = ''; + if (this.grid.fixedColBody) + this.grid.fixedColBody.innerHTML = ''; } else { var tr = this.tr[this.tr.length-1]; while (tr.nextSibling && tr.nextSibling._node.level > this.level) - this.grid.tbody.removeChild(tr.nextSibling); + tr.parentNode.removeChild(tr.nextSibling); + if (this.col_tr) + { + tr = this.col_tr[this.col_tr.length-1]; + while (tr.nextSibling && tr.nextSibling._node.level > this.level) + tr.parentNode.removeChild(tr.nextSibling); + } if (this.leaf != isLeaf) { + var collapser = (this.col_tr || this.tr)[0].cells[0].firstChild; if (isLeaf) { - this.tr[0].cells[0].firstChild.className = 'collapser collapser-inactive'; - removeListener(this.tr[0].cells[0].firstChild, 'click', this._getToggleHandler()); + collapser.className = 'collapser collapser-inactive'; + removeListener(collapser, 'click', this._getToggleHandler()); } else { - this.tr[0].cells[0].firstChild.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; - addListener(this.tr[0].cells[0].firstChild, 'click', this._getToggleHandler()); + collapser.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; + addListener(collapser, 'click', this._getToggleHandler()); } } } @@ -746,20 +948,32 @@ TreeGridNode.prototype.removeChild = function(nodeOrIndex) } if (!this.children[nodeOrIndex]) return false; - var e = this.children[nodeOrIndex].tr[0]; + var tr = this.children[nodeOrIndex].tr[0]; + var col_tr = this.children[nodeOrIndex].col_tr && this.children[nodeOrIndex].col_tr[0]; var l = this.children[nodeOrIndex].level; if (this.children[nodeOrIndex].key !== undefined) delete this.childrenByKey[this.children[nodeOrIndex].key]; this.children.splice(nodeOrIndex, 1); for (var i = nodeOrIndex; i < this.children.length; i++) this.children[i]._index--; - var k; + var k, e; + e = tr; do { k = e; e = e.nextSibling; k.parentNode.removeChild(k); } while (e && e._node.level > l); + if (col_tr) + { + e = col_tr; + do + { + k = e; + e = e.nextSibling; + k.parentNode.removeChild(k); + } while (e && e._node.level > l); + } return true; } @@ -772,29 +986,26 @@ TreeGridNode.prototype.addChildren = function(nodes, insertBefore) insertBefore >= this.children.length) { insertBefore = this.children.length; + // тут нужно получить следующий узел после текущего... e = this; while (e.children.length) e = e.children[e.children.length-1]; - e = e.tr ? e.tr[e.tr.length-1].nextSibling : null; + e = e.tr && e.tr[e.tr.length-1].nextSibling ? e.tr[e.tr.length-1].nextSibling._node : null; } else - e = this.children[insertBefore].tr[0]; + e = this.children[insertBefore]; var trs = []; for (var i = 0; i < nodes.length; i++) { - var child = new TreeGridNode(nodes[i], this.grid, this.level+1, e, this.collapsed); + var child = new TreeGridNode(nodes[i], this.grid, this.level+1, e, this.collapsed, true); child._index = insertBefore+i; this.children.splice(insertBefore+i, 0, child); if (child.key !== undefined) this.childrenByKey[child.key] = child; - // TODO: fix sticky only once per whole node batch, including children - if (this.grid.stickyInit && !this.collapsed) - trs.push.apply(trs, child.tr); } for (var i = insertBefore+nodes.length; i < this.children.length; i++) this.children[i]._index += nodes.length; - if (trs.length) - fixStickyRows(this.grid.table, trs); + this.grid.syncStickyHeaders(); return this.children.slice(insertBefore, nodes.length); } diff --git a/treegridtest.htm b/treegridtest.htm index 427471e..8d047fa 100644 --- a/treegridtest.htm +++ b/treegridtest.htm @@ -5,18 +5,13 @@ - -