1/*! 2 * jquery.fancytree.table.js 3 * 4 * Render tree as table (aka 'tree grid', 'table tree'). 5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 6 * 7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 8 * 9 * Released under the MIT license 10 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11 * 12 * @version 2.38.3 13 * @date 2023-02-01T20:52:50Z 14 */ 15 16(function (factory) { 17 if (typeof define === "function" && define.amd) { 18 // AMD. Register as an anonymous module. 19 define(["jquery", "./jquery.fancytree"], factory); 20 } else if (typeof module === "object" && module.exports) { 21 // Node/CommonJS 22 require("./jquery.fancytree"); 23 module.exports = factory(require("jquery")); 24 } else { 25 // Browser globals 26 factory(jQuery); 27 } 28})(function ($) { 29 "use strict"; 30 31 /****************************************************************************** 32 * Private functions and variables 33 */ 34 var _assert = $.ui.fancytree.assert; 35 36 function insertFirstChild(referenceNode, newNode) { 37 referenceNode.insertBefore(newNode, referenceNode.firstChild); 38 } 39 40 function insertSiblingAfter(referenceNode, newNode) { 41 referenceNode.parentNode.insertBefore( 42 newNode, 43 referenceNode.nextSibling 44 ); 45 } 46 47 /* Show/hide all rows that are structural descendants of `parent`. */ 48 function setChildRowVisibility(parent, flag) { 49 parent.visit(function (node) { 50 var tr = node.tr; 51 // currentFlag = node.hide ? false : flag; // fix for ext-filter 52 if (tr) { 53 tr.style.display = node.hide || !flag ? "none" : ""; 54 } 55 if (!node.expanded) { 56 return "skip"; 57 } 58 }); 59 } 60 61 /* Find node that is rendered in previous row. */ 62 function findPrevRowNode(node) { 63 var i, 64 last, 65 prev, 66 parent = node.parent, 67 siblings = parent ? parent.children : null; 68 69 if (siblings && siblings.length > 1 && siblings[0] !== node) { 70 // use the lowest descendant of the preceeding sibling 71 i = $.inArray(node, siblings); 72 prev = siblings[i - 1]; 73 _assert(prev.tr); 74 // descend to lowest child (with a <tr> tag) 75 while (prev.children && prev.children.length) { 76 last = prev.children[prev.children.length - 1]; 77 if (!last.tr) { 78 break; 79 } 80 prev = last; 81 } 82 } else { 83 // if there is no preceding sibling, use the direct parent 84 prev = parent; 85 } 86 return prev; 87 } 88 89 $.ui.fancytree.registerExtension({ 90 name: "table", 91 version: "2.38.3", 92 // Default options for this extension. 93 options: { 94 checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) 95 indentation: 16, // indent every node level by 16px 96 mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR 97 nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0) 98 }, 99 // Overide virtual methods for this extension. 100 // `this` : is this extension object 101 // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) 102 treeInit: function (ctx) { 103 var i, 104 n, 105 $row, 106 $tbody, 107 tree = ctx.tree, 108 opts = ctx.options, 109 tableOpts = opts.table, 110 $table = tree.widget.element; 111 112 if (tableOpts.customStatus != null) { 113 if (opts.renderStatusColumns == null) { 114 tree.warn( 115 "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead." 116 ); 117 opts.renderStatusColumns = tableOpts.customStatus; 118 } else { 119 $.error( 120 "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead." 121 ); 122 } 123 } 124 if (opts.renderStatusColumns) { 125 if (opts.renderStatusColumns === true) { 126 opts.renderStatusColumns = opts.renderColumns; 127 // } else if( opts.renderStatusColumns === "wide" ) { 128 // opts.renderStatusColumns = _renderStatusNodeWide; 129 } 130 } 131 132 $table.addClass("fancytree-container fancytree-ext-table"); 133 $tbody = $table.find(">tbody"); 134 if (!$tbody.length) { 135 // TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s: 136 if ($table.find(">tr").length) { 137 $.error( 138 "Expected table > tbody > tr. If you see this please open an issue." 139 ); 140 } 141 $tbody = $("<tbody>").appendTo($table); 142 } 143 144 tree.tbody = $tbody[0]; 145 146 // Prepare row templates: 147 // Determine column count from table header if any 148 tree.columnCount = $("thead >tr", $table) 149 .last() 150 .find(">th", $table).length; 151 // Read TR templates from tbody if any 152 $row = $tbody.children("tr").first(); 153 if ($row.length) { 154 n = $row.children("td").length; 155 if (tree.columnCount && n !== tree.columnCount) { 156 tree.warn( 157 "Column count mismatch between thead (" + 158 tree.columnCount + 159 ") and tbody (" + 160 n + 161 "): using tbody." 162 ); 163 tree.columnCount = n; 164 } 165 $row = $row.clone(); 166 } else { 167 // Only thead is defined: create default row markup 168 _assert( 169 tree.columnCount >= 1, 170 "Need either <thead> or <tbody> with <td> elements to determine column count." 171 ); 172 $row = $("<tr />"); 173 for (i = 0; i < tree.columnCount; i++) { 174 $row.append("<td />"); 175 } 176 } 177 $row.find(">td") 178 .eq(tableOpts.nodeColumnIdx) 179 .html("<span class='fancytree-node' />"); 180 if (opts.aria) { 181 $row.attr("role", "row"); 182 $row.find("td").attr("role", "gridcell"); 183 } 184 tree.rowFragment = document.createDocumentFragment(); 185 tree.rowFragment.appendChild($row.get(0)); 186 187 // // If tbody contains a second row, use this as status node template 188 // $row = $tbody.children("tr").eq(1); 189 // if( $row.length === 0 ) { 190 // tree.statusRowFragment = tree.rowFragment; 191 // } else { 192 // $row = $row.clone(); 193 // tree.statusRowFragment = document.createDocumentFragment(); 194 // tree.statusRowFragment.appendChild($row.get(0)); 195 // } 196 // 197 $tbody.empty(); 198 199 // Make sure that status classes are set on the node's <tr> elements 200 tree.statusClassPropName = "tr"; 201 tree.ariaPropName = "tr"; 202 this.nodeContainerAttrName = "tr"; 203 204 // #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table 205 tree.$container = $table; 206 207 this._superApply(arguments); 208 209 // standard Fancytree created a root UL 210 $(tree.rootNode.ul).remove(); 211 tree.rootNode.ul = null; 212 213 // Add container to the TAB chain 214 // #577: Allow to set tabindex to "0", "-1" and "" 215 this.$container.attr("tabindex", opts.tabindex); 216 // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1"); 217 if (opts.aria) { 218 tree.$container 219 .attr("role", "treegrid") 220 .attr("aria-readonly", true); 221 } 222 }, 223 nodeRemoveChildMarkup: function (ctx) { 224 var node = ctx.node; 225 // node.debug("nodeRemoveChildMarkup()"); 226 node.visit(function (n) { 227 if (n.tr) { 228 $(n.tr).remove(); 229 n.tr = null; 230 } 231 }); 232 }, 233 nodeRemoveMarkup: function (ctx) { 234 var node = ctx.node; 235 // node.debug("nodeRemoveMarkup()"); 236 if (node.tr) { 237 $(node.tr).remove(); 238 node.tr = null; 239 } 240 this.nodeRemoveChildMarkup(ctx); 241 }, 242 /* Override standard render. */ 243 nodeRender: function (ctx, force, deep, collapsed, _recursive) { 244 var children, 245 firstTr, 246 i, 247 l, 248 newRow, 249 prevNode, 250 prevTr, 251 subCtx, 252 tree = ctx.tree, 253 node = ctx.node, 254 opts = ctx.options, 255 isRootNode = !node.parent; 256 257 if (tree._enableUpdate === false) { 258 // $.ui.fancytree.debug("*** nodeRender _enableUpdate: false"); 259 return; 260 } 261 if (!_recursive) { 262 ctx.hasCollapsedParents = node.parent && !node.parent.expanded; 263 } 264 // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); 265 if (!isRootNode) { 266 if (node.tr && force) { 267 this.nodeRemoveMarkup(ctx); 268 } 269 if (node.tr) { 270 if (force) { 271 // Set icon, link, and title (normally this is only required on initial render) 272 this.nodeRenderTitle(ctx); // triggers renderColumns() 273 } else { 274 // Update element classes according to node state 275 this.nodeRenderStatus(ctx); 276 } 277 } else { 278 if (ctx.hasCollapsedParents && !deep) { 279 // #166: we assume that the parent will be (recursively) rendered 280 // later anyway. 281 // node.debug("nodeRender ignored due to unrendered parent"); 282 return; 283 } 284 // Create new <tr> after previous row 285 // if( node.isStatusNode() ) { 286 // newRow = tree.statusRowFragment.firstChild.cloneNode(true); 287 // } else { 288 newRow = tree.rowFragment.firstChild.cloneNode(true); 289 // } 290 prevNode = findPrevRowNode(node); 291 // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key); 292 _assert(prevNode); 293 if (collapsed === true && _recursive) { 294 // hide all child rows, so we can use an animation to show it later 295 newRow.style.display = "none"; 296 } else if (deep && ctx.hasCollapsedParents) { 297 // also hide this row if deep === true but any parent is collapsed 298 newRow.style.display = "none"; 299 // newRow.style.color = "red"; 300 } 301 if (prevNode.tr) { 302 insertSiblingAfter(prevNode.tr, newRow); 303 } else { 304 _assert( 305 !prevNode.parent, 306 "prev. row must have a tr, or be system root" 307 ); 308 // tree.tbody.appendChild(newRow); 309 insertFirstChild(tree.tbody, newRow); // #675 310 } 311 node.tr = newRow; 312 if (node.key && opts.generateIds) { 313 node.tr.id = opts.idPrefix + node.key; 314 } 315 node.tr.ftnode = node; 316 // if(opts.aria){ 317 // $(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key); 318 // } 319 node.span = $("span.fancytree-node", node.tr).get(0); 320 // Set icon, link, and title (normally this is only required on initial render) 321 this.nodeRenderTitle(ctx); 322 // Allow tweaking, binding, after node was created for the first time 323 // tree._triggerNodeEvent("createNode", ctx); 324 if (opts.createNode) { 325 opts.createNode.call(tree, { type: "createNode" }, ctx); 326 } 327 } 328 } 329 // Allow tweaking after node state was rendered 330 // tree._triggerNodeEvent("renderNode", ctx); 331 if (opts.renderNode) { 332 opts.renderNode.call(tree, { type: "renderNode" }, ctx); 333 } 334 // Visit child nodes 335 // Add child markup 336 children = node.children; 337 if (children && (isRootNode || deep || node.expanded)) { 338 for (i = 0, l = children.length; i < l; i++) { 339 subCtx = $.extend({}, ctx, { node: children[i] }); 340 subCtx.hasCollapsedParents = 341 subCtx.hasCollapsedParents || !node.expanded; 342 this.nodeRender(subCtx, force, deep, collapsed, true); 343 } 344 } 345 // Make sure, that <tr> order matches node.children order. 346 if (children && !_recursive) { 347 // we only have to do it once, for the root branch 348 prevTr = node.tr || null; 349 firstTr = tree.tbody.firstChild; 350 // Iterate over all descendants 351 node.visit(function (n) { 352 if (n.tr) { 353 if ( 354 !n.parent.expanded && 355 n.tr.style.display !== "none" 356 ) { 357 // fix after a node was dropped over a collapsed 358 n.tr.style.display = "none"; 359 setChildRowVisibility(n, false); 360 } 361 if (n.tr.previousSibling !== prevTr) { 362 node.debug("_fixOrder: mismatch at node: " + n); 363 var nextTr = prevTr ? prevTr.nextSibling : firstTr; 364 tree.tbody.insertBefore(n.tr, nextTr); 365 } 366 prevTr = n.tr; 367 } 368 }); 369 } 370 // Update element classes according to node state 371 // if(!isRootNode){ 372 // this.nodeRenderStatus(ctx); 373 // } 374 }, 375 nodeRenderTitle: function (ctx, title) { 376 var $cb, 377 res, 378 tree = ctx.tree, 379 node = ctx.node, 380 opts = ctx.options, 381 isStatusNode = node.isStatusNode(); 382 383 res = this._super(ctx, title); 384 385 if (node.isRootNode()) { 386 return res; 387 } 388 // Move checkbox to custom column 389 if ( 390 opts.checkbox && 391 !isStatusNode && 392 opts.table.checkboxColumnIdx != null 393 ) { 394 $cb = $("span.fancytree-checkbox", node.span); //.detach(); 395 $(node.tr) 396 .find("td") 397 .eq(+opts.table.checkboxColumnIdx) 398 .html($cb); 399 } 400 // Update element classes according to node state 401 this.nodeRenderStatus(ctx); 402 403 if (isStatusNode) { 404 if (opts.renderStatusColumns) { 405 // Let user code write column content 406 opts.renderStatusColumns.call( 407 tree, 408 { type: "renderStatusColumns" }, 409 ctx 410 ); 411 } else if (opts.table.mergeStatusColumns && node.isTopLevel()) { 412 $(node.tr) 413 .find(">td") 414 .eq(0) 415 .prop("colspan", tree.columnCount) 416 .text(node.title) 417 .addClass("fancytree-status-merged") 418 .nextAll() 419 .remove(); 420 } // else: default rendering for status node: leave other cells empty 421 } else if (opts.renderColumns) { 422 opts.renderColumns.call(tree, { type: "renderColumns" }, ctx); 423 } 424 return res; 425 }, 426 nodeRenderStatus: function (ctx) { 427 var indent, 428 node = ctx.node, 429 opts = ctx.options; 430 431 this._super(ctx); 432 433 $(node.tr).removeClass("fancytree-node"); 434 // indent 435 indent = (node.getLevel() - 1) * opts.table.indentation; 436 if (opts.rtl) { 437 $(node.span).css({ paddingRight: indent + "px" }); 438 } else { 439 $(node.span).css({ paddingLeft: indent + "px" }); 440 } 441 }, 442 /* Expand node, return Deferred.promise. */ 443 nodeSetExpanded: function (ctx, flag, callOpts) { 444 // flag defaults to true 445 flag = flag !== false; 446 447 if ((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) { 448 // Expanded state isn't changed - just call base implementation 449 return this._superApply(arguments); 450 } 451 452 var dfd = new $.Deferred(), 453 subOpts = $.extend({}, callOpts, { 454 noEvents: true, 455 noAnimation: true, 456 }); 457 458 callOpts = callOpts || {}; 459 460 function _afterExpand(ok, args) { 461 // ctx.tree.info("ok:" + ok, args); 462 if (ok) { 463 // #1108 minExpandLevel: 2 together with table extension does not work 464 // don't call when 'ok' is false: 465 setChildRowVisibility(ctx.node, flag); 466 if ( 467 flag && 468 ctx.options.autoScroll && 469 !callOpts.noAnimation && 470 ctx.node.hasChildren() 471 ) { 472 // Scroll down to last child, but keep current node visible 473 ctx.node 474 .getLastChild() 475 .scrollIntoView(true, { topNode: ctx.node }) 476 .always(function () { 477 if (!callOpts.noEvents) { 478 ctx.tree._triggerNodeEvent( 479 flag ? "expand" : "collapse", 480 ctx 481 ); 482 } 483 dfd.resolveWith(ctx.node); 484 }); 485 } else { 486 if (!callOpts.noEvents) { 487 ctx.tree._triggerNodeEvent( 488 flag ? "expand" : "collapse", 489 ctx 490 ); 491 } 492 dfd.resolveWith(ctx.node); 493 } 494 } else { 495 if (!callOpts.noEvents) { 496 ctx.tree._triggerNodeEvent( 497 flag ? "expand" : "collapse", 498 ctx 499 ); 500 } 501 dfd.rejectWith(ctx.node); 502 } 503 } 504 // Call base-expand with disabled events and animation 505 this._super(ctx, flag, subOpts) 506 .done(function () { 507 _afterExpand(true, arguments); 508 }) 509 .fail(function () { 510 _afterExpand(false, arguments); 511 }); 512 return dfd.promise(); 513 }, 514 nodeSetStatus: function (ctx, status, message, details) { 515 if (status === "ok") { 516 var node = ctx.node, 517 firstChild = node.children ? node.children[0] : null; 518 if (firstChild && firstChild.isStatusNode()) { 519 $(firstChild.tr).remove(); 520 } 521 } 522 return this._superApply(arguments); 523 }, 524 treeClear: function (ctx) { 525 this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); 526 return this._superApply(arguments); 527 }, 528 treeDestroy: function (ctx) { 529 this.$container.find("tbody").empty(); 530 if (this.$source) { 531 this.$source.removeClass("fancytree-helper-hidden"); 532 } 533 return this._superApply(arguments); 534 }, 535 /*, 536 treeSetFocus: function(ctx, flag) { 537// alert("treeSetFocus" + ctx.tree.$container); 538 ctx.tree.$container.focus(); 539 $.ui.fancytree.focusTree = ctx.tree; 540 }*/ 541 }); 542 // Value returned by `require('jquery.fancytree..')` 543 return $.ui.fancytree; 544}); // End of closure 545