1/*! 2 * jquery.fancytree.js 3 * Tree view control with support for lazy loading and much more. 4 * https://github.com/mar10/fancytree/ 5 * 6 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 7 * Released under the MIT license 8 * https://github.com/mar10/fancytree/wiki/LicenseInfo 9 * 10 * @version 2.38.3 11 * @date 2023-02-01T20:52:50Z 12 */ 13 14/** Core Fancytree module. 15 */ 16 17// UMD wrapper for the Fancytree core module 18(function (factory) { 19 if (typeof define === "function" && define.amd) { 20 // AMD. Register as an anonymous module. 21 define(["jquery", "./jquery.fancytree.ui-deps"], factory); 22 } else if (typeof module === "object" && module.exports) { 23 // Node/CommonJS 24 require("./jquery.fancytree.ui-deps"); 25 module.exports = factory(require("jquery")); 26 } else { 27 // Browser globals 28 factory(jQuery); 29 } 30})(function ($) { 31 "use strict"; 32 33 // prevent duplicate loading 34 if ($.ui && $.ui.fancytree) { 35 $.ui.fancytree.warn("Fancytree: ignored duplicate include"); 36 return; 37 } 38 39 /****************************************************************************** 40 * Private functions and variables 41 */ 42 43 var i, 44 attr, 45 FT = null, // initialized below 46 TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/' 47 REX_HTML = /[&<>"'/]/g, // Escape those characters 48 REX_TOOLTIP = /[<>"'/]/g, // Don't escape `&` in tooltips 49 RECURSIVE_REQUEST_ERROR = "$recursive_request", 50 INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid", 51 ENTITY_MAP = { 52 "&": "&", 53 "<": "<", 54 ">": ">", 55 '"': """, 56 "'": "'", 57 "/": "/", 58 }, 59 IGNORE_KEYCODES = { 16: true, 17: true, 18: true }, 60 SPECIAL_KEYCODES = { 61 8: "backspace", 62 9: "tab", 63 10: "return", 64 13: "return", 65 // 16: null, 17: null, 18: null, // ignore shift, ctrl, alt 66 19: "pause", 67 20: "capslock", 68 27: "esc", 69 32: "space", 70 33: "pageup", 71 34: "pagedown", 72 35: "end", 73 36: "home", 74 37: "left", 75 38: "up", 76 39: "right", 77 40: "down", 78 45: "insert", 79 46: "del", 80 59: ";", 81 61: "=", 82 // 91: null, 93: null, // ignore left and right meta 83 96: "0", 84 97: "1", 85 98: "2", 86 99: "3", 87 100: "4", 88 101: "5", 89 102: "6", 90 103: "7", 91 104: "8", 92 105: "9", 93 106: "*", 94 107: "+", 95 109: "-", 96 110: ".", 97 111: "/", 98 112: "f1", 99 113: "f2", 100 114: "f3", 101 115: "f4", 102 116: "f5", 103 117: "f6", 104 118: "f7", 105 119: "f8", 106 120: "f9", 107 121: "f10", 108 122: "f11", 109 123: "f12", 110 144: "numlock", 111 145: "scroll", 112 173: "-", 113 186: ";", 114 187: "=", 115 188: ",", 116 189: "-", 117 190: ".", 118 191: "/", 119 192: "`", 120 219: "[", 121 220: "\\", 122 221: "]", 123 222: "'", 124 }, 125 MODIFIERS = { 126 16: "shift", 127 17: "ctrl", 128 18: "alt", 129 91: "meta", 130 93: "meta", 131 }, 132 MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" }, 133 // Boolean attributes that can be set with equivalent class names in the LI tags 134 // Note: v2.23: checkbox and hideCheckbox are *not* in this list 135 CLASS_ATTRS = 136 "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split( 137 " " 138 ), 139 CLASS_ATTR_MAP = {}, 140 // Top-level Fancytree attributes, that can be set by dict 141 TREE_ATTRS = "columns types".split(" "), 142 // TREE_ATTR_MAP = {}, 143 // Top-level FancytreeNode attributes, that can be set by dict 144 NODE_ATTRS = 145 "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type unselectable unselectableIgnore unselectableStatus".split( 146 " " 147 ), 148 NODE_ATTR_MAP = {}, 149 // Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase) 150 NODE_ATTR_LOWERCASE_MAP = {}, 151 // Attribute names that should NOT be added to node.data 152 NONE_NODE_DATA_MAP = { 153 active: true, 154 children: true, 155 data: true, 156 focus: true, 157 }; 158 159 for (i = 0; i < CLASS_ATTRS.length; i++) { 160 CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; 161 } 162 for (i = 0; i < NODE_ATTRS.length; i++) { 163 attr = NODE_ATTRS[i]; 164 NODE_ATTR_MAP[attr] = true; 165 if (attr !== attr.toLowerCase()) { 166 NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr; 167 } 168 } 169 // for(i=0; i<TREE_ATTRS.length; i++) { 170 // TREE_ATTR_MAP[TREE_ATTRS[i]] = true; 171 // } 172 173 function _assert(cond, msg) { 174 // TODO: see qunit.js extractStacktrace() 175 if (!cond) { 176 msg = msg ? ": " + msg : ""; 177 msg = "Fancytree assertion failed" + msg; 178 179 // consoleApply("assert", [!!cond, msg]); 180 181 // #1041: Raised exceptions may not be visible in the browser 182 // console if inside promise chains, so we also print directly: 183 $.ui.fancytree.error(msg); 184 185 // Throw exception: 186 $.error(msg); 187 } 188 } 189 190 function _hasProp(object, property) { 191 return Object.prototype.hasOwnProperty.call(object, property); 192 } 193 194 /* Replacement for the deprecated `jQuery.isFunction()`. */ 195 function _isFunction(obj) { 196 return typeof obj === "function"; 197 } 198 199 /* Replacement for the deprecated `jQuery.trim()`. */ 200 function _trim(text) { 201 return text == null ? "" : text.trim(); 202 } 203 204 /* Replacement for the deprecated `jQuery.isArray()`. */ 205 var _isArray = Array.isArray; 206 207 _assert($.ui, "Fancytree requires jQuery UI (http://jqueryui.com)"); 208 209 function consoleApply(method, args) { 210 var i, 211 s, 212 fn = window.console ? window.console[method] : null; 213 214 if (fn) { 215 try { 216 fn.apply(window.console, args); 217 } catch (e) { 218 // IE 8? 219 s = ""; 220 for (i = 0; i < args.length; i++) { 221 s += args[i]; 222 } 223 fn(s); 224 } 225 } 226 } 227 228 /* support: IE8 Polyfil for Date.now() */ 229 if (!Date.now) { 230 Date.now = function now() { 231 return new Date().getTime(); 232 }; 233 } 234 235 /*Return true if x is a FancytreeNode.*/ 236 function _isNode(x) { 237 return !!(x.tree && x.statusNodeType !== undefined); 238 } 239 240 /** Return true if dotted version string is equal or higher than requested version. 241 * 242 * See http://jsfiddle.net/mar10/FjSAN/ 243 */ 244 function isVersionAtLeast(dottedVersion, major, minor, patch) { 245 var i, 246 v, 247 t, 248 verParts = $.map(_trim(dottedVersion).split("."), function (e) { 249 return parseInt(e, 10); 250 }), 251 testParts = $.map( 252 Array.prototype.slice.call(arguments, 1), 253 function (e) { 254 return parseInt(e, 10); 255 } 256 ); 257 258 for (i = 0; i < testParts.length; i++) { 259 v = verParts[i] || 0; 260 t = testParts[i] || 0; 261 if (v !== t) { 262 return v > t; 263 } 264 } 265 return true; 266 } 267 268 /** 269 * Deep-merge a list of objects (but replace array-type options). 270 * 271 * jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays. 272 * This variant is used to merge extension defaults with user options, and should 273 * merge objects, but override arrays (for example the `triggerStart: [...]` option 274 * of ext-edit). Also `null` values are copied over and not skipped. 275 * 276 * See issue #876 277 * 278 * Example: 279 * _simpleDeepMerge({}, o1, o2); 280 */ 281 function _simpleDeepMerge() { 282 var options, 283 name, 284 src, 285 copy, 286 clone, 287 target = arguments[0] || {}, 288 i = 1, 289 length = arguments.length; 290 291 // Handle case when target is a string or something (possible in deep copy) 292 if (typeof target !== "object" && !_isFunction(target)) { 293 target = {}; 294 } 295 if (i === length) { 296 throw Error("need at least two args"); 297 } 298 for (; i < length; i++) { 299 // Only deal with non-null/undefined values 300 if ((options = arguments[i]) != null) { 301 // Extend the base object 302 for (name in options) { 303 if (_hasProp(options, name)) { 304 src = target[name]; 305 copy = options[name]; 306 // Prevent never-ending loop 307 if (target === copy) { 308 continue; 309 } 310 // Recurse if we're merging plain objects 311 // (NOTE: unlike $.extend, we don't merge arrays, but replace them) 312 if (copy && $.isPlainObject(copy)) { 313 clone = src && $.isPlainObject(src) ? src : {}; 314 // Never move original objects, clone them 315 target[name] = _simpleDeepMerge(clone, copy); 316 // Don't bring in undefined values 317 } else if (copy !== undefined) { 318 target[name] = copy; 319 } 320 } 321 } 322 } 323 } 324 // Return the modified object 325 return target; 326 } 327 328 /** Return a wrapper that calls sub.methodName() and exposes 329 * this : tree 330 * this._local : tree.ext.EXTNAME 331 * this._super : base.methodName.call() 332 * this._superApply : base.methodName.apply() 333 */ 334 function _makeVirtualFunction(methodName, tree, base, extension, extName) { 335 // $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName); 336 // if(rexTestSuper && !rexTestSuper.test(func)){ 337 // // extension.methodName() doesn't call _super(), so no wrapper required 338 // return func; 339 // } 340 // Use an immediate function as closure 341 var proxy = (function () { 342 var prevFunc = tree[methodName], // org. tree method or prev. proxy 343 baseFunc = extension[methodName], // 344 _local = tree.ext[extName], 345 _super = function () { 346 return prevFunc.apply(tree, arguments); 347 }, 348 _superApply = function (args) { 349 return prevFunc.apply(tree, args); 350 }; 351 352 // Return the wrapper function 353 return function () { 354 var prevLocal = tree._local, 355 prevSuper = tree._super, 356 prevSuperApply = tree._superApply; 357 358 try { 359 tree._local = _local; 360 tree._super = _super; 361 tree._superApply = _superApply; 362 return baseFunc.apply(tree, arguments); 363 } finally { 364 tree._local = prevLocal; 365 tree._super = prevSuper; 366 tree._superApply = prevSuperApply; 367 } 368 }; 369 })(); // end of Immediate Function 370 return proxy; 371 } 372 373 /** 374 * Subclass `base` by creating proxy functions 375 */ 376 function _subclassObject(tree, base, extension, extName) { 377 // $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName); 378 for (var attrName in extension) { 379 if (typeof extension[attrName] === "function") { 380 if (typeof tree[attrName] === "function") { 381 // override existing method 382 tree[attrName] = _makeVirtualFunction( 383 attrName, 384 tree, 385 base, 386 extension, 387 extName 388 ); 389 } else if (attrName.charAt(0) === "_") { 390 // Create private methods in tree.ext.EXTENSION namespace 391 tree.ext[extName][attrName] = _makeVirtualFunction( 392 attrName, 393 tree, 394 base, 395 extension, 396 extName 397 ); 398 } else { 399 $.error( 400 "Could not override tree." + 401 attrName + 402 ". Use prefix '_' to create tree." + 403 extName + 404 "._" + 405 attrName 406 ); 407 } 408 } else { 409 // Create member variables in tree.ext.EXTENSION namespace 410 if (attrName !== "options") { 411 tree.ext[extName][attrName] = extension[attrName]; 412 } 413 } 414 } 415 } 416 417 function _getResolvedPromise(context, argArray) { 418 if (context === undefined) { 419 return $.Deferred(function () { 420 this.resolve(); 421 }).promise(); 422 } 423 return $.Deferred(function () { 424 this.resolveWith(context, argArray); 425 }).promise(); 426 } 427 428 function _getRejectedPromise(context, argArray) { 429 if (context === undefined) { 430 return $.Deferred(function () { 431 this.reject(); 432 }).promise(); 433 } 434 return $.Deferred(function () { 435 this.rejectWith(context, argArray); 436 }).promise(); 437 } 438 439 function _makeResolveFunc(deferred, context) { 440 return function () { 441 deferred.resolveWith(context); 442 }; 443 } 444 445 function _getElementDataAsDict($el) { 446 // Evaluate 'data-NAME' attributes with special treatment for 'data-json'. 447 var d = $.extend({}, $el.data()), 448 json = d.json; 449 450 delete d.fancytree; // added to container by widget factory (old jQuery UI) 451 delete d.uiFancytree; // added to container by widget factory 452 453 if (json) { 454 delete d.json; 455 // <li data-json='...'> is already returned as object (http://api.jquery.com/data/#data-html5) 456 d = $.extend(d, json); 457 } 458 return d; 459 } 460 461 function _escapeTooltip(s) { 462 return ("" + s).replace(REX_TOOLTIP, function (s) { 463 return ENTITY_MAP[s]; 464 }); 465 } 466 467 // TODO: use currying 468 function _makeNodeTitleMatcher(s) { 469 s = s.toLowerCase(); 470 return function (node) { 471 return node.title.toLowerCase().indexOf(s) >= 0; 472 }; 473 } 474 475 function _makeNodeTitleStartMatcher(s) { 476 var reMatch = new RegExp("^" + s, "i"); 477 return function (node) { 478 return reMatch.test(node.title); 479 }; 480 } 481 482 /****************************************************************************** 483 * FancytreeNode 484 */ 485 486 /** 487 * Creates a new FancytreeNode 488 * 489 * @class FancytreeNode 490 * @classdesc A FancytreeNode represents the hierarchical data model and operations. 491 * 492 * @param {FancytreeNode} parent 493 * @param {NodeData} obj 494 * 495 * @property {Fancytree} tree The tree instance 496 * @property {FancytreeNode} parent The parent node 497 * @property {string} key Node id (must be unique inside the tree) 498 * @property {string} title Display name (may contain HTML) 499 * @property {object} data Contains all extra data that was passed on node creation 500 * @property {FancytreeNode[] | null | undefined} children Array of child nodes.<br> 501 * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array 502 * to define a node that has no children. 503 * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property. 504 * @property {string} extraClasses Additional CSS classes, added to the node's `<span>`.<br> 505 * Note: use `node.add/remove/toggleClass()` to modify. 506 * @property {boolean} folder Folder nodes have different default icons and click behavior.<br> 507 * Note: Also non-folders may have children. 508 * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'. 509 * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion. 510 * @property {boolean} selected Use isSelected(), setSelected() to access this property. 511 * @property {string} tooltip Alternative description used as hover popup 512 * @property {string} iconTooltip Description used as hover popup for icon. @since 2.27 513 * @property {string} type Node type, used with tree.types map. @since 2.27 514 */ 515 function FancytreeNode(parent, obj) { 516 var i, l, name, cl; 517 518 this.parent = parent; 519 this.tree = parent.tree; 520 this.ul = null; 521 this.li = null; // <li id='key' ftnode=this> tag 522 this.statusNodeType = null; // if this is a temp. node to display the status of its parent 523 this._isLoading = false; // if this node itself is loading 524 this._error = null; // {message: '...'} if a load error occurred 525 this.data = {}; 526 527 // TODO: merge this code with node.toDict() 528 // copy attributes from obj object 529 for (i = 0, l = NODE_ATTRS.length; i < l; i++) { 530 name = NODE_ATTRS[i]; 531 this[name] = obj[name]; 532 } 533 // unselectableIgnore and unselectableStatus imply unselectable 534 if ( 535 this.unselectableIgnore != null || 536 this.unselectableStatus != null 537 ) { 538 this.unselectable = true; 539 } 540 if (obj.hideCheckbox) { 541 $.error( 542 "'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'" 543 ); 544 } 545 // node.data += obj.data 546 if (obj.data) { 547 $.extend(this.data, obj.data); 548 } 549 // Copy all other attributes to this.data.NAME 550 for (name in obj) { 551 if ( 552 !NODE_ATTR_MAP[name] && 553 (this.tree.options.copyFunctionsToData || 554 !_isFunction(obj[name])) && 555 !NONE_NODE_DATA_MAP[name] 556 ) { 557 // node.data.NAME = obj.NAME 558 this.data[name] = obj[name]; 559 } 560 } 561 562 // Fix missing key 563 if (this.key == null) { 564 // test for null OR undefined 565 if (this.tree.options.defaultKey) { 566 this.key = "" + this.tree.options.defaultKey(this); 567 _assert(this.key, "defaultKey() must return a unique key"); 568 } else { 569 this.key = "_" + FT._nextNodeKey++; 570 } 571 } else { 572 this.key = "" + this.key; // Convert to string (#217) 573 } 574 575 // Fix tree.activeNode 576 // TODO: not elegant: we use obj.active as marker to set tree.activeNode 577 // when loading from a dictionary. 578 if (obj.active) { 579 _assert( 580 this.tree.activeNode === null, 581 "only one active node allowed" 582 ); 583 this.tree.activeNode = this; 584 } 585 if (obj.selected) { 586 // #186 587 this.tree.lastSelectedNode = this; 588 } 589 // TODO: handle obj.focus = true 590 591 // Create child nodes 592 cl = obj.children; 593 if (cl) { 594 if (cl.length) { 595 this._setChildren(cl); 596 } else { 597 // if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded' 598 this.children = this.lazy ? [] : null; 599 } 600 } else { 601 this.children = null; 602 } 603 // Add to key/ref map (except for root node) 604 // if( parent ) { 605 this.tree._callHook("treeRegisterNode", this.tree, true, this); 606 // } 607 } 608 609 FancytreeNode.prototype = /** @lends FancytreeNode# */ { 610 /* Return the direct child FancytreeNode with a given key, index. */ 611 _findDirectChild: function (ptr) { 612 var i, 613 l, 614 cl = this.children; 615 616 if (cl) { 617 if (typeof ptr === "string") { 618 for (i = 0, l = cl.length; i < l; i++) { 619 if (cl[i].key === ptr) { 620 return cl[i]; 621 } 622 } 623 } else if (typeof ptr === "number") { 624 return this.children[ptr]; 625 } else if (ptr.parent === this) { 626 return ptr; 627 } 628 } 629 return null; 630 }, 631 // TODO: activate() 632 // TODO: activateSilently() 633 /* Internal helper called in recursive addChildren sequence.*/ 634 _setChildren: function (children) { 635 _assert( 636 children && (!this.children || this.children.length === 0), 637 "only init supported" 638 ); 639 this.children = []; 640 for (var i = 0, l = children.length; i < l; i++) { 641 this.children.push(new FancytreeNode(this, children[i])); 642 } 643 this.tree._callHook( 644 "treeStructureChanged", 645 this.tree, 646 "setChildren" 647 ); 648 }, 649 /** 650 * Append (or insert) a list of child nodes. 651 * 652 * @param {NodeData[]} children array of child node definitions (also single child accepted) 653 * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such). 654 * If omitted, the new children are appended. 655 * @returns {FancytreeNode} first child added 656 * 657 * @see FancytreeNode#applyPatch 658 */ 659 addChildren: function (children, insertBefore) { 660 var i, 661 l, 662 pos, 663 origFirstChild = this.getFirstChild(), 664 origLastChild = this.getLastChild(), 665 firstNode = null, 666 nodeList = []; 667 668 if ($.isPlainObject(children)) { 669 children = [children]; 670 } 671 if (!this.children) { 672 this.children = []; 673 } 674 for (i = 0, l = children.length; i < l; i++) { 675 nodeList.push(new FancytreeNode(this, children[i])); 676 } 677 firstNode = nodeList[0]; 678 if (insertBefore == null) { 679 this.children = this.children.concat(nodeList); 680 } else { 681 // Returns null if insertBefore is not a direct child: 682 insertBefore = this._findDirectChild(insertBefore); 683 pos = $.inArray(insertBefore, this.children); 684 _assert(pos >= 0, "insertBefore must be an existing child"); 685 // insert nodeList after children[pos] 686 this.children.splice.apply( 687 this.children, 688 [pos, 0].concat(nodeList) 689 ); 690 } 691 if (origFirstChild && !insertBefore) { 692 // #708: Fast path -- don't render every child of root, just the new ones! 693 // #723, #729: but only if it's appended to an existing child list 694 for (i = 0, l = nodeList.length; i < l; i++) { 695 nodeList[i].render(); // New nodes were never rendered before 696 } 697 // Adjust classes where status may have changed 698 // Has a first child 699 if (origFirstChild !== this.getFirstChild()) { 700 // Different first child -- recompute classes 701 origFirstChild.renderStatus(); 702 } 703 if (origLastChild !== this.getLastChild()) { 704 // Different last child -- recompute classes 705 origLastChild.renderStatus(); 706 } 707 } else if (!this.parent || this.parent.ul || this.tr) { 708 // render if the parent was rendered (or this is a root node) 709 this.render(); 710 } 711 if (this.tree.options.selectMode === 3) { 712 this.fixSelection3FromEndNodes(); 713 } 714 this.triggerModifyChild( 715 "add", 716 nodeList.length === 1 ? nodeList[0] : null 717 ); 718 return firstNode; 719 }, 720 /** 721 * Add class to node's span tag and to .extraClasses. 722 * 723 * @param {string} className class name 724 * 725 * @since 2.17 726 */ 727 addClass: function (className) { 728 return this.toggleClass(className, true); 729 }, 730 /** 731 * Append or prepend a node, or append a child node. 732 * 733 * This a convenience function that calls addChildren() 734 * 735 * @param {NodeData} node node definition 736 * @param {string} [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') 737 * @returns {FancytreeNode} new node 738 */ 739 addNode: function (node, mode) { 740 if (mode === undefined || mode === "over") { 741 mode = "child"; 742 } 743 switch (mode) { 744 case "after": 745 return this.getParent().addChildren( 746 node, 747 this.getNextSibling() 748 ); 749 case "before": 750 return this.getParent().addChildren(node, this); 751 case "firstChild": 752 // Insert before the first child if any 753 var insertBefore = this.children ? this.children[0] : null; 754 return this.addChildren(node, insertBefore); 755 case "child": 756 case "over": 757 return this.addChildren(node); 758 } 759 _assert(false, "Invalid mode: " + mode); 760 }, 761 /**Add child status nodes that indicate 'More...', etc. 762 * 763 * This also maintains the node's `partload` property. 764 * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes. 765 * @param {string} [mode='child'] 'child'|firstChild' 766 * @since 2.15 767 */ 768 addPagingNode: function (node, mode) { 769 var i, n; 770 771 mode = mode || "child"; 772 if (node === false) { 773 for (i = this.children.length - 1; i >= 0; i--) { 774 n = this.children[i]; 775 if (n.statusNodeType === "paging") { 776 this.removeChild(n); 777 } 778 } 779 this.partload = false; 780 return; 781 } 782 node = $.extend( 783 { 784 title: this.tree.options.strings.moreData, 785 statusNodeType: "paging", 786 icon: false, 787 }, 788 node 789 ); 790 this.partload = true; 791 return this.addNode(node, mode); 792 }, 793 /** 794 * Append new node after this. 795 * 796 * This a convenience function that calls addNode(node, 'after') 797 * 798 * @param {NodeData} node node definition 799 * @returns {FancytreeNode} new node 800 */ 801 appendSibling: function (node) { 802 return this.addNode(node, "after"); 803 }, 804 /** 805 * (experimental) Apply a modification (or navigation) operation. 806 * 807 * @param {string} cmd 808 * @param {object} [opts] 809 * @see Fancytree#applyCommand 810 * @since 2.32 811 */ 812 applyCommand: function (cmd, opts) { 813 return this.tree.applyCommand(cmd, this, opts); 814 }, 815 /** 816 * Modify existing child nodes. 817 * 818 * @param {NodePatch} patch 819 * @returns {$.Promise} 820 * @see FancytreeNode#addChildren 821 */ 822 applyPatch: function (patch) { 823 // patch [key, null] means 'remove' 824 if (patch === null) { 825 this.remove(); 826 return _getResolvedPromise(this); 827 } 828 // TODO: make sure that root node is not collapsed or modified 829 // copy (most) attributes to node.ATTR or node.data.ATTR 830 var name, 831 promise, 832 v, 833 IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global 834 835 for (name in patch) { 836 if (_hasProp(patch, name)) { 837 v = patch[name]; 838 if (!IGNORE_MAP[name] && !_isFunction(v)) { 839 if (NODE_ATTR_MAP[name]) { 840 this[name] = v; 841 } else { 842 this.data[name] = v; 843 } 844 } 845 } 846 } 847 // Remove and/or create children 848 if (_hasProp(patch, "children")) { 849 this.removeChildren(); 850 if (patch.children) { 851 // only if not null and not empty list 852 // TODO: addChildren instead? 853 this._setChildren(patch.children); 854 } 855 // TODO: how can we APPEND or INSERT child nodes? 856 } 857 if (this.isVisible()) { 858 this.renderTitle(); 859 this.renderStatus(); 860 } 861 // Expand collapse (final step, since this may be async) 862 if (_hasProp(patch, "expanded")) { 863 promise = this.setExpanded(patch.expanded); 864 } else { 865 promise = _getResolvedPromise(this); 866 } 867 return promise; 868 }, 869 /** Collapse all sibling nodes. 870 * @returns {$.Promise} 871 */ 872 collapseSiblings: function () { 873 return this.tree._callHook("nodeCollapseSiblings", this); 874 }, 875 /** Copy this node as sibling or child of `node`. 876 * 877 * @param {FancytreeNode} node source node 878 * @param {string} [mode=child] 'before' | 'after' | 'child' 879 * @param {Function} [map] callback function(NodeData, FancytreeNode) that could modify the new node 880 * @returns {FancytreeNode} new 881 */ 882 copyTo: function (node, mode, map) { 883 return node.addNode(this.toDict(true, map), mode); 884 }, 885 /** Count direct and indirect children. 886 * 887 * @param {boolean} [deep=true] pass 'false' to only count direct children 888 * @returns {int} number of child nodes 889 */ 890 countChildren: function (deep) { 891 var cl = this.children, 892 i, 893 l, 894 n; 895 if (!cl) { 896 return 0; 897 } 898 n = cl.length; 899 if (deep !== false) { 900 for (i = 0, l = n; i < l; i++) { 901 n += cl[i].countChildren(); 902 } 903 } 904 return n; 905 }, 906 // TODO: deactivate() 907 /** Write to browser console if debugLevel >= 4 (prepending node info) 908 * 909 * @param {*} msg string or object or array of such 910 */ 911 debug: function (msg) { 912 if (this.tree.options.debugLevel >= 4) { 913 Array.prototype.unshift.call(arguments, this.toString()); 914 consoleApply("log", arguments); 915 } 916 }, 917 /** Deprecated. 918 * @deprecated since 2014-02-16. Use resetLazy() instead. 919 */ 920 discard: function () { 921 this.warn( 922 "FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead." 923 ); 924 return this.resetLazy(); 925 }, 926 /** Remove DOM elements for all descendents. May be called on .collapse event 927 * to keep the DOM small. 928 * @param {boolean} [includeSelf=false] 929 */ 930 discardMarkup: function (includeSelf) { 931 var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup"; 932 this.tree._callHook(fn, this); 933 }, 934 /** Write error to browser console if debugLevel >= 1 (prepending tree info) 935 * 936 * @param {*} msg string or object or array of such 937 */ 938 error: function (msg) { 939 if (this.tree.options.debugLevel >= 1) { 940 Array.prototype.unshift.call(arguments, this.toString()); 941 consoleApply("error", arguments); 942 } 943 }, 944 /**Find all nodes that match condition (excluding self). 945 * 946 * @param {string | function(node)} match title string to search for, or a 947 * callback function that returns `true` if a node is matched. 948 * @returns {FancytreeNode[]} array of nodes (may be empty) 949 */ 950 findAll: function (match) { 951 match = _isFunction(match) ? match : _makeNodeTitleMatcher(match); 952 var res = []; 953 this.visit(function (n) { 954 if (match(n)) { 955 res.push(n); 956 } 957 }); 958 return res; 959 }, 960 /**Find first node that matches condition (excluding self). 961 * 962 * @param {string | function(node)} match title string to search for, or a 963 * callback function that returns `true` if a node is matched. 964 * @returns {FancytreeNode} matching node or null 965 * @see FancytreeNode#findAll 966 */ 967 findFirst: function (match) { 968 match = _isFunction(match) ? match : _makeNodeTitleMatcher(match); 969 var res = null; 970 this.visit(function (n) { 971 if (match(n)) { 972 res = n; 973 return false; 974 } 975 }); 976 return res; 977 }, 978 /** Find a node relative to self. 979 * 980 * @param {number|string} where The keyCode that would normally trigger this move, 981 * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up'). 982 * @returns {FancytreeNode} 983 * @since v2.31 984 */ 985 findRelatedNode: function (where, includeHidden) { 986 return this.tree.findRelatedNode(this, where, includeHidden); 987 }, 988 /* Apply selection state (internal use only) */ 989 _changeSelectStatusAttrs: function (state) { 990 var changed = false, 991 opts = this.tree.options, 992 unselectable = FT.evalOption( 993 "unselectable", 994 this, 995 this, 996 opts, 997 false 998 ), 999 unselectableStatus = FT.evalOption( 1000 "unselectableStatus", 1001 this, 1002 this, 1003 opts, 1004 undefined 1005 ); 1006 1007 if (unselectable && unselectableStatus != null) { 1008 state = unselectableStatus; 1009 } 1010 switch (state) { 1011 case false: 1012 changed = this.selected || this.partsel; 1013 this.selected = false; 1014 this.partsel = false; 1015 break; 1016 case true: 1017 changed = !this.selected || !this.partsel; 1018 this.selected = true; 1019 this.partsel = true; 1020 break; 1021 case undefined: 1022 changed = this.selected || !this.partsel; 1023 this.selected = false; 1024 this.partsel = true; 1025 break; 1026 default: 1027 _assert(false, "invalid state: " + state); 1028 } 1029 // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); 1030 if (changed) { 1031 this.renderStatus(); 1032 } 1033 return changed; 1034 }, 1035 /** 1036 * Fix selection status, after this node was (de)selected in multi-hier mode. 1037 * This includes (de)selecting all children. 1038 */ 1039 fixSelection3AfterClick: function (callOpts) { 1040 var flag = this.isSelected(); 1041 1042 // this.debug("fixSelection3AfterClick()"); 1043 1044 this.visit(function (node) { 1045 node._changeSelectStatusAttrs(flag); 1046 if (node.radiogroup) { 1047 // #931: don't (de)select this branch 1048 return "skip"; 1049 } 1050 }); 1051 this.fixSelection3FromEndNodes(callOpts); 1052 }, 1053 /** 1054 * Fix selection status for multi-hier mode. 1055 * Only end-nodes are considered to update the descendants branch and parents. 1056 * Should be called after this node has loaded new children or after 1057 * children have been modified using the API. 1058 */ 1059 fixSelection3FromEndNodes: function (callOpts) { 1060 var opts = this.tree.options; 1061 1062 // this.debug("fixSelection3FromEndNodes()"); 1063 _assert(opts.selectMode === 3, "expected selectMode 3"); 1064 1065 // Visit all end nodes and adjust their parent's `selected` and `partsel` 1066 // attributes. Return selection state true, false, or undefined. 1067 function _walk(node) { 1068 var i, 1069 l, 1070 child, 1071 s, 1072 state, 1073 allSelected, 1074 someSelected, 1075 unselIgnore, 1076 unselState, 1077 children = node.children; 1078 1079 if (children && children.length) { 1080 // check all children recursively 1081 allSelected = true; 1082 someSelected = false; 1083 1084 for (i = 0, l = children.length; i < l; i++) { 1085 child = children[i]; 1086 // the selection state of a node is not relevant; we need the end-nodes 1087 s = _walk(child); 1088 // if( !child.unselectableIgnore ) { 1089 unselIgnore = FT.evalOption( 1090 "unselectableIgnore", 1091 child, 1092 child, 1093 opts, 1094 false 1095 ); 1096 if (!unselIgnore) { 1097 if (s !== false) { 1098 someSelected = true; 1099 } 1100 if (s !== true) { 1101 allSelected = false; 1102 } 1103 } 1104 } 1105 // eslint-disable-next-line no-nested-ternary 1106 state = allSelected 1107 ? true 1108 : someSelected 1109 ? undefined 1110 : false; 1111 } else { 1112 // This is an end-node: simply report the status 1113 unselState = FT.evalOption( 1114 "unselectableStatus", 1115 node, 1116 node, 1117 opts, 1118 undefined 1119 ); 1120 state = unselState == null ? !!node.selected : !!unselState; 1121 } 1122 // #939: Keep a `partsel` flag that was explicitly set on a lazy node 1123 if ( 1124 node.partsel && 1125 !node.selected && 1126 node.lazy && 1127 node.children == null 1128 ) { 1129 state = undefined; 1130 } 1131 node._changeSelectStatusAttrs(state); 1132 return state; 1133 } 1134 _walk(this); 1135 1136 // Update parent's state 1137 this.visitParents(function (node) { 1138 var i, 1139 l, 1140 child, 1141 state, 1142 unselIgnore, 1143 unselState, 1144 children = node.children, 1145 allSelected = true, 1146 someSelected = false; 1147 1148 for (i = 0, l = children.length; i < l; i++) { 1149 child = children[i]; 1150 unselIgnore = FT.evalOption( 1151 "unselectableIgnore", 1152 child, 1153 child, 1154 opts, 1155 false 1156 ); 1157 if (!unselIgnore) { 1158 unselState = FT.evalOption( 1159 "unselectableStatus", 1160 child, 1161 child, 1162 opts, 1163 undefined 1164 ); 1165 state = 1166 unselState == null 1167 ? !!child.selected 1168 : !!unselState; 1169 // When fixing the parents, we trust the sibling status (i.e. 1170 // we don't recurse) 1171 if (state || child.partsel) { 1172 someSelected = true; 1173 } 1174 if (!state) { 1175 allSelected = false; 1176 } 1177 } 1178 } 1179 // eslint-disable-next-line no-nested-ternary 1180 state = allSelected ? true : someSelected ? undefined : false; 1181 node._changeSelectStatusAttrs(state); 1182 }); 1183 }, 1184 // TODO: focus() 1185 /** 1186 * Update node data. If dict contains 'children', then also replace 1187 * the hole sub tree. 1188 * @param {NodeData} dict 1189 * 1190 * @see FancytreeNode#addChildren 1191 * @see FancytreeNode#applyPatch 1192 */ 1193 fromDict: function (dict) { 1194 // copy all other attributes to this.data.xxx 1195 for (var name in dict) { 1196 if (NODE_ATTR_MAP[name]) { 1197 // node.NAME = dict.NAME 1198 this[name] = dict[name]; 1199 } else if (name === "data") { 1200 // node.data += dict.data 1201 $.extend(this.data, dict.data); 1202 } else if ( 1203 !_isFunction(dict[name]) && 1204 !NONE_NODE_DATA_MAP[name] 1205 ) { 1206 // node.data.NAME = dict.NAME 1207 this.data[name] = dict[name]; 1208 } 1209 } 1210 if (dict.children) { 1211 // recursively set children and render 1212 this.removeChildren(); 1213 this.addChildren(dict.children); 1214 } 1215 this.renderTitle(); 1216 /* 1217 var children = dict.children; 1218 if(children === undefined){ 1219 this.data = $.extend(this.data, dict); 1220 this.render(); 1221 return; 1222 } 1223 dict = $.extend({}, dict); 1224 dict.children = undefined; 1225 this.data = $.extend(this.data, dict); 1226 this.removeChildren(); 1227 this.addChild(children); 1228 */ 1229 }, 1230 /** Return the list of child nodes (undefined for unexpanded lazy nodes). 1231 * @returns {FancytreeNode[] | undefined} 1232 */ 1233 getChildren: function () { 1234 if (this.hasChildren() === undefined) { 1235 // TODO: only required for lazy nodes? 1236 return undefined; // Lazy node: unloaded, currently loading, or load error 1237 } 1238 return this.children; 1239 }, 1240 /** Return the first child node or null. 1241 * @returns {FancytreeNode | null} 1242 */ 1243 getFirstChild: function () { 1244 return this.children ? this.children[0] : null; 1245 }, 1246 /** Return the 0-based child index. 1247 * @returns {int} 1248 */ 1249 getIndex: function () { 1250 // return this.parent.children.indexOf(this); 1251 return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7 1252 }, 1253 /** Return the hierarchical child index (1-based, e.g. '3.2.4'). 1254 * @param {string} [separator="."] 1255 * @param {int} [digits=1] 1256 * @returns {string} 1257 */ 1258 getIndexHier: function (separator, digits) { 1259 separator = separator || "."; 1260 var s, 1261 res = []; 1262 $.each(this.getParentList(false, true), function (i, o) { 1263 s = "" + (o.getIndex() + 1); 1264 if (digits) { 1265 // prepend leading zeroes 1266 s = ("0000000" + s).substr(-digits); 1267 } 1268 res.push(s); 1269 }); 1270 return res.join(separator); 1271 }, 1272 /** Return the parent keys separated by options.keyPathSeparator, e.g. "/id_1/id_17/id_32". 1273 * 1274 * (Unlike `node.getPath()`, this method prepends a "/" and inverts the first argument.) 1275 * 1276 * @see FancytreeNode#getPath 1277 * @param {boolean} [excludeSelf=false] 1278 * @returns {string} 1279 */ 1280 getKeyPath: function (excludeSelf) { 1281 var sep = this.tree.options.keyPathSeparator; 1282 1283 return sep + this.getPath(!excludeSelf, "key", sep); 1284 }, 1285 /** Return the last child of this node or null. 1286 * @returns {FancytreeNode | null} 1287 */ 1288 getLastChild: function () { 1289 return this.children 1290 ? this.children[this.children.length - 1] 1291 : null; 1292 }, 1293 /** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... . 1294 * @returns {int} 1295 */ 1296 getLevel: function () { 1297 var level = 0, 1298 dtn = this.parent; 1299 while (dtn) { 1300 level++; 1301 dtn = dtn.parent; 1302 } 1303 return level; 1304 }, 1305 /** Return the successor node (under the same parent) or null. 1306 * @returns {FancytreeNode | null} 1307 */ 1308 getNextSibling: function () { 1309 // TODO: use indexOf, if available: (not in IE6) 1310 if (this.parent) { 1311 var i, 1312 l, 1313 ac = this.parent.children; 1314 1315 for (i = 0, l = ac.length - 1; i < l; i++) { 1316 // up to length-2, so next(last) = null 1317 if (ac[i] === this) { 1318 return ac[i + 1]; 1319 } 1320 } 1321 } 1322 return null; 1323 }, 1324 /** Return the parent node (null for the system root node). 1325 * @returns {FancytreeNode | null} 1326 */ 1327 getParent: function () { 1328 // TODO: return null for top-level nodes? 1329 return this.parent; 1330 }, 1331 /** Return an array of all parent nodes (top-down). 1332 * @param {boolean} [includeRoot=false] Include the invisible system root node. 1333 * @param {boolean} [includeSelf=false] Include the node itself. 1334 * @returns {FancytreeNode[]} 1335 */ 1336 getParentList: function (includeRoot, includeSelf) { 1337 var l = [], 1338 dtn = includeSelf ? this : this.parent; 1339 while (dtn) { 1340 if (includeRoot || dtn.parent) { 1341 l.unshift(dtn); 1342 } 1343 dtn = dtn.parent; 1344 } 1345 return l; 1346 }, 1347 /** Return a string representing the hierachical node path, e.g. "a/b/c". 1348 * @param {boolean} [includeSelf=true] 1349 * @param {string | function} [part="title"] node property name or callback 1350 * @param {string} [separator="/"] 1351 * @returns {string} 1352 * @since v2.31 1353 */ 1354 getPath: function (includeSelf, part, separator) { 1355 includeSelf = includeSelf !== false; 1356 part = part || "title"; 1357 separator = separator || "/"; 1358 1359 var val, 1360 path = [], 1361 isFunc = _isFunction(part); 1362 1363 this.visitParents(function (n) { 1364 if (n.parent) { 1365 val = isFunc ? part(n) : n[part]; 1366 path.unshift(val); 1367 } 1368 }, includeSelf); 1369 return path.join(separator); 1370 }, 1371 /** Return the predecessor node (under the same parent) or null. 1372 * @returns {FancytreeNode | null} 1373 */ 1374 getPrevSibling: function () { 1375 if (this.parent) { 1376 var i, 1377 l, 1378 ac = this.parent.children; 1379 1380 for (i = 1, l = ac.length; i < l; i++) { 1381 // start with 1, so prev(first) = null 1382 if (ac[i] === this) { 1383 return ac[i - 1]; 1384 } 1385 } 1386 } 1387 return null; 1388 }, 1389 /** 1390 * Return an array of selected descendant nodes. 1391 * @param {boolean} [stopOnParents=false] only return the topmost selected 1392 * node (useful with selectMode 3) 1393 * @returns {FancytreeNode[]} 1394 */ 1395 getSelectedNodes: function (stopOnParents) { 1396 var nodeList = []; 1397 this.visit(function (node) { 1398 if (node.selected) { 1399 nodeList.push(node); 1400 if (stopOnParents === true) { 1401 return "skip"; // stop processing this branch 1402 } 1403 } 1404 }); 1405 return nodeList; 1406 }, 1407 /** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded). 1408 * @returns {boolean | undefined} 1409 */ 1410 hasChildren: function () { 1411 if (this.lazy) { 1412 if (this.children == null) { 1413 // null or undefined: Not yet loaded 1414 return undefined; 1415 } else if (this.children.length === 0) { 1416 // Loaded, but response was empty 1417 return false; 1418 } else if ( 1419 this.children.length === 1 && 1420 this.children[0].isStatusNode() 1421 ) { 1422 // Currently loading or load error 1423 return undefined; 1424 } 1425 return true; 1426 } 1427 return !!(this.children && this.children.length); 1428 }, 1429 /** 1430 * Return true if node has `className` defined in .extraClasses. 1431 * 1432 * @param {string} className class name (separate multiple classes by space) 1433 * @returns {boolean} 1434 * 1435 * @since 2.32 1436 */ 1437 hasClass: function (className) { 1438 return ( 1439 (" " + (this.extraClasses || "") + " ").indexOf( 1440 " " + className + " " 1441 ) >= 0 1442 ); 1443 }, 1444 /** Return true if node has keyboard focus. 1445 * @returns {boolean} 1446 */ 1447 hasFocus: function () { 1448 return this.tree.hasFocus() && this.tree.focusNode === this; 1449 }, 1450 /** Write to browser console if debugLevel >= 3 (prepending node info) 1451 * 1452 * @param {*} msg string or object or array of such 1453 */ 1454 info: function (msg) { 1455 if (this.tree.options.debugLevel >= 3) { 1456 Array.prototype.unshift.call(arguments, this.toString()); 1457 consoleApply("info", arguments); 1458 } 1459 }, 1460 /** Return true if node is active (see also FancytreeNode#isSelected). 1461 * @returns {boolean} 1462 */ 1463 isActive: function () { 1464 return this.tree.activeNode === this; 1465 }, 1466 /** Return true if node is vertically below `otherNode`, i.e. rendered in a subsequent row. 1467 * @param {FancytreeNode} otherNode 1468 * @returns {boolean} 1469 * @since 2.28 1470 */ 1471 isBelowOf: function (otherNode) { 1472 return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5); 1473 }, 1474 /** Return true if node is a direct child of otherNode. 1475 * @param {FancytreeNode} otherNode 1476 * @returns {boolean} 1477 */ 1478 isChildOf: function (otherNode) { 1479 return this.parent && this.parent === otherNode; 1480 }, 1481 /** Return true, if node is a direct or indirect sub node of otherNode. 1482 * @param {FancytreeNode} otherNode 1483 * @returns {boolean} 1484 */ 1485 isDescendantOf: function (otherNode) { 1486 if (!otherNode || otherNode.tree !== this.tree) { 1487 return false; 1488 } 1489 var p = this.parent; 1490 while (p) { 1491 if (p === otherNode) { 1492 return true; 1493 } 1494 if (p === p.parent) { 1495 $.error("Recursive parent link: " + p); 1496 } 1497 p = p.parent; 1498 } 1499 return false; 1500 }, 1501 /** Return true if node is expanded. 1502 * @returns {boolean} 1503 */ 1504 isExpanded: function () { 1505 return !!this.expanded; 1506 }, 1507 /** Return true if node is the first node of its parent's children. 1508 * @returns {boolean} 1509 */ 1510 isFirstSibling: function () { 1511 var p = this.parent; 1512 return !p || p.children[0] === this; 1513 }, 1514 /** Return true if node is a folder, i.e. has the node.folder attribute set. 1515 * @returns {boolean} 1516 */ 1517 isFolder: function () { 1518 return !!this.folder; 1519 }, 1520 /** Return true if node is the last node of its parent's children. 1521 * @returns {boolean} 1522 */ 1523 isLastSibling: function () { 1524 var p = this.parent; 1525 return !p || p.children[p.children.length - 1] === this; 1526 }, 1527 /** Return true if node is lazy (even if data was already loaded) 1528 * @returns {boolean} 1529 */ 1530 isLazy: function () { 1531 return !!this.lazy; 1532 }, 1533 /** Return true if node is lazy and loaded. For non-lazy nodes always return true. 1534 * @returns {boolean} 1535 */ 1536 isLoaded: function () { 1537 return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node 1538 }, 1539 /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending. 1540 * @returns {boolean} 1541 */ 1542 isLoading: function () { 1543 return !!this._isLoading; 1544 }, 1545 /* 1546 * @deprecated since v2.4.0: Use isRootNode() instead 1547 */ 1548 isRoot: function () { 1549 return this.isRootNode(); 1550 }, 1551 /** Return true if node is partially selected (tri-state). 1552 * @returns {boolean} 1553 * @since 2.23 1554 */ 1555 isPartsel: function () { 1556 return !this.selected && !!this.partsel; 1557 }, 1558 /** (experimental) Return true if this is partially loaded. 1559 * @returns {boolean} 1560 * @since 2.15 1561 */ 1562 isPartload: function () { 1563 return !!this.partload; 1564 }, 1565 /** Return true if this is the (invisible) system root node. 1566 * @returns {boolean} 1567 * @since 2.4 1568 */ 1569 isRootNode: function () { 1570 return this.tree.rootNode === this; 1571 }, 1572 /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive). 1573 * @returns {boolean} 1574 */ 1575 isSelected: function () { 1576 return !!this.selected; 1577 }, 1578 /** Return true if this node is a temporarily generated system node like 1579 * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). 1580 * @returns {boolean} 1581 */ 1582 isStatusNode: function () { 1583 return !!this.statusNodeType; 1584 }, 1585 /** Return true if this node is a status node of type 'paging'. 1586 * @returns {boolean} 1587 * @since 2.15 1588 */ 1589 isPagingNode: function () { 1590 return this.statusNodeType === "paging"; 1591 }, 1592 /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. 1593 * @returns {boolean} 1594 * @since 2.4 1595 */ 1596 isTopLevel: function () { 1597 return this.tree.rootNode === this.parent; 1598 }, 1599 /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false. 1600 * @returns {boolean} 1601 */ 1602 isUndefined: function () { 1603 return this.hasChildren() === undefined; // also checks if the only child is a status node 1604 }, 1605 /** Return true if all parent nodes are expanded. Note: this does not check 1606 * whether the node is scrolled into the visible part of the screen. 1607 * @returns {boolean} 1608 */ 1609 isVisible: function () { 1610 var i, 1611 l, 1612 n, 1613 hasFilter = this.tree.enableFilter, 1614 parents = this.getParentList(false, false); 1615 1616 // TODO: check $(n.span).is(":visible") 1617 // i.e. return false for nodes (but not parents) that are hidden 1618 // by a filter 1619 if (hasFilter && !this.match && !this.subMatchCount) { 1620 // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); 1621 return false; 1622 } 1623 1624 for (i = 0, l = parents.length; i < l; i++) { 1625 n = parents[i]; 1626 1627 if (!n.expanded) { 1628 // this.debug("isVisible: HIDDEN (parent collapsed)"); 1629 return false; 1630 } 1631 // if (hasFilter && !n.match && !n.subMatchCount) { 1632 // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); 1633 // return false; 1634 // } 1635 } 1636 // this.debug("isVisible: VISIBLE"); 1637 return true; 1638 }, 1639 /** Deprecated. 1640 * @deprecated since 2014-02-16: use load() instead. 1641 */ 1642 lazyLoad: function (discard) { 1643 $.error( 1644 "FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead." 1645 ); 1646 }, 1647 /** 1648 * Load all children of a lazy node if neccessary. The <i>expanded</i> state is maintained. 1649 * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded. 1650 * @returns {$.Promise} 1651 */ 1652 load: function (forceReload) { 1653 var res, 1654 source, 1655 self = this, 1656 wasExpanded = this.isExpanded(); 1657 1658 _assert(this.isLazy(), "load() requires a lazy node"); 1659 // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" ); 1660 if (!forceReload && !this.isUndefined()) { 1661 return _getResolvedPromise(this); 1662 } 1663 if (this.isLoaded()) { 1664 this.resetLazy(); // also collapses 1665 } 1666 // This method is also called by setExpanded() and loadKeyPath(), so we 1667 // have to avoid recursion. 1668 source = this.tree._triggerNodeEvent("lazyLoad", this); 1669 if (source === false) { 1670 // #69 1671 return _getResolvedPromise(this); 1672 } 1673 _assert( 1674 typeof source !== "boolean", 1675 "lazyLoad event must return source in data.result" 1676 ); 1677 res = this.tree._callHook("nodeLoadChildren", this, source); 1678 if (wasExpanded) { 1679 this.expanded = true; 1680 res.always(function () { 1681 self.render(); 1682 }); 1683 } else { 1684 res.always(function () { 1685 self.renderStatus(); // fix expander icon to 'loaded' 1686 }); 1687 } 1688 return res; 1689 }, 1690 /** Expand all parents and optionally scroll into visible area as neccessary. 1691 * Promise is resolved, when lazy loading and animations are done. 1692 * @param {object} [opts] passed to `setExpanded()`. 1693 * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} 1694 * @returns {$.Promise} 1695 */ 1696 makeVisible: function (opts) { 1697 var i, 1698 self = this, 1699 deferreds = [], 1700 dfd = new $.Deferred(), 1701 parents = this.getParentList(false, false), 1702 len = parents.length, 1703 effects = !(opts && opts.noAnimation === true), 1704 scroll = !(opts && opts.scrollIntoView === false); 1705 1706 // Expand bottom-up, so only the top node is animated 1707 for (i = len - 1; i >= 0; i--) { 1708 // self.debug("pushexpand" + parents[i]); 1709 deferreds.push(parents[i].setExpanded(true, opts)); 1710 } 1711 $.when.apply($, deferreds).done(function () { 1712 // All expands have finished 1713 // self.debug("expand DONE", scroll); 1714 if (scroll) { 1715 self.scrollIntoView(effects).done(function () { 1716 // self.debug("scroll DONE"); 1717 dfd.resolve(); 1718 }); 1719 } else { 1720 dfd.resolve(); 1721 } 1722 }); 1723 return dfd.promise(); 1724 }, 1725 /** Move this node to targetNode. 1726 * @param {FancytreeNode} targetNode 1727 * @param {string} mode <pre> 1728 * 'child': append this node as last child of targetNode. 1729 * This is the default. To be compatble with the D'n'd 1730 * hitMode, we also accept 'over'. 1731 * 'firstChild': add this node as first child of targetNode. 1732 * 'before': add this node as sibling before targetNode. 1733 * 'after': add this node as sibling after targetNode.</pre> 1734 * @param {function} [map] optional callback(FancytreeNode) to allow modifcations 1735 */ 1736 moveTo: function (targetNode, mode, map) { 1737 if (mode === undefined || mode === "over") { 1738 mode = "child"; 1739 } else if (mode === "firstChild") { 1740 if (targetNode.children && targetNode.children.length) { 1741 mode = "before"; 1742 targetNode = targetNode.children[0]; 1743 } else { 1744 mode = "child"; 1745 } 1746 } 1747 var pos, 1748 tree = this.tree, 1749 prevParent = this.parent, 1750 targetParent = 1751 mode === "child" ? targetNode : targetNode.parent; 1752 1753 if (this === targetNode) { 1754 return; 1755 } else if (!this.parent) { 1756 $.error("Cannot move system root"); 1757 } else if (targetParent.isDescendantOf(this)) { 1758 $.error("Cannot move a node to its own descendant"); 1759 } 1760 if (targetParent !== prevParent) { 1761 prevParent.triggerModifyChild("remove", this); 1762 } 1763 // Unlink this node from current parent 1764 if (this.parent.children.length === 1) { 1765 if (this.parent === targetParent) { 1766 return; // #258 1767 } 1768 this.parent.children = this.parent.lazy ? [] : null; 1769 this.parent.expanded = false; 1770 } else { 1771 pos = $.inArray(this, this.parent.children); 1772 _assert(pos >= 0, "invalid source parent"); 1773 this.parent.children.splice(pos, 1); 1774 } 1775 // Remove from source DOM parent 1776 // if(this.parent.ul){ 1777 // this.parent.ul.removeChild(this.li); 1778 // } 1779 1780 // Insert this node to target parent's child list 1781 this.parent = targetParent; 1782 if (targetParent.hasChildren()) { 1783 switch (mode) { 1784 case "child": 1785 // Append to existing target children 1786 targetParent.children.push(this); 1787 break; 1788 case "before": 1789 // Insert this node before target node 1790 pos = $.inArray(targetNode, targetParent.children); 1791 _assert(pos >= 0, "invalid target parent"); 1792 targetParent.children.splice(pos, 0, this); 1793 break; 1794 case "after": 1795 // Insert this node after target node 1796 pos = $.inArray(targetNode, targetParent.children); 1797 _assert(pos >= 0, "invalid target parent"); 1798 targetParent.children.splice(pos + 1, 0, this); 1799 break; 1800 default: 1801 $.error("Invalid mode " + mode); 1802 } 1803 } else { 1804 targetParent.children = [this]; 1805 } 1806 // Parent has no <ul> tag yet: 1807 // if( !targetParent.ul ) { 1808 // // This is the parent's first child: create UL tag 1809 // // (Hidden, because it will be 1810 // targetParent.ul = document.createElement("ul"); 1811 // targetParent.ul.style.display = "none"; 1812 // targetParent.li.appendChild(targetParent.ul); 1813 // } 1814 // // Issue 319: Add to target DOM parent (only if node was already rendered(expanded)) 1815 // if(this.li){ 1816 // targetParent.ul.appendChild(this.li); 1817 // } 1818 1819 // Let caller modify the nodes 1820 if (map) { 1821 targetNode.visit(map, true); 1822 } 1823 if (targetParent === prevParent) { 1824 targetParent.triggerModifyChild("move", this); 1825 } else { 1826 // prevParent.triggerModifyChild("remove", this); 1827 targetParent.triggerModifyChild("add", this); 1828 } 1829 // Handle cross-tree moves 1830 if (tree !== targetNode.tree) { 1831 // Fix node.tree for all source nodes 1832 // _assert(false, "Cross-tree move is not yet implemented."); 1833 this.warn("Cross-tree moveTo is experimental!"); 1834 this.visit(function (n) { 1835 // TODO: fix selection state and activation, ... 1836 n.tree = targetNode.tree; 1837 }, true); 1838 } 1839 1840 // A collaposed node won't re-render children, so we have to remove it manually 1841 // if( !targetParent.expanded ){ 1842 // prevParent.ul.removeChild(this.li); 1843 // } 1844 tree._callHook("treeStructureChanged", tree, "moveTo"); 1845 1846 // Update HTML markup 1847 if (!prevParent.isDescendantOf(targetParent)) { 1848 prevParent.render(); 1849 } 1850 if ( 1851 !targetParent.isDescendantOf(prevParent) && 1852 targetParent !== prevParent 1853 ) { 1854 targetParent.render(); 1855 } 1856 // TODO: fix selection state 1857 // TODO: fix active state 1858 1859 /* 1860 var tree = this.tree; 1861 var opts = tree.options; 1862 var pers = tree.persistence; 1863 1864 // Always expand, if it's below minExpandLevel 1865 // tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel()); 1866 if ( opts.minExpandLevel >= ftnode.getLevel() ) { 1867 // tree.logDebug ("Force expand for %o", ftnode); 1868 this.bExpanded = true; 1869 } 1870 1871 // In multi-hier mode, update the parents selection state 1872 // DT issue #82: only if not initializing, because the children may not exist yet 1873 // if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing ) 1874 // ftnode._fixSelectionState(); 1875 1876 // In multi-hier mode, update the parents selection state 1877 if( ftnode.bSelected && opts.selectMode==3 ) { 1878 var p = this; 1879 while( p ) { 1880 if( !p.hasSubSel ) 1881 p._setSubSel(true); 1882 p = p.parent; 1883 } 1884 } 1885 // render this node and the new child 1886 if ( tree.bEnableUpdate ) 1887 this.render(); 1888 return ftnode; 1889 */ 1890 }, 1891 /** Set focus relative to this node and optionally activate. 1892 * 1893 * 'left' collapses the node if it is expanded, or move to the parent 1894 * otherwise. 1895 * 'right' expands the node if it is collapsed, or move to the first 1896 * child otherwise. 1897 * 1898 * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. 1899 * (Alternatively the keyCode that would normally trigger this move, 1900 * e.g. `$.ui.keyCode.LEFT` = 'left'. 1901 * @param {boolean} [activate=true] 1902 * @returns {$.Promise} 1903 */ 1904 navigate: function (where, activate) { 1905 var node, 1906 KC = $.ui.keyCode; 1907 1908 // Handle optional expand/collapse action for LEFT/RIGHT 1909 switch (where) { 1910 case "left": 1911 case KC.LEFT: 1912 if (this.expanded) { 1913 return this.setExpanded(false); 1914 } 1915 break; 1916 case "right": 1917 case KC.RIGHT: 1918 if (!this.expanded && (this.children || this.lazy)) { 1919 return this.setExpanded(); 1920 } 1921 break; 1922 } 1923 // Otherwise activate or focus the related node 1924 node = this.findRelatedNode(where); 1925 if (node) { 1926 // setFocus/setActive will scroll later (if autoScroll is specified) 1927 try { 1928 node.makeVisible({ scrollIntoView: false }); 1929 } catch (e) {} // #272 1930 if (activate === false) { 1931 node.setFocus(); 1932 return _getResolvedPromise(); 1933 } 1934 return node.setActive(); 1935 } 1936 this.warn("Could not find related node '" + where + "'."); 1937 return _getResolvedPromise(); 1938 }, 1939 /** 1940 * Remove this node (not allowed for system root). 1941 */ 1942 remove: function () { 1943 return this.parent.removeChild(this); 1944 }, 1945 /** 1946 * Remove childNode from list of direct children. 1947 * @param {FancytreeNode} childNode 1948 */ 1949 removeChild: function (childNode) { 1950 return this.tree._callHook("nodeRemoveChild", this, childNode); 1951 }, 1952 /** 1953 * Remove all child nodes and descendents. This converts the node into a leaf.<br> 1954 * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy() 1955 * in order to trigger lazyLoad on next expand. 1956 */ 1957 removeChildren: function () { 1958 return this.tree._callHook("nodeRemoveChildren", this); 1959 }, 1960 /** 1961 * Remove class from node's span tag and .extraClasses. 1962 * 1963 * @param {string} className class name 1964 * 1965 * @since 2.17 1966 */ 1967 removeClass: function (className) { 1968 return this.toggleClass(className, false); 1969 }, 1970 /** 1971 * This method renders and updates all HTML markup that is required 1972 * to display this node in its current state.<br> 1973 * Note: 1974 * <ul> 1975 * <li>It should only be neccessary to call this method after the node object 1976 * was modified by direct access to its properties, because the common 1977 * API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...) 1978 * already handle this. 1979 * <li> {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} 1980 * are implied. If changes are more local, calling only renderTitle() or 1981 * renderStatus() may be sufficient and faster. 1982 * </ul> 1983 * 1984 * @param {boolean} [force=false] re-render, even if html markup was already created 1985 * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed 1986 */ 1987 render: function (force, deep) { 1988 return this.tree._callHook("nodeRender", this, force, deep); 1989 }, 1990 /** Create HTML markup for the node's outer `<span>` (expander, checkbox, icon, and title). 1991 * Implies {@link FancytreeNode#renderStatus}. 1992 * @see Fancytree_Hooks#nodeRenderTitle 1993 */ 1994 renderTitle: function () { 1995 return this.tree._callHook("nodeRenderTitle", this); 1996 }, 1997 /** Update element's CSS classes according to node state. 1998 * @see Fancytree_Hooks#nodeRenderStatus 1999 */ 2000 renderStatus: function () { 2001 return this.tree._callHook("nodeRenderStatus", this); 2002 }, 2003 /** 2004 * (experimental) Replace this node with `source`. 2005 * (Currently only available for paging nodes.) 2006 * @param {NodeData[]} source List of child node definitions 2007 * @since 2.15 2008 */ 2009 replaceWith: function (source) { 2010 var res, 2011 parent = this.parent, 2012 pos = $.inArray(this, parent.children), 2013 self = this; 2014 2015 _assert( 2016 this.isPagingNode(), 2017 "replaceWith() currently requires a paging status node" 2018 ); 2019 2020 res = this.tree._callHook("nodeLoadChildren", this, source); 2021 res.done(function (data) { 2022 // New nodes are currently children of `this`. 2023 var children = self.children; 2024 // Prepend newly loaded child nodes to `this` 2025 // Move new children after self 2026 for (i = 0; i < children.length; i++) { 2027 children[i].parent = parent; 2028 } 2029 parent.children.splice.apply( 2030 parent.children, 2031 [pos + 1, 0].concat(children) 2032 ); 2033 2034 // Remove self 2035 self.children = null; 2036 self.remove(); 2037 // Redraw new nodes 2038 parent.render(); 2039 // TODO: set node.partload = false if this was tha last paging node? 2040 // parent.addPagingNode(false); 2041 }).fail(function () { 2042 self.setExpanded(); 2043 }); 2044 return res; 2045 // $.error("Not implemented: replaceWith()"); 2046 }, 2047 /** 2048 * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad 2049 * event is triggered on next expand. 2050 */ 2051 resetLazy: function () { 2052 this.removeChildren(); 2053 this.expanded = false; 2054 this.lazy = true; 2055 this.children = undefined; 2056 this.renderStatus(); 2057 }, 2058 /** Schedule activity for delayed execution (cancel any pending request). 2059 * scheduleAction('cancel') will only cancel a pending request (if any). 2060 * @param {string} mode 2061 * @param {number} ms 2062 */ 2063 scheduleAction: function (mode, ms) { 2064 if (this.tree.timer) { 2065 clearTimeout(this.tree.timer); 2066 this.tree.debug("clearTimeout(%o)", this.tree.timer); 2067 } 2068 this.tree.timer = null; 2069 var self = this; // required for closures 2070 switch (mode) { 2071 case "cancel": 2072 // Simply made sure that timer was cleared 2073 break; 2074 case "expand": 2075 this.tree.timer = setTimeout(function () { 2076 self.tree.debug("setTimeout: trigger expand"); 2077 self.setExpanded(true); 2078 }, ms); 2079 break; 2080 case "activate": 2081 this.tree.timer = setTimeout(function () { 2082 self.tree.debug("setTimeout: trigger activate"); 2083 self.setActive(true); 2084 }, ms); 2085 break; 2086 default: 2087 $.error("Invalid mode " + mode); 2088 } 2089 // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer); 2090 }, 2091 /** 2092 * 2093 * @param {boolean | PlainObject} [effects=false] animation options. 2094 * @param {object} [options=null] {topNode: null, effects: ..., parent: ...} this node will remain visible in 2095 * any case, even if `this` is outside the scroll pane. 2096 * @returns {$.Promise} 2097 */ 2098 scrollIntoView: function (effects, options) { 2099 if (options !== undefined && _isNode(options)) { 2100 throw Error( 2101 "scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead." 2102 ); 2103 } 2104 // The scroll parent is typically the plain tree's <UL> container. 2105 // For ext-table, we choose the nearest parent that has `position: relative` 2106 // and `overflow` set. 2107 // (This default can be overridden by the local or global `scrollParent` option.) 2108 var opts = $.extend( 2109 { 2110 effects: 2111 effects === true 2112 ? { duration: 200, queue: false } 2113 : effects, 2114 scrollOfs: this.tree.options.scrollOfs, 2115 scrollParent: this.tree.options.scrollParent, 2116 topNode: null, 2117 }, 2118 options 2119 ), 2120 $scrollParent = opts.scrollParent, 2121 $container = this.tree.$container, 2122 overflowY = $container.css("overflow-y"); 2123 2124 if (!$scrollParent) { 2125 if (this.tree.tbody) { 2126 $scrollParent = $container.scrollParent(); 2127 } else if (overflowY === "scroll" || overflowY === "auto") { 2128 $scrollParent = $container; 2129 } else { 2130 // #922 plain tree in a non-fixed-sized UL scrolls inside its parent 2131 $scrollParent = $container.scrollParent(); 2132 } 2133 } else if (!$scrollParent.jquery) { 2134 // Make sure we have a jQuery object 2135 $scrollParent = $($scrollParent); 2136 } 2137 if ( 2138 $scrollParent[0] === document || 2139 $scrollParent[0] === document.body 2140 ) { 2141 // `document` may be returned by $().scrollParent(), if nothing is found, 2142 // but would not work: (see #894) 2143 this.debug( 2144 "scrollIntoView(): normalizing scrollParent to 'window':", 2145 $scrollParent[0] 2146 ); 2147 $scrollParent = $(window); 2148 } 2149 // eslint-disable-next-line one-var 2150 var topNodeY, 2151 nodeY, 2152 horzScrollbarHeight, 2153 containerOffsetTop, 2154 dfd = new $.Deferred(), 2155 self = this, 2156 nodeHeight = $(this.span).height(), 2157 topOfs = opts.scrollOfs.top || 0, 2158 bottomOfs = opts.scrollOfs.bottom || 0, 2159 containerHeight = $scrollParent.height(), 2160 scrollTop = $scrollParent.scrollTop(), 2161 $animateTarget = $scrollParent, 2162 isParentWindow = $scrollParent[0] === window, 2163 topNode = opts.topNode || null, 2164 newScrollTop = null; 2165 2166 // this.debug("scrollIntoView(), scrollTop=" + scrollTop, opts.scrollOfs); 2167 // _assert($(this.span).is(":visible"), "scrollIntoView node is invisible"); // otherwise we cannot calc offsets 2168 if (this.isRootNode() || !this.isVisible()) { 2169 // We cannot calc offsets for hidden elements 2170 this.info("scrollIntoView(): node is invisible."); 2171 return _getResolvedPromise(); 2172 } 2173 if (isParentWindow) { 2174 nodeY = $(this.span).offset().top; 2175 topNodeY = 2176 topNode && topNode.span ? $(topNode.span).offset().top : 0; 2177 $animateTarget = $("html,body"); 2178 } else { 2179 _assert( 2180 $scrollParent[0] !== document && 2181 $scrollParent[0] !== document.body, 2182 "scrollParent should be a simple element or `window`, not document or body." 2183 ); 2184 2185 containerOffsetTop = $scrollParent.offset().top; 2186 nodeY = 2187 $(this.span).offset().top - containerOffsetTop + scrollTop; // relative to scroll parent 2188 topNodeY = topNode 2189 ? $(topNode.span).offset().top - 2190 containerOffsetTop + 2191 scrollTop 2192 : 0; 2193 horzScrollbarHeight = Math.max( 2194 0, 2195 $scrollParent.innerHeight() - $scrollParent[0].clientHeight 2196 ); 2197 containerHeight -= horzScrollbarHeight; 2198 } 2199 2200 // this.debug(" scrollIntoView(), nodeY=" + nodeY + ", containerHeight=" + containerHeight); 2201 if (nodeY < scrollTop + topOfs) { 2202 // Node is above visible container area 2203 newScrollTop = nodeY - topOfs; 2204 // this.debug(" scrollIntoView(), UPPER newScrollTop=" + newScrollTop); 2205 } else if ( 2206 nodeY + nodeHeight > 2207 scrollTop + containerHeight - bottomOfs 2208 ) { 2209 newScrollTop = nodeY + nodeHeight - containerHeight + bottomOfs; 2210 // this.debug(" scrollIntoView(), LOWER newScrollTop=" + newScrollTop); 2211 // If a topNode was passed, make sure that it is never scrolled 2212 // outside the upper border 2213 if (topNode) { 2214 _assert( 2215 topNode.isRootNode() || topNode.isVisible(), 2216 "topNode must be visible" 2217 ); 2218 if (topNodeY < newScrollTop) { 2219 newScrollTop = topNodeY - topOfs; 2220 // this.debug(" scrollIntoView(), TOP newScrollTop=" + newScrollTop); 2221 } 2222 } 2223 } 2224 2225 if (newScrollTop === null) { 2226 dfd.resolveWith(this); 2227 } else { 2228 // this.debug(" scrollIntoView(), SET newScrollTop=" + newScrollTop); 2229 if (opts.effects) { 2230 opts.effects.complete = function () { 2231 dfd.resolveWith(self); 2232 }; 2233 $animateTarget.stop(true).animate( 2234 { 2235 scrollTop: newScrollTop, 2236 }, 2237 opts.effects 2238 ); 2239 } else { 2240 $animateTarget[0].scrollTop = newScrollTop; 2241 dfd.resolveWith(this); 2242 } 2243 } 2244 return dfd.promise(); 2245 }, 2246 2247 /**Activate this node. 2248 * 2249 * The `cell` option requires the ext-table and ext-ariagrid extensions. 2250 * 2251 * @param {boolean} [flag=true] pass false to deactivate 2252 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false, cell: null} 2253 * @returns {$.Promise} 2254 */ 2255 setActive: function (flag, opts) { 2256 return this.tree._callHook("nodeSetActive", this, flag, opts); 2257 }, 2258 /**Expand or collapse this node. Promise is resolved, when lazy loading and animations are done. 2259 * @param {boolean} [flag=true] pass false to collapse 2260 * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false} 2261 * @returns {$.Promise} 2262 */ 2263 setExpanded: function (flag, opts) { 2264 return this.tree._callHook("nodeSetExpanded", this, flag, opts); 2265 }, 2266 /**Set keyboard focus to this node. 2267 * @param {boolean} [flag=true] pass false to blur 2268 * @see Fancytree#setFocus 2269 */ 2270 setFocus: function (flag) { 2271 return this.tree._callHook("nodeSetFocus", this, flag); 2272 }, 2273 /**Select this node, i.e. check the checkbox. 2274 * @param {boolean} [flag=true] pass false to deselect 2275 * @param {object} [opts] additional options. Defaults to {noEvents: false, p 2276 * propagateDown: null, propagateUp: null, callback: null } 2277 */ 2278 setSelected: function (flag, opts) { 2279 return this.tree._callHook("nodeSetSelected", this, flag, opts); 2280 }, 2281 /**Mark a lazy node as 'error', 'loading', 'nodata', or 'ok'. 2282 * @param {string} status 'error'|'loading'|'nodata'|'ok' 2283 * @param {string} [message] 2284 * @param {string} [details] 2285 */ 2286 setStatus: function (status, message, details) { 2287 return this.tree._callHook( 2288 "nodeSetStatus", 2289 this, 2290 status, 2291 message, 2292 details 2293 ); 2294 }, 2295 /**Rename this node. 2296 * @param {string} title 2297 */ 2298 setTitle: function (title) { 2299 this.title = title; 2300 this.renderTitle(); 2301 this.triggerModify("rename"); 2302 }, 2303 /**Sort child list by title. 2304 * @param {function} [cmp] custom compare function(a, b) that returns -1, 0, or 1 (defaults to sort by title). 2305 * @param {boolean} [deep=false] pass true to sort all descendant nodes 2306 */ 2307 sortChildren: function (cmp, deep) { 2308 var i, 2309 l, 2310 cl = this.children; 2311 2312 if (!cl) { 2313 return; 2314 } 2315 cmp = 2316 cmp || 2317 function (a, b) { 2318 var x = a.title.toLowerCase(), 2319 y = b.title.toLowerCase(); 2320 2321 // eslint-disable-next-line no-nested-ternary 2322 return x === y ? 0 : x > y ? 1 : -1; 2323 }; 2324 cl.sort(cmp); 2325 if (deep) { 2326 for (i = 0, l = cl.length; i < l; i++) { 2327 if (cl[i].children) { 2328 cl[i].sortChildren(cmp, "$norender$"); 2329 } 2330 } 2331 } 2332 if (deep !== "$norender$") { 2333 this.render(); 2334 } 2335 this.triggerModifyChild("sort"); 2336 }, 2337 /** Convert node (or whole branch) into a plain object. 2338 * 2339 * The result is compatible with node.addChildren(). 2340 * 2341 * @param {boolean} [recursive=false] include child nodes 2342 * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications. 2343 * Return `false` to ignore this node or `"skip"` to include this node without its children. 2344 * @returns {NodeData} 2345 */ 2346 toDict: function (recursive, callback) { 2347 var i, 2348 l, 2349 node, 2350 res, 2351 dict = {}, 2352 self = this; 2353 2354 $.each(NODE_ATTRS, function (i, a) { 2355 if (self[a] || self[a] === false) { 2356 dict[a] = self[a]; 2357 } 2358 }); 2359 if (!$.isEmptyObject(this.data)) { 2360 dict.data = $.extend({}, this.data); 2361 if ($.isEmptyObject(dict.data)) { 2362 delete dict.data; 2363 } 2364 } 2365 if (callback) { 2366 res = callback(dict, self); 2367 if (res === false) { 2368 return false; // Don't include this node nor its children 2369 } 2370 if (res === "skip") { 2371 recursive = false; // Include this node, but not the children 2372 } 2373 } 2374 if (recursive) { 2375 if (_isArray(this.children)) { 2376 dict.children = []; 2377 for (i = 0, l = this.children.length; i < l; i++) { 2378 node = this.children[i]; 2379 if (!node.isStatusNode()) { 2380 res = node.toDict(true, callback); 2381 if (res !== false) { 2382 dict.children.push(res); 2383 } 2384 } 2385 } 2386 } 2387 } 2388 return dict; 2389 }, 2390 /** 2391 * Set, clear, or toggle class of node's span tag and .extraClasses. 2392 * 2393 * @param {string} className class name (separate multiple classes by space) 2394 * @param {boolean} [flag] true/false to add/remove class. If omitted, class is toggled. 2395 * @returns {boolean} true if a class was added 2396 * 2397 * @since 2.17 2398 */ 2399 toggleClass: function (value, flag) { 2400 var className, 2401 hasClass, 2402 rnotwhite = /\S+/g, 2403 classNames = value.match(rnotwhite) || [], 2404 i = 0, 2405 wasAdded = false, 2406 statusElem = this[this.tree.statusClassPropName], 2407 curClasses = " " + (this.extraClasses || "") + " "; 2408 2409 // this.info("toggleClass('" + value + "', " + flag + ")", curClasses); 2410 // Modify DOM element directly if it already exists 2411 if (statusElem) { 2412 $(statusElem).toggleClass(value, flag); 2413 } 2414 // Modify node.extraClasses to make this change persistent 2415 // Toggle if flag was not passed 2416 while ((className = classNames[i++])) { 2417 hasClass = curClasses.indexOf(" " + className + " ") >= 0; 2418 flag = flag === undefined ? !hasClass : !!flag; 2419 if (flag) { 2420 if (!hasClass) { 2421 curClasses += className + " "; 2422 wasAdded = true; 2423 } 2424 } else { 2425 while (curClasses.indexOf(" " + className + " ") > -1) { 2426 curClasses = curClasses.replace( 2427 " " + className + " ", 2428 " " 2429 ); 2430 } 2431 } 2432 } 2433 this.extraClasses = _trim(curClasses); 2434 // this.info("-> toggleClass('" + value + "', " + flag + "): '" + this.extraClasses + "'"); 2435 return wasAdded; 2436 }, 2437 /** Flip expanded status. */ 2438 toggleExpanded: function () { 2439 return this.tree._callHook("nodeToggleExpanded", this); 2440 }, 2441 /** Flip selection status. */ 2442 toggleSelected: function () { 2443 return this.tree._callHook("nodeToggleSelected", this); 2444 }, 2445 toString: function () { 2446 return "FancytreeNode@" + this.key + "[title='" + this.title + "']"; 2447 // return "<FancytreeNode(#" + this.key + ", '" + this.title + "')>"; 2448 }, 2449 /** 2450 * Trigger `modifyChild` event on a parent to signal that a child was modified. 2451 * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... 2452 * @param {FancytreeNode} [childNode] 2453 * @param {object} [extra] 2454 */ 2455 triggerModifyChild: function (operation, childNode, extra) { 2456 var data, 2457 modifyChild = this.tree.options.modifyChild; 2458 2459 if (modifyChild) { 2460 if (childNode && childNode.parent !== this) { 2461 $.error( 2462 "childNode " + childNode + " is not a child of " + this 2463 ); 2464 } 2465 data = { 2466 node: this, 2467 tree: this.tree, 2468 operation: operation, 2469 childNode: childNode || null, 2470 }; 2471 if (extra) { 2472 $.extend(data, extra); 2473 } 2474 modifyChild({ type: "modifyChild" }, data); 2475 } 2476 }, 2477 /** 2478 * Trigger `modifyChild` event on node.parent(!). 2479 * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... 2480 * @param {object} [extra] 2481 */ 2482 triggerModify: function (operation, extra) { 2483 this.parent.triggerModifyChild(operation, this, extra); 2484 }, 2485 /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br> 2486 * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br> 2487 * Return false if iteration was stopped. 2488 * 2489 * @param {function} fn the callback function. 2490 * Return false to stop iteration, return "skip" to skip this node and 2491 * its children only. 2492 * @param {boolean} [includeSelf=false] 2493 * @returns {boolean} 2494 */ 2495 visit: function (fn, includeSelf) { 2496 var i, 2497 l, 2498 res = true, 2499 children = this.children; 2500 2501 if (includeSelf === true) { 2502 res = fn(this); 2503 if (res === false || res === "skip") { 2504 return res; 2505 } 2506 } 2507 if (children) { 2508 for (i = 0, l = children.length; i < l; i++) { 2509 res = children[i].visit(fn, true); 2510 if (res === false) { 2511 break; 2512 } 2513 } 2514 } 2515 return res; 2516 }, 2517 /** Call fn(node) for all child nodes and recursively load lazy children.<br> 2518 * <b>Note:</b> If you need this method, you probably should consider to review 2519 * your architecture! Recursivley loading nodes is a perfect way for lazy 2520 * programmers to flood the server with requests ;-) 2521 * 2522 * @param {function} [fn] optional callback function. 2523 * Return false to stop iteration, return "skip" to skip this node and 2524 * its children only. 2525 * @param {boolean} [includeSelf=false] 2526 * @returns {$.Promise} 2527 * @since 2.4 2528 */ 2529 visitAndLoad: function (fn, includeSelf, _recursion) { 2530 var dfd, 2531 res, 2532 loaders, 2533 node = this; 2534 2535 // node.debug("visitAndLoad"); 2536 if (fn && includeSelf === true) { 2537 res = fn(node); 2538 if (res === false || res === "skip") { 2539 return _recursion ? res : _getResolvedPromise(); 2540 } 2541 } 2542 if (!node.children && !node.lazy) { 2543 return _getResolvedPromise(); 2544 } 2545 dfd = new $.Deferred(); 2546 loaders = []; 2547 // node.debug("load()..."); 2548 node.load().done(function () { 2549 // node.debug("load()... done."); 2550 for (var i = 0, l = node.children.length; i < l; i++) { 2551 res = node.children[i].visitAndLoad(fn, true, true); 2552 if (res === false) { 2553 dfd.reject(); 2554 break; 2555 } else if (res !== "skip") { 2556 loaders.push(res); // Add promise to the list 2557 } 2558 } 2559 $.when.apply(this, loaders).then(function () { 2560 dfd.resolve(); 2561 }); 2562 }); 2563 return dfd.promise(); 2564 }, 2565 /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br> 2566 * Stop iteration, if fn() returns false.<br> 2567 * Return false if iteration was stopped. 2568 * 2569 * @param {function} fn the callback function. 2570 * Return false to stop iteration, return "skip" to skip this node and children only. 2571 * @param {boolean} [includeSelf=false] 2572 * @returns {boolean} 2573 */ 2574 visitParents: function (fn, includeSelf) { 2575 // Visit parent nodes (bottom up) 2576 if (includeSelf && fn(this) === false) { 2577 return false; 2578 } 2579 var p = this.parent; 2580 while (p) { 2581 if (fn(p) === false) { 2582 return false; 2583 } 2584 p = p.parent; 2585 } 2586 return true; 2587 }, 2588 /** Call fn(node) for all sibling nodes.<br> 2589 * Stop iteration, if fn() returns false.<br> 2590 * Return false if iteration was stopped. 2591 * 2592 * @param {function} fn the callback function. 2593 * Return false to stop iteration. 2594 * @param {boolean} [includeSelf=false] 2595 * @returns {boolean} 2596 */ 2597 visitSiblings: function (fn, includeSelf) { 2598 var i, 2599 l, 2600 n, 2601 ac = this.parent.children; 2602 2603 for (i = 0, l = ac.length; i < l; i++) { 2604 n = ac[i]; 2605 if (includeSelf || n !== this) { 2606 if (fn(n) === false) { 2607 return false; 2608 } 2609 } 2610 } 2611 return true; 2612 }, 2613 /** Write warning to browser console if debugLevel >= 2 (prepending node info) 2614 * 2615 * @param {*} msg string or object or array of such 2616 */ 2617 warn: function (msg) { 2618 if (this.tree.options.debugLevel >= 2) { 2619 Array.prototype.unshift.call(arguments, this.toString()); 2620 consoleApply("warn", arguments); 2621 } 2622 }, 2623 }; 2624 2625 /****************************************************************************** 2626 * Fancytree 2627 */ 2628 /** 2629 * Construct a new tree object. 2630 * 2631 * @class Fancytree 2632 * @classdesc The controller behind a fancytree. 2633 * This class also contains 'hook methods': see {@link Fancytree_Hooks}. 2634 * 2635 * @param {Widget} widget 2636 * 2637 * @property {string} _id Automatically generated unique tree instance ID, e.g. "1". 2638 * @property {string} _ns Automatically generated unique tree namespace, e.g. ".fancytree-1". 2639 * @property {FancytreeNode} activeNode Currently active node or null. 2640 * @property {string} ariaPropName Property name of FancytreeNode that contains the element which will receive the aria attributes. 2641 * Typically "li", but "tr" for table extension. 2642 * @property {jQueryObject} $container Outer `<ul>` element (or `<table>` element for ext-table). 2643 * @property {jQueryObject} $div A jQuery object containing the element used to instantiate the tree widget (`widget.element`) 2644 * @property {object|array} columns Recommended place to store shared column meta data. @since 2.27 2645 * @property {object} data Metadata, i.e. properties that may be passed to `source` in addition to a children array. 2646 * @property {object} ext Hash of all active plugin instances. 2647 * @property {FancytreeNode} focusNode Currently focused node or null. 2648 * @property {FancytreeNode} lastSelectedNode Used to implement selectMode 1 (single select) 2649 * @property {string} nodeContainerAttrName Property name of FancytreeNode that contains the outer element of single nodes. 2650 * Typically "li", but "tr" for table extension. 2651 * @property {FancytreeOptions} options Current options, i.e. default options + options passed to constructor. 2652 * @property {FancytreeNode} rootNode Invisible system root node. 2653 * @property {string} statusClassPropName Property name of FancytreeNode that contains the element which will receive the status classes. 2654 * Typically "span", but "tr" for table extension. 2655 * @property {object} types Map for shared type specific meta data, used with node.type attribute. @since 2.27 2656 * @property {object} viewport See ext-vieport. @since v2.31 2657 * @property {object} widget Base widget instance. 2658 */ 2659 function Fancytree(widget) { 2660 this.widget = widget; 2661 this.$div = widget.element; 2662 this.options = widget.options; 2663 if (this.options) { 2664 if (this.options.lazyload !== undefined) { 2665 $.error( 2666 "The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead." 2667 ); 2668 } 2669 if (this.options.loaderror !== undefined) { 2670 $.error( 2671 "The 'loaderror' event was renamed since 2014-07-03. Use 'loadError' (with uppercase E) instead." 2672 ); 2673 } 2674 if (this.options.fx !== undefined) { 2675 $.error( 2676 "The 'fx' option was replaced by 'toggleEffect' since 2014-11-30." 2677 ); 2678 } 2679 if (this.options.removeNode !== undefined) { 2680 $.error( 2681 "The 'removeNode' event was replaced by 'modifyChild' since 2.20 (2016-09-10)." 2682 ); 2683 } 2684 } 2685 this.ext = {}; // Active extension instances 2686 this.types = {}; 2687 this.columns = {}; 2688 // allow to init tree.data.foo from <div data-foo=''> 2689 this.data = _getElementDataAsDict(this.$div); 2690 // TODO: use widget.uuid instead? 2691 this._id = "" + (this.options.treeId || $.ui.fancytree._nextId++); 2692 // TODO: use widget.eventNamespace instead? 2693 this._ns = ".fancytree-" + this._id; // append for namespaced events 2694 this.activeNode = null; 2695 this.focusNode = null; 2696 this._hasFocus = null; 2697 this._tempCache = {}; 2698 this._lastMousedownNode = null; 2699 this._enableUpdate = true; 2700 this.lastSelectedNode = null; 2701 this.systemFocusElement = null; 2702 this.lastQuicksearchTerm = ""; 2703 this.lastQuicksearchTime = 0; 2704 this.viewport = null; // ext-grid 2705 2706 this.statusClassPropName = "span"; 2707 this.ariaPropName = "li"; 2708 this.nodeContainerAttrName = "li"; 2709 2710 // Remove previous markup if any 2711 this.$div.find(">ul.fancytree-container").remove(); 2712 2713 // Create a node without parent. 2714 var fakeParent = { tree: this }, 2715 $ul; 2716 this.rootNode = new FancytreeNode(fakeParent, { 2717 title: "root", 2718 key: "root_" + this._id, 2719 children: null, 2720 expanded: true, 2721 }); 2722 this.rootNode.parent = null; 2723 2724 // Create root markup 2725 $ul = $("<ul>", { 2726 id: "ft-id-" + this._id, 2727 class: "ui-fancytree fancytree-container fancytree-plain", 2728 }).appendTo(this.$div); 2729 this.$container = $ul; 2730 this.rootNode.ul = $ul[0]; 2731 2732 if (this.options.debugLevel == null) { 2733 this.options.debugLevel = FT.debugLevel; 2734 } 2735 // // Add container to the TAB chain 2736 // // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant 2737 // // #577: Allow to set tabindex to "0", "-1" and "" 2738 // this.$container.attr("tabindex", this.options.tabindex); 2739 2740 // if( this.options.rtl ) { 2741 // this.$container.attr("DIR", "RTL").addClass("fancytree-rtl"); 2742 // // }else{ 2743 // // this.$container.attr("DIR", null).removeClass("fancytree-rtl"); 2744 // } 2745 // if(this.options.aria){ 2746 // this.$container.attr("role", "tree"); 2747 // if( this.options.selectMode !== 1 ) { 2748 // this.$container.attr("aria-multiselectable", true); 2749 // } 2750 // } 2751 } 2752 2753 Fancytree.prototype = /** @lends Fancytree# */ { 2754 /* Return a context object that can be re-used for _callHook(). 2755 * @param {Fancytree | FancytreeNode | EventData} obj 2756 * @param {Event} originalEvent 2757 * @param {Object} extra 2758 * @returns {EventData} 2759 */ 2760 _makeHookContext: function (obj, originalEvent, extra) { 2761 var ctx, tree; 2762 if (obj.node !== undefined) { 2763 // obj is already a context object 2764 if (originalEvent && obj.originalEvent !== originalEvent) { 2765 $.error("invalid args"); 2766 } 2767 ctx = obj; 2768 } else if (obj.tree) { 2769 // obj is a FancytreeNode 2770 tree = obj.tree; 2771 ctx = { 2772 node: obj, 2773 tree: tree, 2774 widget: tree.widget, 2775 options: tree.widget.options, 2776 originalEvent: originalEvent, 2777 typeInfo: tree.types[obj.type] || {}, 2778 }; 2779 } else if (obj.widget) { 2780 // obj is a Fancytree 2781 ctx = { 2782 node: null, 2783 tree: obj, 2784 widget: obj.widget, 2785 options: obj.widget.options, 2786 originalEvent: originalEvent, 2787 }; 2788 } else { 2789 $.error("invalid args"); 2790 } 2791 if (extra) { 2792 $.extend(ctx, extra); 2793 } 2794 return ctx; 2795 }, 2796 /* Trigger a hook function: funcName(ctx, [...]). 2797 * 2798 * @param {string} funcName 2799 * @param {Fancytree|FancytreeNode|EventData} contextObject 2800 * @param {any} [_extraArgs] optional additional arguments 2801 * @returns {any} 2802 */ 2803 _callHook: function (funcName, contextObject, _extraArgs) { 2804 var ctx = this._makeHookContext(contextObject), 2805 fn = this[funcName], 2806 args = Array.prototype.slice.call(arguments, 2); 2807 if (!_isFunction(fn)) { 2808 $.error("_callHook('" + funcName + "') is not a function"); 2809 } 2810 args.unshift(ctx); 2811 // this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args); 2812 return fn.apply(this, args); 2813 }, 2814 _setExpiringValue: function (key, value, ms) { 2815 this._tempCache[key] = { 2816 value: value, 2817 expire: Date.now() + (+ms || 50), 2818 }; 2819 }, 2820 _getExpiringValue: function (key) { 2821 var entry = this._tempCache[key]; 2822 if (entry && entry.expire > Date.now()) { 2823 return entry.value; 2824 } 2825 delete this._tempCache[key]; 2826 return null; 2827 }, 2828 /* Check if this tree has extension `name` enabled. 2829 * 2830 * @param {string} name name of the required extension 2831 */ 2832 _usesExtension: function (name) { 2833 return $.inArray(name, this.options.extensions) >= 0; 2834 }, 2835 /* Check if current extensions dependencies are met and throw an error if not. 2836 * 2837 * This method may be called inside the `treeInit` hook for custom extensions. 2838 * 2839 * @param {string} name name of the required extension 2840 * @param {boolean} [required=true] pass `false` if the extension is optional, but we want to check for order if it is present 2841 * @param {boolean} [before] `true` if `name` must be included before this, `false` otherwise (use `null` if order doesn't matter) 2842 * @param {string} [message] optional error message (defaults to a descriptve error message) 2843 */ 2844 _requireExtension: function (name, required, before, message) { 2845 if (before != null) { 2846 before = !!before; 2847 } 2848 var thisName = this._local.name, 2849 extList = this.options.extensions, 2850 isBefore = 2851 $.inArray(name, extList) < $.inArray(thisName, extList), 2852 isMissing = required && this.ext[name] == null, 2853 badOrder = !isMissing && before != null && before !== isBefore; 2854 2855 _assert( 2856 thisName && thisName !== name, 2857 "invalid or same name '" + thisName + "' (require yourself?)" 2858 ); 2859 2860 if (isMissing || badOrder) { 2861 if (!message) { 2862 if (isMissing || required) { 2863 message = 2864 "'" + 2865 thisName + 2866 "' extension requires '" + 2867 name + 2868 "'"; 2869 if (badOrder) { 2870 message += 2871 " to be registered " + 2872 (before ? "before" : "after") + 2873 " itself"; 2874 } 2875 } else { 2876 message = 2877 "If used together, `" + 2878 name + 2879 "` must be registered " + 2880 (before ? "before" : "after") + 2881 " `" + 2882 thisName + 2883 "`"; 2884 } 2885 } 2886 $.error(message); 2887 return false; 2888 } 2889 return true; 2890 }, 2891 /** Activate node with a given key and fire focus and activate events. 2892 * 2893 * A previously activated node will be deactivated. 2894 * If activeVisible option is set, all parents will be expanded as necessary. 2895 * Pass key = false, to deactivate the current node only. 2896 * @param {string} key 2897 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false} 2898 * @returns {FancytreeNode} activated node (null, if not found) 2899 */ 2900 activateKey: function (key, opts) { 2901 var node = this.getNodeByKey(key); 2902 if (node) { 2903 node.setActive(true, opts); 2904 } else if (this.activeNode) { 2905 this.activeNode.setActive(false, opts); 2906 } 2907 return node; 2908 }, 2909 /** (experimental) Add child status nodes that indicate 'More...', .... 2910 * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes. 2911 * @param {string} [mode='append'] 'child'|firstChild' 2912 * @since 2.15 2913 */ 2914 addPagingNode: function (node, mode) { 2915 return this.rootNode.addPagingNode(node, mode); 2916 }, 2917 /** 2918 * (experimental) Apply a modification (or navigation) operation. 2919 * 2920 * Valid commands: 2921 * - 'moveUp', 'moveDown' 2922 * - 'indent', 'outdent' 2923 * - 'remove' 2924 * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension) 2925 * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard') 2926 * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate 2927 * 2928 * @param {string} cmd 2929 * @param {FancytreeNode} [node=active_node] 2930 * @param {object} [opts] Currently unused 2931 * 2932 * @since 2.32 2933 */ 2934 applyCommand: function (cmd, node, opts_) { 2935 var // clipboard, 2936 refNode; 2937 // opts = $.extend( 2938 // { setActive: true, clipboard: CLIPBOARD }, 2939 // opts_ 2940 // ); 2941 2942 node = node || this.getActiveNode(); 2943 // clipboard = opts.clipboard; 2944 2945 switch (cmd) { 2946 // Sorting and indentation: 2947 case "moveUp": 2948 refNode = node.getPrevSibling(); 2949 if (refNode) { 2950 node.moveTo(refNode, "before"); 2951 node.setActive(); 2952 } 2953 break; 2954 case "moveDown": 2955 refNode = node.getNextSibling(); 2956 if (refNode) { 2957 node.moveTo(refNode, "after"); 2958 node.setActive(); 2959 } 2960 break; 2961 case "indent": 2962 refNode = node.getPrevSibling(); 2963 if (refNode) { 2964 node.moveTo(refNode, "child"); 2965 refNode.setExpanded(); 2966 node.setActive(); 2967 } 2968 break; 2969 case "outdent": 2970 if (!node.isTopLevel()) { 2971 node.moveTo(node.getParent(), "after"); 2972 node.setActive(); 2973 } 2974 break; 2975 // Remove: 2976 case "remove": 2977 refNode = node.getPrevSibling() || node.getParent(); 2978 node.remove(); 2979 if (refNode) { 2980 refNode.setActive(); 2981 } 2982 break; 2983 // Add, edit (requires ext-edit): 2984 case "addChild": 2985 node.editCreateNode("child", ""); 2986 break; 2987 case "addSibling": 2988 node.editCreateNode("after", ""); 2989 break; 2990 case "rename": 2991 node.editStart(); 2992 break; 2993 // Simple clipboard simulation: 2994 // case "cut": 2995 // clipboard = { mode: cmd, data: node }; 2996 // break; 2997 // case "copy": 2998 // clipboard = { 2999 // mode: cmd, 3000 // data: node.toDict(function(d, n) { 3001 // delete d.key; 3002 // }), 3003 // }; 3004 // break; 3005 // case "clear": 3006 // clipboard = null; 3007 // break; 3008 // case "paste": 3009 // if (clipboard.mode === "cut") { 3010 // // refNode = node.getPrevSibling(); 3011 // clipboard.data.moveTo(node, "child"); 3012 // clipboard.data.setActive(); 3013 // } else if (clipboard.mode === "copy") { 3014 // node.addChildren(clipboard.data).setActive(); 3015 // } 3016 // break; 3017 // Navigation commands: 3018 case "down": 3019 case "first": 3020 case "last": 3021 case "left": 3022 case "parent": 3023 case "right": 3024 case "up": 3025 return node.navigate(cmd); 3026 default: 3027 $.error("Unhandled command: '" + cmd + "'"); 3028 } 3029 }, 3030 /** (experimental) Modify existing data model. 3031 * 3032 * @param {Array} patchList array of [key, NodePatch] arrays 3033 * @returns {$.Promise} resolved, when all patches have been applied 3034 * @see TreePatch 3035 */ 3036 applyPatch: function (patchList) { 3037 var dfd, 3038 i, 3039 p2, 3040 key, 3041 patch, 3042 node, 3043 patchCount = patchList.length, 3044 deferredList = []; 3045 3046 for (i = 0; i < patchCount; i++) { 3047 p2 = patchList[i]; 3048 _assert( 3049 p2.length === 2, 3050 "patchList must be an array of length-2-arrays" 3051 ); 3052 key = p2[0]; 3053 patch = p2[1]; 3054 node = key === null ? this.rootNode : this.getNodeByKey(key); 3055 if (node) { 3056 dfd = new $.Deferred(); 3057 deferredList.push(dfd); 3058 node.applyPatch(patch).always(_makeResolveFunc(dfd, node)); 3059 } else { 3060 this.warn("could not find node with key '" + key + "'"); 3061 } 3062 } 3063 // Return a promise that is resolved, when ALL patches were applied 3064 return $.when.apply($, deferredList).promise(); 3065 }, 3066 /* TODO: implement in dnd extension 3067 cancelDrag: function() { 3068 var dd = $.ui.ddmanager.current; 3069 if(dd){ 3070 dd.cancel(); 3071 } 3072 }, 3073 */ 3074 /** Remove all nodes. 3075 * @since 2.14 3076 */ 3077 clear: function (source) { 3078 this._callHook("treeClear", this); 3079 }, 3080 /** Return the number of nodes. 3081 * @returns {integer} 3082 */ 3083 count: function () { 3084 return this.rootNode.countChildren(); 3085 }, 3086 /** Write to browser console if debugLevel >= 4 (prepending tree name) 3087 * 3088 * @param {*} msg string or object or array of such 3089 */ 3090 debug: function (msg) { 3091 if (this.options.debugLevel >= 4) { 3092 Array.prototype.unshift.call(arguments, this.toString()); 3093 consoleApply("log", arguments); 3094 } 3095 }, 3096 /** Destroy this widget, restore previous markup and cleanup resources. 3097 * 3098 * @since 2.34 3099 */ 3100 destroy: function () { 3101 this.widget.destroy(); 3102 }, 3103 /** Enable (or disable) the tree control. 3104 * 3105 * @param {boolean} [flag=true] pass false to disable 3106 * @since 2.30 3107 */ 3108 enable: function (flag) { 3109 if (flag === false) { 3110 this.widget.disable(); 3111 } else { 3112 this.widget.enable(); 3113 } 3114 }, 3115 /** Temporarily suppress rendering to improve performance on bulk-updates. 3116 * 3117 * @param {boolean} flag 3118 * @returns {boolean} previous status 3119 * @since 2.19 3120 */ 3121 enableUpdate: function (flag) { 3122 flag = flag !== false; 3123 if (!!this._enableUpdate === !!flag) { 3124 return flag; 3125 } 3126 this._enableUpdate = flag; 3127 if (flag) { 3128 this.debug("enableUpdate(true): redraw "); //, this._dirtyRoots); 3129 this._callHook("treeStructureChanged", this, "enableUpdate"); 3130 this.render(); 3131 } else { 3132 // this._dirtyRoots = null; 3133 this.debug("enableUpdate(false)..."); 3134 } 3135 return !flag; // return previous value 3136 }, 3137 /** Write error to browser console if debugLevel >= 1 (prepending tree info) 3138 * 3139 * @param {*} msg string or object or array of such 3140 */ 3141 error: function (msg) { 3142 if (this.options.debugLevel >= 1) { 3143 Array.prototype.unshift.call(arguments, this.toString()); 3144 consoleApply("error", arguments); 3145 } 3146 }, 3147 /** Expand (or collapse) all parent nodes. 3148 * 3149 * This convenience method uses `tree.visit()` and `tree.setExpanded()` 3150 * internally. 3151 * 3152 * @param {boolean} [flag=true] pass false to collapse 3153 * @param {object} [opts] passed to setExpanded() 3154 * @since 2.30 3155 */ 3156 expandAll: function (flag, opts) { 3157 var prev = this.enableUpdate(false); 3158 3159 flag = flag !== false; 3160 this.visit(function (node) { 3161 if ( 3162 node.hasChildren() !== false && 3163 node.isExpanded() !== flag 3164 ) { 3165 node.setExpanded(flag, opts); 3166 } 3167 }); 3168 this.enableUpdate(prev); 3169 }, 3170 /**Find all nodes that matches condition. 3171 * 3172 * @param {string | function(node)} match title string to search for, or a 3173 * callback function that returns `true` if a node is matched. 3174 * @returns {FancytreeNode[]} array of nodes (may be empty) 3175 * @see FancytreeNode#findAll 3176 * @since 2.12 3177 */ 3178 findAll: function (match) { 3179 return this.rootNode.findAll(match); 3180 }, 3181 /**Find first node that matches condition. 3182 * 3183 * @param {string | function(node)} match title string to search for, or a 3184 * callback function that returns `true` if a node is matched. 3185 * @returns {FancytreeNode} matching node or null 3186 * @see FancytreeNode#findFirst 3187 * @since 2.12 3188 */ 3189 findFirst: function (match) { 3190 return this.rootNode.findFirst(match); 3191 }, 3192 /** Find the next visible node that starts with `match`, starting at `startNode` 3193 * and wrap-around at the end. 3194 * 3195 * @param {string|function} match 3196 * @param {FancytreeNode} [startNode] defaults to first node 3197 * @returns {FancytreeNode} matching node or null 3198 */ 3199 findNextNode: function (match, startNode) { 3200 //, visibleOnly) { 3201 var res = null, 3202 firstNode = this.getFirstChild(); 3203 3204 match = 3205 typeof match === "string" 3206 ? _makeNodeTitleStartMatcher(match) 3207 : match; 3208 startNode = startNode || firstNode; 3209 3210 function _checkNode(n) { 3211 // console.log("_check " + n) 3212 if (match(n)) { 3213 res = n; 3214 } 3215 if (res || n === startNode) { 3216 return false; 3217 } 3218 } 3219 this.visitRows(_checkNode, { 3220 start: startNode, 3221 includeSelf: false, 3222 }); 3223 // Wrap around search 3224 if (!res && startNode !== firstNode) { 3225 this.visitRows(_checkNode, { 3226 start: firstNode, 3227 includeSelf: true, 3228 }); 3229 } 3230 return res; 3231 }, 3232 /** Find a node relative to another node. 3233 * 3234 * @param {FancytreeNode} node 3235 * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. 3236 * (Alternatively the keyCode that would normally trigger this move, 3237 * e.g. `$.ui.keyCode.LEFT` = 'left'. 3238 * @param {boolean} [includeHidden=false] Not yet implemented 3239 * @returns {FancytreeNode|null} 3240 * @since v2.31 3241 */ 3242 findRelatedNode: function (node, where, includeHidden) { 3243 var res = null, 3244 KC = $.ui.keyCode; 3245 3246 switch (where) { 3247 case "parent": 3248 case KC.BACKSPACE: 3249 if (node.parent && node.parent.parent) { 3250 res = node.parent; 3251 } 3252 break; 3253 case "first": 3254 case KC.HOME: 3255 // First visible node 3256 this.visit(function (n) { 3257 if (n.isVisible()) { 3258 res = n; 3259 return false; 3260 } 3261 }); 3262 break; 3263 case "last": 3264 case KC.END: 3265 this.visit(function (n) { 3266 // last visible node 3267 if (n.isVisible()) { 3268 res = n; 3269 } 3270 }); 3271 break; 3272 case "left": 3273 case KC.LEFT: 3274 if (node.expanded) { 3275 node.setExpanded(false); 3276 } else if (node.parent && node.parent.parent) { 3277 res = node.parent; 3278 } 3279 break; 3280 case "right": 3281 case KC.RIGHT: 3282 if (!node.expanded && (node.children || node.lazy)) { 3283 node.setExpanded(); 3284 res = node; 3285 } else if (node.children && node.children.length) { 3286 res = node.children[0]; 3287 } 3288 break; 3289 case "up": 3290 case KC.UP: 3291 this.visitRows( 3292 function (n) { 3293 res = n; 3294 return false; 3295 }, 3296 { start: node, reverse: true, includeSelf: false } 3297 ); 3298 break; 3299 case "down": 3300 case KC.DOWN: 3301 this.visitRows( 3302 function (n) { 3303 res = n; 3304 return false; 3305 }, 3306 { start: node, includeSelf: false } 3307 ); 3308 break; 3309 default: 3310 this.tree.warn("Unknown relation '" + where + "'."); 3311 } 3312 return res; 3313 }, 3314 // TODO: fromDict 3315 /** 3316 * Generate INPUT elements that can be submitted with html forms. 3317 * 3318 * In selectMode 3 only the topmost selected nodes are considered, unless 3319 * `opts.stopOnParents: false` is passed. 3320 * 3321 * @example 3322 * // Generate input elements for active and selected nodes 3323 * tree.generateFormElements(); 3324 * // Generate input elements selected nodes, using a custom `name` attribute 3325 * tree.generateFormElements("cust_sel", false); 3326 * // Generate input elements using a custom filter 3327 * tree.generateFormElements(true, true, { filter: function(node) { 3328 * return node.isSelected() && node.data.yes; 3329 * }}); 3330 * 3331 * @param {boolean | string} [selected=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID[]') 3332 * @param {boolean | string} [active=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID_active') 3333 * @param {object} [opts] default { filter: null, stopOnParents: true } 3334 */ 3335 generateFormElements: function (selected, active, opts) { 3336 opts = opts || {}; 3337 3338 var nodeList, 3339 selectedName = 3340 typeof selected === "string" 3341 ? selected 3342 : "ft_" + this._id + "[]", 3343 activeName = 3344 typeof active === "string" 3345 ? active 3346 : "ft_" + this._id + "_active", 3347 id = "fancytree_result_" + this._id, 3348 $result = $("#" + id), 3349 stopOnParents = 3350 this.options.selectMode === 3 && 3351 opts.stopOnParents !== false; 3352 3353 if ($result.length) { 3354 $result.empty(); 3355 } else { 3356 $result = $("<div>", { 3357 id: id, 3358 }) 3359 .hide() 3360 .insertAfter(this.$container); 3361 } 3362 if (active !== false && this.activeNode) { 3363 $result.append( 3364 $("<input>", { 3365 type: "radio", 3366 name: activeName, 3367 value: this.activeNode.key, 3368 checked: true, 3369 }) 3370 ); 3371 } 3372 function _appender(node) { 3373 $result.append( 3374 $("<input>", { 3375 type: "checkbox", 3376 name: selectedName, 3377 value: node.key, 3378 checked: true, 3379 }) 3380 ); 3381 } 3382 if (opts.filter) { 3383 this.visit(function (node) { 3384 var res = opts.filter(node); 3385 if (res === "skip") { 3386 return res; 3387 } 3388 if (res !== false) { 3389 _appender(node); 3390 } 3391 }); 3392 } else if (selected !== false) { 3393 nodeList = this.getSelectedNodes(stopOnParents); 3394 $.each(nodeList, function (idx, node) { 3395 _appender(node); 3396 }); 3397 } 3398 }, 3399 /** 3400 * Return the currently active node or null. 3401 * @returns {FancytreeNode} 3402 */ 3403 getActiveNode: function () { 3404 return this.activeNode; 3405 }, 3406 /** Return the first top level node if any (not the invisible root node). 3407 * @returns {FancytreeNode | null} 3408 */ 3409 getFirstChild: function () { 3410 return this.rootNode.getFirstChild(); 3411 }, 3412 /** 3413 * Return node that has keyboard focus or null. 3414 * @returns {FancytreeNode} 3415 */ 3416 getFocusNode: function () { 3417 return this.focusNode; 3418 }, 3419 /** 3420 * Return current option value. 3421 * (Note: this is the preferred variant of `$().fancytree("option", "KEY")`) 3422 * 3423 * @param {string} name option name (may contain '.') 3424 * @returns {any} 3425 */ 3426 getOption: function (optionName) { 3427 return this.widget.option(optionName); 3428 }, 3429 /** 3430 * Return node with a given key or null if not found. 3431 * 3432 * @param {string} key 3433 * @param {FancytreeNode} [searchRoot] only search below this node 3434 * @returns {FancytreeNode | null} 3435 */ 3436 getNodeByKey: function (key, searchRoot) { 3437 // Search the DOM by element ID (assuming this is faster than traversing all nodes). 3438 var el, match; 3439 // TODO: use tree.keyMap if available 3440 // TODO: check opts.generateIds === true 3441 if (!searchRoot) { 3442 el = document.getElementById(this.options.idPrefix + key); 3443 if (el) { 3444 return el.ftnode ? el.ftnode : null; 3445 } 3446 } 3447 // Not found in the DOM, but still may be in an unrendered part of tree 3448 searchRoot = searchRoot || this.rootNode; 3449 match = null; 3450 key = "" + key; // Convert to string (#1005) 3451 searchRoot.visit(function (node) { 3452 if (node.key === key) { 3453 match = node; 3454 return false; // Stop iteration 3455 } 3456 }, true); 3457 return match; 3458 }, 3459 /** Return the invisible system root node. 3460 * @returns {FancytreeNode} 3461 */ 3462 getRootNode: function () { 3463 return this.rootNode; 3464 }, 3465 /** 3466 * Return an array of selected nodes. 3467 * 3468 * Note: you cannot send this result via Ajax directly. Instead the 3469 * node object need to be converted to plain objects, for example 3470 * by using `$.map()` and `node.toDict()`. 3471 * @param {boolean} [stopOnParents=false] only return the topmost selected 3472 * node (useful with selectMode 3) 3473 * @returns {FancytreeNode[]} 3474 */ 3475 getSelectedNodes: function (stopOnParents) { 3476 return this.rootNode.getSelectedNodes(stopOnParents); 3477 }, 3478 /** Return true if the tree control has keyboard focus 3479 * @returns {boolean} 3480 */ 3481 hasFocus: function () { 3482 // var ae = document.activeElement, 3483 // hasFocus = !!( 3484 // ae && $(ae).closest(".fancytree-container").length 3485 // ); 3486 3487 // if (hasFocus !== !!this._hasFocus) { 3488 // this.warn( 3489 // "hasFocus(): fix inconsistent container state, now: " + 3490 // hasFocus 3491 // ); 3492 // this._hasFocus = hasFocus; 3493 // this.$container.toggleClass("fancytree-treefocus", hasFocus); 3494 // } 3495 // return hasFocus; 3496 return !!this._hasFocus; 3497 }, 3498 /** Write to browser console if debugLevel >= 3 (prepending tree name) 3499 * @param {*} msg string or object or array of such 3500 */ 3501 info: function (msg) { 3502 if (this.options.debugLevel >= 3) { 3503 Array.prototype.unshift.call(arguments, this.toString()); 3504 consoleApply("info", arguments); 3505 } 3506 }, 3507 /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. 3508 * @returns {boolean} 3509 * @since 2.32 3510 */ 3511 isLoading: function () { 3512 var res = false; 3513 3514 this.rootNode.visit(function (n) { 3515 // also visit rootNode 3516 if (n._isLoading || n._requestId) { 3517 res = true; 3518 return false; 3519 } 3520 }, true); 3521 return res; 3522 }, 3523 /* 3524 TODO: isInitializing: function() { 3525 return ( this.phase=="init" || this.phase=="postInit" ); 3526 }, 3527 TODO: isReloading: function() { 3528 return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound; 3529 }, 3530 TODO: isUserEvent: function() { 3531 return ( this.phase=="userEvent" ); 3532 }, 3533 */ 3534 3535 /** 3536 * Make sure that a node with a given ID is loaded, by traversing - and 3537 * loading - its parents. This method is meant for lazy hierarchies. 3538 * A callback is executed for every node as we go. 3539 * @example 3540 * // Resolve using node.key: 3541 * tree.loadKeyPath("/_3/_23/_26/_27", function(node, status){ 3542 * if(status === "loaded") { 3543 * console.log("loaded intermediate node " + node); 3544 * }else if(status === "ok") { 3545 * node.activate(); 3546 * } 3547 * }); 3548 * // Use deferred promise: 3549 * tree.loadKeyPath("/_3/_23/_26/_27").progress(function(data){ 3550 * if(data.status === "loaded") { 3551 * console.log("loaded intermediate node " + data.node); 3552 * }else if(data.status === "ok") { 3553 * node.activate(); 3554 * } 3555 * }).done(function(){ 3556 * ... 3557 * }); 3558 * // Custom path segment resolver: 3559 * tree.loadKeyPath("/321/431/21/2", { 3560 * matchKey: function(node, key){ 3561 * return node.data.refKey === key; 3562 * }, 3563 * callback: function(node, status){ 3564 * if(status === "loaded") { 3565 * console.log("loaded intermediate node " + node); 3566 * }else if(status === "ok") { 3567 * node.activate(); 3568 * } 3569 * } 3570 * }); 3571 * @param {string | string[]} keyPathList one or more key paths (e.g. '/3/2_1/7') 3572 * @param {function | object} optsOrCallback callback(node, status) is called for every visited node ('loading', 'loaded', 'ok', 'error'). 3573 * Pass an object to define custom key matchers for the path segments: {callback: function, matchKey: function}. 3574 * @returns {$.Promise} 3575 */ 3576 loadKeyPath: function (keyPathList, optsOrCallback) { 3577 var callback, 3578 i, 3579 path, 3580 self = this, 3581 dfd = new $.Deferred(), 3582 parent = this.getRootNode(), 3583 sep = this.options.keyPathSeparator, 3584 pathSegList = [], 3585 opts = $.extend({}, optsOrCallback); 3586 3587 // Prepare options 3588 if (typeof optsOrCallback === "function") { 3589 callback = optsOrCallback; 3590 } else if (optsOrCallback && optsOrCallback.callback) { 3591 callback = optsOrCallback.callback; 3592 } 3593 opts.callback = function (ctx, node, status) { 3594 if (callback) { 3595 callback.call(ctx, node, status); 3596 } 3597 dfd.notifyWith(ctx, [{ node: node, status: status }]); 3598 }; 3599 if (opts.matchKey == null) { 3600 opts.matchKey = function (node, key) { 3601 return node.key === key; 3602 }; 3603 } 3604 // Convert array of path strings to array of segment arrays 3605 if (!_isArray(keyPathList)) { 3606 keyPathList = [keyPathList]; 3607 } 3608 for (i = 0; i < keyPathList.length; i++) { 3609 path = keyPathList[i]; 3610 // strip leading slash 3611 if (path.charAt(0) === sep) { 3612 path = path.substr(1); 3613 } 3614 // segListMap[path] = { parent: parent, segList: path.split(sep) }; 3615 pathSegList.push(path.split(sep)); 3616 // targetList.push({ parent: parent, segList: path.split(sep)/* , path: path*/}); 3617 } 3618 // The timeout forces async behavior always (even if nodes are all loaded) 3619 // This way a potential progress() event will fire. 3620 setTimeout(function () { 3621 self._loadKeyPathImpl(dfd, opts, parent, pathSegList).done( 3622 function () { 3623 dfd.resolve(); 3624 } 3625 ); 3626 }, 0); 3627 return dfd.promise(); 3628 }, 3629 /* 3630 * Resolve a list of paths, relative to one parent node. 3631 */ 3632 _loadKeyPathImpl: function (dfd, opts, parent, pathSegList) { 3633 var deferredList, 3634 i, 3635 key, 3636 node, 3637 nodeKey, 3638 remain, 3639 remainMap, 3640 tmpParent, 3641 segList, 3642 subDfd, 3643 self = this; 3644 3645 function __findChild(parent, key) { 3646 // console.log("__findChild", key, parent); 3647 var i, 3648 l, 3649 cl = parent.children; 3650 3651 if (cl) { 3652 for (i = 0, l = cl.length; i < l; i++) { 3653 if (opts.matchKey(cl[i], key)) { 3654 return cl[i]; 3655 } 3656 } 3657 } 3658 return null; 3659 } 3660 3661 // console.log("_loadKeyPathImpl, parent=", parent, ", pathSegList=", pathSegList); 3662 3663 // Pass 1: 3664 // Handle all path segments for nodes that are already loaded. 3665 // Collect distinct top-most lazy nodes in a map. 3666 // Note that we can use node.key to de-dupe entries, even if a custom matcher would 3667 // look for other node attributes. 3668 // map[node.key] => {node: node, pathList: [list of remaining rest-paths]} 3669 remainMap = {}; 3670 3671 for (i = 0; i < pathSegList.length; i++) { 3672 segList = pathSegList[i]; 3673 // target = targetList[i]; 3674 3675 // Traverse and pop path segments (i.e. keys), until we hit a lazy, unloaded node 3676 tmpParent = parent; 3677 while (segList.length) { 3678 key = segList.shift(); 3679 node = __findChild(tmpParent, key); 3680 if (!node) { 3681 this.warn( 3682 "loadKeyPath: key not found: " + 3683 key + 3684 " (parent: " + 3685 tmpParent + 3686 ")" 3687 ); 3688 opts.callback(this, key, "error"); 3689 break; 3690 } else if (segList.length === 0) { 3691 opts.callback(this, node, "ok"); 3692 break; 3693 } else if (!node.lazy || node.hasChildren() !== undefined) { 3694 opts.callback(this, node, "loaded"); 3695 tmpParent = node; 3696 } else { 3697 opts.callback(this, node, "loaded"); 3698 key = node.key; //target.segList.join(sep); 3699 if (remainMap[key]) { 3700 remainMap[key].pathSegList.push(segList); 3701 } else { 3702 remainMap[key] = { 3703 parent: node, 3704 pathSegList: [segList], 3705 }; 3706 } 3707 break; 3708 } 3709 } 3710 } 3711 // console.log("_loadKeyPathImpl AFTER pass 1, remainMap=", remainMap); 3712 3713 // Now load all lazy nodes and continue iteration for remaining paths 3714 deferredList = []; 3715 3716 // Avoid jshint warning 'Don't make functions within a loop.': 3717 function __lazyload(dfd, parent, pathSegList) { 3718 // console.log("__lazyload", parent, "pathSegList=", pathSegList); 3719 opts.callback(self, parent, "loading"); 3720 parent 3721 .load() 3722 .done(function () { 3723 self._loadKeyPathImpl 3724 .call(self, dfd, opts, parent, pathSegList) 3725 .always(_makeResolveFunc(dfd, self)); 3726 }) 3727 .fail(function (errMsg) { 3728 self.warn("loadKeyPath: error loading lazy " + parent); 3729 opts.callback(self, node, "error"); 3730 dfd.rejectWith(self); 3731 }); 3732 } 3733 // remainMap contains parent nodes, each with a list of relative sub-paths. 3734 // We start loading all of them now, and pass the the list to each loader. 3735 for (nodeKey in remainMap) { 3736 if (_hasProp(remainMap, nodeKey)) { 3737 remain = remainMap[nodeKey]; 3738 // console.log("for(): remain=", remain, "remainMap=", remainMap); 3739 // key = remain.segList.shift(); 3740 // node = __findChild(remain.parent, key); 3741 // if (node == null) { // #576 3742 // // Issue #576, refactored for v2.27: 3743 // // The root cause was, that sometimes the wrong parent was used here 3744 // // to find the next segment. 3745 // // Falling back to getNodeByKey() was a hack that no longer works if a custom 3746 // // matcher is used, because we cannot assume that a single segment-key is unique 3747 // // throughout the tree. 3748 // self.error("loadKeyPath: error loading child by key '" + key + "' (parent: " + target.parent + ")", target); 3749 // // node = self.getNodeByKey(key); 3750 // continue; 3751 // } 3752 subDfd = new $.Deferred(); 3753 deferredList.push(subDfd); 3754 __lazyload(subDfd, remain.parent, remain.pathSegList); 3755 } 3756 } 3757 // Return a promise that is resolved, when ALL paths were loaded 3758 return $.when.apply($, deferredList).promise(); 3759 }, 3760 /** Re-fire beforeActivate, activate, and (optional) focus events. 3761 * Calling this method in the `init` event, will activate the node that 3762 * was marked 'active' in the source data, and optionally set the keyboard 3763 * focus. 3764 * @param [setFocus=false] 3765 */ 3766 reactivate: function (setFocus) { 3767 var res, 3768 node = this.activeNode; 3769 3770 if (!node) { 3771 return _getResolvedPromise(); 3772 } 3773 this.activeNode = null; // Force re-activating 3774 res = node.setActive(true, { noFocus: true }); 3775 if (setFocus) { 3776 node.setFocus(); 3777 } 3778 return res; 3779 }, 3780 /** Reload tree from source and return a promise. 3781 * @param [source] optional new source (defaults to initial source data) 3782 * @returns {$.Promise} 3783 */ 3784 reload: function (source) { 3785 this._callHook("treeClear", this); 3786 return this._callHook("treeLoad", this, source); 3787 }, 3788 /**Render tree (i.e. create DOM elements for all top-level nodes). 3789 * @param {boolean} [force=false] create DOM elemnts, even if parent is collapsed 3790 * @param {boolean} [deep=false] 3791 */ 3792 render: function (force, deep) { 3793 return this.rootNode.render(force, deep); 3794 }, 3795 /**(De)select all nodes. 3796 * @param {boolean} [flag=true] 3797 * @since 2.28 3798 */ 3799 selectAll: function (flag) { 3800 this.visit(function (node) { 3801 node.setSelected(flag); 3802 }); 3803 }, 3804 // TODO: selectKey: function(key, select) 3805 // TODO: serializeArray: function(stopOnParents) 3806 /** 3807 * @param {boolean} [flag=true] 3808 */ 3809 setFocus: function (flag) { 3810 return this._callHook("treeSetFocus", this, flag); 3811 }, 3812 /** 3813 * Set current option value. 3814 * (Note: this is the preferred variant of `$().fancytree("option", "KEY", VALUE)`) 3815 * @param {string} name option name (may contain '.') 3816 * @param {any} new value 3817 */ 3818 setOption: function (optionName, value) { 3819 return this.widget.option(optionName, value); 3820 }, 3821 /** 3822 * Call console.time() when in debug mode (verbose >= 4). 3823 * 3824 * @param {string} label 3825 */ 3826 debugTime: function (label) { 3827 if (this.options.debugLevel >= 4) { 3828 window.console.time(this + " - " + label); 3829 } 3830 }, 3831 /** 3832 * Call console.timeEnd() when in debug mode (verbose >= 4). 3833 * 3834 * @param {string} label 3835 */ 3836 debugTimeEnd: function (label) { 3837 if (this.options.debugLevel >= 4) { 3838 window.console.timeEnd(this + " - " + label); 3839 } 3840 }, 3841 /** 3842 * Return all nodes as nested list of {@link NodeData}. 3843 * 3844 * @param {boolean} [includeRoot=false] Returns the hidden system root node (and its children) 3845 * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications. 3846 * Return `false` to ignore this node or "skip" to include this node without its children. 3847 * @returns {Array | object} 3848 * @see FancytreeNode#toDict 3849 */ 3850 toDict: function (includeRoot, callback) { 3851 var res = this.rootNode.toDict(true, callback); 3852 return includeRoot ? res : res.children; 3853 }, 3854 /* Implicitly called for string conversions. 3855 * @returns {string} 3856 */ 3857 toString: function () { 3858 return "Fancytree@" + this._id; 3859 // return "<Fancytree(#" + this._id + ")>"; 3860 }, 3861 /* _trigger a widget event with additional node ctx. 3862 * @see EventData 3863 */ 3864 _triggerNodeEvent: function (type, node, originalEvent, extra) { 3865 // this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx); 3866 var ctx = this._makeHookContext(node, originalEvent, extra), 3867 res = this.widget._trigger(type, originalEvent, ctx); 3868 if (res !== false && ctx.result !== undefined) { 3869 return ctx.result; 3870 } 3871 return res; 3872 }, 3873 /* _trigger a widget event with additional tree data. */ 3874 _triggerTreeEvent: function (type, originalEvent, extra) { 3875 // this.debug("_trigger(" + type + ")", ctx); 3876 var ctx = this._makeHookContext(this, originalEvent, extra), 3877 res = this.widget._trigger(type, originalEvent, ctx); 3878 3879 if (res !== false && ctx.result !== undefined) { 3880 return ctx.result; 3881 } 3882 return res; 3883 }, 3884 /** Call fn(node) for all nodes in hierarchical order (depth-first). 3885 * 3886 * @param {function} fn the callback function. 3887 * Return false to stop iteration, return "skip" to skip this node and children only. 3888 * @returns {boolean} false, if the iterator was stopped. 3889 */ 3890 visit: function (fn) { 3891 return this.rootNode.visit(fn, false); 3892 }, 3893 /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br> 3894 * Stop iteration, if fn() returns false.<br> 3895 * Return false if iteration was stopped. 3896 * 3897 * @param {function} fn the callback function. 3898 * Return false to stop iteration, return "skip" to skip this node and children only. 3899 * @param {object} [options] 3900 * Defaults: 3901 * {start: First top node, reverse: false, includeSelf: true, includeHidden: false} 3902 * @returns {boolean} false if iteration was cancelled 3903 * @since 2.28 3904 */ 3905 visitRows: function (fn, opts) { 3906 if (!this.rootNode.hasChildren()) { 3907 return false; 3908 } 3909 if (opts && opts.reverse) { 3910 delete opts.reverse; 3911 return this._visitRowsUp(fn, opts); 3912 } 3913 opts = opts || {}; 3914 3915 var i, 3916 nextIdx, 3917 parent, 3918 res, 3919 siblings, 3920 siblingOfs = 0, 3921 skipFirstNode = opts.includeSelf === false, 3922 includeHidden = !!opts.includeHidden, 3923 checkFilter = !includeHidden && this.enableFilter, 3924 node = opts.start || this.rootNode.children[0]; 3925 3926 parent = node.parent; 3927 while (parent) { 3928 // visit siblings 3929 siblings = parent.children; 3930 nextIdx = siblings.indexOf(node) + siblingOfs; 3931 _assert( 3932 nextIdx >= 0, 3933 "Could not find " + 3934 node + 3935 " in parent's children: " + 3936 parent 3937 ); 3938 3939 for (i = nextIdx; i < siblings.length; i++) { 3940 node = siblings[i]; 3941 if (checkFilter && !node.match && !node.subMatchCount) { 3942 continue; 3943 } 3944 if (!skipFirstNode && fn(node) === false) { 3945 return false; 3946 } 3947 skipFirstNode = false; 3948 // Dive into node's child nodes 3949 if ( 3950 node.children && 3951 node.children.length && 3952 (includeHidden || node.expanded) 3953 ) { 3954 // Disable warning: Functions declared within loops referencing an outer 3955 // scoped variable may lead to confusing semantics: 3956 /*jshint -W083 */ 3957 res = node.visit(function (n) { 3958 if (checkFilter && !n.match && !n.subMatchCount) { 3959 return "skip"; 3960 } 3961 if (fn(n) === false) { 3962 return false; 3963 } 3964 if (!includeHidden && n.children && !n.expanded) { 3965 return "skip"; 3966 } 3967 }, false); 3968 /*jshint +W083 */ 3969 if (res === false) { 3970 return false; 3971 } 3972 } 3973 } 3974 // Visit parent nodes (bottom up) 3975 node = parent; 3976 parent = parent.parent; 3977 siblingOfs = 1; // 3978 } 3979 return true; 3980 }, 3981 /* Call fn(node) for all nodes in vertical order, bottom up. 3982 */ 3983 _visitRowsUp: function (fn, opts) { 3984 var children, 3985 idx, 3986 parent, 3987 includeHidden = !!opts.includeHidden, 3988 node = opts.start || this.rootNode.children[0]; 3989 3990 while (true) { 3991 parent = node.parent; 3992 children = parent.children; 3993 3994 if (children[0] === node) { 3995 // If this is already the first sibling, goto parent 3996 node = parent; 3997 if (!node.parent) { 3998 break; // first node of the tree 3999 } 4000 children = parent.children; 4001 } else { 4002 // Otherwise, goto prev. sibling 4003 idx = children.indexOf(node); 4004 node = children[idx - 1]; 4005 // If the prev. sibling has children, follow down to last descendant 4006 while ( 4007 // See: https://github.com/eslint/eslint/issues/11302 4008 // eslint-disable-next-line no-unmodified-loop-condition 4009 (includeHidden || node.expanded) && 4010 node.children && 4011 node.children.length 4012 ) { 4013 children = node.children; 4014 parent = node; 4015 node = children[children.length - 1]; 4016 } 4017 } 4018 // Skip invisible 4019 if (!includeHidden && !node.isVisible()) { 4020 continue; 4021 } 4022 if (fn(node) === false) { 4023 return false; 4024 } 4025 } 4026 }, 4027 /** Write warning to browser console if debugLevel >= 2 (prepending tree info) 4028 * 4029 * @param {*} msg string or object or array of such 4030 */ 4031 warn: function (msg) { 4032 if (this.options.debugLevel >= 2) { 4033 Array.prototype.unshift.call(arguments, this.toString()); 4034 consoleApply("warn", arguments); 4035 } 4036 }, 4037 }; 4038 4039 /** 4040 * These additional methods of the {@link Fancytree} class are 'hook functions' 4041 * that can be used and overloaded by extensions. 4042 * 4043 * @see [writing extensions](https://github.com/mar10/fancytree/wiki/TutorialExtensions) 4044 * @mixin Fancytree_Hooks 4045 */ 4046 $.extend( 4047 Fancytree.prototype, 4048 /** @lends Fancytree_Hooks# */ 4049 { 4050 /** Default handling for mouse click events. 4051 * 4052 * @param {EventData} ctx 4053 */ 4054 nodeClick: function (ctx) { 4055 var activate, 4056 expand, 4057 // event = ctx.originalEvent, 4058 targetType = ctx.targetType, 4059 node = ctx.node; 4060 4061 // this.debug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which, ctx); 4062 // TODO: use switch 4063 // TODO: make sure clicks on embedded <input> doesn't steal focus (see table sample) 4064 if (targetType === "expander") { 4065 if (node.isLoading()) { 4066 // #495: we probably got a click event while a lazy load is pending. 4067 // The 'expanded' state is not yet set, so 'toggle' would expand 4068 // and trigger lazyLoad again. 4069 // It would be better to allow to collapse/expand the status node 4070 // while loading (instead of ignoring), but that would require some 4071 // more work. 4072 node.debug("Got 2nd click while loading: ignored"); 4073 return; 4074 } 4075 // Clicking the expander icon always expands/collapses 4076 this._callHook("nodeToggleExpanded", ctx); 4077 } else if (targetType === "checkbox") { 4078 // Clicking the checkbox always (de)selects 4079 this._callHook("nodeToggleSelected", ctx); 4080 if (ctx.options.focusOnSelect) { 4081 // #358 4082 this._callHook("nodeSetFocus", ctx, true); 4083 } 4084 } else { 4085 // Honor `clickFolderMode` for 4086 expand = false; 4087 activate = true; 4088 if (node.folder) { 4089 switch (ctx.options.clickFolderMode) { 4090 case 2: // expand only 4091 expand = true; 4092 activate = false; 4093 break; 4094 case 3: // expand and activate 4095 activate = true; 4096 expand = true; //!node.isExpanded(); 4097 break; 4098 // else 1 or 4: just activate 4099 } 4100 } 4101 if (activate) { 4102 this.nodeSetFocus(ctx); 4103 this._callHook("nodeSetActive", ctx, true); 4104 } 4105 if (expand) { 4106 if (!activate) { 4107 // this._callHook("nodeSetFocus", ctx); 4108 } 4109 // this._callHook("nodeSetExpanded", ctx, true); 4110 this._callHook("nodeToggleExpanded", ctx); 4111 } 4112 } 4113 // Make sure that clicks stop, otherwise <a href='#'> jumps to the top 4114 // if(event.target.localName === "a" && event.target.className === "fancytree-title"){ 4115 // event.preventDefault(); 4116 // } 4117 // TODO: return promise? 4118 }, 4119 /** Collapse all other children of same parent. 4120 * 4121 * @param {EventData} ctx 4122 * @param {object} callOpts 4123 */ 4124 nodeCollapseSiblings: function (ctx, callOpts) { 4125 // TODO: return promise? 4126 var ac, 4127 i, 4128 l, 4129 node = ctx.node; 4130 4131 if (node.parent) { 4132 ac = node.parent.children; 4133 for (i = 0, l = ac.length; i < l; i++) { 4134 if (ac[i] !== node && ac[i].expanded) { 4135 this._callHook( 4136 "nodeSetExpanded", 4137 ac[i], 4138 false, 4139 callOpts 4140 ); 4141 } 4142 } 4143 } 4144 }, 4145 /** Default handling for mouse douleclick events. 4146 * @param {EventData} ctx 4147 */ 4148 nodeDblclick: function (ctx) { 4149 // TODO: return promise? 4150 if ( 4151 ctx.targetType === "title" && 4152 ctx.options.clickFolderMode === 4 4153 ) { 4154 // this.nodeSetFocus(ctx); 4155 // this._callHook("nodeSetActive", ctx, true); 4156 this._callHook("nodeToggleExpanded", ctx); 4157 } 4158 // TODO: prevent text selection on dblclicks 4159 if (ctx.targetType === "title") { 4160 ctx.originalEvent.preventDefault(); 4161 } 4162 }, 4163 /** Default handling for mouse keydown events. 4164 * 4165 * NOTE: this may be called with node == null if tree (but no node) has focus. 4166 * @param {EventData} ctx 4167 */ 4168 nodeKeydown: function (ctx) { 4169 // TODO: return promise? 4170 var matchNode, 4171 stamp, 4172 _res, 4173 focusNode, 4174 event = ctx.originalEvent, 4175 node = ctx.node, 4176 tree = ctx.tree, 4177 opts = ctx.options, 4178 which = event.which, 4179 // #909: Use event.key, to get unicode characters. 4180 // We can't use `/\w/.test(key)`, because that would 4181 // only detect plain ascii alpha-numerics. But we still need 4182 // to ignore modifier-only, whitespace, cursor-keys, etc. 4183 key = event.key || String.fromCharCode(which), 4184 specialModifiers = !!( 4185 event.altKey || 4186 event.ctrlKey || 4187 event.metaKey 4188 ), 4189 isAlnum = 4190 !MODIFIERS[which] && 4191 !SPECIAL_KEYCODES[which] && 4192 !specialModifiers, 4193 $target = $(event.target), 4194 handled = true, 4195 activate = !(event.ctrlKey || !opts.autoActivate); 4196 4197 // (node || FT).debug("ftnode.nodeKeydown(" + event.type + "): ftnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which); 4198 // FT.debug( "eventToString(): " + FT.eventToString(event) + ", key='" + key + "', isAlnum: " + isAlnum ); 4199 4200 // Set focus to active (or first node) if no other node has the focus yet 4201 if (!node) { 4202 focusNode = this.getActiveNode() || this.getFirstChild(); 4203 if (focusNode) { 4204 focusNode.setFocus(); 4205 node = ctx.node = this.focusNode; 4206 node.debug("Keydown force focus on active node"); 4207 } 4208 } 4209 4210 if ( 4211 opts.quicksearch && 4212 isAlnum && 4213 !$target.is(":input:enabled") 4214 ) { 4215 // Allow to search for longer streaks if typed in quickly 4216 stamp = Date.now(); 4217 if (stamp - tree.lastQuicksearchTime > 500) { 4218 tree.lastQuicksearchTerm = ""; 4219 } 4220 tree.lastQuicksearchTime = stamp; 4221 tree.lastQuicksearchTerm += key; 4222 // tree.debug("quicksearch find", tree.lastQuicksearchTerm); 4223 matchNode = tree.findNextNode( 4224 tree.lastQuicksearchTerm, 4225 tree.getActiveNode() 4226 ); 4227 if (matchNode) { 4228 matchNode.setActive(); 4229 } 4230 event.preventDefault(); 4231 return; 4232 } 4233 switch (FT.eventToString(event)) { 4234 case "+": 4235 case "=": // 187: '+' @ Chrome, Safari 4236 tree.nodeSetExpanded(ctx, true); 4237 break; 4238 case "-": 4239 tree.nodeSetExpanded(ctx, false); 4240 break; 4241 case "space": 4242 if (node.isPagingNode()) { 4243 tree._triggerNodeEvent("clickPaging", ctx, event); 4244 } else if ( 4245 FT.evalOption("checkbox", node, node, opts, false) 4246 ) { 4247 // #768 4248 tree.nodeToggleSelected(ctx); 4249 } else { 4250 tree.nodeSetActive(ctx, true); 4251 } 4252 break; 4253 case "return": 4254 tree.nodeSetActive(ctx, true); 4255 break; 4256 case "home": 4257 case "end": 4258 case "backspace": 4259 case "left": 4260 case "right": 4261 case "up": 4262 case "down": 4263 _res = node.navigate(event.which, activate); 4264 break; 4265 default: 4266 handled = false; 4267 } 4268 if (handled) { 4269 event.preventDefault(); 4270 } 4271 }, 4272 4273 // /** Default handling for mouse keypress events. */ 4274 // nodeKeypress: function(ctx) { 4275 // var event = ctx.originalEvent; 4276 // }, 4277 4278 // /** Trigger lazyLoad event (async). */ 4279 // nodeLazyLoad: function(ctx) { 4280 // var node = ctx.node; 4281 // if(this._triggerNodeEvent()) 4282 // }, 4283 /** Load child nodes (async). 4284 * 4285 * @param {EventData} ctx 4286 * @param {object[]|object|string|$.Promise|function} source 4287 * @returns {$.Promise} The deferred will be resolved as soon as the (ajax) 4288 * data was rendered. 4289 */ 4290 nodeLoadChildren: function (ctx, source) { 4291 var ajax, 4292 delay, 4293 ajaxDfd = null, 4294 resultDfd, 4295 isAsync = true, 4296 tree = ctx.tree, 4297 node = ctx.node, 4298 nodePrevParent = node.parent, 4299 tag = "nodeLoadChildren", 4300 requestId = Date.now(); 4301 4302 // `source` is a callback: use the returned result instead: 4303 if (_isFunction(source)) { 4304 source = source.call(tree, { type: "source" }, ctx); 4305 _assert( 4306 !_isFunction(source), 4307 "source callback must not return another function" 4308 ); 4309 } 4310 // `source` is already a promise: 4311 if (_isFunction(source.then)) { 4312 // _assert(_isFunction(source.always), "Expected jQuery?"); 4313 ajaxDfd = source; 4314 } else if (source.url) { 4315 // `source` is an Ajax options object 4316 ajax = $.extend({}, ctx.options.ajax, source); 4317 if (ajax.debugDelay) { 4318 // Simulate a slow server 4319 delay = ajax.debugDelay; 4320 delete ajax.debugDelay; // remove debug option 4321 if (_isArray(delay)) { 4322 // random delay range [min..max] 4323 delay = 4324 delay[0] + 4325 Math.random() * (delay[1] - delay[0]); 4326 } 4327 node.warn( 4328 "nodeLoadChildren waiting debugDelay " + 4329 Math.round(delay) + 4330 " ms ..." 4331 ); 4332 ajaxDfd = $.Deferred(function (ajaxDfd) { 4333 setTimeout(function () { 4334 $.ajax(ajax) 4335 .done(function () { 4336 ajaxDfd.resolveWith(this, arguments); 4337 }) 4338 .fail(function () { 4339 ajaxDfd.rejectWith(this, arguments); 4340 }); 4341 }, delay); 4342 }); 4343 } else { 4344 ajaxDfd = $.ajax(ajax); 4345 } 4346 } else if ($.isPlainObject(source) || _isArray(source)) { 4347 // `source` is already a constant dict or list, but we convert 4348 // to a thenable for unified processing. 4349 // 2020-01-03: refactored. 4350 // `ajaxDfd = $.when(source)` would do the trick, but the returned 4351 // promise will resolve async, which broke some tests and 4352 // would probably also break current implementations out there. 4353 // So we mock-up a thenable that resolves synchronously: 4354 ajaxDfd = { 4355 then: function (resolve, reject) { 4356 resolve(source, null, null); 4357 }, 4358 }; 4359 isAsync = false; 4360 } else { 4361 $.error("Invalid source type: " + source); 4362 } 4363 4364 // Check for overlapping requests 4365 if (node._requestId) { 4366 node.warn( 4367 "Recursive load request #" + 4368 requestId + 4369 " while #" + 4370 node._requestId + 4371 " is pending." 4372 ); 4373 node._requestId = requestId; 4374 // node.debug("Send load request #" + requestId); 4375 } 4376 4377 if (isAsync) { 4378 tree.debugTime(tag); 4379 tree.nodeSetStatus(ctx, "loading"); 4380 } 4381 4382 // The async Ajax request has now started... 4383 // Defer the deferred: 4384 // we want to be able to reject invalid responses, even if 4385 // the raw HTTP Ajax XHR resolved as Ok. 4386 // We use the ajaxDfd.then() syntax here, which is compatible with 4387 // jQuery and ECMA6. 4388 // However resultDfd is a jQuery deferred, which is currently the 4389 // expected result type of nodeLoadChildren() 4390 resultDfd = new $.Deferred(); 4391 ajaxDfd.then( 4392 function (data, textStatus, jqXHR) { 4393 // ajaxDfd was resolved, but we reject or resolve resultDfd 4394 // depending on the response data 4395 var errorObj, res; 4396 4397 if ( 4398 (source.dataType === "json" || 4399 source.dataType === "jsonp") && 4400 typeof data === "string" 4401 ) { 4402 $.error( 4403 "Ajax request returned a string (did you get the JSON dataType wrong?)." 4404 ); 4405 } 4406 if (node._requestId && node._requestId > requestId) { 4407 // The expected request time stamp is later than `requestId` 4408 // (which was kept as as closure variable to this handler function) 4409 // node.warn("Ignored load response for obsolete request #" + requestId + " (expected #" + node._requestId + ")"); 4410 resultDfd.rejectWith(this, [ 4411 RECURSIVE_REQUEST_ERROR, 4412 ]); 4413 return; 4414 // } else { 4415 // node.debug("Response returned for load request #" + requestId); 4416 } 4417 if (node.parent === null && nodePrevParent !== null) { 4418 resultDfd.rejectWith(this, [ 4419 INVALID_REQUEST_TARGET_ERROR, 4420 ]); 4421 return; 4422 } 4423 // Allow to adjust the received response data in the `postProcess` event. 4424 if (ctx.options.postProcess) { 4425 // The handler may either 4426 // - modify `ctx.response` in-place (and leave `ctx.result` undefined) 4427 // => res = undefined 4428 // - return a replacement in `ctx.result` 4429 // => res = <new data> 4430 // If res contains an `error` property, an error status is displayed 4431 try { 4432 res = tree._triggerNodeEvent( 4433 "postProcess", 4434 ctx, 4435 ctx.originalEvent, 4436 { 4437 response: data, 4438 error: null, 4439 dataType: source.dataType, 4440 } 4441 ); 4442 if (res.error) { 4443 tree.warn( 4444 "postProcess returned error:", 4445 res 4446 ); 4447 } 4448 } catch (e) { 4449 res = { 4450 error: e, 4451 message: "" + e, 4452 details: "postProcess failed", 4453 }; 4454 } 4455 if (res.error) { 4456 // Either postProcess failed with an exception, or the returned 4457 // result object has an 'error' property attached: 4458 errorObj = $.isPlainObject(res.error) 4459 ? res.error 4460 : { message: res.error }; 4461 errorObj = tree._makeHookContext( 4462 node, 4463 null, 4464 errorObj 4465 ); 4466 resultDfd.rejectWith(this, [errorObj]); 4467 return; 4468 } 4469 if ( 4470 _isArray(res) || 4471 ($.isPlainObject(res) && _isArray(res.children)) 4472 ) { 4473 // Use `ctx.result` if valid 4474 // (otherwise use existing data, which may have been modified in-place) 4475 data = res; 4476 } 4477 } else if ( 4478 data && 4479 _hasProp(data, "d") && 4480 ctx.options.enableAspx 4481 ) { 4482 // Process ASPX WebMethod JSON object inside "d" property 4483 // (only if no postProcess event was defined) 4484 if (ctx.options.enableAspx === 42) { 4485 tree.warn( 4486 "The default for enableAspx will change to `false` in the fututure. " + 4487 "Pass `enableAspx: true` or implement postProcess to silence this warning." 4488 ); 4489 } 4490 data = 4491 typeof data.d === "string" 4492 ? $.parseJSON(data.d) 4493 : data.d; 4494 } 4495 resultDfd.resolveWith(this, [data]); 4496 }, 4497 function (jqXHR, textStatus, errorThrown) { 4498 // ajaxDfd was rejected, so we reject resultDfd as well 4499 var errorObj = tree._makeHookContext(node, null, { 4500 error: jqXHR, 4501 args: Array.prototype.slice.call(arguments), 4502 message: errorThrown, 4503 details: jqXHR.status + ": " + errorThrown, 4504 }); 4505 resultDfd.rejectWith(this, [errorObj]); 4506 } 4507 ); 4508 4509 // The async Ajax request has now started. 4510 // resultDfd will be resolved/rejected after the response arrived, 4511 // was postProcessed, and checked. 4512 // Now we implement the UI update and add the data to the tree. 4513 // We also return this promise to the caller. 4514 resultDfd 4515 .done(function (data) { 4516 tree.nodeSetStatus(ctx, "ok"); 4517 var children, metaData, noDataRes; 4518 4519 if ($.isPlainObject(data)) { 4520 // We got {foo: 'abc', children: [...]} 4521 // Copy extra properties to tree.data.foo 4522 _assert( 4523 node.isRootNode(), 4524 "source may only be an object for root nodes (expecting an array of child objects otherwise)" 4525 ); 4526 _assert( 4527 _isArray(data.children), 4528 "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')" 4529 ); 4530 metaData = data; 4531 children = data.children; 4532 delete metaData.children; 4533 // Copy some attributes to tree.data 4534 $.each(TREE_ATTRS, function (i, attr) { 4535 if (metaData[attr] !== undefined) { 4536 tree[attr] = metaData[attr]; 4537 delete metaData[attr]; 4538 } 4539 }); 4540 // Copy all other attributes to tree.data.NAME 4541 $.extend(tree.data, metaData); 4542 } else { 4543 children = data; 4544 } 4545 _assert( 4546 _isArray(children), 4547 "expected array of children" 4548 ); 4549 node._setChildren(children); 4550 4551 if (tree.options.nodata && children.length === 0) { 4552 if (_isFunction(tree.options.nodata)) { 4553 noDataRes = tree.options.nodata.call( 4554 tree, 4555 { type: "nodata" }, 4556 ctx 4557 ); 4558 } else if ( 4559 tree.options.nodata === true && 4560 node.isRootNode() 4561 ) { 4562 noDataRes = tree.options.strings.noData; 4563 } else if ( 4564 typeof tree.options.nodata === "string" && 4565 node.isRootNode() 4566 ) { 4567 noDataRes = tree.options.nodata; 4568 } 4569 if (noDataRes) { 4570 node.setStatus("nodata", noDataRes); 4571 } 4572 } 4573 // trigger fancytreeloadchildren 4574 tree._triggerNodeEvent("loadChildren", node); 4575 }) 4576 .fail(function (error) { 4577 var ctxErr; 4578 4579 if (error === RECURSIVE_REQUEST_ERROR) { 4580 node.warn( 4581 "Ignored response for obsolete load request #" + 4582 requestId + 4583 " (expected #" + 4584 node._requestId + 4585 ")" 4586 ); 4587 return; 4588 } else if (error === INVALID_REQUEST_TARGET_ERROR) { 4589 node.warn( 4590 "Lazy parent node was removed while loading: discarding response." 4591 ); 4592 return; 4593 } else if (error.node && error.error && error.message) { 4594 // error is already a context object 4595 ctxErr = error; 4596 } else { 4597 ctxErr = tree._makeHookContext(node, null, { 4598 error: error, // it can be jqXHR or any custom error 4599 args: Array.prototype.slice.call(arguments), 4600 message: error 4601 ? error.message || error.toString() 4602 : "", 4603 }); 4604 if (ctxErr.message === "[object Object]") { 4605 ctxErr.message = ""; 4606 } 4607 } 4608 node.warn( 4609 "Load children failed (" + ctxErr.message + ")", 4610 ctxErr 4611 ); 4612 if ( 4613 tree._triggerNodeEvent( 4614 "loadError", 4615 ctxErr, 4616 null 4617 ) !== false 4618 ) { 4619 tree.nodeSetStatus( 4620 ctx, 4621 "error", 4622 ctxErr.message, 4623 ctxErr.details 4624 ); 4625 } 4626 }) 4627 .always(function () { 4628 node._requestId = null; 4629 if (isAsync) { 4630 tree.debugTimeEnd(tag); 4631 } 4632 }); 4633 4634 return resultDfd.promise(); 4635 }, 4636 /** [Not Implemented] */ 4637 nodeLoadKeyPath: function (ctx, keyPathList) { 4638 // TODO: implement and improve 4639 // http://code.google.com/p/dynatree/issues/detail?id=222 4640 }, 4641 /** 4642 * Remove a single direct child of ctx.node. 4643 * @param {EventData} ctx 4644 * @param {FancytreeNode} childNode dircect child of ctx.node 4645 */ 4646 nodeRemoveChild: function (ctx, childNode) { 4647 var idx, 4648 node = ctx.node, 4649 // opts = ctx.options, 4650 subCtx = $.extend({}, ctx, { node: childNode }), 4651 children = node.children; 4652 4653 // FT.debug("nodeRemoveChild()", node.toString(), childNode.toString()); 4654 4655 if (children.length === 1) { 4656 _assert(childNode === children[0], "invalid single child"); 4657 return this.nodeRemoveChildren(ctx); 4658 } 4659 if ( 4660 this.activeNode && 4661 (childNode === this.activeNode || 4662 this.activeNode.isDescendantOf(childNode)) 4663 ) { 4664 this.activeNode.setActive(false); // TODO: don't fire events 4665 } 4666 if ( 4667 this.focusNode && 4668 (childNode === this.focusNode || 4669 this.focusNode.isDescendantOf(childNode)) 4670 ) { 4671 this.focusNode = null; 4672 } 4673 // TODO: persist must take care to clear select and expand cookies 4674 this.nodeRemoveMarkup(subCtx); 4675 this.nodeRemoveChildren(subCtx); 4676 idx = $.inArray(childNode, children); 4677 _assert(idx >= 0, "invalid child"); 4678 // Notify listeners 4679 node.triggerModifyChild("remove", childNode); 4680 // Unlink to support GC 4681 childNode.visit(function (n) { 4682 n.parent = null; 4683 }, true); 4684 this._callHook("treeRegisterNode", this, false, childNode); 4685 // remove from child list 4686 children.splice(idx, 1); 4687 }, 4688 /**Remove HTML markup for all descendents of ctx.node. 4689 * @param {EventData} ctx 4690 */ 4691 nodeRemoveChildMarkup: function (ctx) { 4692 var node = ctx.node; 4693 4694 // FT.debug("nodeRemoveChildMarkup()", node.toString()); 4695 // TODO: Unlink attr.ftnode to support GC 4696 if (node.ul) { 4697 if (node.isRootNode()) { 4698 $(node.ul).empty(); 4699 } else { 4700 $(node.ul).remove(); 4701 node.ul = null; 4702 } 4703 node.visit(function (n) { 4704 n.li = n.ul = null; 4705 }); 4706 } 4707 }, 4708 /**Remove all descendants of ctx.node. 4709 * @param {EventData} ctx 4710 */ 4711 nodeRemoveChildren: function (ctx) { 4712 var //subCtx, 4713 tree = ctx.tree, 4714 node = ctx.node, 4715 children = node.children; 4716 // opts = ctx.options; 4717 4718 // FT.debug("nodeRemoveChildren()", node.toString()); 4719 if (!children) { 4720 return; 4721 } 4722 if (this.activeNode && this.activeNode.isDescendantOf(node)) { 4723 this.activeNode.setActive(false); // TODO: don't fire events 4724 } 4725 if (this.focusNode && this.focusNode.isDescendantOf(node)) { 4726 this.focusNode = null; 4727 } 4728 // TODO: persist must take care to clear select and expand cookies 4729 this.nodeRemoveChildMarkup(ctx); 4730 // Unlink children to support GC 4731 // TODO: also delete this.children (not possible using visit()) 4732 // subCtx = $.extend({}, ctx); 4733 node.triggerModifyChild("remove", null); 4734 node.visit(function (n) { 4735 n.parent = null; 4736 tree._callHook("treeRegisterNode", tree, false, n); 4737 }); 4738 if (node.lazy) { 4739 // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes 4740 node.children = []; 4741 } else { 4742 node.children = null; 4743 } 4744 if (!node.isRootNode()) { 4745 node.expanded = false; // #449, #459 4746 } 4747 this.nodeRenderStatus(ctx); 4748 }, 4749 /**Remove HTML markup for ctx.node and all its descendents. 4750 * @param {EventData} ctx 4751 */ 4752 nodeRemoveMarkup: function (ctx) { 4753 var node = ctx.node; 4754 // FT.debug("nodeRemoveMarkup()", node.toString()); 4755 // TODO: Unlink attr.ftnode to support GC 4756 if (node.li) { 4757 $(node.li).remove(); 4758 node.li = null; 4759 } 4760 this.nodeRemoveChildMarkup(ctx); 4761 }, 4762 /** 4763 * Create `<li><span>..</span> .. </li>` tags for this node. 4764 * 4765 * This method takes care that all HTML markup is created that is required 4766 * to display this node in its current state. 4767 * 4768 * Call this method to create new nodes, or after the strucuture 4769 * was changed (e.g. after moving this node or adding/removing children) 4770 * nodeRenderTitle() and nodeRenderStatus() are implied. 4771 * 4772 * ```html 4773 * <li id='KEY' ftnode=NODE> 4774 * <span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'> 4775 * <span class="fancytree-expander"></span> 4776 * <span class="fancytree-checkbox"></span> // only present in checkbox mode 4777 * <span class="fancytree-icon"></span> 4778 * <a href="#" class="fancytree-title"> Node 1 </a> 4779 * </span> 4780 * <ul> // only present if node has children 4781 * <li id='KEY' ftnode=NODE> child1 ... </li> 4782 * <li id='KEY' ftnode=NODE> child2 ... </li> 4783 * </ul> 4784 * </li> 4785 * ``` 4786 * 4787 * @param {EventData} ctx 4788 * @param {boolean} [force=false] re-render, even if html markup was already created 4789 * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed 4790 * @param {boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later 4791 */ 4792 nodeRender: function (ctx, force, deep, collapsed, _recursive) { 4793 /* This method must take care of all cases where the current data mode 4794 * (i.e. node hierarchy) does not match the current markup. 4795 * 4796 * - node was not yet rendered: 4797 * create markup 4798 * - node was rendered: exit fast 4799 * - children have been added 4800 * - children have been removed 4801 */ 4802 var childLI, 4803 childNode1, 4804 childNode2, 4805 i, 4806 l, 4807 next, 4808 subCtx, 4809 node = ctx.node, 4810 tree = ctx.tree, 4811 opts = ctx.options, 4812 aria = opts.aria, 4813 firstTime = false, 4814 parent = node.parent, 4815 isRootNode = !parent, 4816 children = node.children, 4817 successorLi = null; 4818 // FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString()); 4819 4820 if (tree._enableUpdate === false) { 4821 // tree.debug("no render", tree._enableUpdate); 4822 return; 4823 } 4824 if (!isRootNode && !parent.ul) { 4825 // Calling node.collapse on a deep, unrendered node 4826 return; 4827 } 4828 _assert(isRootNode || parent.ul, "parent UL must exist"); 4829 4830 // Render the node 4831 if (!isRootNode) { 4832 // Discard markup on force-mode, or if it is not linked to parent <ul> 4833 if ( 4834 node.li && 4835 (force || node.li.parentNode !== node.parent.ul) 4836 ) { 4837 if (node.li.parentNode === node.parent.ul) { 4838 // #486: store following node, so we can insert the new markup there later 4839 successorLi = node.li.nextSibling; 4840 } else { 4841 // May happen, when a top-level node was dropped over another 4842 this.debug( 4843 "Unlinking " + 4844 node + 4845 " (must be child of " + 4846 node.parent + 4847 ")" 4848 ); 4849 } 4850 // this.debug("nodeRemoveMarkup..."); 4851 this.nodeRemoveMarkup(ctx); 4852 } 4853 // Create <li><span /> </li> 4854 // node.debug("render..."); 4855 if (node.li) { 4856 // this.nodeRenderTitle(ctx); 4857 this.nodeRenderStatus(ctx); 4858 } else { 4859 // node.debug("render... really"); 4860 firstTime = true; 4861 node.li = document.createElement("li"); 4862 node.li.ftnode = node; 4863 4864 if (node.key && opts.generateIds) { 4865 node.li.id = opts.idPrefix + node.key; 4866 } 4867 node.span = document.createElement("span"); 4868 node.span.className = "fancytree-node"; 4869 if (aria && !node.tr) { 4870 $(node.li).attr("role", "treeitem"); 4871 } 4872 node.li.appendChild(node.span); 4873 4874 // Create inner HTML for the <span> (expander, checkbox, icon, and title) 4875 this.nodeRenderTitle(ctx); 4876 4877 // Allow tweaking and binding, after node was created for the first time 4878 if (opts.createNode) { 4879 opts.createNode.call( 4880 tree, 4881 { type: "createNode" }, 4882 ctx 4883 ); 4884 } 4885 } 4886 // Allow tweaking after node state was rendered 4887 if (opts.renderNode) { 4888 opts.renderNode.call(tree, { type: "renderNode" }, ctx); 4889 } 4890 } 4891 4892 // Visit child nodes 4893 if (children) { 4894 if (isRootNode || node.expanded || deep === true) { 4895 // Create a UL to hold the children 4896 if (!node.ul) { 4897 node.ul = document.createElement("ul"); 4898 if ( 4899 (collapsed === true && !_recursive) || 4900 !node.expanded 4901 ) { 4902 // hide top UL, so we can use an animation to show it later 4903 node.ul.style.display = "none"; 4904 } 4905 if (aria) { 4906 $(node.ul).attr("role", "group"); 4907 } 4908 if (node.li) { 4909 // issue #67 4910 node.li.appendChild(node.ul); 4911 } else { 4912 node.tree.$div.append(node.ul); 4913 } 4914 } 4915 // Add child markup 4916 for (i = 0, l = children.length; i < l; i++) { 4917 subCtx = $.extend({}, ctx, { node: children[i] }); 4918 this.nodeRender(subCtx, force, deep, false, true); 4919 } 4920 // Remove <li> if nodes have moved to another parent 4921 childLI = node.ul.firstChild; 4922 while (childLI) { 4923 childNode2 = childLI.ftnode; 4924 if (childNode2 && childNode2.parent !== node) { 4925 node.debug( 4926 "_fixParent: remove missing " + childNode2, 4927 childLI 4928 ); 4929 next = childLI.nextSibling; 4930 childLI.parentNode.removeChild(childLI); 4931 childLI = next; 4932 } else { 4933 childLI = childLI.nextSibling; 4934 } 4935 } 4936 // Make sure, that <li> order matches node.children order. 4937 childLI = node.ul.firstChild; 4938 for (i = 0, l = children.length - 1; i < l; i++) { 4939 childNode1 = children[i]; 4940 childNode2 = childLI.ftnode; 4941 if (childNode1 === childNode2) { 4942 childLI = childLI.nextSibling; 4943 } else { 4944 // node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2); 4945 node.ul.insertBefore( 4946 childNode1.li, 4947 childNode2.li 4948 ); 4949 } 4950 } 4951 } 4952 } else { 4953 // No children: remove markup if any 4954 if (node.ul) { 4955 // alert("remove child markup for " + node); 4956 this.warn("remove child markup for " + node); 4957 this.nodeRemoveChildMarkup(ctx); 4958 } 4959 } 4960 if (!isRootNode) { 4961 // Update element classes according to node state 4962 // this.nodeRenderStatus(ctx); 4963 // Finally add the whole structure to the DOM, so the browser can render 4964 if (firstTime) { 4965 // #486: successorLi is set, if we re-rendered (i.e. discarded) 4966 // existing markup, which we want to insert at the same position. 4967 // (null is equivalent to append) 4968 // parent.ul.appendChild(node.li); 4969 parent.ul.insertBefore(node.li, successorLi); 4970 } 4971 } 4972 }, 4973 /** Create HTML inside the node's outer `<span>` (i.e. expander, checkbox, 4974 * icon, and title). 4975 * 4976 * nodeRenderStatus() is implied. 4977 * @param {EventData} ctx 4978 * @param {string} [title] optinal new title 4979 */ 4980 nodeRenderTitle: function (ctx, title) { 4981 // set node connector images, links and text 4982 var checkbox, 4983 className, 4984 icon, 4985 nodeTitle, 4986 role, 4987 tabindex, 4988 tooltip, 4989 iconTooltip, 4990 node = ctx.node, 4991 tree = ctx.tree, 4992 opts = ctx.options, 4993 aria = opts.aria, 4994 level = node.getLevel(), 4995 ares = []; 4996 4997 if (title !== undefined) { 4998 node.title = title; 4999 } 5000 if (!node.span || tree._enableUpdate === false) { 5001 // Silently bail out if node was not rendered yet, assuming 5002 // node.render() will be called as the node becomes visible 5003 return; 5004 } 5005 // Connector (expanded, expandable or simple) 5006 role = 5007 aria && node.hasChildren() !== false 5008 ? " role='button'" 5009 : ""; 5010 if (level < opts.minExpandLevel) { 5011 if (!node.lazy) { 5012 node.expanded = true; 5013 } 5014 if (level > 1) { 5015 ares.push( 5016 "<span " + 5017 role + 5018 " class='fancytree-expander fancytree-expander-fixed'></span>" 5019 ); 5020 } 5021 // .. else (i.e. for root level) skip expander/connector alltogether 5022 } else { 5023 ares.push( 5024 "<span " + role + " class='fancytree-expander'></span>" 5025 ); 5026 } 5027 // Checkbox mode 5028 checkbox = FT.evalOption("checkbox", node, node, opts, false); 5029 5030 if (checkbox && !node.isStatusNode()) { 5031 role = aria ? " role='checkbox'" : ""; 5032 className = "fancytree-checkbox"; 5033 if ( 5034 checkbox === "radio" || 5035 (node.parent && node.parent.radiogroup) 5036 ) { 5037 className += " fancytree-radio"; 5038 } 5039 ares.push( 5040 "<span " + role + " class='" + className + "'></span>" 5041 ); 5042 } 5043 // Folder or doctype icon 5044 if (node.data.iconClass !== undefined) { 5045 // 2015-11-16 5046 // Handle / warn about backward compatibility 5047 if (node.icon) { 5048 $.error( 5049 "'iconClass' node option is deprecated since v2.14.0: use 'icon' only instead" 5050 ); 5051 } else { 5052 node.warn( 5053 "'iconClass' node option is deprecated since v2.14.0: use 'icon' instead" 5054 ); 5055 node.icon = node.data.iconClass; 5056 } 5057 } 5058 // If opts.icon is a callback and returns something other than undefined, use that 5059 // else if node.icon is a boolean or string, use that 5060 // else if opts.icon is a boolean or string, use that 5061 // else show standard icon (which may be different for folders or documents) 5062 icon = FT.evalOption("icon", node, node, opts, true); 5063 // if( typeof icon !== "boolean" ) { 5064 // // icon is defined, but not true/false: must be a string 5065 // icon = "" + icon; 5066 // } 5067 if (icon !== false) { 5068 role = aria ? " role='presentation'" : ""; 5069 5070 iconTooltip = FT.evalOption( 5071 "iconTooltip", 5072 node, 5073 node, 5074 opts, 5075 null 5076 ); 5077 iconTooltip = iconTooltip 5078 ? " title='" + _escapeTooltip(iconTooltip) + "'" 5079 : ""; 5080 5081 if (typeof icon === "string") { 5082 if (TEST_IMG.test(icon)) { 5083 // node.icon is an image url. Prepend imagePath 5084 icon = 5085 icon.charAt(0) === "/" 5086 ? icon 5087 : (opts.imagePath || "") + icon; 5088 ares.push( 5089 "<img src='" + 5090 icon + 5091 "' class='fancytree-icon'" + 5092 iconTooltip + 5093 " alt='' />" 5094 ); 5095 } else { 5096 ares.push( 5097 "<span " + 5098 role + 5099 " class='fancytree-custom-icon " + 5100 icon + 5101 "'" + 5102 iconTooltip + 5103 "></span>" 5104 ); 5105 } 5106 } else if (icon.text) { 5107 ares.push( 5108 "<span " + 5109 role + 5110 " class='fancytree-custom-icon " + 5111 (icon.addClass || "") + 5112 "'" + 5113 iconTooltip + 5114 ">" + 5115 FT.escapeHtml(icon.text) + 5116 "</span>" 5117 ); 5118 } else if (icon.html) { 5119 ares.push( 5120 "<span " + 5121 role + 5122 " class='fancytree-custom-icon " + 5123 (icon.addClass || "") + 5124 "'" + 5125 iconTooltip + 5126 ">" + 5127 icon.html + 5128 "</span>" 5129 ); 5130 } else { 5131 // standard icon: theme css will take care of this 5132 ares.push( 5133 "<span " + 5134 role + 5135 " class='fancytree-icon'" + 5136 iconTooltip + 5137 "></span>" 5138 ); 5139 } 5140 } 5141 // Node title 5142 nodeTitle = ""; 5143 if (opts.renderTitle) { 5144 nodeTitle = 5145 opts.renderTitle.call( 5146 tree, 5147 { type: "renderTitle" }, 5148 ctx 5149 ) || ""; 5150 } 5151 if (!nodeTitle) { 5152 tooltip = FT.evalOption("tooltip", node, node, opts, null); 5153 if (tooltip === true) { 5154 tooltip = node.title; 5155 } 5156 // if( node.tooltip ) { 5157 // tooltip = node.tooltip; 5158 // } else if ( opts.tooltip ) { 5159 // tooltip = opts.tooltip === true ? node.title : opts.tooltip.call(tree, node); 5160 // } 5161 tooltip = tooltip 5162 ? " title='" + _escapeTooltip(tooltip) + "'" 5163 : ""; 5164 tabindex = opts.titlesTabbable ? " tabindex='0'" : ""; 5165 5166 nodeTitle = 5167 "<span class='fancytree-title'" + 5168 tooltip + 5169 tabindex + 5170 ">" + 5171 (opts.escapeTitles 5172 ? FT.escapeHtml(node.title) 5173 : node.title) + 5174 "</span>"; 5175 } 5176 ares.push(nodeTitle); 5177 // Note: this will trigger focusout, if node had the focus 5178 //$(node.span).html(ares.join("")); // it will cleanup the jQuery data currently associated with SPAN (if any), but it executes more slowly 5179 node.span.innerHTML = ares.join(""); 5180 // Update CSS classes 5181 this.nodeRenderStatus(ctx); 5182 if (opts.enhanceTitle) { 5183 ctx.$title = $(">span.fancytree-title", node.span); 5184 nodeTitle = 5185 opts.enhanceTitle.call( 5186 tree, 5187 { type: "enhanceTitle" }, 5188 ctx 5189 ) || ""; 5190 } 5191 }, 5192 /** Update element classes according to node state. 5193 * @param {EventData} ctx 5194 */ 5195 nodeRenderStatus: function (ctx) { 5196 // Set classes for current status 5197 var $ariaElem, 5198 node = ctx.node, 5199 tree = ctx.tree, 5200 opts = ctx.options, 5201 // nodeContainer = node[tree.nodeContainerAttrName], 5202 hasChildren = node.hasChildren(), 5203 isLastSib = node.isLastSibling(), 5204 aria = opts.aria, 5205 cn = opts._classNames, 5206 cnList = [], 5207 statusElem = node[tree.statusClassPropName]; 5208 5209 if (!statusElem || tree._enableUpdate === false) { 5210 // if this function is called for an unrendered node, ignore it (will be updated on nect render anyway) 5211 return; 5212 } 5213 if (aria) { 5214 $ariaElem = $(node.tr || node.li); 5215 } 5216 // Build a list of class names that we will add to the node <span> 5217 cnList.push(cn.node); 5218 if (tree.activeNode === node) { 5219 cnList.push(cn.active); 5220 // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); 5221 // tree.$container.removeAttr("tabindex"); 5222 // }else{ 5223 // $(">span.fancytree-title", statusElem).removeAttr("tabindex"); 5224 // tree.$container.attr("tabindex", "0"); 5225 } 5226 if (tree.focusNode === node) { 5227 cnList.push(cn.focused); 5228 } 5229 if (node.expanded) { 5230 cnList.push(cn.expanded); 5231 } 5232 if (aria) { 5233 if (hasChildren === false) { 5234 $ariaElem.removeAttr("aria-expanded"); 5235 } else { 5236 $ariaElem.attr("aria-expanded", Boolean(node.expanded)); 5237 } 5238 } 5239 if (node.folder) { 5240 cnList.push(cn.folder); 5241 } 5242 if (hasChildren !== false) { 5243 cnList.push(cn.hasChildren); 5244 } 5245 // TODO: required? 5246 if (isLastSib) { 5247 cnList.push(cn.lastsib); 5248 } 5249 if (node.lazy && node.children == null) { 5250 cnList.push(cn.lazy); 5251 } 5252 if (node.partload) { 5253 cnList.push(cn.partload); 5254 } 5255 if (node.partsel) { 5256 cnList.push(cn.partsel); 5257 } 5258 if (FT.evalOption("unselectable", node, node, opts, false)) { 5259 cnList.push(cn.unselectable); 5260 } 5261 if (node._isLoading) { 5262 cnList.push(cn.loading); 5263 } 5264 if (node._error) { 5265 cnList.push(cn.error); 5266 } 5267 if (node.statusNodeType) { 5268 cnList.push(cn.statusNodePrefix + node.statusNodeType); 5269 } 5270 if (node.selected) { 5271 cnList.push(cn.selected); 5272 if (aria) { 5273 $ariaElem.attr("aria-selected", true); 5274 } 5275 } else if (aria) { 5276 $ariaElem.attr("aria-selected", false); 5277 } 5278 if (node.extraClasses) { 5279 cnList.push(node.extraClasses); 5280 } 5281 // IE6 doesn't correctly evaluate multiple class names, 5282 // so we create combined class names that can be used in the CSS 5283 if (hasChildren === false) { 5284 cnList.push( 5285 cn.combinedExpanderPrefix + "n" + (isLastSib ? "l" : "") 5286 ); 5287 } else { 5288 cnList.push( 5289 cn.combinedExpanderPrefix + 5290 (node.expanded ? "e" : "c") + 5291 (node.lazy && node.children == null ? "d" : "") + 5292 (isLastSib ? "l" : "") 5293 ); 5294 } 5295 cnList.push( 5296 cn.combinedIconPrefix + 5297 (node.expanded ? "e" : "c") + 5298 (node.folder ? "f" : "") 5299 ); 5300 // node.span.className = cnList.join(" "); 5301 statusElem.className = cnList.join(" "); 5302 5303 // TODO: we should not set this in the <span> tag also, if we set it here: 5304 // Maybe most (all) of the classes should be set in LI instead of SPAN? 5305 if (node.li) { 5306 // #719: we have to consider that there may be already other classes: 5307 $(node.li).toggleClass(cn.lastsib, isLastSib); 5308 } 5309 }, 5310 /** Activate node. 5311 * flag defaults to true. 5312 * If flag is true, the node is activated (must be a synchronous operation) 5313 * If flag is false, the node is deactivated (must be a synchronous operation) 5314 * @param {EventData} ctx 5315 * @param {boolean} [flag=true] 5316 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false} 5317 * @returns {$.Promise} 5318 */ 5319 nodeSetActive: function (ctx, flag, callOpts) { 5320 // Handle user click / [space] / [enter], according to clickFolderMode. 5321 callOpts = callOpts || {}; 5322 var subCtx, 5323 node = ctx.node, 5324 tree = ctx.tree, 5325 opts = ctx.options, 5326 noEvents = callOpts.noEvents === true, 5327 noFocus = callOpts.noFocus === true, 5328 scroll = callOpts.scrollIntoView !== false, 5329 isActive = node === tree.activeNode; 5330 5331 // flag defaults to true 5332 flag = flag !== false; 5333 // node.debug("nodeSetActive", flag); 5334 5335 if (isActive === flag) { 5336 // Nothing to do 5337 return _getResolvedPromise(node); 5338 } 5339 // #1042: don't scroll between mousedown/-up when clicking an embedded link 5340 if ( 5341 scroll && 5342 ctx.originalEvent && 5343 $(ctx.originalEvent.target).is("a,:checkbox") 5344 ) { 5345 node.info("Not scrolling while clicking an embedded link."); 5346 scroll = false; 5347 } 5348 if ( 5349 flag && 5350 !noEvents && 5351 this._triggerNodeEvent( 5352 "beforeActivate", 5353 node, 5354 ctx.originalEvent 5355 ) === false 5356 ) { 5357 // Callback returned false 5358 return _getRejectedPromise(node, ["rejected"]); 5359 } 5360 if (flag) { 5361 if (tree.activeNode) { 5362 _assert( 5363 tree.activeNode !== node, 5364 "node was active (inconsistency)" 5365 ); 5366 subCtx = $.extend({}, ctx, { node: tree.activeNode }); 5367 tree.nodeSetActive(subCtx, false); 5368 _assert( 5369 tree.activeNode === null, 5370 "deactivate was out of sync?" 5371 ); 5372 } 5373 5374 if (opts.activeVisible) { 5375 // If no focus is set (noFocus: true) and there is no focused node, this node is made visible. 5376 // scroll = noFocus && tree.focusNode == null; 5377 // #863: scroll by default (unless `scrollIntoView: false` was passed) 5378 node.makeVisible({ scrollIntoView: scroll }); 5379 } 5380 tree.activeNode = node; 5381 tree.nodeRenderStatus(ctx); 5382 if (!noFocus) { 5383 tree.nodeSetFocus(ctx); 5384 } 5385 if (!noEvents) { 5386 tree._triggerNodeEvent( 5387 "activate", 5388 node, 5389 ctx.originalEvent 5390 ); 5391 } 5392 } else { 5393 _assert( 5394 tree.activeNode === node, 5395 "node was not active (inconsistency)" 5396 ); 5397 tree.activeNode = null; 5398 this.nodeRenderStatus(ctx); 5399 if (!noEvents) { 5400 ctx.tree._triggerNodeEvent( 5401 "deactivate", 5402 node, 5403 ctx.originalEvent 5404 ); 5405 } 5406 } 5407 return _getResolvedPromise(node); 5408 }, 5409 /** Expand or collapse node, return Deferred.promise. 5410 * 5411 * @param {EventData} ctx 5412 * @param {boolean} [flag=true] 5413 * @param {object} [opts] additional options. Defaults to `{noAnimation: false, noEvents: false}` 5414 * @returns {$.Promise} The deferred will be resolved as soon as the (lazy) 5415 * data was retrieved, rendered, and the expand animation finished. 5416 */ 5417 nodeSetExpanded: function (ctx, flag, callOpts) { 5418 callOpts = callOpts || {}; 5419 var _afterLoad, 5420 dfd, 5421 i, 5422 l, 5423 parents, 5424 prevAC, 5425 node = ctx.node, 5426 tree = ctx.tree, 5427 opts = ctx.options, 5428 noAnimation = callOpts.noAnimation === true, 5429 noEvents = callOpts.noEvents === true; 5430 5431 // flag defaults to true 5432 flag = flag !== false; 5433 5434 // node.debug("nodeSetExpanded(" + flag + ")"); 5435 5436 if ($(node.li).hasClass(opts._classNames.animating)) { 5437 node.warn( 5438 "setExpanded(" + flag + ") while animating: ignored." 5439 ); 5440 return _getRejectedPromise(node, ["recursion"]); 5441 } 5442 5443 if ((node.expanded && flag) || (!node.expanded && !flag)) { 5444 // Nothing to do 5445 // node.debug("nodeSetExpanded(" + flag + "): nothing to do"); 5446 return _getResolvedPromise(node); 5447 } else if (flag && !node.lazy && !node.hasChildren()) { 5448 // Prevent expanding of empty nodes 5449 // return _getRejectedPromise(node, ["empty"]); 5450 return _getResolvedPromise(node); 5451 } else if (!flag && node.getLevel() < opts.minExpandLevel) { 5452 // Prevent collapsing locked levels 5453 return _getRejectedPromise(node, ["locked"]); 5454 } else if ( 5455 !noEvents && 5456 this._triggerNodeEvent( 5457 "beforeExpand", 5458 node, 5459 ctx.originalEvent 5460 ) === false 5461 ) { 5462 // Callback returned false 5463 return _getRejectedPromise(node, ["rejected"]); 5464 } 5465 // If this node inside a collpased node, no animation and scrolling is needed 5466 if (!noAnimation && !node.isVisible()) { 5467 noAnimation = callOpts.noAnimation = true; 5468 } 5469 5470 dfd = new $.Deferred(); 5471 5472 // Auto-collapse mode: collapse all siblings 5473 if (flag && !node.expanded && opts.autoCollapse) { 5474 parents = node.getParentList(false, true); 5475 prevAC = opts.autoCollapse; 5476 try { 5477 opts.autoCollapse = false; 5478 for (i = 0, l = parents.length; i < l; i++) { 5479 // TODO: should return promise? 5480 this._callHook( 5481 "nodeCollapseSiblings", 5482 parents[i], 5483 callOpts 5484 ); 5485 } 5486 } finally { 5487 opts.autoCollapse = prevAC; 5488 } 5489 } 5490 // Trigger expand/collapse after expanding 5491 dfd.done(function () { 5492 var lastChild = node.getLastChild(); 5493 5494 if ( 5495 flag && 5496 opts.autoScroll && 5497 !noAnimation && 5498 lastChild && 5499 tree._enableUpdate 5500 ) { 5501 // Scroll down to last child, but keep current node visible 5502 lastChild 5503 .scrollIntoView(true, { topNode: node }) 5504 .always(function () { 5505 if (!noEvents) { 5506 ctx.tree._triggerNodeEvent( 5507 flag ? "expand" : "collapse", 5508 ctx 5509 ); 5510 } 5511 }); 5512 } else { 5513 if (!noEvents) { 5514 ctx.tree._triggerNodeEvent( 5515 flag ? "expand" : "collapse", 5516 ctx 5517 ); 5518 } 5519 } 5520 }); 5521 // vvv Code below is executed after loading finished: 5522 _afterLoad = function (callback) { 5523 var cn = opts._classNames, 5524 isVisible, 5525 isExpanded, 5526 effect = opts.toggleEffect; 5527 5528 node.expanded = flag; 5529 tree._callHook( 5530 "treeStructureChanged", 5531 ctx, 5532 flag ? "expand" : "collapse" 5533 ); 5534 // Create required markup, but make sure the top UL is hidden, so we 5535 // can animate later 5536 tree._callHook("nodeRender", ctx, false, false, true); 5537 5538 // Hide children, if node is collapsed 5539 if (node.ul) { 5540 isVisible = node.ul.style.display !== "none"; 5541 isExpanded = !!node.expanded; 5542 if (isVisible === isExpanded) { 5543 node.warn( 5544 "nodeSetExpanded: UL.style.display already set" 5545 ); 5546 } else if (!effect || noAnimation) { 5547 node.ul.style.display = 5548 node.expanded || !parent ? "" : "none"; 5549 } else { 5550 // The UI toggle() effect works with the ext-wide extension, 5551 // while jQuery.animate() has problems when the title span 5552 // has position: absolute. 5553 // Since jQuery UI 1.12, the blind effect requires the parent 5554 // element to have 'position: relative'. 5555 // See #716, #717 5556 $(node.li).addClass(cn.animating); // #717 5557 5558 if (_isFunction($(node.ul)[effect.effect])) { 5559 // tree.debug( "use jquery." + effect.effect + " method" ); 5560 $(node.ul)[effect.effect]({ 5561 duration: effect.duration, 5562 always: function () { 5563 // node.debug("fancytree-animating end: " + node.li.className); 5564 $(this).removeClass(cn.animating); // #716 5565 $(node.li).removeClass(cn.animating); // #717 5566 callback(); 5567 }, 5568 }); 5569 } else { 5570 // The UI toggle() effect works with the ext-wide extension, 5571 // while jQuery.animate() has problems when the title span 5572 // has positon: absolute. 5573 // Since jQuery UI 1.12, the blind effect requires the parent 5574 // element to have 'position: relative'. 5575 // See #716, #717 5576 // tree.debug("use specified effect (" + effect.effect + ") with the jqueryui.toggle method"); 5577 5578 // try to stop an animation that might be already in progress 5579 $(node.ul).stop(true, true); //< does not work after resetLazy has been called for a node whose animation wasn't complete and effect was "blind" 5580 5581 // dirty fix to remove a defunct animation (effect: "blind") after resetLazy has been called 5582 $(node.ul) 5583 .parent() 5584 .find(".ui-effects-placeholder") 5585 .remove(); 5586 5587 $(node.ul).toggle( 5588 effect.effect, 5589 effect.options, 5590 effect.duration, 5591 function () { 5592 // node.debug("fancytree-animating end: " + node.li.className); 5593 $(this).removeClass(cn.animating); // #716 5594 $(node.li).removeClass(cn.animating); // #717 5595 callback(); 5596 } 5597 ); 5598 } 5599 return; 5600 } 5601 } 5602 callback(); 5603 }; 5604 // ^^^ Code above is executed after loading finshed. 5605 5606 // Load lazy nodes, if any. Then continue with _afterLoad() 5607 if (flag && node.lazy && node.hasChildren() === undefined) { 5608 // node.debug("nodeSetExpanded: load start..."); 5609 node.load() 5610 .done(function () { 5611 // node.debug("nodeSetExpanded: load done"); 5612 if (dfd.notifyWith) { 5613 // requires jQuery 1.6+ 5614 dfd.notifyWith(node, ["loaded"]); 5615 } 5616 _afterLoad(function () { 5617 dfd.resolveWith(node); 5618 }); 5619 }) 5620 .fail(function (errMsg) { 5621 _afterLoad(function () { 5622 dfd.rejectWith(node, [ 5623 "load failed (" + errMsg + ")", 5624 ]); 5625 }); 5626 }); 5627 /* 5628 var source = tree._triggerNodeEvent("lazyLoad", node, ctx.originalEvent); 5629 _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result"); 5630 node.debug("nodeSetExpanded: load start..."); 5631 this._callHook("nodeLoadChildren", ctx, source).done(function(){ 5632 node.debug("nodeSetExpanded: load done"); 5633 if(dfd.notifyWith){ // requires jQuery 1.6+ 5634 dfd.notifyWith(node, ["loaded"]); 5635 } 5636 _afterLoad.call(tree); 5637 }).fail(function(errMsg){ 5638 dfd.rejectWith(node, ["load failed (" + errMsg + ")"]); 5639 }); 5640 */ 5641 } else { 5642 _afterLoad(function () { 5643 dfd.resolveWith(node); 5644 }); 5645 } 5646 // node.debug("nodeSetExpanded: returns"); 5647 return dfd.promise(); 5648 }, 5649 /** Focus or blur this node. 5650 * @param {EventData} ctx 5651 * @param {boolean} [flag=true] 5652 */ 5653 nodeSetFocus: function (ctx, flag) { 5654 // ctx.node.debug("nodeSetFocus(" + flag + ")"); 5655 var ctx2, 5656 tree = ctx.tree, 5657 node = ctx.node, 5658 opts = tree.options, 5659 // et = ctx.originalEvent && ctx.originalEvent.type, 5660 isInput = ctx.originalEvent 5661 ? $(ctx.originalEvent.target).is(":input") 5662 : false; 5663 5664 flag = flag !== false; 5665 5666 // (node || tree).debug("nodeSetFocus(" + flag + "), event: " + et + ", isInput: "+ isInput); 5667 // Blur previous node if any 5668 if (tree.focusNode) { 5669 if (tree.focusNode === node && flag) { 5670 // node.debug("nodeSetFocus(" + flag + "): nothing to do"); 5671 return; 5672 } 5673 ctx2 = $.extend({}, ctx, { node: tree.focusNode }); 5674 tree.focusNode = null; 5675 this._triggerNodeEvent("blur", ctx2); 5676 this._callHook("nodeRenderStatus", ctx2); 5677 } 5678 // Set focus to container and node 5679 if (flag) { 5680 if (!this.hasFocus()) { 5681 node.debug("nodeSetFocus: forcing container focus"); 5682 this._callHook("treeSetFocus", ctx, true, { 5683 calledByNode: true, 5684 }); 5685 } 5686 node.makeVisible({ scrollIntoView: false }); 5687 tree.focusNode = node; 5688 if (opts.titlesTabbable) { 5689 if (!isInput) { 5690 // #621 5691 $(node.span).find(".fancytree-title").focus(); 5692 } 5693 } 5694 if (opts.aria) { 5695 // Set active descendant to node's span ID (create one, if needed) 5696 $(tree.$container).attr( 5697 "aria-activedescendant", 5698 $(node.tr || node.li) 5699 .uniqueId() 5700 .attr("id") 5701 ); 5702 // "ftal_" + opts.idPrefix + node.key); 5703 } 5704 // $(node.span).find(".fancytree-title").focus(); 5705 this._triggerNodeEvent("focus", ctx); 5706 5707 // determine if we have focus on or inside tree container 5708 var hasFancytreeFocus = 5709 document.activeElement === tree.$container.get(0) || 5710 $(document.activeElement, tree.$container).length >= 1; 5711 5712 if (!hasFancytreeFocus) { 5713 // We cannot set KB focus to a node, so use the tree container 5714 // #563, #570: IE scrolls on every call to .focus(), if the container 5715 // is partially outside the viewport. So do it only, when absolutely 5716 // necessary. 5717 $(tree.$container).focus(); 5718 } 5719 5720 // if( opts.autoActivate ){ 5721 // tree.nodeSetActive(ctx, true); 5722 // } 5723 if (opts.autoScroll) { 5724 node.scrollIntoView(); 5725 } 5726 this._callHook("nodeRenderStatus", ctx); 5727 } 5728 }, 5729 /** (De)Select node, return new status (sync). 5730 * 5731 * @param {EventData} ctx 5732 * @param {boolean} [flag=true] 5733 * @param {object} [opts] additional options. Defaults to {noEvents: false, 5734 * propagateDown: null, propagateUp: null, 5735 * callback: null, 5736 * } 5737 * @returns {boolean} previous status 5738 */ 5739 nodeSetSelected: function (ctx, flag, callOpts) { 5740 callOpts = callOpts || {}; 5741 var node = ctx.node, 5742 tree = ctx.tree, 5743 opts = ctx.options, 5744 noEvents = callOpts.noEvents === true, 5745 parent = node.parent; 5746 5747 // flag defaults to true 5748 flag = flag !== false; 5749 5750 // node.debug("nodeSetSelected(" + flag + ")", ctx); 5751 5752 // Cannot (de)select unselectable nodes directly (only by propagation or 5753 // by setting the `.selected` property) 5754 if (FT.evalOption("unselectable", node, node, opts, false)) { 5755 return; 5756 } 5757 5758 // Remember the user's intent, in case down -> up propagation prevents 5759 // applying it to node.selected 5760 node._lastSelectIntent = flag; // Confusing use of '!' 5761 5762 // Nothing to do? 5763 if (!!node.selected === flag) { 5764 if (opts.selectMode === 3 && node.partsel && !flag) { 5765 // If propagation prevented selecting this node last time, we still 5766 // want to allow to apply setSelected(false) now 5767 } else { 5768 return flag; 5769 } 5770 } 5771 5772 if ( 5773 !noEvents && 5774 this._triggerNodeEvent( 5775 "beforeSelect", 5776 node, 5777 ctx.originalEvent 5778 ) === false 5779 ) { 5780 return !!node.selected; 5781 } 5782 if (flag && opts.selectMode === 1) { 5783 // single selection mode (we don't uncheck all tree nodes, for performance reasons) 5784 if (tree.lastSelectedNode) { 5785 tree.lastSelectedNode.setSelected(false); 5786 } 5787 node.selected = flag; 5788 } else if ( 5789 opts.selectMode === 3 && 5790 parent && 5791 !parent.radiogroup && 5792 !node.radiogroup 5793 ) { 5794 // multi-hierarchical selection mode 5795 node.selected = flag; 5796 node.fixSelection3AfterClick(callOpts); 5797 } else if (parent && parent.radiogroup) { 5798 node.visitSiblings(function (n) { 5799 n._changeSelectStatusAttrs(flag && n === node); 5800 }, true); 5801 } else { 5802 // default: selectMode: 2, multi selection mode 5803 node.selected = flag; 5804 } 5805 this.nodeRenderStatus(ctx); 5806 tree.lastSelectedNode = flag ? node : null; 5807 if (!noEvents) { 5808 tree._triggerNodeEvent("select", ctx); 5809 } 5810 }, 5811 /** Show node status (ok, loading, error, nodata) using styles and a dummy child node. 5812 * 5813 * @param {EventData} ctx 5814 * @param status 5815 * @param message 5816 * @param details 5817 * @since 2.3 5818 */ 5819 nodeSetStatus: function (ctx, status, message, details) { 5820 var node = ctx.node, 5821 tree = ctx.tree; 5822 5823 function _clearStatusNode() { 5824 // Remove dedicated dummy node, if any 5825 var firstChild = node.children ? node.children[0] : null; 5826 if (firstChild && firstChild.isStatusNode()) { 5827 try { 5828 // I've seen exceptions here with loadKeyPath... 5829 if (node.ul) { 5830 node.ul.removeChild(firstChild.li); 5831 firstChild.li = null; // avoid leaks (DT issue 215) 5832 } 5833 } catch (e) {} 5834 if (node.children.length === 1) { 5835 node.children = []; 5836 } else { 5837 node.children.shift(); 5838 } 5839 tree._callHook( 5840 "treeStructureChanged", 5841 ctx, 5842 "clearStatusNode" 5843 ); 5844 } 5845 } 5846 function _setStatusNode(data, type) { 5847 // Create/modify the dedicated dummy node for 'loading...' or 5848 // 'error!' status. (only called for direct child of the invisible 5849 // system root) 5850 var firstChild = node.children ? node.children[0] : null; 5851 if (firstChild && firstChild.isStatusNode()) { 5852 $.extend(firstChild, data); 5853 firstChild.statusNodeType = type; 5854 tree._callHook("nodeRenderTitle", firstChild); 5855 } else { 5856 node._setChildren([data]); 5857 tree._callHook( 5858 "treeStructureChanged", 5859 ctx, 5860 "setStatusNode" 5861 ); 5862 node.children[0].statusNodeType = type; 5863 tree.render(); 5864 } 5865 return node.children[0]; 5866 } 5867 5868 switch (status) { 5869 case "ok": 5870 _clearStatusNode(); 5871 node._isLoading = false; 5872 node._error = null; 5873 node.renderStatus(); 5874 break; 5875 case "loading": 5876 if (!node.parent) { 5877 _setStatusNode( 5878 { 5879 title: 5880 tree.options.strings.loading + 5881 (message ? " (" + message + ")" : ""), 5882 // icon: true, // needed for 'loding' icon 5883 checkbox: false, 5884 tooltip: details, 5885 }, 5886 status 5887 ); 5888 } 5889 node._isLoading = true; 5890 node._error = null; 5891 node.renderStatus(); 5892 break; 5893 case "error": 5894 _setStatusNode( 5895 { 5896 title: 5897 tree.options.strings.loadError + 5898 (message ? " (" + message + ")" : ""), 5899 // icon: false, 5900 checkbox: false, 5901 tooltip: details, 5902 }, 5903 status 5904 ); 5905 node._isLoading = false; 5906 node._error = { message: message, details: details }; 5907 node.renderStatus(); 5908 break; 5909 case "nodata": 5910 _setStatusNode( 5911 { 5912 title: message || tree.options.strings.noData, 5913 // icon: false, 5914 checkbox: false, 5915 tooltip: details, 5916 }, 5917 status 5918 ); 5919 node._isLoading = false; 5920 node._error = null; 5921 node.renderStatus(); 5922 break; 5923 default: 5924 $.error("invalid node status " + status); 5925 } 5926 }, 5927 /** 5928 * 5929 * @param {EventData} ctx 5930 */ 5931 nodeToggleExpanded: function (ctx) { 5932 return this.nodeSetExpanded(ctx, !ctx.node.expanded); 5933 }, 5934 /** 5935 * @param {EventData} ctx 5936 */ 5937 nodeToggleSelected: function (ctx) { 5938 var node = ctx.node, 5939 flag = !node.selected; 5940 5941 // In selectMode: 3 this node may be unselected+partsel, even if 5942 // setSelected(true) was called before, due to `unselectable` children. 5943 // In this case, we now toggle as `setSelected(false)` 5944 if ( 5945 node.partsel && 5946 !node.selected && 5947 node._lastSelectIntent === true 5948 ) { 5949 flag = false; 5950 node.selected = true; // so it is not considered 'nothing to do' 5951 } 5952 node._lastSelectIntent = flag; 5953 return this.nodeSetSelected(ctx, flag); 5954 }, 5955 /** Remove all nodes. 5956 * @param {EventData} ctx 5957 */ 5958 treeClear: function (ctx) { 5959 var tree = ctx.tree; 5960 tree.activeNode = null; 5961 tree.focusNode = null; 5962 tree.$div.find(">ul.fancytree-container").empty(); 5963 // TODO: call destructors and remove reference loops 5964 tree.rootNode.children = null; 5965 tree._callHook("treeStructureChanged", ctx, "clear"); 5966 }, 5967 /** Widget was created (called only once, even it re-initialized). 5968 * @param {EventData} ctx 5969 */ 5970 treeCreate: function (ctx) {}, 5971 /** Widget was destroyed. 5972 * @param {EventData} ctx 5973 */ 5974 treeDestroy: function (ctx) { 5975 this.$div.find(">ul.fancytree-container").remove(); 5976 if (this.$source) { 5977 this.$source.removeClass("fancytree-helper-hidden"); 5978 } 5979 }, 5980 /** Widget was (re-)initialized. 5981 * @param {EventData} ctx 5982 */ 5983 treeInit: function (ctx) { 5984 var tree = ctx.tree, 5985 opts = tree.options; 5986 5987 //this.debug("Fancytree.treeInit()"); 5988 // Add container to the TAB chain 5989 // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant 5990 // #577: Allow to set tabindex to "0", "-1" and "" 5991 tree.$container.attr("tabindex", opts.tabindex); 5992 5993 // Copy some attributes to tree.data 5994 $.each(TREE_ATTRS, function (i, attr) { 5995 if (opts[attr] !== undefined) { 5996 tree.info("Move option " + attr + " to tree"); 5997 tree[attr] = opts[attr]; 5998 delete opts[attr]; 5999 } 6000 }); 6001 6002 if (opts.checkboxAutoHide) { 6003 tree.$container.addClass("fancytree-checkbox-auto-hide"); 6004 } 6005 if (opts.rtl) { 6006 tree.$container 6007 .attr("DIR", "RTL") 6008 .addClass("fancytree-rtl"); 6009 } else { 6010 tree.$container 6011 .removeAttr("DIR") 6012 .removeClass("fancytree-rtl"); 6013 } 6014 if (opts.aria) { 6015 tree.$container.attr("role", "tree"); 6016 if (opts.selectMode !== 1) { 6017 tree.$container.attr("aria-multiselectable", true); 6018 } 6019 } 6020 this.treeLoad(ctx); 6021 }, 6022 /** Parse Fancytree from source, as configured in the options. 6023 * @param {EventData} ctx 6024 * @param {object} [source] optional new source (use last data otherwise) 6025 */ 6026 treeLoad: function (ctx, source) { 6027 var metaData, 6028 type, 6029 $ul, 6030 tree = ctx.tree, 6031 $container = ctx.widget.element, 6032 dfd, 6033 // calling context for root node 6034 rootCtx = $.extend({}, ctx, { node: this.rootNode }); 6035 6036 if (tree.rootNode.children) { 6037 this.treeClear(ctx); 6038 } 6039 source = source || this.options.source; 6040 6041 if (!source) { 6042 type = $container.data("type") || "html"; 6043 switch (type) { 6044 case "html": 6045 // There should be an embedded `<ul>` with initial nodes, 6046 // but another `<ul class='fancytree-container'>` is appended 6047 // to the tree's <div> on startup anyway. 6048 $ul = $container 6049 .find(">ul") 6050 .not(".fancytree-container") 6051 .first(); 6052 6053 if ($ul.length) { 6054 $ul.addClass( 6055 "ui-fancytree-source fancytree-helper-hidden" 6056 ); 6057 source = $.ui.fancytree.parseHtml($ul); 6058 // allow to init tree.data.foo from <ul data-foo=''> 6059 this.data = $.extend( 6060 this.data, 6061 _getElementDataAsDict($ul) 6062 ); 6063 } else { 6064 FT.warn( 6065 "No `source` option was passed and container does not contain `<ul>`: assuming `source: []`." 6066 ); 6067 source = []; 6068 } 6069 break; 6070 case "json": 6071 source = $.parseJSON($container.text()); 6072 // $container already contains the <ul>, but we remove the plain (json) text 6073 // $container.empty(); 6074 $container 6075 .contents() 6076 .filter(function () { 6077 return this.nodeType === 3; 6078 }) 6079 .remove(); 6080 if ($.isPlainObject(source)) { 6081 // We got {foo: 'abc', children: [...]} 6082 _assert( 6083 _isArray(source.children), 6084 "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')" 6085 ); 6086 metaData = source; 6087 source = source.children; 6088 delete metaData.children; 6089 // Copy some attributes to tree.data 6090 $.each(TREE_ATTRS, function (i, attr) { 6091 if (metaData[attr] !== undefined) { 6092 tree[attr] = metaData[attr]; 6093 delete metaData[attr]; 6094 } 6095 }); 6096 // Copy extra properties to tree.data.foo 6097 $.extend(tree.data, metaData); 6098 } 6099 break; 6100 default: 6101 $.error("Invalid data-type: " + type); 6102 } 6103 } else if (typeof source === "string") { 6104 // TODO: source is an element ID 6105 $.error("Not implemented"); 6106 } 6107 6108 // preInit is fired when the widget markup is created, but nodes 6109 // not yet loaded 6110 tree._triggerTreeEvent("preInit", null); 6111 6112 // Trigger fancytreeinit after nodes have been loaded 6113 dfd = this.nodeLoadChildren(rootCtx, source) 6114 .done(function () { 6115 tree._callHook( 6116 "treeStructureChanged", 6117 ctx, 6118 "loadChildren" 6119 ); 6120 tree.render(); 6121 if (ctx.options.selectMode === 3) { 6122 tree.rootNode.fixSelection3FromEndNodes(); 6123 } 6124 if (tree.activeNode && tree.options.activeVisible) { 6125 tree.activeNode.makeVisible(); 6126 } 6127 tree._triggerTreeEvent("init", null, { status: true }); 6128 }) 6129 .fail(function () { 6130 tree.render(); 6131 tree._triggerTreeEvent("init", null, { status: false }); 6132 }); 6133 return dfd; 6134 }, 6135 /** Node was inserted into or removed from the tree. 6136 * @param {EventData} ctx 6137 * @param {boolean} add 6138 * @param {FancytreeNode} node 6139 */ 6140 treeRegisterNode: function (ctx, add, node) { 6141 ctx.tree._callHook( 6142 "treeStructureChanged", 6143 ctx, 6144 add ? "addNode" : "removeNode" 6145 ); 6146 }, 6147 /** Widget got focus. 6148 * @param {EventData} ctx 6149 * @param {boolean} [flag=true] 6150 */ 6151 treeSetFocus: function (ctx, flag, callOpts) { 6152 var targetNode; 6153 6154 flag = flag !== false; 6155 6156 // this.debug("treeSetFocus(" + flag + "), callOpts: ", callOpts, this.hasFocus()); 6157 // this.debug(" focusNode: " + this.focusNode); 6158 // this.debug(" activeNode: " + this.activeNode); 6159 if (flag !== this.hasFocus()) { 6160 this._hasFocus = flag; 6161 if (!flag && this.focusNode) { 6162 // Node also looses focus if widget blurs 6163 this.focusNode.setFocus(false); 6164 } else if (flag && (!callOpts || !callOpts.calledByNode)) { 6165 $(this.$container).focus(); 6166 } 6167 this.$container.toggleClass("fancytree-treefocus", flag); 6168 this._triggerTreeEvent(flag ? "focusTree" : "blurTree"); 6169 if (flag && !this.activeNode) { 6170 // #712: Use last mousedowned node ('click' event fires after focusin) 6171 targetNode = 6172 this._lastMousedownNode || this.getFirstChild(); 6173 if (targetNode) { 6174 targetNode.setFocus(); 6175 } 6176 } 6177 } 6178 }, 6179 /** Widget option was set using `$().fancytree("option", "KEY", VALUE)`. 6180 * 6181 * Note: `key` may reference a nested option, e.g. 'dnd5.scroll'. 6182 * In this case `value`contains the complete, modified `dnd5` option hash. 6183 * We can check for changed values like 6184 * if( value.scroll !== tree.options.dnd5.scroll ) {...} 6185 * 6186 * @param {EventData} ctx 6187 * @param {string} key option name 6188 * @param {any} value option value 6189 */ 6190 treeSetOption: function (ctx, key, value) { 6191 var tree = ctx.tree, 6192 callDefault = true, 6193 callCreate = false, 6194 callRender = false; 6195 6196 switch (key) { 6197 case "aria": 6198 case "checkbox": 6199 case "icon": 6200 case "minExpandLevel": 6201 case "tabindex": 6202 // tree._callHook("treeCreate", tree); 6203 callCreate = true; 6204 callRender = true; 6205 break; 6206 case "checkboxAutoHide": 6207 tree.$container.toggleClass( 6208 "fancytree-checkbox-auto-hide", 6209 !!value 6210 ); 6211 break; 6212 case "escapeTitles": 6213 case "tooltip": 6214 callRender = true; 6215 break; 6216 case "rtl": 6217 if (value === false) { 6218 tree.$container 6219 .removeAttr("DIR") 6220 .removeClass("fancytree-rtl"); 6221 } else { 6222 tree.$container 6223 .attr("DIR", "RTL") 6224 .addClass("fancytree-rtl"); 6225 } 6226 callRender = true; 6227 break; 6228 case "source": 6229 callDefault = false; 6230 tree._callHook("treeLoad", tree, value); 6231 callRender = true; 6232 break; 6233 } 6234 tree.debug( 6235 "set option " + 6236 key + 6237 "=" + 6238 value + 6239 " <" + 6240 typeof value + 6241 ">" 6242 ); 6243 if (callDefault) { 6244 if (this.widget._super) { 6245 // jQuery UI 1.9+ 6246 this.widget._super.call(this.widget, key, value); 6247 } else { 6248 // jQuery UI <= 1.8, we have to manually invoke the _setOption method from the base widget 6249 $.Widget.prototype._setOption.call( 6250 this.widget, 6251 key, 6252 value 6253 ); 6254 } 6255 } 6256 if (callCreate) { 6257 tree._callHook("treeCreate", tree); 6258 } 6259 if (callRender) { 6260 tree.render(true, false); // force, not-deep 6261 } 6262 }, 6263 /** A Node was added, removed, moved, or it's visibility changed. 6264 * @param {EventData} ctx 6265 */ 6266 treeStructureChanged: function (ctx, type) {}, 6267 } 6268 ); 6269 6270 /******************************************************************************* 6271 * jQuery UI widget boilerplate 6272 */ 6273 6274 /** 6275 * The plugin (derrived from [jQuery.Widget](http://api.jqueryui.com/jQuery.widget/)). 6276 * 6277 * **Note:** 6278 * These methods implement the standard jQuery UI widget API. 6279 * It is recommended to use methods of the {Fancytree} instance instead 6280 * 6281 * @example 6282 * // DEPRECATED: Access jQuery UI widget methods and members: 6283 * var tree = $("#tree").fancytree("getTree"); 6284 * var node = $("#tree").fancytree("getActiveNode"); 6285 * 6286 * // RECOMMENDED: Use the Fancytree object API 6287 * var tree = $.ui.fancytree.getTree("#tree"); 6288 * var node = tree.getActiveNode(); 6289 * 6290 * // or you may already have stored the tree instance upon creation: 6291 * import {createTree, version} from 'jquery.fancytree' 6292 * const tree = createTree('#tree', { ... }); 6293 * var node = tree.getActiveNode(); 6294 * 6295 * @see {Fancytree_Static#getTree} 6296 * @deprecated Use methods of the {Fancytree} instance instead 6297 * @mixin Fancytree_Widget 6298 */ 6299 6300 $.widget( 6301 "ui.fancytree", 6302 /** @lends Fancytree_Widget# */ 6303 { 6304 /**These options will be used as defaults 6305 * @type {FancytreeOptions} 6306 */ 6307 options: { 6308 activeVisible: true, 6309 ajax: { 6310 type: "GET", 6311 cache: false, // false: Append random '_' argument to the request url to prevent caching. 6312 // timeout: 0, // >0: Make sure we get an ajax error if server is unreachable 6313 dataType: "json", // Expect json format and pass json object to callbacks. 6314 }, 6315 aria: true, 6316 autoActivate: true, 6317 autoCollapse: false, 6318 autoScroll: false, 6319 checkbox: false, 6320 clickFolderMode: 4, 6321 copyFunctionsToData: false, 6322 debugLevel: null, // 0..4 (null: use global setting $.ui.fancytree.debugLevel) 6323 disabled: false, // TODO: required anymore? 6324 enableAspx: 42, // TODO: this is truethy, but distinguishable from true: default will change to false in the future 6325 escapeTitles: false, 6326 extensions: [], 6327 focusOnSelect: false, 6328 generateIds: false, 6329 icon: true, 6330 idPrefix: "ft_", 6331 keyboard: true, 6332 keyPathSeparator: "/", 6333 minExpandLevel: 1, 6334 nodata: true, // (bool, string, or callback) display message, when no data available 6335 quicksearch: false, 6336 rtl: false, 6337 scrollOfs: { top: 0, bottom: 0 }, 6338 scrollParent: null, 6339 selectMode: 2, 6340 strings: { 6341 loading: "Loading...", // … would be escaped when escapeTitles is true 6342 loadError: "Load error!", 6343 moreData: "More...", 6344 noData: "No data.", 6345 }, 6346 tabindex: "0", 6347 titlesTabbable: false, 6348 toggleEffect: { effect: "slideToggle", duration: 200 }, //< "toggle" or "slideToggle" to use jQuery instead of jQueryUI for toggleEffect animation 6349 tooltip: false, 6350 treeId: null, 6351 _classNames: { 6352 active: "fancytree-active", 6353 animating: "fancytree-animating", 6354 combinedExpanderPrefix: "fancytree-exp-", 6355 combinedIconPrefix: "fancytree-ico-", 6356 error: "fancytree-error", 6357 expanded: "fancytree-expanded", 6358 focused: "fancytree-focused", 6359 folder: "fancytree-folder", 6360 hasChildren: "fancytree-has-children", 6361 lastsib: "fancytree-lastsib", 6362 lazy: "fancytree-lazy", 6363 loading: "fancytree-loading", 6364 node: "fancytree-node", 6365 partload: "fancytree-partload", 6366 partsel: "fancytree-partsel", 6367 radio: "fancytree-radio", 6368 selected: "fancytree-selected", 6369 statusNodePrefix: "fancytree-statusnode-", 6370 unselectable: "fancytree-unselectable", 6371 }, 6372 // events 6373 lazyLoad: null, 6374 postProcess: null, 6375 }, 6376 _deprecationWarning: function (name) { 6377 var tree = this.tree; 6378 6379 if (tree && tree.options.debugLevel >= 3) { 6380 tree.warn( 6381 "$().fancytree('" + 6382 name + 6383 "') is deprecated (see https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Widget.html" 6384 ); 6385 } 6386 }, 6387 /* Set up the widget, Called on first $().fancytree() */ 6388 _create: function () { 6389 this.tree = new Fancytree(this); 6390 6391 this.$source = 6392 this.source || this.element.data("type") === "json" 6393 ? this.element 6394 : this.element.find(">ul").first(); 6395 // Subclass Fancytree instance with all enabled extensions 6396 var extension, 6397 extName, 6398 i, 6399 opts = this.options, 6400 extensions = opts.extensions, 6401 base = this.tree; 6402 6403 for (i = 0; i < extensions.length; i++) { 6404 extName = extensions[i]; 6405 extension = $.ui.fancytree._extensions[extName]; 6406 if (!extension) { 6407 $.error( 6408 "Could not apply extension '" + 6409 extName + 6410 "' (it is not registered, did you forget to include it?)" 6411 ); 6412 } 6413 // Add extension options as tree.options.EXTENSION 6414 // _assert(!this.tree.options[extName], "Extension name must not exist as option name: " + extName); 6415 6416 // console.info("extend " + extName, extension.options, this.tree.options[extName]) 6417 // issue #876: we want to replace custom array-options, not merge them 6418 this.tree.options[extName] = _simpleDeepMerge( 6419 {}, 6420 extension.options, 6421 this.tree.options[extName] 6422 ); 6423 // this.tree.options[extName] = $.extend(true, {}, extension.options, this.tree.options[extName]); 6424 6425 // console.info("extend " + extName + " =>", this.tree.options[extName]) 6426 // console.info("extend " + extName + " org default =>", extension.options) 6427 6428 // Add a namespace tree.ext.EXTENSION, to hold instance data 6429 _assert( 6430 this.tree.ext[extName] === undefined, 6431 "Extension name must not exist as Fancytree.ext attribute: '" + 6432 extName + 6433 "'" 6434 ); 6435 // this.tree[extName] = extension; 6436 this.tree.ext[extName] = {}; 6437 // Subclass Fancytree methods using proxies. 6438 _subclassObject(this.tree, base, extension, extName); 6439 // current extension becomes base for the next extension 6440 base = extension; 6441 } 6442 // 6443 if (opts.icons !== undefined) { 6444 // 2015-11-16 6445 if (opts.icon === true) { 6446 this.tree.warn( 6447 "'icons' tree option is deprecated since v2.14.0: use 'icon' instead" 6448 ); 6449 opts.icon = opts.icons; 6450 } else { 6451 $.error( 6452 "'icons' tree option is deprecated since v2.14.0: use 'icon' only instead" 6453 ); 6454 } 6455 } 6456 if (opts.iconClass !== undefined) { 6457 // 2015-11-16 6458 if (opts.icon) { 6459 $.error( 6460 "'iconClass' tree option is deprecated since v2.14.0: use 'icon' only instead" 6461 ); 6462 } else { 6463 this.tree.warn( 6464 "'iconClass' tree option is deprecated since v2.14.0: use 'icon' instead" 6465 ); 6466 opts.icon = opts.iconClass; 6467 } 6468 } 6469 if (opts.tabbable !== undefined) { 6470 // 2016-04-04 6471 opts.tabindex = opts.tabbable ? "0" : "-1"; 6472 this.tree.warn( 6473 "'tabbable' tree option is deprecated since v2.17.0: use 'tabindex='" + 6474 opts.tabindex + 6475 "' instead" 6476 ); 6477 } 6478 // 6479 this.tree._callHook("treeCreate", this.tree); 6480 // Note: 'fancytreecreate' event is fired by widget base class 6481 // this.tree._triggerTreeEvent("create"); 6482 }, 6483 6484 /* Called on every $().fancytree() */ 6485 _init: function () { 6486 this.tree._callHook("treeInit", this.tree); 6487 // TODO: currently we call bind after treeInit, because treeInit 6488 // might change tree.$container. 6489 // It would be better, to move event binding into hooks altogether 6490 this._bind(); 6491 }, 6492 6493 /* Use the _setOption method to respond to changes to options. */ 6494 _setOption: function (key, value) { 6495 return this.tree._callHook( 6496 "treeSetOption", 6497 this.tree, 6498 key, 6499 value 6500 ); 6501 }, 6502 6503 /** Use the destroy method to clean up any modifications your widget has made to the DOM */ 6504 _destroy: function () { 6505 this._unbind(); 6506 this.tree._callHook("treeDestroy", this.tree); 6507 // In jQuery UI 1.8, you must invoke the destroy method from the base widget 6508 // $.Widget.prototype.destroy.call(this); 6509 // TODO: delete tree and nodes to make garbage collect easier? 6510 // TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method 6511 }, 6512 6513 // ------------------------------------------------------------------------- 6514 6515 /* Remove all event handlers for our namespace */ 6516 _unbind: function () { 6517 var ns = this.tree._ns; 6518 this.element.off(ns); 6519 this.tree.$container.off(ns); 6520 $(document).off(ns); 6521 }, 6522 /* Add mouse and kyboard handlers to the container */ 6523 _bind: function () { 6524 var self = this, 6525 opts = this.options, 6526 tree = this.tree, 6527 ns = tree._ns; 6528 // selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" ) 6529 6530 // Remove all previuous handlers for this tree 6531 this._unbind(); 6532 6533 //alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container); 6534 // tree.debug("bind events; container: ", tree.$container); 6535 tree.$container 6536 .on("focusin" + ns + " focusout" + ns, function (event) { 6537 var node = FT.getNode(event), 6538 flag = event.type === "focusin"; 6539 6540 if (!flag && node && $(event.target).is("a")) { 6541 // #764 6542 node.debug( 6543 "Ignored focusout on embedded <a> element." 6544 ); 6545 return; 6546 } 6547 // tree.treeOnFocusInOut.call(tree, event); 6548 // tree.debug("Tree container got event " + event.type, node, event, FT.getEventTarget(event)); 6549 if (flag) { 6550 if (tree._getExpiringValue("focusin")) { 6551 // #789: IE 11 may send duplicate focusin events 6552 tree.debug("Ignored double focusin."); 6553 return; 6554 } 6555 tree._setExpiringValue("focusin", true, 50); 6556 6557 if (!node) { 6558 // #789: IE 11 may send focusin before mousdown(?) 6559 node = tree._getExpiringValue("mouseDownNode"); 6560 if (node) { 6561 tree.debug( 6562 "Reconstruct mouse target for focusin from recent event." 6563 ); 6564 } 6565 } 6566 } 6567 if (node) { 6568 // For example clicking into an <input> that is part of a node 6569 tree._callHook( 6570 "nodeSetFocus", 6571 tree._makeHookContext(node, event), 6572 flag 6573 ); 6574 } else { 6575 if ( 6576 tree.tbody && 6577 $(event.target).parents( 6578 "table.fancytree-container > thead" 6579 ).length 6580 ) { 6581 // #767: ignore events in the table's header 6582 tree.debug( 6583 "Ignore focus event outside table body.", 6584 event 6585 ); 6586 } else { 6587 tree._callHook("treeSetFocus", tree, flag); 6588 } 6589 } 6590 }) 6591 .on( 6592 "selectstart" + ns, 6593 "span.fancytree-title", 6594 function (event) { 6595 // prevent mouse-drags to select text ranges 6596 // tree.debug("<span title> got event " + event.type); 6597 event.preventDefault(); 6598 } 6599 ) 6600 .on("keydown" + ns, function (event) { 6601 // TODO: also bind keyup and keypress 6602 // tree.debug("got event " + event.type + ", hasFocus:" + tree.hasFocus()); 6603 // if(opts.disabled || opts.keyboard === false || !tree.hasFocus() ){ 6604 if (opts.disabled || opts.keyboard === false) { 6605 return true; 6606 } 6607 var res, 6608 node = tree.focusNode, // node may be null 6609 ctx = tree._makeHookContext(node || tree, event), 6610 prevPhase = tree.phase; 6611 6612 try { 6613 tree.phase = "userEvent"; 6614 // If a 'fancytreekeydown' handler returns false, skip the default 6615 // handling (implemented by tree.nodeKeydown()). 6616 if (node) { 6617 res = tree._triggerNodeEvent( 6618 "keydown", 6619 node, 6620 event 6621 ); 6622 } else { 6623 res = tree._triggerTreeEvent("keydown", event); 6624 } 6625 if (res === "preventNav") { 6626 res = true; // prevent keyboard navigation, but don't prevent default handling of embedded input controls 6627 } else if (res !== false) { 6628 res = tree._callHook("nodeKeydown", ctx); 6629 } 6630 return res; 6631 } finally { 6632 tree.phase = prevPhase; 6633 } 6634 }) 6635 .on("mousedown" + ns, function (event) { 6636 var et = FT.getEventTarget(event); 6637 // self.tree.debug("event(" + event.type + "): node: ", et.node); 6638 // #712: Store the clicked node, so we can use it when we get a focusin event 6639 // ('click' event fires after focusin) 6640 // tree.debug("event(" + event.type + "): node: ", et.node); 6641 tree._lastMousedownNode = et ? et.node : null; 6642 // #789: Store the node also for a short period, so we can use it 6643 // in a *resulting* focusin event 6644 tree._setExpiringValue( 6645 "mouseDownNode", 6646 tree._lastMousedownNode 6647 ); 6648 }) 6649 .on("click" + ns + " dblclick" + ns, function (event) { 6650 if (opts.disabled) { 6651 return true; 6652 } 6653 var ctx, 6654 et = FT.getEventTarget(event), 6655 node = et.node, 6656 tree = self.tree, 6657 prevPhase = tree.phase; 6658 6659 // self.tree.debug("event(" + event.type + "): node: ", node); 6660 if (!node) { 6661 return true; // Allow bubbling of other events 6662 } 6663 ctx = tree._makeHookContext(node, event); 6664 // self.tree.debug("event(" + event.type + "): node: ", node); 6665 try { 6666 tree.phase = "userEvent"; 6667 switch (event.type) { 6668 case "click": 6669 ctx.targetType = et.type; 6670 if (node.isPagingNode()) { 6671 return ( 6672 tree._triggerNodeEvent( 6673 "clickPaging", 6674 ctx, 6675 event 6676 ) === true 6677 ); 6678 } 6679 return tree._triggerNodeEvent( 6680 "click", 6681 ctx, 6682 event 6683 ) === false 6684 ? false 6685 : tree._callHook("nodeClick", ctx); 6686 case "dblclick": 6687 ctx.targetType = et.type; 6688 return tree._triggerNodeEvent( 6689 "dblclick", 6690 ctx, 6691 event 6692 ) === false 6693 ? false 6694 : tree._callHook("nodeDblclick", ctx); 6695 } 6696 } finally { 6697 tree.phase = prevPhase; 6698 } 6699 }); 6700 }, 6701 /** Return the active node or null. 6702 * @returns {FancytreeNode} 6703 * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>). 6704 */ 6705 getActiveNode: function () { 6706 this._deprecationWarning("getActiveNode"); 6707 return this.tree.activeNode; 6708 }, 6709 /** Return the matching node or null. 6710 * @param {string} key 6711 * @returns {FancytreeNode} 6712 * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>). 6713 */ 6714 getNodeByKey: function (key) { 6715 this._deprecationWarning("getNodeByKey"); 6716 return this.tree.getNodeByKey(key); 6717 }, 6718 /** Return the invisible system root node. 6719 * @returns {FancytreeNode} 6720 * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>). 6721 */ 6722 getRootNode: function () { 6723 this._deprecationWarning("getRootNode"); 6724 return this.tree.rootNode; 6725 }, 6726 /** Return the current tree instance. 6727 * @returns {Fancytree} 6728 * @deprecated Use `$.ui.fancytree.getTree()` instead (<a href="Fancytree_Widget.html">example above</a>). 6729 */ 6730 getTree: function () { 6731 this._deprecationWarning("getTree"); 6732 return this.tree; 6733 }, 6734 } 6735 ); 6736 6737 // $.ui.fancytree was created by the widget factory. Create a local shortcut: 6738 FT = $.ui.fancytree; 6739 6740 /** 6741 * Static members in the `$.ui.fancytree` namespace. 6742 * This properties and methods can be accessed without instantiating a concrete 6743 * Fancytree instance. 6744 * 6745 * @example 6746 * // Access static members: 6747 * var node = $.ui.fancytree.getNode(element); 6748 * alert($.ui.fancytree.version); 6749 * 6750 * @mixin Fancytree_Static 6751 */ 6752 $.extend( 6753 $.ui.fancytree, 6754 /** @lends Fancytree_Static# */ 6755 { 6756 /** Version number `"MAJOR.MINOR.PATCH"` 6757 * @type {string} */ 6758 version: "2.38.3", // Set to semver by 'grunt release' 6759 /** @type {string} 6760 * @description `"production" for release builds` */ 6761 buildType: "production", // Set to 'production' by 'grunt build' 6762 /** @type {int} 6763 * @description 0: silent .. 5: verbose (default: 3 for release builds). */ 6764 debugLevel: 3, // Set to 3 by 'grunt build' 6765 // Used by $.ui.fancytree.debug() and as default for tree.options.debugLevel 6766 6767 _nextId: 1, 6768 _nextNodeKey: 1, 6769 _extensions: {}, 6770 // focusTree: null, 6771 6772 /** Expose class object as `$.ui.fancytree._FancytreeClass`. 6773 * Useful to extend `$.ui.fancytree._FancytreeClass.prototype`. 6774 * @type {Fancytree} 6775 */ 6776 _FancytreeClass: Fancytree, 6777 /** Expose class object as $.ui.fancytree._FancytreeNodeClass 6778 * Useful to extend `$.ui.fancytree._FancytreeNodeClass.prototype`. 6779 * @type {FancytreeNode} 6780 */ 6781 _FancytreeNodeClass: FancytreeNode, 6782 /* Feature checks to provide backwards compatibility */ 6783 jquerySupports: { 6784 // http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at 6785 positionMyOfs: isVersionAtLeast($.ui.version, 1, 9), 6786 }, 6787 /** Throw an error if condition fails (debug method). 6788 * @param {boolean} cond 6789 * @param {string} msg 6790 */ 6791 assert: function (cond, msg) { 6792 return _assert(cond, msg); 6793 }, 6794 /** Create a new Fancytree instance on a target element. 6795 * 6796 * @param {Element | jQueryObject | string} el Target DOM element or selector 6797 * @param {FancytreeOptions} [opts] Fancytree options 6798 * @returns {Fancytree} new tree instance 6799 * @example 6800 * var tree = $.ui.fancytree.createTree("#tree", { 6801 * source: {url: "my/webservice"} 6802 * }); // Create tree for this matching element 6803 * 6804 * @since 2.25 6805 */ 6806 createTree: function (el, opts) { 6807 var $tree = $(el).fancytree(opts); 6808 return FT.getTree($tree); 6809 }, 6810 /** Return a function that executes *fn* at most every *timeout* ms. 6811 * @param {integer} timeout 6812 * @param {function} fn 6813 * @param {boolean} [invokeAsap=false] 6814 * @param {any} [ctx] 6815 */ 6816 debounce: function (timeout, fn, invokeAsap, ctx) { 6817 var timer; 6818 if (arguments.length === 3 && typeof invokeAsap !== "boolean") { 6819 ctx = invokeAsap; 6820 invokeAsap = false; 6821 } 6822 return function () { 6823 var args = arguments; 6824 ctx = ctx || this; 6825 // eslint-disable-next-line no-unused-expressions 6826 invokeAsap && !timer && fn.apply(ctx, args); 6827 clearTimeout(timer); 6828 timer = setTimeout(function () { 6829 // eslint-disable-next-line no-unused-expressions 6830 invokeAsap || fn.apply(ctx, args); 6831 timer = null; 6832 }, timeout); 6833 }; 6834 }, 6835 /** Write message to console if debugLevel >= 4 6836 * @param {string} msg 6837 */ 6838 debug: function (msg) { 6839 if ($.ui.fancytree.debugLevel >= 4) { 6840 consoleApply("log", arguments); 6841 } 6842 }, 6843 /** Write error message to console if debugLevel >= 1. 6844 * @param {string} msg 6845 */ 6846 error: function (msg) { 6847 if ($.ui.fancytree.debugLevel >= 1) { 6848 consoleApply("error", arguments); 6849 } 6850 }, 6851 /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. 6852 * 6853 * @param {string} s 6854 * @returns {string} 6855 */ 6856 escapeHtml: function (s) { 6857 return ("" + s).replace(REX_HTML, function (s) { 6858 return ENTITY_MAP[s]; 6859 }); 6860 }, 6861 /** Make jQuery.position() arguments backwards compatible, i.e. if 6862 * jQuery UI version <= 1.8, convert 6863 * { my: "left+3 center", at: "left bottom", of: $target } 6864 * to 6865 * { my: "left center", at: "left bottom", of: $target, offset: "3 0" } 6866 * 6867 * See http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at 6868 * and http://jsfiddle.net/mar10/6xtu9a4e/ 6869 * 6870 * @param {object} opts 6871 * @returns {object} the (potentially modified) original opts hash object 6872 */ 6873 fixPositionOptions: function (opts) { 6874 if (opts.offset || ("" + opts.my + opts.at).indexOf("%") >= 0) { 6875 $.error( 6876 "expected new position syntax (but '%' is not supported)" 6877 ); 6878 } 6879 if (!$.ui.fancytree.jquerySupports.positionMyOfs) { 6880 var // parse 'left+3 center' into ['left+3 center', 'left', '+3', 'center', undefined] 6881 myParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec( 6882 opts.my 6883 ), 6884 atParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec( 6885 opts.at 6886 ), 6887 // convert to numbers 6888 dx = 6889 (myParts[2] ? +myParts[2] : 0) + 6890 (atParts[2] ? +atParts[2] : 0), 6891 dy = 6892 (myParts[4] ? +myParts[4] : 0) + 6893 (atParts[4] ? +atParts[4] : 0); 6894 6895 opts = $.extend({}, opts, { 6896 // make a copy and overwrite 6897 my: myParts[1] + " " + myParts[3], 6898 at: atParts[1] + " " + atParts[3], 6899 }); 6900 if (dx || dy) { 6901 opts.offset = "" + dx + " " + dy; 6902 } 6903 } 6904 return opts; 6905 }, 6906 /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event. 6907 * 6908 * @param {Event} event Mouse event, e.g. click, ... 6909 * @returns {object} Return a {node: FancytreeNode, type: TYPE} object 6910 * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined 6911 */ 6912 getEventTarget: function (event) { 6913 var $target, 6914 tree, 6915 tcn = event && event.target ? event.target.className : "", 6916 res = { node: this.getNode(event.target), type: undefined }; 6917 // We use a fast version of $(res.node).hasClass() 6918 // See http://jsperf.com/test-for-classname/2 6919 if (/\bfancytree-title\b/.test(tcn)) { 6920 res.type = "title"; 6921 } else if (/\bfancytree-expander\b/.test(tcn)) { 6922 res.type = 6923 res.node.hasChildren() === false 6924 ? "prefix" 6925 : "expander"; 6926 // }else if( /\bfancytree-checkbox\b/.test(tcn) || /\bfancytree-radio\b/.test(tcn) ){ 6927 } else if (/\bfancytree-checkbox\b/.test(tcn)) { 6928 res.type = "checkbox"; 6929 } else if (/\bfancytree(-custom)?-icon\b/.test(tcn)) { 6930 res.type = "icon"; 6931 } else if (/\bfancytree-node\b/.test(tcn)) { 6932 // Somewhere near the title 6933 res.type = "title"; 6934 } else if (event && event.target) { 6935 $target = $(event.target); 6936 if ($target.is("ul[role=group]")) { 6937 // #nnn: Clicking right to a node may hit the surrounding UL 6938 tree = res.node && res.node.tree; 6939 (tree || FT).debug("Ignoring click on outer UL."); 6940 res.node = null; 6941 } else if ($target.closest(".fancytree-title").length) { 6942 // #228: clicking an embedded element inside a title 6943 res.type = "title"; 6944 } else if ($target.closest(".fancytree-checkbox").length) { 6945 // E.g. <svg> inside checkbox span 6946 res.type = "checkbox"; 6947 } else if ($target.closest(".fancytree-expander").length) { 6948 res.type = "expander"; 6949 } 6950 } 6951 return res; 6952 }, 6953 /** Return a string describing the affected node region for a mouse event. 6954 * 6955 * @param {Event} event Mouse event, e.g. click, mousemove, ... 6956 * @returns {string} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined 6957 */ 6958 getEventTargetType: function (event) { 6959 return this.getEventTarget(event).type; 6960 }, 6961 /** Return a FancytreeNode instance from element, event, or jQuery object. 6962 * 6963 * @param {Element | jQueryObject | Event} el 6964 * @returns {FancytreeNode} matching node or null 6965 */ 6966 getNode: function (el) { 6967 if (el instanceof FancytreeNode) { 6968 return el; // el already was a FancytreeNode 6969 } else if (el instanceof $) { 6970 el = el[0]; // el was a jQuery object: use the DOM element 6971 } else if (el.originalEvent !== undefined) { 6972 el = el.target; // el was an Event 6973 } 6974 while (el) { 6975 if (el.ftnode) { 6976 return el.ftnode; 6977 } 6978 el = el.parentNode; 6979 } 6980 return null; 6981 }, 6982 /** Return a Fancytree instance, from element, index, event, or jQueryObject. 6983 * 6984 * @param {Element | jQueryObject | Event | integer | string} [el] 6985 * @returns {Fancytree} matching tree or null 6986 * @example 6987 * $.ui.fancytree.getTree(); // Get first Fancytree instance on page 6988 * $.ui.fancytree.getTree(1); // Get second Fancytree instance on page 6989 * $.ui.fancytree.getTree(event); // Get tree for this mouse- or keyboard event 6990 * $.ui.fancytree.getTree("foo"); // Get tree for this `opts.treeId` 6991 * $.ui.fancytree.getTree("#tree"); // Get tree for this matching element 6992 * 6993 * @since 2.13 6994 */ 6995 getTree: function (el) { 6996 var widget, 6997 orgEl = el; 6998 6999 if (el instanceof Fancytree) { 7000 return el; // el already was a Fancytree 7001 } 7002 if (el === undefined) { 7003 el = 0; // get first tree 7004 } 7005 if (typeof el === "number") { 7006 el = $(".fancytree-container").eq(el); // el was an integer: return nth instance 7007 } else if (typeof el === "string") { 7008 // `el` may be a treeId or a selector: 7009 el = $("#ft-id-" + orgEl).eq(0); 7010 if (!el.length) { 7011 el = $(orgEl).eq(0); // el was a selector: use first match 7012 } 7013 } else if ( 7014 el instanceof Element || 7015 el instanceof HTMLDocument 7016 ) { 7017 el = $(el); 7018 } else if (el instanceof $) { 7019 el = el.eq(0); // el was a jQuery object: use the first 7020 } else if (el.originalEvent !== undefined) { 7021 el = $(el.target); // el was an Event 7022 } 7023 // el is a jQuery object wit one element here 7024 el = el.closest(":ui-fancytree"); 7025 widget = el.data("ui-fancytree") || el.data("fancytree"); // the latter is required by jQuery <= 1.8 7026 return widget ? widget.tree : null; 7027 }, 7028 /** Return an option value that has a default, but may be overridden by a 7029 * callback or a node instance attribute. 7030 * 7031 * Evaluation sequence: 7032 * 7033 * If `tree.options.<optionName>` is a callback that returns something, use that. 7034 * Else if `node.<optionName>` is defined, use that. 7035 * Else if `tree.options.<optionName>` is a value, use that. 7036 * Else use `defaultValue`. 7037 * 7038 * @param {string} optionName name of the option property (on node and tree) 7039 * @param {FancytreeNode} node passed to the callback 7040 * @param {object} nodeObject where to look for the local option property, e.g. `node` or `node.data` 7041 * @param {object} treeOption where to look for the tree option, e.g. `tree.options` or `tree.options.dnd5` 7042 * @param {any} [defaultValue] 7043 * @returns {any} 7044 * 7045 * @example 7046 * // Check for node.foo, tree,options.foo(), and tree.options.foo: 7047 * $.ui.fancytree.evalOption("foo", node, node, tree.options); 7048 * // Check for node.data.bar, tree,options.qux.bar(), and tree.options.qux.bar: 7049 * $.ui.fancytree.evalOption("bar", node, node.data, tree.options.qux); 7050 * 7051 * @since 2.22 7052 */ 7053 evalOption: function ( 7054 optionName, 7055 node, 7056 nodeObject, 7057 treeOptions, 7058 defaultValue 7059 ) { 7060 var ctx, 7061 res, 7062 tree = node.tree, 7063 treeOpt = treeOptions[optionName], 7064 nodeOpt = nodeObject[optionName]; 7065 7066 if (_isFunction(treeOpt)) { 7067 ctx = { 7068 node: node, 7069 tree: tree, 7070 widget: tree.widget, 7071 options: tree.widget.options, 7072 typeInfo: tree.types[node.type] || {}, 7073 }; 7074 res = treeOpt.call(tree, { type: optionName }, ctx); 7075 if (res == null) { 7076 res = nodeOpt; 7077 } 7078 } else { 7079 res = nodeOpt == null ? treeOpt : nodeOpt; 7080 } 7081 if (res == null) { 7082 res = defaultValue; // no option set at all: return default 7083 } 7084 return res; 7085 }, 7086 /** Set expander, checkbox, or node icon, supporting string and object format. 7087 * 7088 * @param {Element | jQueryObject} span 7089 * @param {string} baseClass 7090 * @param {string | object} icon 7091 * @since 2.27 7092 */ 7093 setSpanIcon: function (span, baseClass, icon) { 7094 var $span = $(span); 7095 7096 if (typeof icon === "string") { 7097 $span.attr("class", baseClass + " " + icon); 7098 } else { 7099 // support object syntax: { text: ligature, addClasse: classname } 7100 if (icon.text) { 7101 $span.text("" + icon.text); 7102 } else if (icon.html) { 7103 span.innerHTML = icon.html; 7104 } 7105 $span.attr( 7106 "class", 7107 baseClass + " " + (icon.addClass || "") 7108 ); 7109 } 7110 }, 7111 /** Convert a keydown or mouse event to a canonical string like 'ctrl+a', 7112 * 'ctrl+shift+f2', 'shift+leftdblclick'. 7113 * 7114 * This is especially handy for switch-statements in event handlers. 7115 * 7116 * @param {event} 7117 * @returns {string} 7118 * 7119 * @example 7120 7121 switch( $.ui.fancytree.eventToString(event) ) { 7122 case "-": 7123 tree.nodeSetExpanded(ctx, false); 7124 break; 7125 case "shift+return": 7126 tree.nodeSetActive(ctx, true); 7127 break; 7128 case "down": 7129 res = node.navigate(event.which, activate); 7130 break; 7131 default: 7132 handled = false; 7133 } 7134 if( handled ){ 7135 event.preventDefault(); 7136 } 7137 */ 7138 eventToString: function (event) { 7139 // Poor-man's hotkeys. See here for a complete implementation: 7140 // https://github.com/jeresig/jquery.hotkeys 7141 var which = event.which, 7142 et = event.type, 7143 s = []; 7144 7145 if (event.altKey) { 7146 s.push("alt"); 7147 } 7148 if (event.ctrlKey) { 7149 s.push("ctrl"); 7150 } 7151 if (event.metaKey) { 7152 s.push("meta"); 7153 } 7154 if (event.shiftKey) { 7155 s.push("shift"); 7156 } 7157 7158 if (et === "click" || et === "dblclick") { 7159 s.push(MOUSE_BUTTONS[event.button] + et); 7160 } else if (et === "wheel") { 7161 s.push(et); 7162 } else if (!IGNORE_KEYCODES[which]) { 7163 s.push( 7164 SPECIAL_KEYCODES[which] || 7165 String.fromCharCode(which).toLowerCase() 7166 ); 7167 } 7168 return s.join("+"); 7169 }, 7170 /** Write message to console if debugLevel >= 3 7171 * @param {string} msg 7172 */ 7173 info: function (msg) { 7174 if ($.ui.fancytree.debugLevel >= 3) { 7175 consoleApply("info", arguments); 7176 } 7177 }, 7178 /* @deprecated: use eventToString(event) instead. 7179 */ 7180 keyEventToString: function (event) { 7181 this.warn( 7182 "keyEventToString() is deprecated: use eventToString()" 7183 ); 7184 return this.eventToString(event); 7185 }, 7186 /** Return a wrapped handler method, that provides `this._super`. 7187 * 7188 * @example 7189 // Implement `opts.createNode` event to add the 'draggable' attribute 7190 $.ui.fancytree.overrideMethod(ctx.options, "createNode", function(event, data) { 7191 // Default processing if any 7192 this._super.apply(this, arguments); 7193 // Add 'draggable' attribute 7194 data.node.span.draggable = true; 7195 }); 7196 * 7197 * @param {object} instance 7198 * @param {string} methodName 7199 * @param {function} handler 7200 * @param {object} [context] optional context 7201 */ 7202 overrideMethod: function (instance, methodName, handler, context) { 7203 var prevSuper, 7204 _super = instance[methodName] || $.noop; 7205 7206 instance[methodName] = function () { 7207 var self = context || this; 7208 7209 try { 7210 prevSuper = self._super; 7211 self._super = _super; 7212 return handler.apply(self, arguments); 7213 } finally { 7214 self._super = prevSuper; 7215 } 7216 }; 7217 }, 7218 /** 7219 * Parse tree data from HTML <ul> markup 7220 * 7221 * @param {jQueryObject} $ul 7222 * @returns {NodeData[]} 7223 */ 7224 parseHtml: function ($ul) { 7225 var classes, 7226 className, 7227 extraClasses, 7228 i, 7229 iPos, 7230 l, 7231 tmp, 7232 tmp2, 7233 $children = $ul.find(">li"), 7234 children = []; 7235 7236 $children.each(function () { 7237 var allData, 7238 lowerCaseAttr, 7239 $li = $(this), 7240 $liSpan = $li.find(">span", this).first(), 7241 $liA = $liSpan.length ? null : $li.find(">a").first(), 7242 d = { tooltip: null, data: {} }; 7243 7244 if ($liSpan.length) { 7245 d.title = $liSpan.html(); 7246 } else if ($liA && $liA.length) { 7247 // If a <li><a> tag is specified, use it literally and extract href/target. 7248 d.title = $liA.html(); 7249 d.data.href = $liA.attr("href"); 7250 d.data.target = $liA.attr("target"); 7251 d.tooltip = $liA.attr("title"); 7252 } else { 7253 // If only a <li> tag is specified, use the trimmed string up to 7254 // the next child <ul> tag. 7255 d.title = $li.html(); 7256 iPos = d.title.search(/<ul/i); 7257 if (iPos >= 0) { 7258 d.title = d.title.substring(0, iPos); 7259 } 7260 } 7261 d.title = _trim(d.title); 7262 7263 // Make sure all fields exist 7264 for (i = 0, l = CLASS_ATTRS.length; i < l; i++) { 7265 d[CLASS_ATTRS[i]] = undefined; 7266 } 7267 // Initialize to `true`, if class is set and collect extraClasses 7268 classes = this.className.split(" "); 7269 extraClasses = []; 7270 for (i = 0, l = classes.length; i < l; i++) { 7271 className = classes[i]; 7272 if (CLASS_ATTR_MAP[className]) { 7273 d[className] = true; 7274 } else { 7275 extraClasses.push(className); 7276 } 7277 } 7278 d.extraClasses = extraClasses.join(" "); 7279 7280 // Parse node options from ID, title and class attributes 7281 tmp = $li.attr("title"); 7282 if (tmp) { 7283 d.tooltip = tmp; // overrides <a title='...'> 7284 } 7285 tmp = $li.attr("id"); 7286 if (tmp) { 7287 d.key = tmp; 7288 } 7289 // Translate hideCheckbox -> checkbox:false 7290 if ($li.attr("hideCheckbox")) { 7291 d.checkbox = false; 7292 } 7293 // Add <li data-NAME='...'> as node.data.NAME 7294 allData = _getElementDataAsDict($li); 7295 if (allData && !$.isEmptyObject(allData)) { 7296 // #507: convert data-hidecheckbox (lower case) to hideCheckbox 7297 for (lowerCaseAttr in NODE_ATTR_LOWERCASE_MAP) { 7298 if (_hasProp(allData, lowerCaseAttr)) { 7299 allData[ 7300 NODE_ATTR_LOWERCASE_MAP[lowerCaseAttr] 7301 ] = allData[lowerCaseAttr]; 7302 delete allData[lowerCaseAttr]; 7303 } 7304 } 7305 // #56: Allow to set special node.attributes from data-... 7306 for (i = 0, l = NODE_ATTRS.length; i < l; i++) { 7307 tmp = NODE_ATTRS[i]; 7308 tmp2 = allData[tmp]; 7309 if (tmp2 != null) { 7310 delete allData[tmp]; 7311 d[tmp] = tmp2; 7312 } 7313 } 7314 // All other data-... goes to node.data... 7315 $.extend(d.data, allData); 7316 } 7317 // Recursive reading of child nodes, if LI tag contains an UL tag 7318 $ul = $li.find(">ul").first(); 7319 if ($ul.length) { 7320 d.children = $.ui.fancytree.parseHtml($ul); 7321 } else { 7322 d.children = d.lazy ? undefined : null; 7323 } 7324 children.push(d); 7325 // FT.debug("parse ", d, children); 7326 }); 7327 return children; 7328 }, 7329 /** Add Fancytree extension definition to the list of globally available extensions. 7330 * 7331 * @param {object} definition 7332 */ 7333 registerExtension: function (definition) { 7334 _assert( 7335 definition.name != null, 7336 "extensions must have a `name` property." 7337 ); 7338 _assert( 7339 definition.version != null, 7340 "extensions must have a `version` property." 7341 ); 7342 $.ui.fancytree._extensions[definition.name] = definition; 7343 }, 7344 /** Replacement for the deprecated `jQuery.trim()`. 7345 * 7346 * @param {string} text 7347 */ 7348 trim: _trim, 7349 /** Inverse of escapeHtml(). 7350 * 7351 * @param {string} s 7352 * @returns {string} 7353 */ 7354 unescapeHtml: function (s) { 7355 var e = document.createElement("div"); 7356 e.innerHTML = s; 7357 return e.childNodes.length === 0 7358 ? "" 7359 : e.childNodes[0].nodeValue; 7360 }, 7361 /** Write warning message to console if debugLevel >= 2. 7362 * @param {string} msg 7363 */ 7364 warn: function (msg) { 7365 if ($.ui.fancytree.debugLevel >= 2) { 7366 consoleApply("warn", arguments); 7367 } 7368 }, 7369 } 7370 ); 7371 7372 // Value returned by `require('jquery.fancytree')` 7373 return $.ui.fancytree; 7374}); // End of closure 7375