From bcc9a26ef21193711ea6af75dbd43ee52da548c9 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Wed, 5 Jul 2017 16:42:38 +0300 Subject: [PATCH] virtual scroll v2 --- treegrid.js | 319 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 212 insertions(+), 107 deletions(-) diff --git a/treegrid.js b/treegrid.js index c99bfa5..0967f0c 100644 --- a/treegrid.js +++ b/treegrid.js @@ -95,6 +95,7 @@ function TreeGrid(options) if (options.stickyHeaders) { this.stickyHeaders = true; + this.minItemHeight = 24; this.toSync = []; this.wrapper = document.createElement('div'); // table for fixed cell @@ -227,9 +228,8 @@ function htmlspecialchars(text) .replace(/>/g, '>'); } -function findNodes(node, start, count, result) +function findSubNodes(node, start, count, result) { - result = result || []; var skipped = 0, added = 0, counts; for (var i = 0; i < node.children.length && added < count; i++) { @@ -244,7 +244,7 @@ function findNodes(node, start, count, result) } if (!node.children[i].collapsed) { - counts = findNodes(node.children[i], start-skipped, count-added, result); + counts = findSubNodes(node.children[i], start-skipped, count-added, result); skipped += counts[0]; added += counts[1]; } @@ -252,66 +252,15 @@ function findNodes(node, start, count, result) return [ skipped, added ]; } -TreeGrid.prototype.syncView = function() +TreeGrid.prototype._findNodes = function(node, start, count) +{ + var result = []; + findSubNodes(node, start, count, result); + return result; +} + +TreeGrid.prototype._renderNodes = function(nodes) { - if (!this.table.offsetParent) - return; - var itemheight = 24; - var reserve = 300; - var offsetHeight = this.wrapper.offsetHeight - this.thead.offsetHeight; - var scrollTop = this.tableWrapper.scrollTop; - var scrollHeight = itemheight*this.nodeCount + 2*reserve; - var effheight = (scrollHeight - offsetHeight+1) / this.nodeCount; - var first = Math.floor(scrollTop / effheight); - var firstOffset = scrollTop - first*effheight; - var count = Math.floor(offsetHeight/itemheight); - var nodes = []; - findNodes(this.root, first, count, nodes); - var prefix_tr, prefix_col_tr, suffix_tr, suffix_col_tr; - if (this.tbody.firstChild && this.tbody.firstChild.is_prefix) - { - prefix_tr = this.tbody.firstChild; - if (this.stickyHeaders) - prefix_col_tr = this.fixedColBody.firstChild; - } - if (this.tbody.lastChild && this.tbody.lastChild.is_suffix) - { - suffix_tr = this.tbody.lastChild; - if (this.stickyHeaders) - suffix_col_tr = this.fixedColBody.lastChild; - } - this.tbody.innerHTML = ''; - if (this.stickyHeaders) - this.fixedColBody.innerHTML = ''; - var tr, placeholderHeight; - if (first > 0) - { - placeholderHeight = Math.floor(first * itemheight)+'px'; - if (!prefix_tr) - { - prefix_tr = document.createElement('tr'); - prefix_tr.is_prefix = true; - prefix_tr.appendChild(document.createElement('td')); - } - this.tbody.appendChild(prefix_tr); - prefix_tr.firstChild.style.height = placeholderHeight; - if (this.stickyHeaders) - { - if (!prefix_col_tr) - { - prefix_col_tr = document.createElement('tr'); - prefix_col_tr.is_prefix = true; - prefix_col_tr.appendChild(document.createElement('td')); - } - this.fixedColBody.appendChild(prefix_col_tr); - prefix_col_tr.firstChild.style.height = placeholderHeight; - } - } - if (this.renderedNodes) - { - for (var i = 0; i < this.renderedNodes.length; i++) - this.renderedNodes[i]._reuse = false; - } for (var i = 0; i < nodes.length; i++) { if (!nodes[i].tr) @@ -337,42 +286,227 @@ TreeGrid.prototype.syncView = function() else nodes[i]._reuse = true; nodes[i].render(undefined, undefined, false, true); - for (var j = 0; j < nodes[i].tr.length; j++) - this.tbody.appendChild(nodes[i].tr[j]); - if (this.stickyHeaders) - for (var j = 0; j < nodes[i].col_tr.length; j++) - this.fixedColBody.appendChild(nodes[i].col_tr[j]); + this.renderedNodes.push(nodes[i]); } +} + +TreeGrid.prototype._beginSync = function() +{ if (this.renderedNodes) { for (var i = 0; i < this.renderedNodes.length; i++) + this.renderedNodes[i]._reuse = false; + } + this.oldRenderedNodes = this.renderedNodes; + this.renderedNodes = []; +} + +TreeGrid.prototype._syncEndNode = function(tr, col_tr) +{ + var before = this.lastSyncChild, beforeCol = this.lastSyncColChild; + if (!this.lastSyncChild) + { + this.lastSyncChild = this.tbody.lastChild; + this.lastSyncColChild = this.fixedColBody && this.fixedColBody.lastChild; + } + else + { + this.lastSyncChild = this.lastSyncChild.previousSibling; + if (this.fixedColBody) + this.lastSyncColChild = this.lastSyncColChild.previousSibling; + } + if (this.lastSyncChild != tr) + { + this.tbody.insertBefore(tr, before); + this.lastSyncChild = tr; + if (this.fixedColBody) { - if (!this.renderedNodes[i]._reuse && this.renderedNodes[i].tr) + this.fixedColBody.insertBefore(col_tr, beforeCol); + this.lastSyncColChild = col_tr; + } + } +} + +TreeGrid.prototype._syncEnd = function(nodes) +{ + this.lastSyncChild = undefined; + this.lastSyncColChild = undefined; + for (var i = nodes.length-1; i >= 0; i--) + { + for (var j = nodes[i].tr.length-1; j >= 0; j--) + { + this._syncEndNode(nodes[i].tr[j], this.fixedColBody && nodes[i].col_tr[j]); + } + } + this.firstSyncChild = this.tbody.firstChild; + this.firstSyncColChild = this.fixedColBody && this.fixedColBody.firstChild; +} + +TreeGrid.prototype._syncStartNode = function(tr, col_tr) +{ + if (this.firstSyncChild != tr) + { + this.tbody.insertBefore(tr, this.firstSyncChild); + if (this.fixedColBody) + this.fixedColBody.insertBefore(col_tr, this.firstSyncColChild); + } + else + { + this.firstSyncChild = this.firstSyncChild.nextSibling; + if (this.fixedColBody) + this.firstSyncColChild = this.firstSyncColChild.nextSibling; + } +} + +TreeGrid.prototype._syncStart = function(nodes) +{ + for (var i = 0; i < nodes.length; i++) + { + for (var j = 0; j < nodes[i].tr.length; j++) + { + this._syncStartNode(nodes[i].tr[j], this.fixedColBody && nodes[i].col_tr[j]); + } + } +} + +TreeGrid.prototype._addPlaceholder = function(elems) +{ + if (elems <= 0) + return; + var height = elems*this.minItemHeight; + var tr = document.createElement('tr'), col_tr; + tr.is_placeholder = true; + tr.appendChild(document.createElement('td')); + tr.style.height = height+'px'; + if (this.fixedColBody) + { + col_tr = document.createElement('tr'); + col_tr.is_placeholder = true; + col_tr.appendChild(document.createElement('td')); + col_tr.style.height = height+'px'; + } + this._syncStartNode(tr, col_tr); +} + +TreeGrid.prototype._finishSync = function() +{ + if (this.oldRenderedNodes) + { + for (var i = 0; i < this.oldRenderedNodes.length; i++) + { + if (!this.oldRenderedNodes[i]._reuse && this.oldRenderedNodes[i].tr) { - this.renderedNodes[i].tr = null; - this.renderedNodes[i].col_tr = null; - this.renderedNodes[i]._oldCells = []; + this.oldRenderedNodes[i].tr = null; + this.oldRenderedNodes[i].col_tr = null; + this.oldRenderedNodes[i]._oldCells = []; + } + delete this.oldRenderedNodes[i]._reuse; + } + this.oldRenderedNodes = null; + } + while (this.firstSyncChild && this.firstSyncChild != this.lastSyncChild) + { + var next = this.firstSyncChild.nextSibling; + this.tbody.removeChild(this.firstSyncChild); + this.firstSyncChild = next; + } + if (this.fixedColBody) + { + while (this.firstSyncColChild && this.firstSyncColChild != this.lastSyncColChild) + { + var next = this.firstSyncColChild.nextSibling; + this.fixedColBody.removeChild(this.firstSyncColChild); + this.firstSyncColChild = next; + } + } +} + +TreeGrid.prototype._findVisibleNodeOffset = function(nodes, offsetHeight) +{ + var h = 0, nh = 0, h1, h2; + for (var i = nodes.length-1; i >= 0; i--) + { + nh = 0; + if (this.fixedColBody) + { + for (var j = 0; j < nodes[i].tr.length; j++) + { + h1 = nodes[i].tr[j].offsetHeight||0; + h2 = nodes[i].col_tr[j].offsetHeight||0; + nh += h1 < h2 ? h2 : h1; + } + } + else + for (var j = 0; j < nodes[i].tr.length; j++) + nh += nodes[i].tr[j].offsetHeight||0; + h += nh; + if (h >= offsetHeight) + { + return i + (h-offsetHeight)/nh; + } + } + return 0; +} + +TreeGrid.prototype.syncView = function() +{ + if (!this.table.offsetParent) + return; + var scrollPos = this.tableWrapper.scrollTop / (this.tableWrapper.scrollHeight - this.tableWrapper.offsetHeight + 1); + var offsetHeight = this.wrapper.offsetHeight - this.thead.offsetHeight; + var visibleItems = Math.ceil(offsetHeight/this.minItemHeight); + var endStart = this.nodeCount - visibleItems; + if (endStart < 0) + endStart = 0; + // Always render last items + var lastNodes = this._findNodes(this.root, endStart, this.nodeCount-endStart); + this._beginSync(); + this._renderNodes(lastNodes); + this._syncEnd(lastNodes); + if (endStart > 0) + { + // Calculate virtual scroll + var lastFirst = endStart+this._findVisibleNodeOffset(lastNodes, offsetHeight); + var firstVisible = scrollPos*lastFirst; + var rangeStart = Math.floor(firstVisible); + var rangeCount = visibleItems; + this._addPlaceholder(rangeStart); + if (rangeStart >= endStart) + { + // Nothing more to render + } + else + { + if (rangeStart+rangeCount > endStart) + { + rangeCount = endStart-rangeStart; + } + var visibleNodes = this._findNodes(this.root, rangeStart, rangeCount); + this._renderNodes(visibleNodes); + this._syncStart(visibleNodes); + if (endStart > rangeStart+rangeCount) + { + this._addPlaceholder(endStart-rangeStart-rangeCount); } } } - this.renderedNodes = nodes; - var totalHeight = 0; + this._finishSync(); + // Then sync row sizes for fixed column to work properly if (this.stickyHeaders) { if (this.fixedCol.offsetWidth) this.setStickyColumnWidth(this.fixedCol.offsetWidth+'px'); var h1, h2; var h = []; - for (var i = first > 0 ? 1 : 0; i < this.tbody.rows.length; i++) + for (var i = 0; i < this.tbody.rows.length; i++) { h1 = this.tbody.rows[i].offsetHeight||0; h2 = this.fixedColBody.rows[i].offsetHeight||0; h[i] = h1 < h2 ? h2 : h1; - totalHeight += h[i]; } - for (var i = first > 0 ? 1 : 0; i < this.tbody.rows.length; i++) + for (var i = 0; i < this.tbody.rows.length; i++) { - if (h[i]) + if (h[i] && !this.tbody.rows[i].is_placeholder) { this.tbody.rows[i].style.height = h[i]+'px'; this.fixedColBody.rows[i].style.height = h[i]+'px'; @@ -380,34 +514,6 @@ TreeGrid.prototype.syncView = function() } this.syncStickyRow(); } - else - { - for (var i = first > 0 ? 1 : 0; i < this.tbody.rows.length; i++) - totalHeight += this.tbody.rows[i].offsetHeight||0; - } - if (first+nodes.length < this.nodeCount) - { - placeholderHeight = Math.floor(scrollHeight - totalHeight - first*itemheight)+'px'; - if (!suffix_tr) - { - suffix_tr = document.createElement('tr'); - suffix_tr.is_suffix = true; - suffix_tr.appendChild(document.createElement('td')); - } - this.tbody.appendChild(suffix_tr); - suffix_tr.firstChild.style.height = placeholderHeight; - if (this.stickyHeaders) - { - if (!suffix_col_tr) - { - suffix_col_tr = document.createElement('tr'); - suffix_col_tr.is_prefix = true; - suffix_col_tr.appendChild(document.createElement('td')); - } - this.fixedColBody.appendChild(suffix_col_tr); - suffix_col_tr.firstChild.style.height = placeholderHeight; - } - } } TreeGrid.prototype.nodeOnHover = function(hover, ev) @@ -1018,10 +1124,9 @@ function setHiddenNodes(node) { if (!node.children) return; - var nv; + var nv = node.visible && !node.collapsed; for (var i = 0; i < node.children.length; i++) { - nv = node.visible && !node.collapsed; if (nv != node.children[i].visible) { node.grid.nodeCount += nv ? 1 : -1;