/** * 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: , * children: [ node... ], * leaf: true/false, * collapsed: true/false, * [ rows: ], * data: * }, ... ] * * header: [ 'html' or { innerHTML, style, className, title }, ... ] * * Readonly properties: * * TG.table - grid 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 ; } * // 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: , * value: * } } * // called before a cell is stopped being edited * TG.onStopCellEdit = function (node, subrowIndex, colIndex, value, td) { return { html: } } * */ 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, '&') .replace(/'/g, ''') // ' .replace(/"/g, '"') // " .replace(//g, '>'); } 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 = '
'; td.style.minHeight = td.offsetHeight+'px'; td.style.height = 'inherit'; td.parentNode.style.height = '1px'; } else td.innerHTML = ''; 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); } })();