treegrid/treegrid.js

1402 lines
45 KiB
JavaScript

/**
* Very simple and fast tree grid/table, with support for:
* - sticky(fixed) row
* - sticky(fixed) column
* - virtual scrolling
* - dynamic loading of child nodes
*
* License: MPL 2.0+, (c) Vitaliy Filippov 2016+
* Version: 2017-07-13
*/
/**
* USAGE:
*
* 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
* // for multi-row nodes renderer must return array of multiple rows
* renderer = function(node) { return [ { innerHTML, style, className, title }, ... ] }
*
* items: [ node={
* key: <unique key of this node across siblings>,
* children: [ node... ],
* leaf: true/false,
* collapsed: true/false,
* [ rows: <number of rows in this node> ],
* data: <user data>
* }, ... ]
*
* header: [ 'html' or { innerHTML, style, className, title }, ... ]
*
* Readonly properties:
*
* TG.table - grid <table> element
* TG.root - root node
*
* Methods:
*
* // change header and clear grid contents
* TG.setHeader(header);
*
* // change "leaf" status of a node and its children
* node.setChildren(isLeaf, newChildren);
*
* // expand/collapse a node
* node.toggle();
*
* // change node.data and re-render node
* node.render();
*
* // use simple cell editing
* TG.initCellEditing();
*
* // use simple mouse cell selection
* // boolean useMultiple: whether to initialise multi-cell selection (true) or single-cell (false)
* TG.initCellSelection(useMultiple);
*
* Callbacks:
*
* // called before expanding node. should return false if you want to prevent expanding the node
* TG.onExpand = function(node) {};
* // called before a cell is selected with mouse
* TG.onCellSelect = function(node, subrowIndex, colIndex, td) { return <whether to allow selection (boolean)>; }
* // called when all currently selected cells are deselected
* TG.onDeselectAllCells = function() {}
* // called before a cell is edited
* TG.onStartCellEdit = function(node, subrowIndex, colIndex, td) { return {
* abort: <whether to prevent editing>,
* value: <override initial textbox value>
* } }
* // called before a cell is stopped being edited
* TG.onStopCellEdit = function (node, subrowIndex, colIndex, value, td) { return { html: <new cell content> } }
*
*/
function TreeGrid(options)
{
var self = this;
this.renderer = options.renderer;
this.bindProps = { 'style': 1, 'className': 1, 'title': 1, 'innerHTML': 1 };
if (options.bind)
for (var i in options.bind)
this.bindProps[i] = 1;
this.levelIndent = 20;
this.table = document.createElement('table');
this.table.tabIndex = 0;
this.table.className = 'grid';
this.thead = document.createElement('thead');
this.tbody = document.createElement('tbody');
this.table.appendChild(this.thead);
this.table.appendChild(this.tbody);
this.thead.appendChild(document.createElement('tr'));
this.wrapper = document.createElement('div');
this.stickyRow = options.stickyHeaders||options.stickyRow;
this.stickyColumn = options.stickyHeaders||options.stickyColumn;
this.minItemHeight = options.minItemHeight||21;
this.scrollTimeout = options.scrollTimeout||0;
if (this.stickyRow || this.stickyColumn)
{
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)
{
addListener(window, 'resize', function()
{
self.syncStickyHeaders(true);
});
}
}
addListener(window, 'resize', function()
{
self.syncView(true);
});
// table body wrapper
this.wrapper.className = 'grid-wrapper';
this.tableWrapper = document.createElement('div');
this.tableWrapper.tabIndex = 1;
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';
}
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 (!self.scrollTimeout)
self.syncView();
else if (!self.scrollSyncTimer)
self.scrollSyncTimer = setTimeout(function() { self.syncView(); self.scrollSyncTimer = null; }, self.scrollTimeout);
});
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);
this.nodeCount = 0;
this.placeholders = {};
new TreeGridNode({ children: options.items }, this, null, 0);
}
function TreeGridNode(node, grid, parentNode, insertIndex, skipSync)
{
this.grid = grid;
this.level = parentNode ? parentNode.level + 1 : -1;
this.key = node.key;
if (this.key !== undefined)
parentNode.childrenByKey[this.key] = this;
this.data = node.data;
if (parentNode)
{
this.leaf = node.leaf;
this._oldCells = [];
this.collapsed = node.collapsed || !node.leaf && (!node.children || !node.children.length);
this.visible = parentNode.visible && !parentNode.collapsed;
grid.nodeCount += this.visible ? 1 : 0;
parentNode.children.splice(insertIndex, 0, this);
}
else
{
this.visible = true;
grid.root = this;
}
this.children = [];
this.childrenByKey = {};
if (node.children)
{
for (var i = 0; i < node.children.length; i++)
new TreeGridNode(node.children[i], grid, this, i, true);
}
if (!skipSync)
this.grid.syncView();
}
(function() {
function htmlspecialchars(text)
{
return (''+text).replace(/&/g, '&amp;')
.replace(/'/g, '&apos;') // '
.replace(/"/g, '&quot;') // "
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function findSubNodes(node, start, count, result)
{
var skipped = 0, added = 0, counts;
for (var i = 0; i < node.children.length && added < count; i++)
{
if (skipped < start)
{
skipped++;
}
else
{
result.push(node.children[i]);
added++;
}
if (!node.children[i].collapsed)
{
counts = findSubNodes(node.children[i], start-skipped, count-added, result);
skipped += counts[0];
added += counts[1];
}
}
return [ skipped, added ];
}
TreeGrid.prototype._findNodes = function(start, count)
{
var result = [];
findSubNodes(this.root, start, count, result);
return result;
}
function findNodeIndex(node, target)
{
var index = 0, result;
for (var i = 0; i < node.children.length; i++)
{
if (node.children[i] == target)
return [ index, true ];
index++;
if (!node.children[i].collapsed)
{
result = findNodeIndex(node.children[i], target);
index += result[0];
if (result[1])
return [ index, true ];
}
}
return [ index, false ];
}
TreeGrid.prototype._findNodeIndex = function(node)
{
var result = findNodeIndex(this.root, node);
if (!result[1])
return -1;
return result[0];
}
TreeGrid.prototype._renderNodes = function(nodes)
{
for (var i = 0; i < nodes.length; i++)
{
if (!nodes[i].tr)
{
nodes[i].tr = [];
for (var j = 0; j < (nodes[i].rows||1); j++)
{
tr = document.createElement('tr');
tr._node = nodes[i];
nodes[i].tr.push(tr);
}
if (this.stickyColumn)
{
nodes[i].col_tr = [];
for (var j = 0; j < (nodes[i].rows||1); j++)
{
var tr = document.createElement('tr');
tr._node = nodes[i];
nodes[i].col_tr.push(tr);
}
}
}
else
nodes[i]._reuse = true;
nodes[i].render(undefined, undefined, false, 2);
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)
{
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._setPlaceholder = function(name, height)
{
if (!this.placeholders[name])
{
this.placeholders[name] = [];
for (var i = 0; i < (this.fixedColBody ? 2 : 1); i++)
{
this.placeholders[name][i] = document.createElement('tr');
this.placeholders[name][i].is_placeholder = true;
this.placeholders[name][i].appendChild(document.createElement('td'));
}
}
if (height !== undefined)
{
this.placeholders[name][0].firstChild.setAttribute('colspan', this.header.length-(this.fixedColBody ? 1 : 0));
this.placeholders[name][0].style.height = height+'px';
this.placeholders[name][0].style.display = height > 0 ? '' : 'none';
if (this.fixedColBody)
{
this.placeholders[name][1].style.height = height+'px';
this.placeholders[name][1].style.display = height > 0 ? '' : 'none';
}
}
}
TreeGrid.prototype._addPlaceholder = function(name, height)
{
this._setPlaceholder(name, height);
this._syncStartNode(this.placeholders[name][0], this.placeholders[name][1]);
}
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)
{
if (this.oldRenderedNodes[i].unrenderHooks)
{
var hooks = this.oldRenderedNodes[i].unrenderHooks;
for (var k in hooks)
hooks[k]();
delete this.oldRenderedNodes[i].unrenderHooks;
}
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, h1, h2, r = 0, notfound = true;
for (var i = nodes.length-1; i >= 0; i--)
{
nh = getNodeHeight(nodes[i]);
h += nh;
if (notfound && h >= offsetHeight)
{
notfound = false;
r = i + (h-offsetHeight)/nh;
}
}
this.endItemHeight = h;
return r;
}
function getNodeHeight(node)
{
var nh = 0;
if (node.col_tr)
{
for (var j = 0; j < node.tr.length; j++)
{
h1 = node.tr[j].getBoundingClientRect().height||0;
h2 = node.col_tr[j].getBoundingClientRect().height||0;
nh += h1 < h2 ? h2 : h1;
}
}
else
for (var j = 0; j < node.tr.length; j++)
nh += node.tr[j].getBoundingClientRect().height||0;
return nh;
}
TreeGrid.prototype.syncView = function()
{
if (!this.table.offsetParent)
return;
var headerHeight = (this.stickyRow ? this.thead.getBoundingClientRect().height : 0);
var offsetHeight = this.tableWrapper.clientHeight;
var scrollTop = this.tableWrapper.scrollTop;
var visibleItems = Math.ceil(offsetHeight/this.minItemHeight);
var endStart = this.nodeCount - visibleItems;
var targetHeight;
if (endStart < 0)
endStart = 0;
// Always render last items
var lastNodes = this._findNodes(endStart, this.nodeCount-endStart);
this._setPlaceholder('top', 0);
this._beginSync();
this._renderNodes(lastNodes);
this._syncEnd(lastNodes);
this.lastFirstNodeIndex = 0;
if (endStart > 0)
{
// Calculate virtual scroll
this.lastFirstNodeIndex = endStart+this._findVisibleNodeOffset(lastNodes, offsetHeight-headerHeight);
var avgItemHeight = this.endItemHeight / lastNodes.length;
avgItemHeight = (avgItemHeight < this.minItemHeight ? this.minItemHeight : avgItemHeight);
targetHeight = this.nodeCount*avgItemHeight + headerHeight;
if (this.stickyColumn)
this.fixedColSizer.style.height = targetHeight+'px';
this.tableSizer.style.height = targetHeight+'px';
var scrollPos = targetHeight > offsetHeight ? scrollTop / (targetHeight - offsetHeight) : 0;
if (scrollPos > 1)
scrollPos = 1;
var firstVisible = scrollPos*this.lastFirstNodeIndex;
var rangeStart = Math.floor(firstVisible);
var rangeCount = visibleItems;
this._addPlaceholder('top', 0);
this._setPlaceholder('mid', 0);
var firstItemHeight;
if (rangeStart >= endStart)
{
// Nothing more to render
firstItemHeight = 0;
for (var i = rangeStart-1; i >= endStart; i--)
{
firstItemHeight += getNodeHeight(lastNodes[i-endStart]);
}
firstItemHeight += getNodeHeight(lastNodes[rangeStart-endStart])*(firstVisible-rangeStart);
}
else
{
if (rangeStart+rangeCount > endStart)
{
rangeCount = endStart-rangeStart;
}
var visibleNodes = this._findNodes(rangeStart, rangeCount);
this._renderNodes(visibleNodes);
this._syncStart(visibleNodes);
this._addPlaceholder('mid', 0);
firstItemHeight = getNodeHeight(visibleNodes[0])*(firstVisible-rangeStart);
}
this._setPlaceholder('top', scrollTop - firstItemHeight);
}
else if (this.stickyColumn)
{
this.fixedColSizer.style.height = 0;
this.tableSizer.style.height = 0;
}
this._finishSync();
// Then sync row sizes for fixed column to work properly
var total = 0;
if (this.stickyColumn)
{
total = this.syncStickyHeaders(true);
}
else
{
this.syncStickyRow();
if (endStart > 0 && endStart > rangeStart+rangeCount)
for (var i = 0; i < this.tbody.rows.length; i++)
if (!this.tbody.rows[i].is_placeholder)
total += (this.tbody.rows[i].getBoundingClientRect().height||0);
}
if (endStart > 0 && endStart > rangeStart+rangeCount)
{
this._setPlaceholder('mid', targetHeight - headerHeight - total - (scrollTop-firstItemHeight));
}
}
TreeGrid.prototype.scrollToNode = function(node)
{
// With virtual scrolling, we must reverse-engineer positioning to find the node
var nodeIndex = this._findNodeIndex(node);
if (nodeIndex < 0)
return false;
if (this.lastFirstNodeIndex > 0)
{
var offsetHeight = this.tableWrapper.clientHeight;
this.tableWrapper.scrollTop = nodeIndex/this.lastFirstNodeIndex * (this.tableWrapper.scrollHeight - offsetHeight);
}
return true;
}
TreeGrid.prototype.nodeOnHover = function(hover, ev)
{
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)
{
e = e.previousSibling;
subrow++;
}
if (!e._node.tr)
return;
if (hover)
{
if (this.hoveredNode && this.hoveredNode.tr)
{
this.hoveredNode.tr[this.hoveredRow].className = this.hoveredNode.tr[this.hoveredRow].className.replace(/ hover/g, '');
if (this.stickyColumn)
this.hoveredNode.col_tr[this.hoveredRow].className = this.hoveredNode.col_tr[this.hoveredRow].className.replace(/ hover/g, '');
}
e._node.tr[subrow].className += ' hover';
this.hoveredNode = e._node;
this.hoveredRow = subrow;
}
else
{
e._node.tr[subrow].className = e._node.tr[subrow].className.replace(/ hover/g, '');
this.hoveredNode = null;
}
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.fixedCol.style.width = this.fixedColWrapper.clientWidth+'px';
this.tableWrapper.style.paddingLeft = (w-this.stickyBorderWidth)+'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.wrapper.offsetParent)
return 0;
var total = 0;
if (this.stickyColumn)
{
if (this.fixedCol.offsetWidth)
this.setStickyColumnWidth(this.fixedCol.getBoundingClientRect().width+'px');
var h1, h2, h = [];
if (sync_all)
{
for (var i = 0; i < this.tbody.rows.length; i++)
{
h1 = this.tbody.rows[i].getBoundingClientRect().height||0;
h2 = this.fixedColBody.rows[i].getBoundingClientRect().height||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)
{
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].getBoundingClientRect().height;
h2 = this.toSync[i].col_tr[k].getBoundingClientRect().height;
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;
}
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].getBoundingClientRect().width || 0;
this.fixedRow.style.width = this.table.getBoundingClientRect().width+'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.fixedRowWrapper.style.right = (this.tableWrapper.offsetWidth-this.tableWrapper.clientWidth)+'px';
if (this.stickyColumn)
{
this.fixedColWrapper2.style.bottom = (this.tableWrapper.getBoundingClientRect().height-this.tableWrapper.clientHeight)+'px';
var frh = this.fixedRow.getBoundingClientRect().height;
this.fixedColWrapper.style.paddingTop = frh+'px';
if (this.fixedCell.rows[0].cells[0])
this.fixedCell.rows[0].cells[0].style.height = frh+'px';
}
}
TreeGrid.prototype.setHeader = function(newHeader)
{
var tr = this.thead.rows[0];
tr.innerHTML = '';
if (this.stickyRow)
{
this.sizeRow.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.stickyColumn && i == 0)
{
(this.fixedCell || this.fixedColHeader).rows[0].appendChild(th);
}
else
{
tr.appendChild(th);
if (this.stickyRow)
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, []);
}
TreeGrid.prototype.onExpand = function(node)
{
// handle node expand/collapse here
return true;
}
TreeGrid.prototype._setProps = function(el, props)
{
if (props === undefined || props === null)
{
for (var j in this.bindProps)
{
if (j == 'innerHTML' || j == 'className')
el[j] = '';
else
el.setAttribute(j, '');
}
}
else if (typeof props == 'string')
{
el.innerHTML = props;
}
else
{
for (var j in this.bindProps)
{
if (j == 'innerHTML' || j == 'className')
el[j] = (props[j] !== undefined ? props[j] : '');
else
el.setAttribute(j, (props[j] !== undefined ? props[j] : ''));
}
}
}
// Simple cell editing
TreeGrid.prototype._getCellIndex = function(n, sticky)
{
var i = 0, p = n;
while ((p = p.previousSibling))
i += (p.nodeType != 3 ? 1 : 0);
if (sticky && this.stickyRow)
{
p = n.parentNode.parentNode.parentNode;
if (p == this.table || p == this.fixedRow)
i++;
}
return i;
}
TreeGrid.prototype._getSubrow = function(cell)
{
var row = cell.parentNode, subrow = 0;
while (row.previousSibling && row.previousSibling._node == row._node)
{
row = row.previousSibling;
subrow++;
}
return subrow;
}
TreeGrid.prototype.initCellEditing = function()
{
var self = this;
self.editedCells = []; // FIXME maybe remove and use just class selector
addListener(this.table, 'dblclick', function(evt)
{
evt = evt||window.event;
var td = evt.target||evt.srcElement;
while (td.nodeName != 'TABLE' && td.nodeName != 'TD')
td = td.parentNode;
if (td.nodeName == 'TD' && td.parentNode._node && td.className.indexOf('celleditor') < 0)
{
var node = td.parentNode._node;
var subrow = self._getSubrow(td);
var cellIndex = self._getCellIndex(td, true);
var params = self.onStartCellEdit && self.onStartCellEdit(node, subrow, cellIndex, td) || {};
if (params.abort)
return;
self.startCellEditing(td, params, node, subrow, cellIndex);
}
});
addListener(document.body, 'click', function(evt)
{
if (!self.editedCells.length)
return;
var td = evt.target||evt.srcElement;
while (td && td.nodeName != 'TD')
td = td.parentNode;
if (td && /\bcelleditor\b/.exec(td.className))
return;
for (var i = self.editedCells.length-1; i >= 0; i--)
self.stopCellEditing(self.editedCells[i], true);
self.editedCells = [];
});
}
TreeGrid.prototype.startCellEditing = function(td, params, node, subrow, cellIndex)
{
var self = this;
if (!node)
{
node = td.parentNode._node;
subrow = self._getSubrow(td);
cellIndex = self._getCellIndex(td, true);
}
node.unrenderHooks = node.unrenderHooks||{};
node.unrenderHooks.editing = function()
{
self.stopCellEditing(td, false, node, subrow, cellIndex);
};
self.editedCells.push(td);
if (params.value === undefined)
params.value = td.innerHTML;
td._origWidth = td.style.width;
td._origMinWidth = td.style.minWidth;
td.style.width = td.style.minWidth = td.getBoundingClientRect().width+'px';
td.className += ' celleditor';
if ('ActiveXObject' in window)
{
td.innerHTML = '<div><input type="text" value="'+htmlspecialchars(params.value)+'" /></div>';
td.style.minHeight = td.offsetHeight+'px';
td.style.height = 'inherit';
td.parentNode.style.height = '1px';
}
else
td.innerHTML = '<input type="text" value="'+htmlspecialchars(params.value)+'" />';
var inp = td.getElementsByTagName('input')[0];
addListener(inp, 'keydown', function(evt)
{
if (evt.keyCode == 13 || evt.keyCode == 10)
self.stopCellEditing(td);
});
inp.focus();
}
TreeGrid.prototype.stopCellEditing = function(td, _int, node, subrow, cellIndex)
{
if (td.offsetParent)
{
node = td.parentNode._node;
subrow = this._getSubrow(td);
cellIndex = this._getCellIndex(td, true);
}
delete node.unrenderHooks.editing;
td.style.width = td._origWidth;
td.style.minWidth = td._origMinWidth;
var inp = td.getElementsByTagName('input')[0];
node._oldCells[cellIndex] = undefined;
var params = this.onStopCellEdit &&
this.onStopCellEdit(node, subrow, cellIndex, inp ? inp.value : null, td) || {};
if (td.offsetParent)
{
node.render(cellIndex, subrow, true, 0);
}
if (!_int)
{
for (var i = 0; i < this.editedCells.length; i++)
{
if (this.editedCells[i] == td)
{
this.editedCells.splice(i, 1);
break;
}
}
}
}
// Simple cell selection
// FIXME add to unrenderHooks?
TreeGrid.prototype.initCellSelection = function(useMultiple)
{
var self = this;
self.selectCell = function(cell)
{
if (!cell.parentNode._node || cell.className.indexOf(' selected') >= 0)
return;
var i = self._getCellIndex(cell, true);
var subrow = self._getSubrow(cell);
if (!self.onCellSelect || self.onCellSelect(cell.parentNode._node, subrow, i, cell))
cell.className += ' selected';
};
self.deselectAll = function(cell)
{
var els = (self.wrapper||self.table).querySelectorAll('td.selected');
var deselected = [];
if (self.onDeselectAllCells)
self.onDeselectAllCells();
for (var i = 0; i < els.length; i++)
els[i].className = els[i].className.replace(' selected', '');
};
if (useMultiple)
initMultiCellSelection(self);
else
initSingleCellSelection(self);
}
function initSingleCellSelection(self)
{
addListener(self.wrapper||self.table, 'mousedown', function(evt)
{
self.deselectAll();
if (evt.which != 1)
return;
var td = evt.target||evt.srcElement;
while (td && (td.nodeName != 'TD' && td.nodeName != 'TH'))
td = td.parentNode;
if (td)
self.selectCell(td);
});
}
function initMultiCellSelection(self)
{
var startDrag, dragDiv;
self.table.className += ' disable-text-select';
addListener(self.wrapper||self.table, 'mousedown', function(evt)
{
evt = getEventCoord(evt);
if (!evt.shiftKey)
self.deselectAll();
if (evt.which != 1)
return;
startDrag = [ evt.pageX, evt.pageY ];
if (!dragDiv)
{
dragDiv = document.createElement('div');
dragDiv.className = 'selection-rect';
dragDiv.style.display = 'none';
document.body.appendChild(dragDiv);
}
});
addListener(document.body, 'mousemove', function(evt)
{
if (startDrag)
{
evt = getEventCoord(evt);
if (dragDiv.style.display == 'none')
{
if ((startDrag[0] < evt.pageX-10 || startDrag[0] > evt.pageX+10) &&
(startDrag[1] < evt.pageY-10 || startDrag[1] > evt.pageY+10))
dragDiv.style.display = 'block';
else
return;
}
if (startDrag[0] < evt.pageX)
{
dragDiv.style.left = startDrag[0]+'px';
dragDiv.style.width = (evt.pageX-startDrag[0])+'px';
}
else
{
dragDiv.style.left = evt.pageX+'px';
dragDiv.style.width = (startDrag[0]-evt.pageX)+'px';
}
if (startDrag[1] < evt.pageY)
{
dragDiv.style.top = startDrag[1]+'px';
dragDiv.style.height = (evt.pageY-startDrag[1])+'px';
}
else
{
dragDiv.style.top = evt.pageY+'px';
dragDiv.style.height = (startDrag[1]-evt.pageY)+'px';
}
}
});
addListener(document.body, 'mouseup', function(evt)
{
if (startDrag)
{
evt = getEventCoord(evt);
dragDiv.style.display = 'none';
var x1 = startDrag[0], y1 = startDrag[1], x2 = evt.pageX, y2 = evt.pageY;
if (x2 < x1)
{
var t = x2;
x2 = x1;
x1 = t;
}
if (y2 < y1)
{
var t = y2;
y2 = y1;
y1 = t;
}
startDrag = null;
if (x2 > x1+10 && y2 > y1+10)
{
var bodyPos = self.tbody.getBoundingClientRect();
if (x1 < bodyPos.left+self.tbody.offsetWidth &&
y1 < bodyPos.top+self.tbody.offsetHeight &&
x2 > bodyPos.left && y2 > bodyPos.top)
{
if (x1 <= bodyPos.left)
x1 = bodyPos.left+1;
if (y1 <= bodyPos.top)
y1 = bodyPos.top+1;
if (x2 >= bodyPos.left+self.tbody.offsetWidth)
x2 = bodyPos.left+self.tbody.offsetWidth-1;
if (y2 >= bodyPos.top+self.tbody.offsetHeight)
y2 = bodyPos.top+self.tbody.offsetHeight-1;
var e1 = document.elementFromPoint(x1, y1);
var e2 = document.elementFromPoint(x2, y2);
while (e1 && e1.nodeName != 'TD')
e1 = e1.parentNode;
while (e2 && e2.nodeName != 'TD')
e2 = e2.parentNode;
if (e1 && e2)
{
var col1 = self._getCellIndex(e1);
var col2 = self._getCellIndex(e2);
var row = e1.parentNode;
do
{
if (row.style.display != 'none' && row.style.visibility != 'hidden')
for (var i = col1; i <= col2; i++)
if (!self.stickyInit || i != 1)
self.selectCell(row.cells[i]);
if (row == e2.parentNode)
break;
row = row.nextSibling;
} while (row);
}
}
}
else
{
var t = evt.target || evt.srcElement;
self.selectCell(t);
}
if (x2 > x1+10 && y2 > y1+10 || evt.shiftKey)
{
if (document.selection)
document.selection.empty();
else
window.getSelection().removeAllRanges();
}
}
});
}
// Tree grid node class
TreeGridNode.prototype.getKey = function()
{
return this.key;
}
TreeGridNode.prototype.getChildByKey = function(key)
{
return this.childrenByKey[key];
}
TreeGridNode.prototype._addCollapser = function()
{
var collapser = document.createElement('div');
if (this.leaf)
collapser.className = 'collapser collapser-inactive';
else
{
collapser.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded';
addListener(collapser, 'click', this._getToggleHandler());
}
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)
{
var p0 = curStyle(c0).paddingLeft || '';
if (p0.substr(p0.length-2) == 'px')
p0 = parseInt(p0.substr(0, p0.length-2));
else
p0 = 0;
this.grid._initialPadding = p0;
}
c0.style.paddingLeft = (this.grid._initialPadding + this.grid.levelIndent * this.level + 20) + 'px';
}
TreeGridNode.prototype._hashEqual = function(a, b)
{
for (var i in a)
if (b[i] !== a[i])
return false;
for (var i in b)
if (b[i] !== a[i])
return false;
return true;
}
TreeGridNode.prototype._renderCell = function(rowIndex, colIndex, cell, force)
{
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
if (force || !this._oldCells[rowIndex] || !this._oldCells[rowIndex][colIndex] ||
!this._hashEqual(this._oldCells[rowIndex][colIndex], cell||{}))
{
var old;
if (colIndex == 0 && rowIndex == 0)
{
old = {
width: tr.cells[ri].style.width,
height: tr.cells[ri].style.height,
position: tr.cells[ri].style.position,
display: tr.cells[ri].style.display,
zIndex: tr.cells[ri].style.zIndex
};
}
this.grid._setProps(tr.cells[ri], cell);
if (colIndex == 0)
{
if (rowIndex == 0)
this._addCollapser();
else
tr.cells[ri].style.paddingLeft = (this.grid._initialPadding + this.grid.levelIndent * this.level + 20) + 'px';
}
return true;
}
return false;
}
TreeGridNode.prototype.render = function(colidx, rowidx, force, skipSync)
{
if (!this.tr)
{
// Node is not visible, skip rendering
return false;
}
var cells = this.grid.renderer(this, colidx, rowidx);
if (this.tr.length == 1)
cells = [ cells ];
if (colidx === null || colidx < 0 || colidx >= this.grid.header.length)
colidx = undefined;
if (rowidx === null || rowidx < 0 || rowidx >= this.tr.length)
rowidx = undefined;
var modified = false;
if (rowidx !== undefined)
{
if (colidx !== undefined)
{
modified = this._renderCell(rowidx, colidx, cells[rowidx] && cells[rowidx][colidx], force) || modified;
this._oldCells[rowidx] = this._oldCells[rowidx] || [];
this._oldCells[rowidx][colidx] = cells[rowidx] && cells[rowidx][colidx];
}
else
{
for (var j = 0; j < this.grid.header.length; j++)
modified = this._renderCell(rowidx, j, cells[rowidx] && cells[rowidx][j], force) || modified;
this._oldCells[rowidx] = cells[rowidx];
}
}
else if (colidx !== undefined)
{
for (var i = 0; i < this.tr.length; i++)
{
modified = this._renderCell(i, colidx, cells[i] && cells[i][colidx], force) || modified;
this._oldCells[i] = this._oldCells[i] || [];
this._oldCells[i][colidx] = cells[i] && cells[i][colidx];
}
}
else
{
for (var i = 0; i < this.tr.length; i++)
for (var j = 0; j < this.grid.header.length; j++)
modified = this._renderCell(i, j, cells[i] && cells[i][j], force) || modified;
this._oldCells = cells;
}
if (modified && this.grid.stickyColumn && skipSync !== 2)
{
this.grid.toSync.push(this);
if (!skipSync)
this.grid.syncStickyHeaders();
}
return modified;
}
TreeGridNode.prototype._getToggleHandler = function()
{
var self = this;
if (!self._toggleHandlerClosure)
self._toggleHandlerClosure = function(e) { self._toggleHandler(); };
return self._toggleHandlerClosure;
}
TreeGridNode.prototype._toggleHandler = function()
{
if (!this.grid.onExpand(this))
return;
this.toggle();
}
function setHiddenNodes(node)
{
if (!node.children)
return;
var nv = node.visible && !node.collapsed;
for (var i = 0; i < node.children.length; i++)
{
if (nv != node.children[i].visible)
{
node.grid.nodeCount += nv ? 1 : -1;
node.children[i].visible = nv;
setHiddenNodes(node.children[i]);
}
}
}
TreeGridNode.prototype.toggle = function(skipSync)
{
if (this.leaf)
return;
this.collapsed = !this.collapsed;
if (this.tr)
{
(this.col_tr || this.tr)[0].cells[0].firstChild.className =
this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded';
}
setHiddenNodes(this);
if (!skipSync)
this.grid.syncView();
}
TreeGridNode.prototype.setChildren = function(isLeaf, newChildren, skipSync)
{
this.leaf = isLeaf;
if (this.visible)
{
this.visible = false;
setHiddenNodes(this);
this.visible = true;
}
this.children = [];
this.childrenByKey = {};
this.addChildren(newChildren, null, skipSync);
}
TreeGridNode.prototype.removeChild = function(index, skipSync)
{
if (!this.children[index])
return false;
if (this.children[index].key !== undefined)
delete this.childrenByKey[this.children[index].key];
this.children.splice(index, 1);
if (!skipSync)
this.grid.syncView();
return true;
}
TreeGridNode.prototype.addChildren = function(nodes, insertBefore, skipSync)
{
var e;
if (insertBefore === undefined || insertBefore === null || insertBefore === false ||
insertBefore >= this.children.length)
{
insertBefore = this.children.length;
}
else if (insertBefore < 0)
insertBefore = 0;
else
e = this.children[insertBefore];
for (var i = 0; i < nodes.length; i++)
{
new TreeGridNode(nodes[i], this.grid, this, insertBefore+i, true);
}
if (!skipSync)
this.grid.syncView();
return this.children.slice(insertBefore, nodes.length);
}
})();