/** * Very simple and fast tree grid/table, compatible with dynamic loading and jQuery fixedHeaderTable * License: MPL 2.0+, (c) Vitaliy Filippov 2016+ * Version: 2016-03-09 */ /** * USAGE: * * var TG = new TreeGrid(items, header); * document.body.appendChild(TG.table); * * items: [ node={ * cells: [ 'html' or { innerHTML, style, className, title }, ... ], * children: [ node... ], * leaf: true/false, * collapsed: true/false, * data: * }, ... ] * * header: [ 'html' or { innerHTML, style, className, title }, ... ] */ function TreeGrid(items, header) { this.bindProps = { 'style': 1, 'className': 1, 'title': 1, 'innerHTML': 1 }; this.levelIndent = 20; this.table = document.createElement('table'); 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.setHeader(header); new TreeGridNode({ children: items }, this); } TreeGrid.prototype.setHeader = function(newHeader) { var tr = this.thead.rows[0]; tr.innerHTML = ''; for (var i = 0; i < newHeader.length; i++) { var th = document.createElement('th'); this._setProps(th, newHeader[i]); tr.appendChild(th); } 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 (typeof props == 'string') el.innerHTML = props; else for (var j in this.bindProps) if (props[j]) el[j] = props[j]; } // Simple cell editing TreeGrid.prototype.getNodeIndex = function(n) { var i = 0; while ((n = n.previousSibling)) i += (n.nodeType != 3 ? 1 : 0); return i; } // onStartEdit(node, colIndex, td), onStopEdit(node, colIndex, value, td) TreeGrid.prototype.initCellEditing = function(onStartEdit, onStopEdit) { var self = this; self.onStartCellEdit = onStartEdit; self.onStopCellEdit = onStopEdit; self.editedCells = []; // FIXME maybe remove and use just class selector addListener(this.table, 'dblclick', function(evt) { 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 != 'celleditor') { var params = self.onStartCellEdit && self.onStartCellEdit(td.parentNode._node, self.getNodeIndex(td), td) || {}; if (params.abort) return; self.editedCells.push(td); td.className += ' celleditor'; if (params.value === undefined) params.value = td.innerHTML; td.innerHTML = ''; addListener(td.firstChild, 'keydown', function(evt) { if (evt.keyCode == 13 || evt.keyCode == 10) self.stopCellEditing(td); }); td.firstChild.focus(); } }); 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], 1); self.editedCells = []; }); } TreeGrid.prototype.stopCellEditing = function(td, _int) { var i = this.getNodeIndex(td); var params = this.onStopCellEdit && this.onStopCellEdit(td.parentNode._node, i, td.firstChild.value, td) || {}; td.innerHTML = params.html || td.firstChild.value; if (typeof td.parentNode._node.cells[i] == 'object') td.parentNode._node.cells[i].innerHTML = td.innerHTML; else td.parentNode._node.cells[i] = td.innerHTML; td.className = ''; if (!_int) { for (var i = 0; i < this.editedCells.length; i++) { if (this.editedCells[i] == td) { this.editedCells.splice(i, 1); break; } } } } // Tree grid node class function TreeGridNode(node, grid, level, insertBefore, startHidden) { this.grid = grid; this.level = level; this.data = node.data; if (this.level === undefined) this.level = -1; if (!grid.root) grid.root = this; else { this.leaf = node.leaf; this.collapsed = node.collapsed || !node.leaf && (!node.children || !node.children.length); this.cells = node.cells || []; this.tr = document.createElement('tr'); if (startHidden) this.tr.style.display = 'none'; this.tr._node = this; for (var i = 0; i < this.grid.header.length; i++) { var td = document.createElement('td'); if (this.cells[i]) grid._setProps(td, this.cells[i]); this.tr.appendChild(td); } var collapser = document.createElement('div'); if (this.leaf) collapser.className = 'collapser collapser-inactive'; else { collapser.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; var self = this; addListener(collapser, 'click', function(e) { self._toggleHandler(); }); } var c0 = this.tr.cells[0]; c0.childNodes.length ? c0.insertBefore(collapser, c0.firstChild) : c0.appendChild(collapser); if (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; grid._initialPadding = p0; } c0.style.paddingLeft = (grid._initialPadding + grid.levelIndent * level + 20) + 'px'; insertBefore ? grid.tbody.insertBefore(this.tr, insertBefore) : grid.tbody.appendChild(this.tr); } this.children = []; if (node.children) for (var i = 0; i < node.children.length; i++) this.children.push(new TreeGridNode(node.children[i], grid, this.level+1, insertBefore, this.collapsed)); } TreeGridNode.prototype._toggleHandler = function() { if (!this.grid.onExpand(this)) return; this.toggle(); } TreeGridNode.prototype.toggle = function() { if (this.leaf) return; this.collapsed = !this.collapsed; this.tr.cells[0].firstChild.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; if (this.collapsed) { // collapse all children var c = this.tr.nextSibling; while (c && c._node.level > this.level) { c.style.display = 'none'; c = c.nextSibling; } } else { // expand all children except children of collapsed subnodes var st = this.children.concat([]); while (st.length) { var e = st.pop(); e.tr.style.display = ''; if (!e.collapsed) st = st.concat(e.children); } } } TreeGridNode.prototype.setChildren = function(isLeaf, newChildren) { this.leaf = isLeaf; if (!this.tr) this.grid.tbody.innerHTML = ''; else { while (this.tr.nextSibling && this.tr.nextSibling._node.level > this.level) this.grid.tbody.removeChild(this.tr.nextSibling); if (this.leaf) this.tr.cells[0].firstChild.className = 'collapser collapser-inactive'; else this.tr.cells[0].firstChild.className = this.collapsed ? 'collapser collapser-collapsed' : 'collapser collapser-expanded'; } this.children = []; var insertBefore = this.tr && this.tr.nextSibling; for (var i = 0; i < newChildren.length; i++) this.children.push(new TreeGridNode(newChildren[i], this.grid, this.level+1, insertBefore, this.collapsed)); }