1/*! 2 * jquery.fancytree.grid.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 (http://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 FT = $.ui.fancytree, 35 _assert = FT.assert, 36 SCROLL_MODE = "wheel"; // 'wheel' | 'scroll' 37 // EPS = 1.0; 38 39 /* 40 * [ext-grid] ... 41 * 42 * @alias Fancytree#_addScrollbar 43 * @requires jquery.fancytree.grid.js 44 */ 45 function _addScrollbar(table) { 46 var sbWidth = 10, 47 $table = $(table), 48 position = $table.position(), 49 // top = $table.find("tbody").position().top, 50 51 $sb = $("<div>", { 52 class: "fancytree-scrollbar", 53 css: { 54 border: "1px solid gray", 55 position: "absolute", 56 top: position.top, 57 left: position.left + $table.width(), 58 width: sbWidth, 59 height: $table.find("tbody").height(), 60 }, 61 }); 62 63 $table 64 .css({ 65 "margin-right": sbWidth, 66 }) 67 .after($sb); 68 69 return $sb; 70 } 71 72 /* 73 * [ext-grid] Invalidate renumber status, i.e. trigger renumber next time. 74 * 75 * @alias Fancytree#_renumberReset 76 * @requires jquery.fancytree.grid.js 77 */ 78 $.ui.fancytree._FancytreeClass.prototype._renumberReset = function () { 79 // this.debug("_renumberReset()"); 80 this.visibleNodeList = null; 81 }; 82 83 /* 84 * [ext-grid] Adjust the start value if the content would be outside otherwise. 85 * 86 * @alias Fancytree#_fixStart 87 * @requires jquery.fancytree.grid.js 88 */ 89 $.ui.fancytree._FancytreeClass.prototype._fixStart = function ( 90 start, 91 apply 92 ) { 93 var vp = this.viewport, 94 nodeList = this.visibleNodeList; 95 96 start = start == null ? vp.start : start; 97 // this.debug("_fixStart(" + start + ", " + !!apply + ")"); 98 var orgStart = start; 99 // Don't scroll down below bottom node 100 if (nodeList) { 101 start = Math.min(start, this.visibleNodeList.length - vp.count); 102 start = Math.max(start, 0, start); 103 if (start !== orgStart) { 104 this.debug("Adjust start " + orgStart + " => " + start); 105 if (apply) { 106 vp.start = start; 107 } 108 } 109 } 110 return start; 111 }; 112 113 /* 114 * [ext-grid] ... 115 * 116 * @alias Fancytree#_shiftViewport 117 * @requires jquery.fancytree.grid.js 118 */ 119 $.ui.fancytree._FancytreeClass.prototype._shiftViewport = function ( 120 mode, 121 ofs 122 ) { 123 this.debug("_shiftViewport", mode, ofs); 124 switch (mode) { 125 case "vscroll": 126 if (ofs) { 127 this.setViewport({ 128 start: this.viewport.start + (ofs > 0 ? 1 : -1), 129 }); 130 } 131 break; 132 133 default: 134 throw Error("Invalid mode: " + mode); 135 } 136 }; 137 138 /** 139 * [ext-grid] Return true if viewport cannot be scrolled down any further. 140 * 141 * @alias Fancytree#isViewportBottom 142 * @requires jquery.fancytree.grid.js 143 */ 144 $.ui.fancytree._FancytreeClass.prototype.isViewportBottom = function () { 145 return ( 146 this.viewport.start + this.viewport.count >= 147 this.visibleNodeList.length 148 ); 149 }; 150 151 /** 152 * [ext-grid] Define a subset of rows/columns to display and redraw. 153 * 154 * @param {object | boolean} options viewport boundaries and status. 155 * 156 * @alias Fancytree#setViewport 157 * @requires jquery.fancytree.grid.js 158 */ 159 $.ui.fancytree._FancytreeClass.prototype.setViewport = function (opts) { 160 if (typeof opts === "boolean") { 161 this.debug("setViewport( " + opts + ")"); 162 return this.setViewport({ enabled: opts }); 163 } 164 opts = opts || {}; 165 var i, 166 count, 167 start, 168 newRow, 169 redrawReason = "", 170 vp = this.viewport, 171 diffVp = { start: 0, count: 0, enabled: null, force: null }, 172 newVp = $.extend({}, vp), 173 trList = this.tbody.children, 174 trCount = trList.length; 175 176 // Sanitize viewport settings and check if we need to redraw 177 this.debug("setViewport(" + opts.start + ", +" + opts.count + ")"); 178 if (opts.force) { 179 redrawReason += "force"; 180 diffVp.force = true; 181 } 182 183 opts.enabled = opts.enabled !== false; // default to true 184 if (vp.enabled !== opts.enabled) { 185 redrawReason += "enable"; 186 newVp.enabled = diffVp.enabled = opts.enabled; 187 } 188 189 start = opts.start == null ? vp.start : Math.max(0, +opts.start); 190 // Adjust start value to assure the current content is inside vp 191 start = this._fixStart(start, false); 192 193 if (vp.start !== +start) { 194 redrawReason += "start"; 195 newVp.start = start; 196 diffVp.start = start - vp.start; 197 } 198 199 count = opts.count == null ? vp.count : Math.max(1, +opts.count); 200 if (vp.count !== +count) { 201 redrawReason += "count"; 202 newVp.count = count; 203 diffVp.count = count - vp.count; 204 } 205 // if (vp.left !== +opts.left) { 206 // diffVp.left = left - vp.left; 207 // newVp.left = opts.left; 208 // redrawReason += "left"; 209 // } 210 // if (vp.right !== +opts.right) { 211 // diffVp.right = right - vp.right; 212 // newVp.right = opts.right; 213 // redrawReason += "right"; 214 // } 215 216 if (!redrawReason) { 217 return false; 218 } 219 // Let user cancel or modify the update 220 var info = { 221 next: newVp, 222 diff: diffVp, 223 reason: redrawReason, 224 scrollOnly: redrawReason === "start", 225 }; 226 if ( 227 !opts.noEvents && 228 this._triggerTreeEvent("beforeUpdateViewport", null, info) === false 229 ) { 230 return false; 231 } 232 info.prev = $.extend({}, vp); 233 delete info.next; 234 // vp.enabled = newVp.enabled; 235 vp.start = newVp.start; 236 vp.count = newVp.count; 237 238 // Make sure we have the correct count of TRs 239 var prevPhase = this.isVpUpdating; 240 241 if (trCount > count) { 242 for (i = 0; i < trCount - count; i++) { 243 delete this.tbody.lastChild.ftnode; 244 this.tbody.removeChild(this.tbody.lastChild); 245 } 246 } else if (trCount < count) { 247 for (i = 0; i < count - trCount; i++) { 248 newRow = this.rowFragment.firstChild.cloneNode(true); 249 this.tbody.appendChild(newRow); 250 } 251 } 252 trCount = trList.length; 253 254 // Update visible node cache if needed 255 var force = opts.force; 256 this.redrawViewport(force); 257 258 if (!opts.noEvents) { 259 this._triggerTreeEvent("updateViewport", null, info); 260 } 261 262 this.isVpUpdating = prevPhase; 263 return true; 264 }; 265 266 /** 267 * [ext-grid] Calculate the viewport count from current scroll wrapper height. 268 * 269 * @alias Fancytree#adjustViewportSize 270 * @requires jquery.fancytree.grid.js 271 */ 272 $.ui.fancytree._FancytreeClass.prototype.adjustViewportSize = function () { 273 _assert( 274 this.scrollWrapper, 275 "No parent div.fancytree-grid-container found." 276 ); 277 if (this.isVpUpdating) { 278 this.debug("Ignoring adjustViewportSize() during VP update."); 279 return; 280 } 281 // Calculate how many rows fit into current container height 282 var $table = this.$container, 283 wrapper = this.scrollWrapper, 284 trHeight = $table.find(">tbody>tr").first().height() || 0, 285 tableHeight = $table.height(), 286 headHeight = tableHeight - this.viewport.count * trHeight, 287 wrapperHeight = wrapper.offsetHeight, 288 free = wrapperHeight - headHeight, 289 newCount = trHeight ? Math.floor(free / trHeight) : 0; 290 291 // console.info( 292 // "set container height", 293 // $(this) 294 // .parent(".fancytree-grid-container") 295 // .height() 296 // ); 297 298 this.setViewport({ count: newCount }); 299 // if (SCROLL_MODE === "scroll") { 300 // // Add bottom margin to the table, to make sure the wrapper becomes 301 // // scrollable 302 // var mb = wrapperHeight - $table.height() - 2.0 * EPS; 303 // this.debug("margin-bottom=" + mb); 304 // $table.css("margin-bottom", mb); 305 // } 306 }; 307 308 /* 309 * [ext-grid] Calculate the scroll container dimension from the current tree table. 310 * 311 * @alias Fancytree#initViewportWrapper 312 * @requires jquery.fancytree.grid.js 313 */ 314 $.ui.fancytree._FancytreeClass.prototype._initViewportWrapper = 315 function () { 316 var // wrapper = this.scrollWrapper, 317 // $wrapper = $(wrapper), 318 tree = this; 319 320 // if (SCROLL_MODE === "scroll") { 321 // $wrapper.on("scroll", function(e) { 322 // var viewport = tree.viewport, 323 // curTop = wrapper.scrollTop, 324 // homeTop = viewport.start === 0 ? 0 : EPS, 325 // dy = viewport.start === 0 ? 1 : curTop - EPS; //homeTop; 326 327 // tree.debug( 328 // "Got 'scroll' event: scrollTop=" + 329 // curTop + 330 // ", homeTop=" + 331 // homeTop + 332 // ", start=" + 333 // viewport.start + 334 // ", dy=" + 335 // dy 336 // ); 337 // if (tree.isVpUpdating) { 338 // tree.debug("Ignoring scroll during VP update."); 339 // return; 340 // } else if (curTop === homeTop) { 341 // tree.debug("Ignoring scroll to neutral " + homeTop + "."); 342 // return; 343 // } 344 // tree._shiftViewport("vscroll", dy); 345 // homeTop = viewport.start === 0 ? 0 : EPS; 346 // setTimeout(function() { 347 // tree.debug( 348 // "scrollTop(" + 349 // wrapper.scrollTop + 350 // " -> " + 351 // homeTop + 352 // ")..." 353 // ); 354 // wrapper.scrollTop = homeTop; 355 // }, 0); 356 // }); 357 // } 358 if (SCROLL_MODE === "wheel") { 359 this.$container.on("wheel", function (e) { 360 var orgEvent = e.originalEvent, 361 viewport = tree.viewport, 362 dy = orgEvent.deltaY; // * orgEvent.wheelDeltaY; 363 364 if ( 365 !dy || 366 e.altKey || 367 e.ctrlKey || 368 e.metaKey || 369 e.shiftKey 370 ) { 371 return true; 372 } 373 if (dy < 0 && viewport.start === 0) { 374 return true; 375 } 376 if (dy > 0 && tree.isViewportBottom()) { 377 return true; 378 } 379 tree.debug( 380 "Got 'wheel' event: dy=" + 381 dy + 382 ", mode=" + 383 orgEvent.deltaMode 384 ); 385 tree._shiftViewport("vscroll", dy); 386 return false; 387 }); 388 } 389 }; 390 391 /* 392 * [ext-grid] Renumber and collect all visible rows. 393 * 394 * @param {bool} [force=false] 395 * @param {FancytreeNode | int} [startIdx=0] 396 * @alias Fancytree#_renumberVisibleNodes 397 * @requires jquery.fancytree.grid.js 398 */ 399 $.ui.fancytree._FancytreeClass.prototype._renumberVisibleNodes = function ( 400 force, 401 startIdx 402 ) { 403 if ( 404 (!this.options.viewport.enabled || this.visibleNodeList != null) && 405 force !== true 406 ) { 407 // this.debug("_renumberVisibleNodes() ignored."); 408 return false; 409 } 410 this.debugTime("_renumberVisibleNodes()"); 411 var i = 0, 412 prevLength = this.visibleNodeList ? this.visibleNodeList.length : 0, 413 visibleNodeList = (this.visibleNodeList = []); 414 415 // Reset previous data 416 this.visit(function (node) { 417 node._rowIdx = null; 418 // node.span = null; 419 // if (node.tr) { 420 // delete node.tr.ftnode; 421 // node.tr = null; 422 // } 423 }); 424 // Iterate over all *visible* nodes 425 this.visitRows(function (node) { 426 node._rowIdx = i++; 427 visibleNodeList.push(node); 428 }); 429 this.debugTimeEnd("_renumberVisibleNodes()"); 430 if (i !== prevLength) { 431 this._triggerTreeEvent("updateViewport", null, { 432 reason: "renumber", 433 diff: { start: 0, count: 0, enabled: null, force: null }, 434 next: $.extend({}, this.viewport), 435 // visibleCount: prevLength, 436 // cur: i, 437 }); 438 } 439 }; 440 441 /** 442 * [ext-grid] Render all visible nodes into the viweport. 443 * 444 * @param {bool} [force=false] 445 * @alias Fancytree#redrawViewport 446 * @requires jquery.fancytree.grid.js 447 */ 448 $.ui.fancytree._FancytreeClass.prototype.redrawViewport = function (force) { 449 if (this._enableUpdate === false) { 450 // tree.debug("no render", tree._enableUpdate); 451 return; 452 } 453 this.debugTime("redrawViewport()"); 454 this._renumberVisibleNodes(force); 455 // Adjust vp.start value to assure the current content is inside: 456 this._fixStart(null, true); 457 458 var i = 0, 459 vp = this.viewport, 460 visibleNodeList = this.visibleNodeList, 461 start = vp.start, 462 bottom = start + vp.count, 463 tr, 464 _renderCount = 0, 465 trIdx = 0, 466 trList = this.tbody.children, 467 prevPhase = this.isVpUpdating; 468 469 // Reset previous data 470 this.visit(function (node) { 471 // node.debug("redrawViewport(): _rowIdx=" + node._rowIdx); 472 node.span = null; 473 if (node.tr) { 474 delete node.tr.ftnode; 475 node.tr = null; 476 } 477 }); 478 479 // Redraw the whole tree, erasing all node markup before and after 480 // the viewport 481 482 for (i = start; i < bottom; i++) { 483 var node = visibleNodeList[i]; 484 485 tr = trList[trIdx]; 486 487 if (!node) { 488 // TODO: make trailing empty rows configurable (custom template or remove TRs) 489 var newRow = this.rowFragment.firstChild.cloneNode(true); 490 this.tbody.replaceChild(newRow, tr); 491 trIdx++; 492 continue; 493 } 494 if (tr !== node.tr) { 495 node.tr = tr; 496 node.render(); 497 _renderCount++; 498 499 // TODO: 500 // Implement scrolling by re-using existing markup 501 // e.g. shifting TRs or TR child elements instead of 502 // re-creating all the time 503 } 504 trIdx++; 505 } 506 this.isVpUpdating = prevPhase; 507 this.debugTimeEnd("redrawViewport()"); 508 }; 509 510 $.ui.fancytree.registerExtension({ 511 name: "grid", 512 version: "2.38.3", 513 // Default options for this extension. 514 options: { 515 checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) 516 indentation: 16, // indent every node level by 16px 517 mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR 518 nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0) 519 }, 520 // Overide virtual methods for this extension. 521 // `this` : is this extension object 522 // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) 523 treeInit: function (ctx) { 524 var i, 525 columnCount, 526 n, 527 $row, 528 $tbody, 529 tree = ctx.tree, 530 opts = ctx.options, 531 tableOpts = opts.table, 532 $table = tree.widget.element, 533 $scrollWrapper = $table.parent(".fancytree-grid-container"); 534 535 if ($.inArray("table", opts.extensions) >= 0) { 536 $.error("ext-grid and ext-table are mutually exclusive."); 537 } 538 if (opts.renderStatusColumns === true) { 539 opts.renderStatusColumns = opts.renderColumns; 540 } 541 // Note: we also re-use CSS rules from ext-table 542 $table.addClass( 543 "fancytree-container fancytree-ext-grid fancytree-ext-table" 544 ); 545 $tbody = $table.find(">tbody"); 546 if (!$tbody.length) { 547 // TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s: 548 if ($table.find(">tr").length) { 549 $.error( 550 "Expected table > tbody > tr. If you see this, please open an issue." 551 ); 552 } 553 $tbody = $("<tbody>").appendTo($table); 554 } 555 556 tree.tbody = $tbody[0]; 557 558 // Prepare row templates: 559 // Determine column count from table header if any 560 columnCount = $("thead >tr", $table).last().find(">th").length; 561 // Read TR templates from tbody if any 562 $row = $tbody.children("tr").first(); 563 if ($row.length) { 564 n = $row.children("td").length; 565 if (columnCount && n !== columnCount) { 566 tree.warn( 567 "Column count mismatch between thead (" + 568 columnCount + 569 ") and tbody (" + 570 n + 571 "): using tbody." 572 ); 573 columnCount = n; 574 } 575 $row = $row.clone(); 576 } else { 577 // Only thead is defined: create default row markup 578 _assert( 579 columnCount >= 1, 580 "Need either <thead> or <tbody> with <td> elements to determine column count." 581 ); 582 $row = $("<tr />"); 583 for (i = 0; i < columnCount; i++) { 584 $row.append("<td />"); 585 } 586 } 587 $row.find(">td") 588 .eq(tableOpts.nodeColumnIdx) 589 .html("<span class='fancytree-node' />"); 590 if (opts.aria) { 591 $row.attr("role", "row"); 592 $row.find("td").attr("role", "gridcell"); 593 } 594 tree.rowFragment = document.createDocumentFragment(); 595 tree.rowFragment.appendChild($row.get(0)); 596 597 $tbody.empty(); 598 599 // Make sure that status classes are set on the node's <tr> elements 600 tree.statusClassPropName = "tr"; 601 tree.ariaPropName = "tr"; 602 this.nodeContainerAttrName = "tr"; 603 604 // #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-grid 605 tree.$container = $table; 606 if ($scrollWrapper.length) { 607 tree.scrollWrapper = $scrollWrapper[0]; 608 this._initViewportWrapper(); 609 } else { 610 tree.scrollWrapper = null; 611 } 612 613 // Scrolling is implemented completely differently here 614 $.ui.fancytree.overrideMethod( 615 $.ui.fancytree._FancytreeNodeClass.prototype, 616 "scrollIntoView", 617 function (effects, options) { 618 var node = this, 619 tree = node.tree, 620 topNode = options && options.topNode, 621 vp = tree.viewport, 622 start = vp ? vp.start : null; 623 624 if (!tree.viewport) { 625 return node._super.apply(this, arguments); 626 } 627 if (node._rowIdx < vp.start) { 628 start = node._rowIdx; 629 } else if (node._rowIdx >= vp.start + vp.count) { 630 start = node._rowIdx - vp.count + 1; 631 } 632 if (topNode && topNode._rowIdx < start) { 633 start = topNode._rowIdx; 634 } 635 tree.setViewport({ start: start }); 636 // Return a resolved promise 637 return $.Deferred(function () { 638 this.resolveWith(node); 639 }).promise(); 640 } 641 ); 642 643 tree.visibleNodeList = null; // Set by _renumberVisibleNodes() 644 tree.viewport = { 645 enabled: true, 646 start: 0, 647 count: 10, 648 left: 0, 649 right: 0, 650 }; 651 this.setViewport( 652 $.extend( 653 { 654 // enabled: true, 655 autoSize: true, 656 start: 0, 657 count: 10, 658 left: 0, 659 right: 0, 660 keepEmptyRows: true, 661 noEvents: true, 662 }, 663 opts.viewport 664 ) 665 ); 666 // tree.$scrollbar = _addScrollbar($table); 667 668 this._superApply(arguments); 669 670 // standard Fancytree created a root UL 671 $(tree.rootNode.ul).remove(); 672 tree.rootNode.ul = null; 673 674 // Add container to the TAB chain 675 // #577: Allow to set tabindex to "0", "-1" and "" 676 this.$container.attr("tabindex", opts.tabindex); 677 // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1"); 678 if (opts.aria) { 679 tree.$container 680 .attr("role", "treegrid") 681 .attr("aria-readonly", true); 682 } 683 }, 684 nodeKeydown: function (ctx) { 685 var nextNode = null, 686 nextIdx = null, 687 tree = ctx.tree, 688 node = ctx.node, 689 nodeList = tree.visibleNodeList, 690 // treeOpts = ctx.options, 691 viewport = tree.viewport, 692 event = ctx.originalEvent, 693 eventString = FT.eventToString(event); 694 695 tree.debug("nodeKeydown(" + eventString + ")"); 696 697 switch (eventString) { 698 case "home": 699 case "meta+up": 700 nextIdx = 0; 701 break; 702 case "end": 703 case "meta+down": 704 nextIdx = nodeList.length - 1; 705 break; 706 case "pageup": 707 nextIdx = node._rowIdx - viewport.count; 708 break; 709 case "pagedown": 710 nextIdx = node._rowIdx + viewport.count; 711 break; 712 } 713 if (nextIdx != null) { 714 nextIdx = Math.min(Math.max(0, nextIdx), nodeList.length - 1); 715 nextNode = nodeList[nextIdx]; 716 nextNode.makeVisible(); 717 nextNode.setActive(); 718 return false; 719 } 720 return this._superApply(arguments); 721 }, 722 nodeRemoveChildMarkup: function (ctx) { 723 var node = ctx.node; 724 725 node.visit(function (n) { 726 if (n.tr) { 727 delete n.tr.ftnode; 728 n.tr = null; 729 n.span = null; 730 } 731 }); 732 }, 733 nodeRemoveMarkup: function (ctx) { 734 var node = ctx.node; 735 736 if (node.tr) { 737 delete node.tr.ftnode; 738 node.tr = null; 739 node.span = null; 740 } 741 this.nodeRemoveChildMarkup(ctx); 742 }, 743 /* Override standard render. */ 744 nodeRender: function (ctx, force, deep, collapsed, _recursive) { 745 var children, 746 i, 747 l, 748 outsideViewport, 749 subCtx, 750 tree = ctx.tree, 751 node = ctx.node; 752 753 if (tree._enableUpdate === false) { 754 node.debug("nodeRender(): _enableUpdate: false"); 755 return; 756 } 757 var opts = ctx.options, 758 viewport = tree.viewport.enabled ? tree.viewport : null, 759 start = viewport && viewport.start > 0 ? +viewport.start : 0, 760 bottom = viewport ? start + viewport.count - 1 : 0, 761 isRootNode = !node.parent; 762 763 _assert(viewport); 764 765 // node.debug("nodeRender(): " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); 766 if (!_recursive) { 767 // node.debug("nodeRender(): start top node"); 768 if (isRootNode && viewport) { 769 node.debug("nodeRender(): redrawViewport() instead"); 770 return ctx.tree.redrawViewport(); 771 } 772 ctx.hasCollapsedParents = node.parent && !node.parent.expanded; 773 // Make sure visible row indices are up-to-date 774 if (viewport) { 775 tree._renumberVisibleNodes(); 776 } 777 } 778 779 if (!isRootNode) { 780 outsideViewport = 781 viewport && 782 (node._rowIdx < start || 783 node._rowIdx >= start + viewport.count); 784 785 // node.debug( 786 // "nodeRender(): idx=" + 787 // node._rowIdx + 788 // ", outside=" + 789 // outsideViewport + 790 // ", TR count=" + 791 // tree.tbody.rows.length 792 // ); 793 if (outsideViewport) { 794 // node.debug("nodeRender(): outsideViewport: ignored"); 795 return; 796 } 797 if (!node.tr) { 798 if (node._rowIdx == null) { 799 // node.warn("nodeRender(): ignoring hidden"); 800 return; 801 } 802 node.debug("nodeRender(): creating new TR."); 803 node.tr = tree.tbody.rows[node._rowIdx - start]; 804 } 805 // _assert( 806 // node.tr, 807 // "nodeRender() called for node.tr == null: " + node 808 // ); 809 node.tr.ftnode = node; 810 811 if (node.key && opts.generateIds) { 812 node.tr.id = opts.idPrefix + node.key; 813 } 814 node.span = $("span.fancytree-node", node.tr).get(0); 815 816 // Set icon, link, and title (normally this is only required on initial render) 817 // var ctx = this._makeHookContext(node); 818 this.nodeRenderTitle(ctx); // triggers renderColumns() 819 820 // Allow tweaking, binding, after node was created for the first time 821 if (opts.createNode) { 822 opts.createNode.call(this, { type: "createNode" }, ctx); 823 } 824 } 825 // Allow tweaking after node state was rendered 826 if (opts.renderNode) { 827 opts.renderNode.call(tree, { type: "renderNode" }, ctx); 828 } 829 // Visit child nodes 830 // Add child markup 831 children = node.children; 832 _assert(!deep, "deep is not supported"); 833 834 if (children && (isRootNode || deep || node.expanded)) { 835 for (i = 0, l = children.length; i < l; i++) { 836 var child = children[i]; 837 838 if (viewport && child._rowIdx > bottom) { 839 children[i].debug("BREAK render children loop"); 840 return false; 841 } 842 subCtx = $.extend({}, ctx, { node: child }); 843 subCtx.hasCollapsedParents = 844 subCtx.hasCollapsedParents || !node.expanded; 845 this.nodeRender(subCtx, force, deep, collapsed, true); 846 } 847 } 848 }, 849 nodeRenderTitle: function (ctx, title) { 850 var $cb, 851 res, 852 tree = ctx.tree, 853 node = ctx.node, 854 opts = ctx.options, 855 isStatusNode = node.isStatusNode(); 856 857 res = this._super(ctx, title); 858 859 if (node.isRootNode()) { 860 return res; 861 } 862 // Move checkbox to custom column 863 if ( 864 opts.checkbox && 865 !isStatusNode && 866 opts.table.checkboxColumnIdx != null 867 ) { 868 $cb = $("span.fancytree-checkbox", node.span); //.detach(); 869 $(node.tr) 870 .find("td") 871 .eq(+opts.table.checkboxColumnIdx) 872 .html($cb); 873 } 874 // Update element classes according to node state 875 this.nodeRenderStatus(ctx); 876 877 if (isStatusNode) { 878 if (opts.renderStatusColumns) { 879 // Let user code write column content 880 opts.renderStatusColumns.call( 881 tree, 882 { type: "renderStatusColumns" }, 883 ctx 884 ); 885 } else if (opts.grid.mergeStatusColumns && node.isTopLevel()) { 886 node.warn("mergeStatusColumns is not yet implemented."); 887 // This approach would not work, since the roe may be re-used: 888 // $(node.tr) 889 // .find(">td") 890 // .eq(0) 891 // .prop("colspan", tree.columnCount) 892 // .text(node.title) 893 // .addClass("fancytree-status-merged") 894 // .nextAll() 895 // .remove(); 896 } // else: default rendering for status node: leave other cells empty 897 } else if (opts.renderColumns) { 898 opts.renderColumns.call(tree, { type: "renderColumns" }, ctx); 899 } 900 return res; 901 }, 902 nodeRenderStatus: function (ctx) { 903 var indent, 904 node = ctx.node, 905 opts = ctx.options; 906 907 this._super(ctx); 908 909 $(node.tr).removeClass("fancytree-node"); 910 // indent 911 indent = (node.getLevel() - 1) * opts.table.indentation; 912 if (opts.rtl) { 913 $(node.span).css({ paddingRight: indent + "px" }); 914 } else { 915 $(node.span).css({ paddingLeft: indent + "px" }); 916 } 917 }, 918 /* Expand node, return Deferred.promise. */ 919 nodeSetExpanded: function (ctx, flag, callOpts) { 920 var node = ctx.node, 921 tree = ctx.tree; 922 923 // flag defaults to true 924 flag = flag !== false; 925 926 if ((node.expanded && flag) || (!node.expanded && !flag)) { 927 // Expanded state isn't changed - just call base implementation 928 return this._superApply(arguments); 929 } 930 931 var dfd = new $.Deferred(), 932 subOpts = $.extend({}, callOpts, { 933 noEvents: true, 934 noAnimation: true, 935 }); 936 937 callOpts = callOpts || {}; 938 939 function _afterExpand(ok) { 940 tree.redrawViewport(true); 941 942 if (ok) { 943 if ( 944 flag && 945 ctx.options.autoScroll && 946 !callOpts.noAnimation && 947 node.hasChildren() 948 ) { 949 // Scroll down to last child, but keep current node visible 950 node.getLastChild() 951 .scrollIntoView(true, { topNode: node }) 952 .always(function () { 953 if (!callOpts.noEvents) { 954 tree._triggerNodeEvent( 955 flag ? "expand" : "collapse", 956 ctx 957 ); 958 } 959 dfd.resolveWith(node); 960 }); 961 } else { 962 if (!callOpts.noEvents) { 963 tree._triggerNodeEvent( 964 flag ? "expand" : "collapse", 965 ctx 966 ); 967 } 968 dfd.resolveWith(node); 969 } 970 } else { 971 if (!callOpts.noEvents) { 972 tree._triggerNodeEvent( 973 flag ? "expand" : "collapse", 974 ctx 975 ); 976 } 977 dfd.rejectWith(node); 978 } 979 } 980 // Call base-expand with disabled events and animation 981 this._super(ctx, flag, subOpts) 982 .done(function () { 983 _afterExpand(true); 984 }) 985 .fail(function () { 986 _afterExpand(false); 987 }); 988 return dfd.promise(); 989 }, 990 treeClear: function (ctx) { 991 // this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); 992 // this._renumberReset(); // Invalidate visible row cache 993 return this._superApply(arguments); 994 }, 995 treeDestroy: function (ctx) { 996 this.$container.find("tbody").empty(); 997 this.$container.off("wheel"); 998 if (this.$source) { 999 this.$source.removeClass("fancytree-helper-hidden"); 1000 } 1001 this._renumberReset(); // Invalidate visible row cache 1002 return this._superApply(arguments); 1003 }, 1004 treeStructureChanged: function (ctx, type) { 1005 // debugger; 1006 if (type !== "addNode" || ctx.tree.visibleNodeList) { 1007 // this.debug("treeStructureChanged(" + type + ")"); 1008 this._renumberReset(); // Invalidate visible row cache 1009 } 1010 }, 1011 }); 1012 // Value returned by `require('jquery.fancytree..')` 1013 return $.ui.fancytree; 1014}); // End of closure 1015