/*! * jquery.fancytree.table.js * * Render tree as table (aka 'treegrid', 'tabletree'). * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * * Copyright (c) 2014, Martin Wendt (http://wwWendt.de) * * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.1.0 * @date 2014-05-29T16:44 */ ;(function($, window, document, undefined) { "use strict"; /* ***************************************************************************** * Private functions and variables */ function _assert(cond, msg){ msg = msg || ""; if(!cond){ $.error("Assertion failed " + msg); } } function insertSiblingAfter(referenceNode, newNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } /* Show/hide all rows that are structural descendants of `parent`. */ function setChildRowVisibility(parent, flag) { parent.visit(function(node){ var tr = node.tr; // currentFlag = node.hide ? false : flag; // fix for ext-filter if(tr){ tr.style.display = (node.hide || !flag) ? "none" : ""; } if(!node.expanded){ return "skip"; } }); } /* Find node that is rendered in previous row. */ function findPrevRowNode(node){ var i, last, prev, parent = node.parent, siblings = parent ? parent.children : null; if(siblings && siblings.length > 1 && siblings[0] !== node){ // use the lowest descendant of the preceeding sibling i = $.inArray(node, siblings); prev = siblings[i - 1]; _assert(prev.tr); // descend to lowest child (with a tag) while(prev.children){ last = prev.children[prev.children.length - 1]; if(!last.tr){ break; } prev = last; } }else{ // if there is no preceding sibling, use the direct parent prev = parent; } return prev; } $.ui.fancytree.registerExtension({ name: "table", version: "0.2.0", // Default options for this extension. options: { checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) customStatus: false, // true: generate renderColumns events for status nodes indentation: 16, // indent every node level by 16px nodeColumnIdx: 0 // render node expander, icon, and title to this column (default: #0) }, // Overide virtual methods for this extension. // `this` : is this extension object // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) treeInit: function(ctx){ var i, $row, tdRole, tree = ctx.tree, $table = tree.widget.element; $table.addClass("fancytree-container fancytree-ext-table"); tree.tbody = $table.find("> tbody")[0]; tree.columnCount = $("thead >tr >th", $table).length; $(tree.tbody).empty(); tree.rowFragment = document.createDocumentFragment(); $row = $(""); tdRole = ""; if(ctx.options.aria){ $row.attr("role", "row"); tdRole = " role='gridcell'"; } for(i=0; i"); }else{ $row.append(""); } } tree.rowFragment.appendChild($row.get(0)); // Make sure that status classes are set on the node's elements tree.statusClassPropName = "tr"; tree.ariaPropName = "tr"; this.nodeContainerAttrName = "tr"; this._super(ctx); // standard Fancytree created a root UL $(tree.rootNode.ul).remove(); tree.rootNode.ul = null; tree.$container = $table; // Add container to the TAB chain this.$container.attr("tabindex", this.options.tabbable ? "0" : "-1"); if(this.options.aria){ tree.$container .attr("role", "treegrid") .attr("aria-readonly", true); } }, /* Called by nodeRender to sync node order with tag order.*/ // nodeFixOrder: function(ctx) { // }, nodeRemoveChildMarkup: function(ctx) { var node = ctx.node; // DT.debug("nodeRemoveChildMarkup()", node.toString()); node.visit(function(n){ if(n.tr){ $(n.tr).remove(); n.tr = null; } }); }, nodeRemoveMarkup: function(ctx) { var node = ctx.node; // DT.debug("nodeRemoveMarkup()", node.toString()); if(node.tr){ $(node.tr).remove(); node.tr = null; } this.nodeRemoveChildMarkup(ctx); }, /* Override standard render. */ nodeRender: function(ctx, force, deep, collapsed, _recursive) { var children, firstTr, i, l, newRow, prevNode, prevTr, subCtx, tree = ctx.tree, node = ctx.node, opts = ctx.options, isRootNode = !node.parent; if( !_recursive ){ ctx.hasCollapsedParents = node.parent && !node.parent.expanded; } // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); if( !isRootNode ){ if(!node.tr){ if( ctx.hasCollapsedParents /*&& !node.parent.tr*/ ) { // #166: we assume that the parent will be (recursively) rendered // later anyway. node.debug("nodeRender ignored due to unrendered parent"); return; } // Create new after previous row newRow = tree.rowFragment.firstChild.cloneNode(true); prevNode = findPrevRowNode(node); // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key); _assert(prevNode); if(collapsed === true && _recursive){ // hide all child rows, so we can use an animation to show it later newRow.style.display = "none"; }else if(deep && ctx.hasCollapsedParents){ // also hide this row if deep === true but any parent is collapsed newRow.style.display = "none"; // newRow.style.color = "red"; } if(!prevNode.tr){ _assert(!prevNode.parent, "prev. row must have a tr, or is system root"); tree.tbody.appendChild(newRow); }else{ insertSiblingAfter(prevNode.tr, newRow); } node.tr = newRow; if( node.key && opts.generateIds ){ node.tr.id = opts.idPrefix + node.key; } node.tr.ftnode = node; if(opts.aria){ // TODO: why doesn't this work: // node.li.role = "treeitem"; $(node.tr).attr("aria-labelledby", "ftal_" + node.key); } node.span = $("span.fancytree-node", node.tr).get(0); // Set icon, link, and title (normally this is only required on initial render) this.nodeRenderTitle(ctx); // Allow tweaking, binding, after node was created for the first time // tree._triggerNodeEvent("createNode", ctx); if ( opts.createNode ){ opts.createNode.call(tree, {type: "createNode"}, ctx); } } else { if( force ) { // Set icon, link, and title (normally this is only required on initial render) this.nodeRenderTitle(ctx); // triggers renderColumns() } else { // Update element classes according to node state this.nodeRenderStatus(ctx); } } } // Allow tweaking after node state was rendered // tree._triggerNodeEvent("renderNode", ctx); if ( opts.renderNode ){ opts.renderNode.call(tree, {type: "renderNode"}, ctx); } // Visit child nodes // Add child markup children = node.children; if(children && (isRootNode || deep || node.expanded)){ for(i=0, l=children.length; i order matches node.children order. if(children && !_recursive){ // we only have to do it once, for the root branch prevTr = node.tr || null; firstTr = tree.tbody.firstChild; // Iterate over all descendants node.visit(function(n){ if(n.tr){ if(!n.parent.expanded && n.tr.style.display !== "none"){ // fix after a node was dropped over a collapsed n.tr.style.display = "none"; setChildRowVisibility(n, false); } if(n.tr.previousSibling !== prevTr){ node.debug("_fixOrder: mismatch at node: " + n); var nextTr = prevTr ? prevTr.nextSibling : firstTr; tree.tbody.insertBefore(n.tr, nextTr); } prevTr = n.tr; } }); } // Update element classes according to node state // if(!isRootNode){ // this.nodeRenderStatus(ctx); // } }, nodeRenderTitle: function(ctx, title) { var $cb, node = ctx.node, opts = ctx.options; this._super(ctx); // Move checkbox to custom column if(opts.checkbox && opts.table.checkboxColumnIdx != null ){ $cb = $("span.fancytree-checkbox", node.span).detach(); $(node.tr).find("td:first").html($cb); } // Let user code write column content // ctx.tree._triggerNodeEvent("renderColumns", node); // Update element classes according to node state if( ! node.isRoot() ){ this.nodeRenderStatus(ctx); } if( !opts.table.customStatus && node.isStatusNode() ) { // default rendering for status node: leave other cells empty } else if ( opts.renderColumns ) { opts.renderColumns.call(ctx.tree, {type: "renderColumns"}, ctx); } }, nodeRenderStatus: function(ctx) { var indent, node = ctx.node, opts = ctx.options; this._super(ctx); $(node.tr).removeClass("fancytree-node"); // indent indent = (node.getLevel() - 1) * opts.table.indentation; $(node.span).css({marginLeft: indent + "px"}); }, /* Expand node, return Deferred.promise. */ nodeSetExpanded: function(ctx, flag, opts) { var dfd = new $.Deferred(), subOpts = $.extend({}, opts, {noEvents: true, noAnimation: true}); opts = opts || {}; function _afterExpand(ok) { flag = (flag !== false); setChildRowVisibility(ctx.node, flag); if( ok ) { if( flag && ctx.options.autoScroll && !opts.noAnimation && ctx.node.hasChildren() ) { // Scroll down to last child, but keep current node visible ctx.node.getLastChild().scrollIntoView(true, {topNode: ctx.node}).always(function(){ if( !opts.noEvents ) { ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); } dfd.resolveWith(ctx.node); }); } else { if( !opts.noEvents ) { ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); } dfd.resolveWith(ctx.node); } } else { if( !opts.noEvents ) { ctx.tree._triggerNodeEvent(flag ? "expand" : "collapse", ctx); } dfd.rejectWith(ctx.node); } } // Call base-expand with disabled events and animation this._super(ctx, flag, subOpts).done(function () { _afterExpand(true); }).fail(function () { _afterExpand(false); }); return dfd.promise(); }, nodeSetStatus: function(ctx, status, message, details) { if(status === "ok"){ var node = ctx.node, firstChild = ( node.children ? node.children[0] : null ); if ( firstChild && firstChild.isStatusNode() ) { $(firstChild.tr).remove(); } } this._super(ctx, status, message, details); }, treeClear: function(ctx) { this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); return this._super(ctx); } /*, treeSetFocus: function(ctx, flag) { // alert("treeSetFocus" + ctx.tree.$container); ctx.tree.$container.focus(); $.ui.fancytree.focusTree = ctx.tree; }*/ }); }(jQuery, window, document));