/** * Very simple and fast tree grid/table, compatible with dynamic loading and jQuery fixedHeaderTable * License: MPL 2.0+, (c) Vitaliy Filippov 2016 */ /** * 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 * }, ... ] * * 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; // header change clears the whole grid by now if (this.root) this.root.setChildren([]); } 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]; } function TreeGridNode(node, grid, level, insertBefore, startHidden) { this.grid = grid; this.level = level; 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._level = this.level; 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) + '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._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) { if (!this.tr) this.grid.tbody.innerHTML = ''; else while (this.tr.nextSibling && this.tr.nextSibling._level > this.level) this.grid.tbody.removeChild(this.tr.nextSibling); this.leaf = isLeaf; 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)); }