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 7376// Extending Fancytree 7377// =================== 7378// 7379// See also the [live demo](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html) of this code. 7380// 7381// Every extension should have a comment header containing some information 7382// about the author, copyright and licensing. Also a pointer to the latest 7383// source code. 7384// Prefix with `/*!` so the comment is not removed by the minifier. 7385 7386/*! 7387 * jquery.fancytree.childcounter.js 7388 * 7389 * Add a child counter bubble to tree nodes. 7390 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 7391 * 7392 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 7393 * 7394 * Released under the MIT license 7395 * https://github.com/mar10/fancytree/wiki/LicenseInfo 7396 * 7397 * @version 2.38.3 7398 * @date 2023-02-01T20:52:50Z 7399 */ 7400 7401// To keep the global namespace clean, we wrap everything in a closure. 7402// The UMD wrapper pattern defines the dependencies on jQuery and the 7403// Fancytree core module, and makes sure that we can use the `require()` 7404// syntax with package loaders. 7405 7406(function (factory) { 7407 if (typeof define === "function" && define.amd) { 7408 // AMD. Register as an anonymous module. 7409 define(["jquery", "./jquery.fancytree"], factory); 7410 } else if (typeof module === "object" && module.exports) { 7411 // Node/CommonJS 7412 require("./jquery.fancytree"); 7413 module.exports = factory(require("jquery")); 7414 } else { 7415 // Browser globals 7416 factory(jQuery); 7417 } 7418})(function ($) { 7419 // Consider to use [strict mode](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) 7420 "use strict"; 7421 7422 // The [coding guidelines](http://contribute.jquery.org/style-guide/js/) 7423 // require jshint /eslint compliance. 7424 // But for this sample, we want to allow unused variables for demonstration purpose. 7425 7426 /*eslint-disable no-unused-vars */ 7427 7428 // Adding methods 7429 // -------------- 7430 7431 // New member functions can be added to the `Fancytree` class. 7432 // This function will be available for every tree instance: 7433 // 7434 // var tree = $.ui.fancytree.getTree("#tree"); 7435 // tree.countSelected(false); 7436 7437 $.ui.fancytree._FancytreeClass.prototype.countSelected = function ( 7438 topOnly 7439 ) { 7440 var tree = this, 7441 treeOptions = tree.options; 7442 7443 return tree.getSelectedNodes(topOnly).length; 7444 }; 7445 7446 // The `FancytreeNode` class can also be easily extended. This would be called 7447 // like 7448 // node.updateCounters(); 7449 // 7450 // It is also good practice to add a docstring comment. 7451 /** 7452 * [ext-childcounter] Update counter badges for `node` and its parents. 7453 * May be called in the `loadChildren` event, to update parents of lazy loaded 7454 * nodes. 7455 * @alias FancytreeNode#updateCounters 7456 * @requires jquery.fancytree.childcounters.js 7457 */ 7458 $.ui.fancytree._FancytreeNodeClass.prototype.updateCounters = function () { 7459 var node = this, 7460 $badge = $("span.fancytree-childcounter", node.span), 7461 extOpts = node.tree.options.childcounter, 7462 count = node.countChildren(extOpts.deep); 7463 7464 node.data.childCounter = count; 7465 if ( 7466 (count || !extOpts.hideZeros) && 7467 (!node.isExpanded() || !extOpts.hideExpanded) 7468 ) { 7469 if (!$badge.length) { 7470 $badge = $("<span class='fancytree-childcounter'/>").appendTo( 7471 $( 7472 "span.fancytree-icon,span.fancytree-custom-icon", 7473 node.span 7474 ) 7475 ); 7476 } 7477 $badge.text(count); 7478 } else { 7479 $badge.remove(); 7480 } 7481 if (extOpts.deep && !node.isTopLevel() && !node.isRootNode()) { 7482 node.parent.updateCounters(); 7483 } 7484 }; 7485 7486 // Finally, we can extend the widget API and create functions that are called 7487 // like so: 7488 // 7489 // $("#tree").fancytree("widgetMethod1", "abc"); 7490 7491 $.ui.fancytree.prototype.widgetMethod1 = function (arg1) { 7492 var tree = this.tree; 7493 return arg1; 7494 }; 7495 7496 // Register a Fancytree extension 7497 // ------------------------------ 7498 // A full blown extension, extension is available for all trees and can be 7499 // enabled like so (see also the [live demo](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html)): 7500 // 7501 // <script src="../src/jquery.fancytree.js"></script> 7502 // <script src="../src/jquery.fancytree.childcounter.js"></script> 7503 // ... 7504 // 7505 // $("#tree").fancytree({ 7506 // extensions: ["childcounter"], 7507 // childcounter: { 7508 // hideExpanded: true 7509 // }, 7510 // ... 7511 // }); 7512 // 7513 7514 /* 'childcounter' extension */ 7515 $.ui.fancytree.registerExtension({ 7516 // Every extension must be registered by a unique name. 7517 name: "childcounter", 7518 // Version information should be compliant with [semver](http://semver.org) 7519 version: "2.38.3", 7520 7521 // Extension specific options and their defaults. 7522 // This options will be available as `tree.options.childcounter.hideExpanded` 7523 7524 options: { 7525 deep: true, 7526 hideZeros: true, 7527 hideExpanded: false, 7528 }, 7529 7530 // Attributes other than `options` (or functions) can be defined here, and 7531 // will be added to the tree.ext.EXTNAME namespace, in this case `tree.ext.childcounter.foo`. 7532 // They can also be accessed as `this._local.foo` from within the extension 7533 // methods. 7534 foo: 42, 7535 7536 // Local functions are prefixed with an underscore '_'. 7537 // Callable as `this._local._appendCounter()`. 7538 7539 _appendCounter: function (bar) { 7540 var tree = this; 7541 }, 7542 7543 // **Override virtual methods for this extension.** 7544 // 7545 // Fancytree implements a number of 'hook methods', prefixed by 'node...' or 'tree...'. 7546 // with a `ctx` argument (see [EventData](https://wwWendt.de/tech/fancytree/doc/jsdoc/global.html#EventData) 7547 // for details) and an extended calling context:<br> 7548 // `this` : the Fancytree instance<br> 7549 // `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME)<br> 7550 // `this._super`: the virtual function that was overridden (member of previous extension or Fancytree) 7551 // 7552 // See also the [complete list of available hook functions](https://wwWendt.de/tech/fancytree/doc/jsdoc/Fancytree_Hooks.html). 7553 7554 /* Init */ 7555 // `treeInit` is triggered when a tree is initalized. We can set up classes or 7556 // bind event handlers here... 7557 treeInit: function (ctx) { 7558 var tree = this, // same as ctx.tree, 7559 opts = ctx.options, 7560 extOpts = ctx.options.childcounter; 7561 // Optionally check for dependencies with other extensions 7562 /* this._requireExtension("glyph", false, false); */ 7563 // Call the base implementation 7564 this._superApply(arguments); 7565 // Add a class to the tree container 7566 this.$container.addClass("fancytree-ext-childcounter"); 7567 }, 7568 7569 // Destroy this tree instance (we only call the default implementation, so 7570 // this method could as well be omitted). 7571 7572 treeDestroy: function (ctx) { 7573 this._superApply(arguments); 7574 }, 7575 7576 // Overload the `renderTitle` hook, to append a counter badge 7577 nodeRenderTitle: function (ctx, title) { 7578 var node = ctx.node, 7579 extOpts = ctx.options.childcounter, 7580 count = 7581 node.data.childCounter == null 7582 ? node.countChildren(extOpts.deep) 7583 : +node.data.childCounter; 7584 // Let the base implementation render the title 7585 // We use `_super()` instead of `_superApply()` here, since it is a little bit 7586 // more performant when called often 7587 this._super(ctx, title); 7588 // Append a counter badge 7589 if ( 7590 (count || !extOpts.hideZeros) && 7591 (!node.isExpanded() || !extOpts.hideExpanded) 7592 ) { 7593 $( 7594 "span.fancytree-icon,span.fancytree-custom-icon", 7595 node.span 7596 ).append( 7597 $("<span class='fancytree-childcounter'/>").text(count) 7598 ); 7599 } 7600 }, 7601 // Overload the `setExpanded` hook, so the counters are updated 7602 nodeSetExpanded: function (ctx, flag, callOpts) { 7603 var tree = ctx.tree, 7604 node = ctx.node; 7605 // Let the base implementation expand/collapse the node, then redraw the title 7606 // after the animation has finished 7607 return this._superApply(arguments).always(function () { 7608 tree.nodeRenderTitle(ctx); 7609 }); 7610 }, 7611 7612 // End of extension definition 7613 }); 7614 // Value returned by `require('jquery.fancytree..')` 7615 return $.ui.fancytree; 7616}); // End of closure 7617 7618/*! 7619 * 7620 * jquery.fancytree.clones.js 7621 * Support faster lookup of nodes by key and shared ref-ids. 7622 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 7623 * 7624 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 7625 * 7626 * Released under the MIT license 7627 * https://github.com/mar10/fancytree/wiki/LicenseInfo 7628 * 7629 * @version 2.38.3 7630 * @date 2023-02-01T20:52:50Z 7631 */ 7632 7633(function (factory) { 7634 if (typeof define === "function" && define.amd) { 7635 // AMD. Register as an anonymous module. 7636 define(["jquery", "./jquery.fancytree"], factory); 7637 } else if (typeof module === "object" && module.exports) { 7638 // Node/CommonJS 7639 require("./jquery.fancytree"); 7640 module.exports = factory(require("jquery")); 7641 } else { 7642 // Browser globals 7643 factory(jQuery); 7644 } 7645})(function ($) { 7646 "use strict"; 7647 7648 /******************************************************************************* 7649 * Private functions and variables 7650 */ 7651 7652 var _assert = $.ui.fancytree.assert; 7653 7654 /* Return first occurrence of member from array. */ 7655 function _removeArrayMember(arr, elem) { 7656 // TODO: use Array.indexOf for IE >= 9 7657 var i; 7658 for (i = arr.length - 1; i >= 0; i--) { 7659 if (arr[i] === elem) { 7660 arr.splice(i, 1); 7661 return true; 7662 } 7663 } 7664 return false; 7665 } 7666 7667 /** 7668 * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) 7669 * 7670 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a> 7671 * @see http://github.com/garycourt/murmurhash-js 7672 * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a> 7673 * @see http://sites.google.com/site/murmurhash/ 7674 * 7675 * @param {string} key ASCII only 7676 * @param {boolean} [asString=false] 7677 * @param {number} seed Positive integer only 7678 * @return {number} 32-bit positive integer hash 7679 */ 7680 function hashMurmur3(key, asString, seed) { 7681 /*eslint-disable no-bitwise */ 7682 var h1b, 7683 k1, 7684 remainder = key.length & 3, 7685 bytes = key.length - remainder, 7686 h1 = seed, 7687 c1 = 0xcc9e2d51, 7688 c2 = 0x1b873593, 7689 i = 0; 7690 7691 while (i < bytes) { 7692 k1 = 7693 (key.charCodeAt(i) & 0xff) | 7694 ((key.charCodeAt(++i) & 0xff) << 8) | 7695 ((key.charCodeAt(++i) & 0xff) << 16) | 7696 ((key.charCodeAt(++i) & 0xff) << 24); 7697 ++i; 7698 7699 k1 = 7700 ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 7701 0xffffffff; 7702 k1 = (k1 << 15) | (k1 >>> 17); 7703 k1 = 7704 ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 7705 0xffffffff; 7706 7707 h1 ^= k1; 7708 h1 = (h1 << 13) | (h1 >>> 19); 7709 h1b = 7710 ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 7711 0xffffffff; 7712 h1 = 7713 (h1b & 0xffff) + 7714 0x6b64 + 7715 ((((h1b >>> 16) + 0xe654) & 0xffff) << 16); 7716 } 7717 7718 k1 = 0; 7719 7720 switch (remainder) { 7721 case 3: 7722 k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; 7723 // fall through 7724 case 2: 7725 k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; 7726 // fall through 7727 case 1: 7728 k1 ^= key.charCodeAt(i) & 0xff; 7729 7730 k1 = 7731 ((k1 & 0xffff) * c1 + 7732 ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 7733 0xffffffff; 7734 k1 = (k1 << 15) | (k1 >>> 17); 7735 k1 = 7736 ((k1 & 0xffff) * c2 + 7737 ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 7738 0xffffffff; 7739 h1 ^= k1; 7740 } 7741 7742 h1 ^= key.length; 7743 7744 h1 ^= h1 >>> 16; 7745 h1 = 7746 ((h1 & 0xffff) * 0x85ebca6b + 7747 ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 7748 0xffffffff; 7749 h1 ^= h1 >>> 13; 7750 h1 = 7751 ((h1 & 0xffff) * 0xc2b2ae35 + 7752 ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 7753 0xffffffff; 7754 h1 ^= h1 >>> 16; 7755 7756 if (asString) { 7757 // Convert to 8 digit hex string 7758 return ("0000000" + (h1 >>> 0).toString(16)).substr(-8); 7759 } 7760 return h1 >>> 0; 7761 /*eslint-enable no-bitwise */ 7762 } 7763 7764 /* 7765 * Return a unique key for node by calculating the hash of the parents refKey-list. 7766 */ 7767 function calcUniqueKey(node) { 7768 var key, 7769 h1, 7770 path = $.map(node.getParentList(false, true), function (e) { 7771 return e.refKey || e.key; 7772 }); 7773 7774 path = path.join("/"); 7775 // 32-bit has a high probability of collisions, so we pump up to 64-bit 7776 // https://security.stackexchange.com/q/209882/207588 7777 7778 h1 = hashMurmur3(path, true); 7779 key = "id_" + h1 + hashMurmur3(h1 + path, true); 7780 7781 return key; 7782 } 7783 7784 /** 7785 * [ext-clones] Return a list of clone-nodes (i.e. same refKey) or null. 7786 * @param {boolean} [includeSelf=false] 7787 * @returns {FancytreeNode[] | null} 7788 * 7789 * @alias FancytreeNode#getCloneList 7790 * @requires jquery.fancytree.clones.js 7791 */ 7792 $.ui.fancytree._FancytreeNodeClass.prototype.getCloneList = function ( 7793 includeSelf 7794 ) { 7795 var key, 7796 tree = this.tree, 7797 refList = tree.refMap[this.refKey] || null, 7798 keyMap = tree.keyMap; 7799 7800 if (refList) { 7801 key = this.key; 7802 // Convert key list to node list 7803 if (includeSelf) { 7804 refList = $.map(refList, function (val) { 7805 return keyMap[val]; 7806 }); 7807 } else { 7808 refList = $.map(refList, function (val) { 7809 return val === key ? null : keyMap[val]; 7810 }); 7811 if (refList.length < 1) { 7812 refList = null; 7813 } 7814 } 7815 } 7816 return refList; 7817 }; 7818 7819 /** 7820 * [ext-clones] Return true if this node has at least another clone with same refKey. 7821 * @returns {boolean} 7822 * 7823 * @alias FancytreeNode#isClone 7824 * @requires jquery.fancytree.clones.js 7825 */ 7826 $.ui.fancytree._FancytreeNodeClass.prototype.isClone = function () { 7827 var refKey = this.refKey || null, 7828 refList = (refKey && this.tree.refMap[refKey]) || null; 7829 return !!(refList && refList.length > 1); 7830 }; 7831 7832 /** 7833 * [ext-clones] Update key and/or refKey for an existing node. 7834 * @param {string} key 7835 * @param {string} refKey 7836 * @returns {boolean} 7837 * 7838 * @alias FancytreeNode#reRegister 7839 * @requires jquery.fancytree.clones.js 7840 */ 7841 $.ui.fancytree._FancytreeNodeClass.prototype.reRegister = function ( 7842 key, 7843 refKey 7844 ) { 7845 key = key == null ? null : "" + key; 7846 refKey = refKey == null ? null : "" + refKey; 7847 // this.debug("reRegister", key, refKey); 7848 7849 var tree = this.tree, 7850 prevKey = this.key, 7851 prevRefKey = this.refKey, 7852 keyMap = tree.keyMap, 7853 refMap = tree.refMap, 7854 refList = refMap[prevRefKey] || null, 7855 // curCloneKeys = refList ? node.getCloneList(true), 7856 modified = false; 7857 7858 // Key has changed: update all references 7859 if (key != null && key !== this.key) { 7860 if (keyMap[key]) { 7861 $.error( 7862 "[ext-clones] reRegister(" + 7863 key + 7864 "): already exists: " + 7865 this 7866 ); 7867 } 7868 // Update keyMap 7869 delete keyMap[prevKey]; 7870 keyMap[key] = this; 7871 // Update refMap 7872 if (refList) { 7873 refMap[prevRefKey] = $.map(refList, function (e) { 7874 return e === prevKey ? key : e; 7875 }); 7876 } 7877 this.key = key; 7878 modified = true; 7879 } 7880 7881 // refKey has changed 7882 if (refKey != null && refKey !== this.refKey) { 7883 // Remove previous refKeys 7884 if (refList) { 7885 if (refList.length === 1) { 7886 delete refMap[prevRefKey]; 7887 } else { 7888 refMap[prevRefKey] = $.map(refList, function (e) { 7889 return e === prevKey ? null : e; 7890 }); 7891 } 7892 } 7893 // Add refKey 7894 if (refMap[refKey]) { 7895 refMap[refKey].append(key); 7896 } else { 7897 refMap[refKey] = [this.key]; 7898 } 7899 this.refKey = refKey; 7900 modified = true; 7901 } 7902 return modified; 7903 }; 7904 7905 /** 7906 * [ext-clones] Define a refKey for an existing node. 7907 * @param {string} refKey 7908 * @returns {boolean} 7909 * 7910 * @alias FancytreeNode#setRefKey 7911 * @requires jquery.fancytree.clones.js 7912 * @since 2.16 7913 */ 7914 $.ui.fancytree._FancytreeNodeClass.prototype.setRefKey = function (refKey) { 7915 return this.reRegister(null, refKey); 7916 }; 7917 7918 /** 7919 * [ext-clones] Return all nodes with a given refKey (null if not found). 7920 * @param {string} refKey 7921 * @param {FancytreeNode} [rootNode] optionally restrict results to descendants of this node 7922 * @returns {FancytreeNode[] | null} 7923 * @alias Fancytree#getNodesByRef 7924 * @requires jquery.fancytree.clones.js 7925 */ 7926 $.ui.fancytree._FancytreeClass.prototype.getNodesByRef = function ( 7927 refKey, 7928 rootNode 7929 ) { 7930 var keyMap = this.keyMap, 7931 refList = this.refMap[refKey] || null; 7932 7933 if (refList) { 7934 // Convert key list to node list 7935 if (rootNode) { 7936 refList = $.map(refList, function (val) { 7937 var node = keyMap[val]; 7938 return node.isDescendantOf(rootNode) ? node : null; 7939 }); 7940 } else { 7941 refList = $.map(refList, function (val) { 7942 return keyMap[val]; 7943 }); 7944 } 7945 if (refList.length < 1) { 7946 refList = null; 7947 } 7948 } 7949 return refList; 7950 }; 7951 7952 /** 7953 * [ext-clones] Replace a refKey with a new one. 7954 * @param {string} oldRefKey 7955 * @param {string} newRefKey 7956 * @alias Fancytree#changeRefKey 7957 * @requires jquery.fancytree.clones.js 7958 */ 7959 $.ui.fancytree._FancytreeClass.prototype.changeRefKey = function ( 7960 oldRefKey, 7961 newRefKey 7962 ) { 7963 var i, 7964 node, 7965 keyMap = this.keyMap, 7966 refList = this.refMap[oldRefKey] || null; 7967 7968 if (refList) { 7969 for (i = 0; i < refList.length; i++) { 7970 node = keyMap[refList[i]]; 7971 node.refKey = newRefKey; 7972 } 7973 delete this.refMap[oldRefKey]; 7974 this.refMap[newRefKey] = refList; 7975 } 7976 }; 7977 7978 /******************************************************************************* 7979 * Extension code 7980 */ 7981 $.ui.fancytree.registerExtension({ 7982 name: "clones", 7983 version: "2.38.3", 7984 // Default options for this extension. 7985 options: { 7986 highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers 7987 highlightClones: false, // set 'fancytree-clone' class on any node that has at least one clone 7988 }, 7989 7990 treeCreate: function (ctx) { 7991 this._superApply(arguments); 7992 ctx.tree.refMap = {}; 7993 ctx.tree.keyMap = {}; 7994 }, 7995 treeInit: function (ctx) { 7996 this.$container.addClass("fancytree-ext-clones"); 7997 _assert(ctx.options.defaultKey == null); 7998 // Generate unique / reproducible default keys 7999 ctx.options.defaultKey = function (node) { 8000 return calcUniqueKey(node); 8001 }; 8002 // The default implementation loads initial data 8003 this._superApply(arguments); 8004 }, 8005 treeClear: function (ctx) { 8006 ctx.tree.refMap = {}; 8007 ctx.tree.keyMap = {}; 8008 return this._superApply(arguments); 8009 }, 8010 treeRegisterNode: function (ctx, add, node) { 8011 var refList, 8012 len, 8013 tree = ctx.tree, 8014 keyMap = tree.keyMap, 8015 refMap = tree.refMap, 8016 key = node.key, 8017 refKey = node && node.refKey != null ? "" + node.refKey : null; 8018 8019 // ctx.tree.debug("clones.treeRegisterNode", add, node); 8020 8021 if (node.isStatusNode()) { 8022 return this._super(ctx, add, node); 8023 } 8024 8025 if (add) { 8026 if (keyMap[node.key] != null) { 8027 var other = keyMap[node.key], 8028 msg = 8029 "clones.treeRegisterNode: duplicate key '" + 8030 node.key + 8031 "': /" + 8032 node.getPath(true) + 8033 " => " + 8034 other.getPath(true); 8035 // Sometimes this exception is not visible in the console, 8036 // so we also write it: 8037 tree.error(msg); 8038 $.error(msg); 8039 } 8040 keyMap[key] = node; 8041 8042 if (refKey) { 8043 refList = refMap[refKey]; 8044 if (refList) { 8045 refList.push(key); 8046 if ( 8047 refList.length === 2 && 8048 ctx.options.clones.highlightClones 8049 ) { 8050 // Mark peer node, if it just became a clone (no need to 8051 // mark current node, since it will be rendered later anyway) 8052 keyMap[refList[0]].renderStatus(); 8053 } 8054 } else { 8055 refMap[refKey] = [key]; 8056 } 8057 // node.debug("clones.treeRegisterNode: add clone =>", refMap[refKey]); 8058 } 8059 } else { 8060 if (keyMap[key] == null) { 8061 $.error( 8062 "clones.treeRegisterNode: node.key not registered: " + 8063 node.key 8064 ); 8065 } 8066 delete keyMap[key]; 8067 if (refKey) { 8068 refList = refMap[refKey]; 8069 // node.debug("clones.treeRegisterNode: remove clone BEFORE =>", refMap[refKey]); 8070 if (refList) { 8071 len = refList.length; 8072 if (len <= 1) { 8073 _assert(len === 1); 8074 _assert(refList[0] === key); 8075 delete refMap[refKey]; 8076 } else { 8077 _removeArrayMember(refList, key); 8078 // Unmark peer node, if this was the only clone 8079 if ( 8080 len === 2 && 8081 ctx.options.clones.highlightClones 8082 ) { 8083 // node.debug("clones.treeRegisterNode: last =>", node.getCloneList()); 8084 keyMap[refList[0]].renderStatus(); 8085 } 8086 } 8087 // node.debug("clones.treeRegisterNode: remove clone =>", refMap[refKey]); 8088 } 8089 } 8090 } 8091 return this._super(ctx, add, node); 8092 }, 8093 nodeRenderStatus: function (ctx) { 8094 var $span, 8095 res, 8096 node = ctx.node; 8097 8098 res = this._super(ctx); 8099 8100 if (ctx.options.clones.highlightClones) { 8101 $span = $(node[ctx.tree.statusClassPropName]); 8102 // Only if span already exists 8103 if ($span.length && node.isClone()) { 8104 // node.debug("clones.nodeRenderStatus: ", ctx.options.clones.highlightClones); 8105 $span.addClass("fancytree-clone"); 8106 } 8107 } 8108 return res; 8109 }, 8110 nodeSetActive: function (ctx, flag, callOpts) { 8111 var res, 8112 scpn = ctx.tree.statusClassPropName, 8113 node = ctx.node; 8114 8115 res = this._superApply(arguments); 8116 8117 if (ctx.options.clones.highlightActiveClones && node.isClone()) { 8118 $.each(node.getCloneList(true), function (idx, n) { 8119 // n.debug("clones.nodeSetActive: ", flag !== false); 8120 $(n[scpn]).toggleClass( 8121 "fancytree-active-clone", 8122 flag !== false 8123 ); 8124 }); 8125 } 8126 return res; 8127 }, 8128 }); 8129 // Value returned by `require('jquery.fancytree..')` 8130 return $.ui.fancytree; 8131}); // End of closure 8132 8133/*! 8134 * jquery.fancytree.dnd.js 8135 * 8136 * Drag-and-drop support (jQuery UI draggable/droppable). 8137 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 8138 * 8139 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 8140 * 8141 * Released under the MIT license 8142 * https://github.com/mar10/fancytree/wiki/LicenseInfo 8143 * 8144 * @version 2.38.3 8145 * @date 2023-02-01T20:52:50Z 8146 */ 8147 8148(function (factory) { 8149 if (typeof define === "function" && define.amd) { 8150 // AMD. Register as an anonymous module. 8151 define([ 8152 "jquery", 8153 "jquery-ui/ui/widgets/draggable", 8154 "jquery-ui/ui/widgets/droppable", 8155 "./jquery.fancytree", 8156 ], factory); 8157 } else if (typeof module === "object" && module.exports) { 8158 // Node/CommonJS 8159 require("./jquery.fancytree"); 8160 module.exports = factory(require("jquery")); 8161 } else { 8162 // Browser globals 8163 factory(jQuery); 8164 } 8165})(function ($) { 8166 "use strict"; 8167 8168 /****************************************************************************** 8169 * Private functions and variables 8170 */ 8171 var didRegisterDnd = false, 8172 classDropAccept = "fancytree-drop-accept", 8173 classDropAfter = "fancytree-drop-after", 8174 classDropBefore = "fancytree-drop-before", 8175 classDropOver = "fancytree-drop-over", 8176 classDropReject = "fancytree-drop-reject", 8177 classDropTarget = "fancytree-drop-target"; 8178 8179 /* Convert number to string and prepend +/-; return empty string for 0.*/ 8180 function offsetString(n) { 8181 // eslint-disable-next-line no-nested-ternary 8182 return n === 0 ? "" : n > 0 ? "+" + n : "" + n; 8183 } 8184 8185 //--- Extend ui.draggable event handling -------------------------------------- 8186 8187 function _registerDnd() { 8188 if (didRegisterDnd) { 8189 return; 8190 } 8191 8192 // Register proxy-functions for draggable.start/drag/stop 8193 8194 $.ui.plugin.add("draggable", "connectToFancytree", { 8195 start: function (event, ui) { 8196 // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10 8197 var draggable = 8198 $(this).data("ui-draggable") || 8199 $(this).data("draggable"), 8200 sourceNode = ui.helper.data("ftSourceNode") || null; 8201 8202 if (sourceNode) { 8203 // Adjust helper offset, so cursor is slightly outside top/left corner 8204 draggable.offset.click.top = -2; 8205 draggable.offset.click.left = +16; 8206 // Trigger dragStart event 8207 // TODO: when called as connectTo..., the return value is ignored(?) 8208 return sourceNode.tree.ext.dnd._onDragEvent( 8209 "start", 8210 sourceNode, 8211 null, 8212 event, 8213 ui, 8214 draggable 8215 ); 8216 } 8217 }, 8218 drag: function (event, ui) { 8219 var ctx, 8220 isHelper, 8221 logObject, 8222 // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10 8223 draggable = 8224 $(this).data("ui-draggable") || 8225 $(this).data("draggable"), 8226 sourceNode = ui.helper.data("ftSourceNode") || null, 8227 prevTargetNode = ui.helper.data("ftTargetNode") || null, 8228 targetNode = $.ui.fancytree.getNode(event.target), 8229 dndOpts = sourceNode && sourceNode.tree.options.dnd; 8230 8231 // logObject = sourceNode || prevTargetNode || $.ui.fancytree; 8232 // logObject.debug("Drag event:", event, event.shiftKey); 8233 if (event.target && !targetNode) { 8234 // We got a drag event, but the targetNode could not be found 8235 // at the event location. This may happen, 8236 // 1. if the mouse jumped over the drag helper, 8237 // 2. or if a non-fancytree element is dragged 8238 // We ignore it: 8239 isHelper = 8240 $(event.target).closest( 8241 "div.fancytree-drag-helper,#fancytree-drop-marker" 8242 ).length > 0; 8243 if (isHelper) { 8244 logObject = 8245 sourceNode || prevTargetNode || $.ui.fancytree; 8246 logObject.debug("Drag event over helper: ignored."); 8247 return; 8248 } 8249 } 8250 ui.helper.data("ftTargetNode", targetNode); 8251 8252 if (dndOpts && dndOpts.updateHelper) { 8253 ctx = sourceNode.tree._makeHookContext(sourceNode, event, { 8254 otherNode: targetNode, 8255 ui: ui, 8256 draggable: draggable, 8257 dropMarker: $("#fancytree-drop-marker"), 8258 }); 8259 dndOpts.updateHelper.call(sourceNode.tree, sourceNode, ctx); 8260 } 8261 8262 // Leaving a tree node 8263 if (prevTargetNode && prevTargetNode !== targetNode) { 8264 prevTargetNode.tree.ext.dnd._onDragEvent( 8265 "leave", 8266 prevTargetNode, 8267 sourceNode, 8268 event, 8269 ui, 8270 draggable 8271 ); 8272 } 8273 if (targetNode) { 8274 if (!targetNode.tree.options.dnd.dragDrop) { 8275 // not enabled as drop target 8276 } else if (targetNode === prevTargetNode) { 8277 // Moving over same node 8278 targetNode.tree.ext.dnd._onDragEvent( 8279 "over", 8280 targetNode, 8281 sourceNode, 8282 event, 8283 ui, 8284 draggable 8285 ); 8286 } else { 8287 // Entering this node first time 8288 targetNode.tree.ext.dnd._onDragEvent( 8289 "enter", 8290 targetNode, 8291 sourceNode, 8292 event, 8293 ui, 8294 draggable 8295 ); 8296 targetNode.tree.ext.dnd._onDragEvent( 8297 "over", 8298 targetNode, 8299 sourceNode, 8300 event, 8301 ui, 8302 draggable 8303 ); 8304 } 8305 } 8306 // else go ahead with standard event handling 8307 }, 8308 stop: function (event, ui) { 8309 var logObject, 8310 // 'draggable' was renamed to 'ui-draggable' since jQueryUI 1.10: 8311 draggable = 8312 $(this).data("ui-draggable") || 8313 $(this).data("draggable"), 8314 sourceNode = ui.helper.data("ftSourceNode") || null, 8315 targetNode = ui.helper.data("ftTargetNode") || null, 8316 dropped = event.type === "mouseup" && event.which === 1; 8317 8318 if (!dropped) { 8319 logObject = sourceNode || targetNode || $.ui.fancytree; 8320 logObject.debug("Drag was cancelled"); 8321 } 8322 if (targetNode) { 8323 if (dropped) { 8324 targetNode.tree.ext.dnd._onDragEvent( 8325 "drop", 8326 targetNode, 8327 sourceNode, 8328 event, 8329 ui, 8330 draggable 8331 ); 8332 } 8333 targetNode.tree.ext.dnd._onDragEvent( 8334 "leave", 8335 targetNode, 8336 sourceNode, 8337 event, 8338 ui, 8339 draggable 8340 ); 8341 } 8342 if (sourceNode) { 8343 sourceNode.tree.ext.dnd._onDragEvent( 8344 "stop", 8345 sourceNode, 8346 null, 8347 event, 8348 ui, 8349 draggable 8350 ); 8351 } 8352 }, 8353 }); 8354 8355 didRegisterDnd = true; 8356 } 8357 8358 /****************************************************************************** 8359 * Drag and drop support 8360 */ 8361 function _initDragAndDrop(tree) { 8362 var dnd = tree.options.dnd || null, 8363 glyph = tree.options.glyph || null; 8364 8365 // Register 'connectToFancytree' option with ui.draggable 8366 if (dnd) { 8367 _registerDnd(); 8368 } 8369 // Attach ui.draggable to this Fancytree instance 8370 if (dnd && dnd.dragStart) { 8371 tree.widget.element.draggable( 8372 $.extend( 8373 { 8374 addClasses: false, 8375 // DT issue 244: helper should be child of scrollParent: 8376 appendTo: tree.$container, 8377 // appendTo: "body", 8378 containment: false, 8379 // containment: "parent", 8380 delay: 0, 8381 distance: 4, 8382 revert: false, 8383 scroll: true, // to disable, also set css 'position: inherit' on ul.fancytree-container 8384 scrollSpeed: 7, 8385 scrollSensitivity: 10, 8386 // Delegate draggable.start, drag, and stop events to our handler 8387 connectToFancytree: true, 8388 // Let source tree create the helper element 8389 helper: function (event) { 8390 var $helper, 8391 $nodeTag, 8392 opts, 8393 sourceNode = $.ui.fancytree.getNode( 8394 event.target 8395 ); 8396 8397 if (!sourceNode) { 8398 // #405, DT issue 211: might happen, if dragging a table *header* 8399 return "<div>ERROR?: helper requested but sourceNode not found</div>"; 8400 } 8401 opts = sourceNode.tree.options.dnd; 8402 $nodeTag = $(sourceNode.span); 8403 // Only event and node argument is available 8404 $helper = $( 8405 "<div class='fancytree-drag-helper'><span class='fancytree-drag-helper-img' /></div>" 8406 ) 8407 .css({ zIndex: 3, position: "relative" }) // so it appears above ext-wide selection bar 8408 .append( 8409 $nodeTag 8410 .find("span.fancytree-title") 8411 .clone() 8412 ); 8413 8414 // Attach node reference to helper object 8415 $helper.data("ftSourceNode", sourceNode); 8416 8417 // Support glyph symbols instead of icons 8418 if (glyph) { 8419 $helper 8420 .find(".fancytree-drag-helper-img") 8421 .addClass( 8422 glyph.map._addClass + 8423 " " + 8424 glyph.map.dragHelper 8425 ); 8426 } 8427 // Allow to modify the helper, e.g. to add multi-node-drag feedback 8428 if (opts.initHelper) { 8429 opts.initHelper.call( 8430 sourceNode.tree, 8431 sourceNode, 8432 { 8433 node: sourceNode, 8434 tree: sourceNode.tree, 8435 originalEvent: event, 8436 ui: { helper: $helper }, 8437 } 8438 ); 8439 } 8440 // We return an unconnected element, so `draggable` will add this 8441 // to the parent specified as `appendTo` option 8442 return $helper; 8443 }, 8444 start: function (event, ui) { 8445 var sourceNode = ui.helper.data("ftSourceNode"); 8446 return !!sourceNode; // Abort dragging if no node could be found 8447 }, 8448 }, 8449 tree.options.dnd.draggable 8450 ) 8451 ); 8452 } 8453 // Attach ui.droppable to this Fancytree instance 8454 if (dnd && dnd.dragDrop) { 8455 tree.widget.element.droppable( 8456 $.extend( 8457 { 8458 addClasses: false, 8459 tolerance: "intersect", 8460 greedy: false, 8461 /* 8462 activate: function(event, ui) { 8463 tree.debug("droppable - activate", event, ui, this); 8464 }, 8465 create: function(event, ui) { 8466 tree.debug("droppable - create", event, ui); 8467 }, 8468 deactivate: function(event, ui) { 8469 tree.debug("droppable - deactivate", event, ui); 8470 }, 8471 drop: function(event, ui) { 8472 tree.debug("droppable - drop", event, ui); 8473 }, 8474 out: function(event, ui) { 8475 tree.debug("droppable - out", event, ui); 8476 }, 8477 over: function(event, ui) { 8478 tree.debug("droppable - over", event, ui); 8479 } 8480*/ 8481 }, 8482 tree.options.dnd.droppable 8483 ) 8484 ); 8485 } 8486 } 8487 8488 /****************************************************************************** 8489 * 8490 */ 8491 8492 $.ui.fancytree.registerExtension({ 8493 name: "dnd", 8494 version: "2.38.3", 8495 // Default options for this extension. 8496 options: { 8497 // Make tree nodes accept draggables 8498 autoExpandMS: 1000, // Expand nodes after n milliseconds of hovering. 8499 draggable: null, // Additional options passed to jQuery draggable 8500 droppable: null, // Additional options passed to jQuery droppable 8501 focusOnClick: false, // Focus, although draggable cancels mousedown event (#270) 8502 preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. 8503 preventRecursiveMoves: true, // Prevent dropping nodes on own descendants 8504 smartRevert: true, // set draggable.revert = true if drop was rejected 8505 dropMarkerOffsetX: -24, // absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) 8506 dropMarkerInsertOffsetX: -16, // additional offset for drop-marker with hitMode = "before"/"after" 8507 // Events (drag support) 8508 dragStart: null, // Callback(sourceNode, data), return true, to enable dnd 8509 dragStop: null, // Callback(sourceNode, data) 8510 initHelper: null, // Callback(sourceNode, data) 8511 updateHelper: null, // Callback(sourceNode, data) 8512 // Events (drop support) 8513 dragEnter: null, // Callback(targetNode, data) 8514 dragOver: null, // Callback(targetNode, data) 8515 dragExpand: null, // Callback(targetNode, data), return false to prevent autoExpand 8516 dragDrop: null, // Callback(targetNode, data) 8517 dragLeave: null, // Callback(targetNode, data) 8518 }, 8519 8520 treeInit: function (ctx) { 8521 var tree = ctx.tree; 8522 this._superApply(arguments); 8523 // issue #270: draggable eats mousedown events 8524 if (tree.options.dnd.dragStart) { 8525 tree.$container.on("mousedown", function (event) { 8526 // if( !tree.hasFocus() && ctx.options.dnd.focusOnClick ) { 8527 if (ctx.options.dnd.focusOnClick) { 8528 // #270 8529 var node = $.ui.fancytree.getNode(event); 8530 if (node) { 8531 node.debug( 8532 "Re-enable focus that was prevented by jQuery UI draggable." 8533 ); 8534 // node.setFocus(); 8535 // $(node.span).closest(":tabbable").focus(); 8536 // $(event.target).trigger("focus"); 8537 // $(event.target).closest(":tabbable").trigger("focus"); 8538 } 8539 setTimeout(function () { 8540 // #300 8541 $(event.target).closest(":tabbable").focus(); 8542 }, 10); 8543 } 8544 }); 8545 } 8546 _initDragAndDrop(tree); 8547 }, 8548 /* Display drop marker according to hitMode ('after', 'before', 'over'). */ 8549 _setDndStatus: function ( 8550 sourceNode, 8551 targetNode, 8552 helper, 8553 hitMode, 8554 accept 8555 ) { 8556 var markerOffsetX, 8557 pos, 8558 markerAt = "center", 8559 instData = this._local, 8560 dndOpt = this.options.dnd, 8561 glyphOpt = this.options.glyph, 8562 $source = sourceNode ? $(sourceNode.span) : null, 8563 $target = $(targetNode.span), 8564 $targetTitle = $target.find("span.fancytree-title"); 8565 8566 if (!instData.$dropMarker) { 8567 instData.$dropMarker = $( 8568 "<div id='fancytree-drop-marker'></div>" 8569 ) 8570 .hide() 8571 .css({ "z-index": 1000 }) 8572 .prependTo($(this.$div).parent()); 8573 // .prependTo("body"); 8574 8575 if (glyphOpt) { 8576 instData.$dropMarker.addClass( 8577 glyphOpt.map._addClass + " " + glyphOpt.map.dropMarker 8578 ); 8579 } 8580 } 8581 if ( 8582 hitMode === "after" || 8583 hitMode === "before" || 8584 hitMode === "over" 8585 ) { 8586 markerOffsetX = dndOpt.dropMarkerOffsetX || 0; 8587 switch (hitMode) { 8588 case "before": 8589 markerAt = "top"; 8590 markerOffsetX += dndOpt.dropMarkerInsertOffsetX || 0; 8591 break; 8592 case "after": 8593 markerAt = "bottom"; 8594 markerOffsetX += dndOpt.dropMarkerInsertOffsetX || 0; 8595 break; 8596 } 8597 8598 pos = { 8599 my: "left" + offsetString(markerOffsetX) + " center", 8600 at: "left " + markerAt, 8601 of: $targetTitle, 8602 }; 8603 if (this.options.rtl) { 8604 pos.my = "right" + offsetString(-markerOffsetX) + " center"; 8605 pos.at = "right " + markerAt; 8606 } 8607 instData.$dropMarker 8608 .toggleClass(classDropAfter, hitMode === "after") 8609 .toggleClass(classDropOver, hitMode === "over") 8610 .toggleClass(classDropBefore, hitMode === "before") 8611 .toggleClass("fancytree-rtl", !!this.options.rtl) 8612 .show() 8613 .position($.ui.fancytree.fixPositionOptions(pos)); 8614 } else { 8615 instData.$dropMarker.hide(); 8616 } 8617 if ($source) { 8618 $source 8619 .toggleClass(classDropAccept, accept === true) 8620 .toggleClass(classDropReject, accept === false); 8621 } 8622 $target 8623 .toggleClass( 8624 classDropTarget, 8625 hitMode === "after" || 8626 hitMode === "before" || 8627 hitMode === "over" 8628 ) 8629 .toggleClass(classDropAfter, hitMode === "after") 8630 .toggleClass(classDropBefore, hitMode === "before") 8631 .toggleClass(classDropAccept, accept === true) 8632 .toggleClass(classDropReject, accept === false); 8633 8634 helper 8635 .toggleClass(classDropAccept, accept === true) 8636 .toggleClass(classDropReject, accept === false); 8637 }, 8638 8639 /* 8640 * Handles drag'n'drop functionality. 8641 * 8642 * A standard jQuery drag-and-drop process may generate these calls: 8643 * 8644 * start: 8645 * _onDragEvent("start", sourceNode, null, event, ui, draggable); 8646 * drag: 8647 * _onDragEvent("leave", prevTargetNode, sourceNode, event, ui, draggable); 8648 * _onDragEvent("over", targetNode, sourceNode, event, ui, draggable); 8649 * _onDragEvent("enter", targetNode, sourceNode, event, ui, draggable); 8650 * stop: 8651 * _onDragEvent("drop", targetNode, sourceNode, event, ui, draggable); 8652 * _onDragEvent("leave", targetNode, sourceNode, event, ui, draggable); 8653 * _onDragEvent("stop", sourceNode, null, event, ui, draggable); 8654 */ 8655 _onDragEvent: function ( 8656 eventName, 8657 node, 8658 otherNode, 8659 event, 8660 ui, 8661 draggable 8662 ) { 8663 // if(eventName !== "over"){ 8664 // this.debug("tree.ext.dnd._onDragEvent(%s, %o, %o) - %o", eventName, node, otherNode, this); 8665 // } 8666 var accept, 8667 nodeOfs, 8668 parentRect, 8669 rect, 8670 relPos, 8671 relPos2, 8672 enterResponse, 8673 hitMode, 8674 r, 8675 opts = this.options, 8676 dnd = opts.dnd, 8677 ctx = this._makeHookContext(node, event, { 8678 otherNode: otherNode, 8679 ui: ui, 8680 draggable: draggable, 8681 }), 8682 res = null, 8683 self = this, 8684 $nodeTag = $(node.span); 8685 8686 if (dnd.smartRevert) { 8687 draggable.options.revert = "invalid"; 8688 } 8689 8690 switch (eventName) { 8691 case "start": 8692 if (node.isStatusNode()) { 8693 res = false; 8694 } else if (dnd.dragStart) { 8695 res = dnd.dragStart(node, ctx); 8696 } 8697 if (res === false) { 8698 this.debug("tree.dragStart() cancelled"); 8699 //draggable._clear(); 8700 // NOTE: the return value seems to be ignored (drag is not cancelled, when false is returned) 8701 // TODO: call this._cancelDrag()? 8702 ui.helper.trigger("mouseup").hide(); 8703 } else { 8704 if (dnd.smartRevert) { 8705 // #567, #593: fix revert position 8706 // rect = node.li.getBoundingClientRect(); 8707 rect = 8708 node[ 8709 ctx.tree.nodeContainerAttrName 8710 ].getBoundingClientRect(); 8711 parentRect = $( 8712 draggable.options.appendTo 8713 )[0].getBoundingClientRect(); 8714 draggable.originalPosition.left = Math.max( 8715 0, 8716 rect.left - parentRect.left 8717 ); 8718 draggable.originalPosition.top = Math.max( 8719 0, 8720 rect.top - parentRect.top 8721 ); 8722 } 8723 $nodeTag.addClass("fancytree-drag-source"); 8724 // Register global handlers to allow cancel 8725 $(document).on( 8726 "keydown.fancytree-dnd,mousedown.fancytree-dnd", 8727 function (event) { 8728 // node.tree.debug("dnd global event", event.type, event.which); 8729 if ( 8730 event.type === "keydown" && 8731 event.which === $.ui.keyCode.ESCAPE 8732 ) { 8733 self.ext.dnd._cancelDrag(); 8734 } else if (event.type === "mousedown") { 8735 self.ext.dnd._cancelDrag(); 8736 } 8737 } 8738 ); 8739 } 8740 break; 8741 8742 case "enter": 8743 if ( 8744 dnd.preventRecursiveMoves && 8745 node.isDescendantOf(otherNode) 8746 ) { 8747 r = false; 8748 } else { 8749 r = dnd.dragEnter ? dnd.dragEnter(node, ctx) : null; 8750 } 8751 if (!r) { 8752 // convert null, undefined, false to false 8753 res = false; 8754 } else if (Array.isArray(r)) { 8755 // TODO: also accept passing an object of this format directly 8756 res = { 8757 over: $.inArray("over", r) >= 0, 8758 before: $.inArray("before", r) >= 0, 8759 after: $.inArray("after", r) >= 0, 8760 }; 8761 } else { 8762 res = { 8763 over: r === true || r === "over", 8764 before: r === true || r === "before", 8765 after: r === true || r === "after", 8766 }; 8767 } 8768 ui.helper.data("enterResponse", res); 8769 // this.debug("helper.enterResponse: %o", res); 8770 break; 8771 8772 case "over": 8773 enterResponse = ui.helper.data("enterResponse"); 8774 hitMode = null; 8775 if (enterResponse === false) { 8776 // Don't call dragOver if onEnter returned false. 8777 // break; 8778 } else if (typeof enterResponse === "string") { 8779 // Use hitMode from onEnter if provided. 8780 hitMode = enterResponse; 8781 } else { 8782 // Calculate hitMode from relative cursor position. 8783 nodeOfs = $nodeTag.offset(); 8784 relPos = { 8785 x: event.pageX - nodeOfs.left, 8786 y: event.pageY - nodeOfs.top, 8787 }; 8788 relPos2 = { 8789 x: relPos.x / $nodeTag.width(), 8790 y: relPos.y / $nodeTag.height(), 8791 }; 8792 8793 if (enterResponse.after && relPos2.y > 0.75) { 8794 hitMode = "after"; 8795 } else if ( 8796 !enterResponse.over && 8797 enterResponse.after && 8798 relPos2.y > 0.5 8799 ) { 8800 hitMode = "after"; 8801 } else if (enterResponse.before && relPos2.y <= 0.25) { 8802 hitMode = "before"; 8803 } else if ( 8804 !enterResponse.over && 8805 enterResponse.before && 8806 relPos2.y <= 0.5 8807 ) { 8808 hitMode = "before"; 8809 } else if (enterResponse.over) { 8810 hitMode = "over"; 8811 } 8812 // Prevent no-ops like 'before source node' 8813 // TODO: these are no-ops when moving nodes, but not in copy mode 8814 if (dnd.preventVoidMoves) { 8815 if (node === otherNode) { 8816 this.debug( 8817 " drop over source node prevented" 8818 ); 8819 hitMode = null; 8820 } else if ( 8821 hitMode === "before" && 8822 otherNode && 8823 node === otherNode.getNextSibling() 8824 ) { 8825 this.debug( 8826 " drop after source node prevented" 8827 ); 8828 hitMode = null; 8829 } else if ( 8830 hitMode === "after" && 8831 otherNode && 8832 node === otherNode.getPrevSibling() 8833 ) { 8834 this.debug( 8835 " drop before source node prevented" 8836 ); 8837 hitMode = null; 8838 } else if ( 8839 hitMode === "over" && 8840 otherNode && 8841 otherNode.parent === node && 8842 otherNode.isLastSibling() 8843 ) { 8844 this.debug( 8845 " drop last child over own parent prevented" 8846 ); 8847 hitMode = null; 8848 } 8849 } 8850 // this.debug("hitMode: %s - %s - %s", hitMode, (node.parent === otherNode), node.isLastSibling()); 8851 ui.helper.data("hitMode", hitMode); 8852 } 8853 // Auto-expand node (only when 'over' the node, not 'before', or 'after') 8854 if ( 8855 hitMode !== "before" && 8856 hitMode !== "after" && 8857 dnd.autoExpandMS && 8858 node.hasChildren() !== false && 8859 !node.expanded && 8860 (!dnd.dragExpand || dnd.dragExpand(node, ctx) !== false) 8861 ) { 8862 node.scheduleAction("expand", dnd.autoExpandMS); 8863 } 8864 if (hitMode && dnd.dragOver) { 8865 // TODO: http://code.google.com/p/dynatree/source/detail?r=625 8866 ctx.hitMode = hitMode; 8867 res = dnd.dragOver(node, ctx); 8868 } 8869 accept = res !== false && hitMode !== null; 8870 if (dnd.smartRevert) { 8871 draggable.options.revert = !accept; 8872 } 8873 this._local._setDndStatus( 8874 otherNode, 8875 node, 8876 ui.helper, 8877 hitMode, 8878 accept 8879 ); 8880 break; 8881 8882 case "drop": 8883 hitMode = ui.helper.data("hitMode"); 8884 if (hitMode && dnd.dragDrop) { 8885 ctx.hitMode = hitMode; 8886 dnd.dragDrop(node, ctx); 8887 } 8888 break; 8889 8890 case "leave": 8891 // Cancel pending expand request 8892 node.scheduleAction("cancel"); 8893 ui.helper.data("enterResponse", null); 8894 ui.helper.data("hitMode", null); 8895 this._local._setDndStatus( 8896 otherNode, 8897 node, 8898 ui.helper, 8899 "out", 8900 undefined 8901 ); 8902 if (dnd.dragLeave) { 8903 dnd.dragLeave(node, ctx); 8904 } 8905 break; 8906 8907 case "stop": 8908 $nodeTag.removeClass("fancytree-drag-source"); 8909 $(document).off(".fancytree-dnd"); 8910 if (dnd.dragStop) { 8911 dnd.dragStop(node, ctx); 8912 } 8913 break; 8914 8915 default: 8916 $.error("Unsupported drag event: " + eventName); 8917 } 8918 return res; 8919 }, 8920 8921 _cancelDrag: function () { 8922 var dd = $.ui.ddmanager.current; 8923 if (dd) { 8924 dd.cancel(); 8925 } 8926 }, 8927 }); 8928 // Value returned by `require('jquery.fancytree..')` 8929 return $.ui.fancytree; 8930}); // End of closure 8931 8932/*! 8933 * jquery.fancytree.dnd5.js 8934 * 8935 * Drag-and-drop support (native HTML5). 8936 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 8937 * 8938 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 8939 * 8940 * Released under the MIT license 8941 * https://github.com/mar10/fancytree/wiki/LicenseInfo 8942 * 8943 * @version 2.38.3 8944 * @date 2023-02-01T20:52:50Z 8945 */ 8946 8947/* 8948 #TODO 8949 Compatiblity when dragging between *separate* windows: 8950 8951 Drag from Chrome Edge FF IE11 Safari 8952 To Chrome ok ok ok NO ? 8953 Edge ok ok ok NO ? 8954 FF ok ok ok NO ? 8955 IE 11 ok ok ok ok ? 8956 Safari ? ? ? ? ok 8957 8958 */ 8959 8960(function (factory) { 8961 if (typeof define === "function" && define.amd) { 8962 // AMD. Register as an anonymous module. 8963 define(["jquery", "./jquery.fancytree"], factory); 8964 } else if (typeof module === "object" && module.exports) { 8965 // Node/CommonJS 8966 require("./jquery.fancytree"); 8967 module.exports = factory(require("jquery")); 8968 } else { 8969 // Browser globals 8970 factory(jQuery); 8971 } 8972})(function ($) { 8973 "use strict"; 8974 8975 /****************************************************************************** 8976 * Private functions and variables 8977 */ 8978 var FT = $.ui.fancytree, 8979 isMac = /Mac/.test(navigator.platform), 8980 classDragSource = "fancytree-drag-source", 8981 classDragRemove = "fancytree-drag-remove", 8982 classDropAccept = "fancytree-drop-accept", 8983 classDropAfter = "fancytree-drop-after", 8984 classDropBefore = "fancytree-drop-before", 8985 classDropOver = "fancytree-drop-over", 8986 classDropReject = "fancytree-drop-reject", 8987 classDropTarget = "fancytree-drop-target", 8988 nodeMimeType = "application/x-fancytree-node", 8989 $dropMarker = null, 8990 $dragImage, 8991 $extraHelper, 8992 SOURCE_NODE = null, 8993 SOURCE_NODE_LIST = null, 8994 $sourceList = null, 8995 DRAG_ENTER_RESPONSE = null, 8996 // SESSION_DATA = null, // plain object passed to events as `data` 8997 SUGGESTED_DROP_EFFECT = null, 8998 REQUESTED_DROP_EFFECT = null, 8999 REQUESTED_EFFECT_ALLOWED = null, 9000 LAST_HIT_MODE = null, 9001 DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode 9002 9003 /* */ 9004 function _clearGlobals() { 9005 DRAG_ENTER_RESPONSE = null; 9006 DRAG_OVER_STAMP = null; 9007 REQUESTED_DROP_EFFECT = null; 9008 REQUESTED_EFFECT_ALLOWED = null; 9009 SUGGESTED_DROP_EFFECT = null; 9010 SOURCE_NODE = null; 9011 SOURCE_NODE_LIST = null; 9012 if ($sourceList) { 9013 $sourceList.removeClass(classDragSource + " " + classDragRemove); 9014 } 9015 $sourceList = null; 9016 if ($dropMarker) { 9017 $dropMarker.hide(); 9018 } 9019 // Take this badge off of me - I can't use it anymore: 9020 if ($extraHelper) { 9021 $extraHelper.remove(); 9022 $extraHelper = null; 9023 } 9024 } 9025 9026 /* Convert number to string and prepend +/-; return empty string for 0.*/ 9027 function offsetString(n) { 9028 // eslint-disable-next-line no-nested-ternary 9029 return n === 0 ? "" : n > 0 ? "+" + n : "" + n; 9030 } 9031 9032 /* Convert a dragEnter() or dragOver() response to a canonical form. 9033 * Return false or plain object 9034 * @param {string|object|boolean} r 9035 * @return {object|false} 9036 */ 9037 function normalizeDragEnterResponse(r) { 9038 var res; 9039 9040 if (!r) { 9041 return false; 9042 } 9043 if ($.isPlainObject(r)) { 9044 res = { 9045 over: !!r.over, 9046 before: !!r.before, 9047 after: !!r.after, 9048 }; 9049 } else if (Array.isArray(r)) { 9050 res = { 9051 over: $.inArray("over", r) >= 0, 9052 before: $.inArray("before", r) >= 0, 9053 after: $.inArray("after", r) >= 0, 9054 }; 9055 } else { 9056 res = { 9057 over: r === true || r === "over", 9058 before: r === true || r === "before", 9059 after: r === true || r === "after", 9060 }; 9061 } 9062 if (Object.keys(res).length === 0) { 9063 return false; 9064 } 9065 // if( Object.keys(res).length === 1 ) { 9066 // res.unique = res[0]; 9067 // } 9068 return res; 9069 } 9070 9071 /* Convert a dataTransfer.effectAllowed to a canonical form. 9072 * Return false or plain object 9073 * @param {string|boolean} r 9074 * @return {object|false} 9075 */ 9076 // function normalizeEffectAllowed(r) { 9077 // if (!r || r === "none") { 9078 // return false; 9079 // } 9080 // var all = r === "all", 9081 // res = { 9082 // copy: all || /copy/i.test(r), 9083 // link: all || /link/i.test(r), 9084 // move: all || /move/i.test(r), 9085 // }; 9086 9087 // return res; 9088 // } 9089 9090 /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ 9091 function autoScroll(tree, event) { 9092 var spOfs, 9093 scrollTop, 9094 delta, 9095 dndOpts = tree.options.dnd5, 9096 sp = tree.$scrollParent[0], 9097 sensitivity = dndOpts.scrollSensitivity, 9098 speed = dndOpts.scrollSpeed, 9099 scrolled = 0; 9100 9101 if (sp !== document && sp.tagName !== "HTML") { 9102 spOfs = tree.$scrollParent.offset(); 9103 scrollTop = sp.scrollTop; 9104 if (spOfs.top + sp.offsetHeight - event.pageY < sensitivity) { 9105 delta = 9106 sp.scrollHeight - 9107 tree.$scrollParent.innerHeight() - 9108 scrollTop; 9109 // console.log ("sp.offsetHeight: " + sp.offsetHeight 9110 // + ", spOfs.top: " + spOfs.top 9111 // + ", scrollTop: " + scrollTop 9112 // + ", innerHeight: " + tree.$scrollParent.innerHeight() 9113 // + ", scrollHeight: " + sp.scrollHeight 9114 // + ", delta: " + delta 9115 // ); 9116 if (delta > 0) { 9117 sp.scrollTop = scrolled = scrollTop + speed; 9118 } 9119 } else if (scrollTop > 0 && event.pageY - spOfs.top < sensitivity) { 9120 sp.scrollTop = scrolled = scrollTop - speed; 9121 } 9122 } else { 9123 scrollTop = $(document).scrollTop(); 9124 if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) { 9125 scrolled = scrollTop - speed; 9126 $(document).scrollTop(scrolled); 9127 } else if ( 9128 $(window).height() - (event.pageY - scrollTop) < 9129 sensitivity 9130 ) { 9131 scrolled = scrollTop + speed; 9132 $(document).scrollTop(scrolled); 9133 } 9134 } 9135 if (scrolled) { 9136 tree.debug("autoScroll: " + scrolled + "px"); 9137 } 9138 return scrolled; 9139 } 9140 9141 /* Guess dropEffect from modifier keys. 9142 * Using rules suggested here: 9143 * https://ux.stackexchange.com/a/83769 9144 * @returns 9145 * 'copy', 'link', 'move', or 'none' 9146 */ 9147 function evalEffectModifiers(tree, event, effectDefault) { 9148 var res = effectDefault; 9149 9150 if (isMac) { 9151 if (event.metaKey && event.altKey) { 9152 // Mac: [Control] + [Option] 9153 res = "link"; 9154 } else if (event.ctrlKey) { 9155 // Chrome on Mac: [Control] 9156 res = "link"; 9157 } else if (event.metaKey) { 9158 // Mac: [Command] 9159 res = "move"; 9160 } else if (event.altKey) { 9161 // Mac: [Option] 9162 res = "copy"; 9163 } 9164 } else { 9165 if (event.ctrlKey) { 9166 // Windows: [Ctrl] 9167 res = "copy"; 9168 } else if (event.shiftKey) { 9169 // Windows: [Shift] 9170 res = "move"; 9171 } else if (event.altKey) { 9172 // Windows: [Alt] 9173 res = "link"; 9174 } 9175 } 9176 if (res !== SUGGESTED_DROP_EFFECT) { 9177 tree.info( 9178 "evalEffectModifiers: " + 9179 event.type + 9180 " - evalEffectModifiers(): " + 9181 SUGGESTED_DROP_EFFECT + 9182 " -> " + 9183 res 9184 ); 9185 } 9186 SUGGESTED_DROP_EFFECT = res; 9187 // tree.debug("evalEffectModifiers: " + res); 9188 return res; 9189 } 9190 /* 9191 * Check if the previous callback (dragEnter, dragOver, ...) has changed 9192 * the `data` object and apply those settings. 9193 * 9194 * Safari: 9195 * It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain 9196 * even if the cursor changes when [Alt] or [Ctrl] are pressed (?) 9197 * Using rules suggested here: 9198 * https://ux.stackexchange.com/a/83769 9199 * @returns 9200 * 'copy', 'link', 'move', or 'none' 9201 */ 9202 function prepareDropEffectCallback(event, data) { 9203 var tree = data.tree, 9204 dataTransfer = data.dataTransfer; 9205 9206 if (event.type === "dragstart") { 9207 data.effectAllowed = tree.options.dnd5.effectAllowed; 9208 data.dropEffect = tree.options.dnd5.dropEffectDefault; 9209 } else { 9210 data.effectAllowed = REQUESTED_EFFECT_ALLOWED; 9211 data.dropEffect = REQUESTED_DROP_EFFECT; 9212 } 9213 data.dropEffectSuggested = evalEffectModifiers( 9214 tree, 9215 event, 9216 tree.options.dnd5.dropEffectDefault 9217 ); 9218 data.isMove = data.dropEffect === "move"; 9219 data.files = dataTransfer.files || []; 9220 9221 // if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) { 9222 // tree.warn( 9223 // "prepareDropEffectCallback(" + 9224 // event.type + 9225 // "): dataTransfer.effectAllowed changed from " + 9226 // REQUESTED_EFFECT_ALLOWED + 9227 // " -> " + 9228 // dataTransfer.effectAllowed 9229 // ); 9230 // } 9231 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 9232 // tree.warn( 9233 // "prepareDropEffectCallback(" + 9234 // event.type + 9235 // "): dataTransfer.dropEffect changed from requested " + 9236 // REQUESTED_DROP_EFFECT + 9237 // " to " + 9238 // dataTransfer.dropEffect 9239 // ); 9240 // } 9241 } 9242 9243 function applyDropEffectCallback(event, data, allowDrop) { 9244 var tree = data.tree, 9245 dataTransfer = data.dataTransfer; 9246 9247 if ( 9248 event.type !== "dragstart" && 9249 REQUESTED_EFFECT_ALLOWED !== data.effectAllowed 9250 ) { 9251 tree.warn( 9252 "effectAllowed should only be changed in dragstart event: " + 9253 event.type + 9254 ": data.effectAllowed changed from " + 9255 REQUESTED_EFFECT_ALLOWED + 9256 " -> " + 9257 data.effectAllowed 9258 ); 9259 } 9260 9261 if (allowDrop === false) { 9262 tree.info("applyDropEffectCallback: allowDrop === false"); 9263 data.effectAllowed = "none"; 9264 data.dropEffect = "none"; 9265 } 9266 // if (REQUESTED_DROP_EFFECT !== data.dropEffect) { 9267 // tree.debug( 9268 // "applyDropEffectCallback(" + 9269 // event.type + 9270 // "): data.dropEffect changed from previous " + 9271 // REQUESTED_DROP_EFFECT + 9272 // " to " + 9273 // data.dropEffect 9274 // ); 9275 // } 9276 9277 data.isMove = data.dropEffect === "move"; 9278 // data.isMove = data.dropEffectSuggested === "move"; 9279 9280 // `effectAllowed` must only be defined in dragstart event, so we 9281 // store it in a global variable for reference 9282 if (event.type === "dragstart") { 9283 REQUESTED_EFFECT_ALLOWED = data.effectAllowed; 9284 REQUESTED_DROP_EFFECT = data.dropEffect; 9285 } 9286 9287 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 9288 // data.tree.info( 9289 // "applyDropEffectCallback(" + 9290 // event.type + 9291 // "): dataTransfer.dropEffect changed from " + 9292 // REQUESTED_DROP_EFFECT + 9293 // " -> " + 9294 // dataTransfer.dropEffect 9295 // ); 9296 // } 9297 dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED; 9298 dataTransfer.dropEffect = REQUESTED_DROP_EFFECT; 9299 9300 // tree.debug( 9301 // "applyDropEffectCallback(" + 9302 // event.type + 9303 // "): set " + 9304 // dataTransfer.dropEffect + 9305 // "/" + 9306 // dataTransfer.effectAllowed 9307 // ); 9308 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 9309 // data.tree.warn( 9310 // "applyDropEffectCallback(" + 9311 // event.type + 9312 // "): could not set dataTransfer.dropEffect to " + 9313 // REQUESTED_DROP_EFFECT + 9314 // ": got " + 9315 // dataTransfer.dropEffect 9316 // ); 9317 // } 9318 return REQUESTED_DROP_EFFECT; 9319 } 9320 9321 /* Handle dragover event (fired every x ms) on valid drop targets. 9322 * 9323 * - Auto-scroll when cursor is in border regions 9324 * - Apply restrictioan like 'preventVoidMoves' 9325 * - Calculate hit mode 9326 * - Calculate drop effect 9327 * - Trigger dragOver() callback to let user modify hit mode and drop effect 9328 * - Adjust the drop marker accordingly 9329 * 9330 * @returns hitMode 9331 */ 9332 function handleDragOver(event, data) { 9333 // Implement auto-scrolling 9334 if (data.options.dnd5.scroll) { 9335 autoScroll(data.tree, event); 9336 } 9337 // Bail out with previous response if we get an invalid dragover 9338 if (!data.node) { 9339 data.tree.warn("Ignored dragover for non-node"); //, event, data); 9340 return LAST_HIT_MODE; 9341 } 9342 9343 var markerOffsetX, 9344 nodeOfs, 9345 pos, 9346 relPosY, 9347 hitMode = null, 9348 tree = data.tree, 9349 options = tree.options, 9350 dndOpts = options.dnd5, 9351 targetNode = data.node, 9352 sourceNode = data.otherNode, 9353 markerAt = "center", 9354 $target = $(targetNode.span), 9355 $targetTitle = $target.find("span.fancytree-title"); 9356 9357 if (DRAG_ENTER_RESPONSE === false) { 9358 tree.debug("Ignored dragover, since dragenter returned false."); 9359 return false; 9360 } else if (typeof DRAG_ENTER_RESPONSE === "string") { 9361 $.error("assert failed: dragenter returned string"); 9362 } 9363 // Calculate hitMode from relative cursor position. 9364 nodeOfs = $target.offset(); 9365 relPosY = (event.pageY - nodeOfs.top) / $target.height(); 9366 if (event.pageY === undefined) { 9367 tree.warn("event.pageY is undefined: see issue #1013."); 9368 } 9369 9370 if (DRAG_ENTER_RESPONSE.after && relPosY > 0.75) { 9371 hitMode = "after"; 9372 } else if ( 9373 !DRAG_ENTER_RESPONSE.over && 9374 DRAG_ENTER_RESPONSE.after && 9375 relPosY > 0.5 9376 ) { 9377 hitMode = "after"; 9378 } else if (DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) { 9379 hitMode = "before"; 9380 } else if ( 9381 !DRAG_ENTER_RESPONSE.over && 9382 DRAG_ENTER_RESPONSE.before && 9383 relPosY <= 0.5 9384 ) { 9385 hitMode = "before"; 9386 } else if (DRAG_ENTER_RESPONSE.over) { 9387 hitMode = "over"; 9388 } 9389 // Prevent no-ops like 'before source node' 9390 // TODO: these are no-ops when moving nodes, but not in copy mode 9391 if (dndOpts.preventVoidMoves && data.dropEffect === "move") { 9392 if (targetNode === sourceNode) { 9393 targetNode.debug("Drop over source node prevented."); 9394 hitMode = null; 9395 } else if ( 9396 hitMode === "before" && 9397 sourceNode && 9398 targetNode === sourceNode.getNextSibling() 9399 ) { 9400 targetNode.debug("Drop after source node prevented."); 9401 hitMode = null; 9402 } else if ( 9403 hitMode === "after" && 9404 sourceNode && 9405 targetNode === sourceNode.getPrevSibling() 9406 ) { 9407 targetNode.debug("Drop before source node prevented."); 9408 hitMode = null; 9409 } else if ( 9410 hitMode === "over" && 9411 sourceNode && 9412 sourceNode.parent === targetNode && 9413 sourceNode.isLastSibling() 9414 ) { 9415 targetNode.debug("Drop last child over own parent prevented."); 9416 hitMode = null; 9417 } 9418 } 9419 // Let callback modify the calculated hitMode 9420 data.hitMode = hitMode; 9421 if (hitMode && dndOpts.dragOver) { 9422 prepareDropEffectCallback(event, data); 9423 dndOpts.dragOver(targetNode, data); 9424 var allowDrop = !!hitMode; 9425 applyDropEffectCallback(event, data, allowDrop); 9426 hitMode = data.hitMode; 9427 } 9428 LAST_HIT_MODE = hitMode; 9429 // 9430 if (hitMode === "after" || hitMode === "before" || hitMode === "over") { 9431 markerOffsetX = dndOpts.dropMarkerOffsetX || 0; 9432 switch (hitMode) { 9433 case "before": 9434 markerAt = "top"; 9435 markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0; 9436 break; 9437 case "after": 9438 markerAt = "bottom"; 9439 markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0; 9440 break; 9441 } 9442 9443 pos = { 9444 my: "left" + offsetString(markerOffsetX) + " center", 9445 at: "left " + markerAt, 9446 of: $targetTitle, 9447 }; 9448 if (options.rtl) { 9449 pos.my = "right" + offsetString(-markerOffsetX) + " center"; 9450 pos.at = "right " + markerAt; 9451 // console.log("rtl", pos); 9452 } 9453 $dropMarker 9454 .toggleClass(classDropAfter, hitMode === "after") 9455 .toggleClass(classDropOver, hitMode === "over") 9456 .toggleClass(classDropBefore, hitMode === "before") 9457 .show() 9458 .position(FT.fixPositionOptions(pos)); 9459 } else { 9460 $dropMarker.hide(); 9461 // console.log("hide dropmarker") 9462 } 9463 9464 $(targetNode.span) 9465 .toggleClass( 9466 classDropTarget, 9467 hitMode === "after" || 9468 hitMode === "before" || 9469 hitMode === "over" 9470 ) 9471 .toggleClass(classDropAfter, hitMode === "after") 9472 .toggleClass(classDropBefore, hitMode === "before") 9473 .toggleClass(classDropAccept, hitMode === "over") 9474 .toggleClass(classDropReject, hitMode === false); 9475 9476 return hitMode; 9477 } 9478 9479 /* 9480 * Handle dragstart drag dragend events on the container 9481 */ 9482 function onDragEvent(event) { 9483 var json, 9484 tree = this, 9485 dndOpts = tree.options.dnd5, 9486 node = FT.getNode(event), 9487 dataTransfer = 9488 event.dataTransfer || event.originalEvent.dataTransfer, 9489 data = { 9490 tree: tree, 9491 node: node, 9492 options: tree.options, 9493 originalEvent: event.originalEvent, 9494 widget: tree.widget, 9495 dataTransfer: dataTransfer, 9496 useDefaultImage: true, 9497 dropEffect: undefined, 9498 dropEffectSuggested: undefined, 9499 effectAllowed: undefined, // set by dragstart 9500 files: undefined, // only for drop events 9501 isCancelled: undefined, // set by dragend 9502 isMove: undefined, 9503 }; 9504 9505 switch (event.type) { 9506 case "dragstart": 9507 if (!node) { 9508 tree.info("Ignored dragstart on a non-node."); 9509 return false; 9510 } 9511 // Store current source node in different formats 9512 SOURCE_NODE = node; 9513 9514 // Also optionally store selected nodes 9515 if (dndOpts.multiSource === false) { 9516 SOURCE_NODE_LIST = [node]; 9517 } else if (dndOpts.multiSource === true) { 9518 if (node.isSelected()) { 9519 SOURCE_NODE_LIST = tree.getSelectedNodes(); 9520 } else { 9521 SOURCE_NODE_LIST = [node]; 9522 } 9523 } else { 9524 SOURCE_NODE_LIST = dndOpts.multiSource(node, data); 9525 } 9526 // Cache as array of jQuery objects for faster access: 9527 $sourceList = $( 9528 $.map(SOURCE_NODE_LIST, function (n) { 9529 return n.span; 9530 }) 9531 ); 9532 // Set visual feedback 9533 $sourceList.addClass(classDragSource); 9534 9535 // Set payload 9536 // Note: 9537 // Transfer data is only accessible on dragstart and drop! 9538 // For all other events the formats and kinds in the drag 9539 // data store list of items representing dragged data can be 9540 // enumerated, but the data itself is unavailable and no new 9541 // data can be added. 9542 var nodeData = node.toDict(true, dndOpts.sourceCopyHook); 9543 nodeData.treeId = node.tree._id; 9544 json = JSON.stringify(nodeData); 9545 try { 9546 dataTransfer.setData(nodeMimeType, json); 9547 dataTransfer.setData("text/html", $(node.span).html()); 9548 dataTransfer.setData("text/plain", node.title); 9549 } catch (ex) { 9550 // IE only accepts 'text' type 9551 tree.warn( 9552 "Could not set data (IE only accepts 'text') - " + ex 9553 ); 9554 } 9555 // We always need to set the 'text' type if we want to drag 9556 // Because IE 11 only accepts this single type. 9557 // If we pass JSON here, IE can can access all node properties, 9558 // even when the source lives in another window. (D'n'd inside 9559 // the same window will always work.) 9560 // The drawback is, that in this case ALL browsers will see 9561 // the JSON representation as 'text', so dragging 9562 // to a text field will insert the JSON string instead of 9563 // the node title. 9564 if (dndOpts.setTextTypeJson) { 9565 dataTransfer.setData("text", json); 9566 } else { 9567 dataTransfer.setData("text", node.title); 9568 } 9569 9570 // Set the allowed drag modes (combinations of move, copy, and link) 9571 // (effectAllowed can only be set in the dragstart event.) 9572 // This can be overridden in the dragStart() callback 9573 prepareDropEffectCallback(event, data); 9574 9575 // Let user cancel or modify above settings 9576 // Realize potential changes by previous callback 9577 if (dndOpts.dragStart(node, data) === false) { 9578 // Cancel dragging 9579 // dataTransfer.dropEffect = "none"; 9580 _clearGlobals(); 9581 return false; 9582 } 9583 applyDropEffectCallback(event, data); 9584 9585 // Unless user set `data.useDefaultImage` to false in dragStart, 9586 // generata a default drag image now: 9587 $extraHelper = null; 9588 9589 if (data.useDefaultImage) { 9590 // Set the title as drag image (otherwise it would contain the expander) 9591 $dragImage = $(node.span).find(".fancytree-title"); 9592 9593 if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) { 9594 // Add a counter badge to node title if dragging more than one node. 9595 // We want this, because the element that is used as drag image 9596 // must be *visible* in the DOM, so we cannot create some hidden 9597 // custom markup. 9598 // See https://kryogenix.org/code/browser/custom-drag-image.html 9599 // Also, since IE 11 and Edge don't support setDragImage() alltogether, 9600 // it gives som feedback to the user. 9601 // The badge will be removed later on drag end. 9602 $extraHelper = $( 9603 "<span class='fancytree-childcounter'/>" 9604 ) 9605 .text("+" + (SOURCE_NODE_LIST.length - 1)) 9606 .appendTo($dragImage); 9607 } 9608 if (dataTransfer.setDragImage) { 9609 // IE 11 and Edge do not support this 9610 dataTransfer.setDragImage($dragImage[0], -10, -10); 9611 } 9612 } 9613 return true; 9614 9615 case "drag": 9616 // Called every few milliseconds (no matter if the 9617 // cursor is over a valid drop target) 9618 // data.tree.info("drag", SOURCE_NODE) 9619 prepareDropEffectCallback(event, data); 9620 dndOpts.dragDrag(node, data); 9621 applyDropEffectCallback(event, data); 9622 9623 $sourceList.toggleClass(classDragRemove, data.isMove); 9624 break; 9625 9626 case "dragend": 9627 // Called at the end of a d'n'd process (after drop) 9628 // Note caveat: If drop removed the dragged source element, 9629 // we may not get this event, since the target does not exist 9630 // anymore 9631 prepareDropEffectCallback(event, data); 9632 9633 _clearGlobals(); 9634 9635 data.isCancelled = !LAST_HIT_MODE; 9636 dndOpts.dragEnd(node, data, !LAST_HIT_MODE); 9637 // applyDropEffectCallback(event, data); 9638 break; 9639 } 9640 } 9641 /* 9642 * Handle dragenter dragover dragleave drop events on the container 9643 */ 9644 function onDropEvent(event) { 9645 var json, 9646 allowAutoExpand, 9647 nodeData, 9648 isSourceFtNode, 9649 r, 9650 res, 9651 tree = this, 9652 dndOpts = tree.options.dnd5, 9653 allowDrop = null, 9654 node = FT.getNode(event), 9655 dataTransfer = 9656 event.dataTransfer || event.originalEvent.dataTransfer, 9657 data = { 9658 tree: tree, 9659 node: node, 9660 options: tree.options, 9661 originalEvent: event.originalEvent, 9662 widget: tree.widget, 9663 hitMode: DRAG_ENTER_RESPONSE, 9664 dataTransfer: dataTransfer, 9665 otherNode: SOURCE_NODE || null, 9666 otherNodeList: SOURCE_NODE_LIST || null, 9667 otherNodeData: null, // set by drop event 9668 useDefaultImage: true, 9669 dropEffect: undefined, 9670 dropEffectSuggested: undefined, 9671 effectAllowed: undefined, // set by dragstart 9672 files: null, // list of File objects (may be []) 9673 isCancelled: undefined, // set by drop event 9674 isMove: undefined, 9675 }; 9676 9677 // data.isMove = dropEffect === "move"; 9678 9679 switch (event.type) { 9680 case "dragenter": 9681 // The dragenter event is fired when a dragged element or 9682 // text selection enters a valid drop target. 9683 9684 DRAG_OVER_STAMP = null; 9685 if (!node) { 9686 // Sometimes we get dragenter for the container element 9687 tree.debug( 9688 "Ignore non-node " + 9689 event.type + 9690 ": " + 9691 event.target.tagName + 9692 "." + 9693 event.target.className 9694 ); 9695 DRAG_ENTER_RESPONSE = false; 9696 break; 9697 } 9698 9699 $(node.span) 9700 .addClass(classDropOver) 9701 .removeClass(classDropAccept + " " + classDropReject); 9702 9703 // Data is only readable in the dragstart and drop event, 9704 // but we can check for the type: 9705 isSourceFtNode = 9706 $.inArray(nodeMimeType, dataTransfer.types) >= 0; 9707 9708 if (dndOpts.preventNonNodes && !isSourceFtNode) { 9709 node.debug("Reject dropping a non-node."); 9710 DRAG_ENTER_RESPONSE = false; 9711 break; 9712 } else if ( 9713 dndOpts.preventForeignNodes && 9714 (!SOURCE_NODE || SOURCE_NODE.tree !== node.tree) 9715 ) { 9716 node.debug("Reject dropping a foreign node."); 9717 DRAG_ENTER_RESPONSE = false; 9718 break; 9719 } else if ( 9720 dndOpts.preventSameParent && 9721 data.otherNode && 9722 data.otherNode.tree === node.tree && 9723 node.parent === data.otherNode.parent 9724 ) { 9725 node.debug("Reject dropping as sibling (same parent)."); 9726 DRAG_ENTER_RESPONSE = false; 9727 break; 9728 } else if ( 9729 dndOpts.preventRecursion && 9730 data.otherNode && 9731 data.otherNode.tree === node.tree && 9732 node.isDescendantOf(data.otherNode) 9733 ) { 9734 node.debug("Reject dropping below own ancestor."); 9735 DRAG_ENTER_RESPONSE = false; 9736 break; 9737 } else if (dndOpts.preventLazyParents && !node.isLoaded()) { 9738 node.warn("Drop over unloaded target node prevented."); 9739 DRAG_ENTER_RESPONSE = false; 9740 break; 9741 } 9742 $dropMarker.show(); 9743 9744 // Call dragEnter() to figure out if (and where) dropping is allowed 9745 prepareDropEffectCallback(event, data); 9746 r = dndOpts.dragEnter(node, data); 9747 9748 res = normalizeDragEnterResponse(r); 9749 // alert("res:" + JSON.stringify(res)) 9750 DRAG_ENTER_RESPONSE = res; 9751 9752 allowDrop = res && (res.over || res.before || res.after); 9753 9754 applyDropEffectCallback(event, data, allowDrop); 9755 break; 9756 9757 case "dragover": 9758 if (!node) { 9759 tree.debug( 9760 "Ignore non-node " + 9761 event.type + 9762 ": " + 9763 event.target.tagName + 9764 "." + 9765 event.target.className 9766 ); 9767 break; 9768 } 9769 // The dragover event is fired when an element or text 9770 // selection is being dragged over a valid drop target 9771 // (every few hundred milliseconds). 9772 // tree.debug( 9773 // event.type + 9774 // ": dropEffect: " + 9775 // dataTransfer.dropEffect 9776 // ); 9777 prepareDropEffectCallback(event, data); 9778 LAST_HIT_MODE = handleDragOver(event, data); 9779 9780 // The flag controls the preventDefault() below: 9781 allowDrop = !!LAST_HIT_MODE; 9782 allowAutoExpand = 9783 LAST_HIT_MODE === "over" || LAST_HIT_MODE === false; 9784 9785 if ( 9786 allowAutoExpand && 9787 !node.expanded && 9788 node.hasChildren() !== false 9789 ) { 9790 if (!DRAG_OVER_STAMP) { 9791 DRAG_OVER_STAMP = Date.now(); 9792 } else if ( 9793 dndOpts.autoExpandMS && 9794 Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS && 9795 !node.isLoading() && 9796 (!dndOpts.dragExpand || 9797 dndOpts.dragExpand(node, data) !== false) 9798 ) { 9799 node.setExpanded(); 9800 } 9801 } else { 9802 DRAG_OVER_STAMP = null; 9803 } 9804 break; 9805 9806 case "dragleave": 9807 // NOTE: dragleave is fired AFTER the dragenter event of the 9808 // FOLLOWING element. 9809 if (!node) { 9810 tree.debug( 9811 "Ignore non-node " + 9812 event.type + 9813 ": " + 9814 event.target.tagName + 9815 "." + 9816 event.target.className 9817 ); 9818 break; 9819 } 9820 if (!$(node.span).hasClass(classDropOver)) { 9821 node.debug("Ignore dragleave (multi)."); 9822 break; 9823 } 9824 $(node.span).removeClass( 9825 classDropOver + 9826 " " + 9827 classDropAccept + 9828 " " + 9829 classDropReject 9830 ); 9831 node.scheduleAction("cancel"); 9832 dndOpts.dragLeave(node, data); 9833 $dropMarker.hide(); 9834 break; 9835 9836 case "drop": 9837 // Data is only readable in the (dragstart and) drop event: 9838 9839 if ($.inArray(nodeMimeType, dataTransfer.types) >= 0) { 9840 nodeData = dataTransfer.getData(nodeMimeType); 9841 tree.info( 9842 event.type + 9843 ": getData('application/x-fancytree-node'): '" + 9844 nodeData + 9845 "'" 9846 ); 9847 } 9848 if (!nodeData) { 9849 // 1. Source is not a Fancytree node, or 9850 // 2. If the FT mime type was set, but returns '', this 9851 // is probably IE 11 (which only supports 'text') 9852 nodeData = dataTransfer.getData("text"); 9853 tree.info( 9854 event.type + ": getData('text'): '" + nodeData + "'" 9855 ); 9856 } 9857 if (nodeData) { 9858 try { 9859 // 'text' type may contain JSON if IE is involved 9860 // and setTextTypeJson option was set 9861 json = JSON.parse(nodeData); 9862 if (json.title !== undefined) { 9863 data.otherNodeData = json; 9864 } 9865 } catch (ex) { 9866 // assume 'text' type contains plain text, so `otherNodeData` 9867 // should not be set 9868 } 9869 } 9870 tree.debug( 9871 event.type + 9872 ": nodeData: '" + 9873 nodeData + 9874 "', otherNodeData: ", 9875 data.otherNodeData 9876 ); 9877 9878 $(node.span).removeClass( 9879 classDropOver + 9880 " " + 9881 classDropAccept + 9882 " " + 9883 classDropReject 9884 ); 9885 9886 // Let user implement the actual drop operation 9887 data.hitMode = LAST_HIT_MODE; 9888 prepareDropEffectCallback(event, data, !LAST_HIT_MODE); 9889 data.isCancelled = !LAST_HIT_MODE; 9890 9891 var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span, 9892 orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree; 9893 9894 dndOpts.dragDrop(node, data); 9895 // applyDropEffectCallback(event, data); 9896 9897 // Prevent browser's default drop handling, i.e. open as link, ... 9898 event.preventDefault(); 9899 9900 if (orgSourceElem && !document.body.contains(orgSourceElem)) { 9901 // The drop handler removed the original drag source from 9902 // the DOM, so the dragend event will probaly not fire. 9903 if (orgSourceTree === tree) { 9904 tree.debug( 9905 "Drop handler removed source element: generating dragEnd." 9906 ); 9907 dndOpts.dragEnd(SOURCE_NODE, data); 9908 } else { 9909 tree.warn( 9910 "Drop handler removed source element: dragend event may be lost." 9911 ); 9912 } 9913 } 9914 9915 _clearGlobals(); 9916 9917 break; 9918 } 9919 // Dnd API madness: we must PREVENT default handling to enable dropping 9920 if (allowDrop) { 9921 event.preventDefault(); 9922 return false; 9923 } 9924 } 9925 9926 /** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject. 9927 * 9928 * @returns {FancytreeNode[]} List of nodes (empty if no drag operation) 9929 * @example 9930 * $.ui.fancytree.getDragNodeList(); 9931 * 9932 * @alias Fancytree_Static#getDragNodeList 9933 * @requires jquery.fancytree.dnd5.js 9934 * @since 2.31 9935 */ 9936 $.ui.fancytree.getDragNodeList = function () { 9937 return SOURCE_NODE_LIST || []; 9938 }; 9939 9940 /** [ext-dnd5] Return the FancytreeNode that is currently being dragged. 9941 * 9942 * If multiple nodes are dragged, only the first is returned. 9943 * 9944 * @returns {FancytreeNode | null} dragged nodes or null if no drag operation 9945 * @example 9946 * $.ui.fancytree.getDragNode(); 9947 * 9948 * @alias Fancytree_Static#getDragNode 9949 * @requires jquery.fancytree.dnd5.js 9950 * @since 2.31 9951 */ 9952 $.ui.fancytree.getDragNode = function () { 9953 return SOURCE_NODE; 9954 }; 9955 9956 /****************************************************************************** 9957 * 9958 */ 9959 9960 $.ui.fancytree.registerExtension({ 9961 name: "dnd5", 9962 version: "2.38.3", 9963 // Default options for this extension. 9964 options: { 9965 autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering 9966 dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after" 9967 dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) 9968 // #1021 `document.body` is not available yet 9969 dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root) 9970 multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed 9971 effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event) 9972 // dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string. 9973 dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver). 9974 preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees 9975 preventLazyParents: true, // Prevent dropping items on unloaded lazy Fancytree nodes 9976 preventNonNodes: false, // Prevent dropping items other than Fancytree nodes 9977 preventRecursion: true, // Prevent dropping nodes on own descendants 9978 preventSameParent: false, // Prevent dropping nodes under same direct parent 9979 preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. 9980 scroll: true, // Enable auto-scrolling while dragging 9981 scrollSensitivity: 20, // Active top/bottom margin in pixel 9982 scrollSpeed: 5, // Pixel per event 9983 setTextTypeJson: false, // Allow dragging of nodes to different IE windows 9984 sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38 9985 // Events (drag support) 9986 dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag 9987 dragDrag: $.noop, // Callback(sourceNode, data) 9988 dragEnd: $.noop, // Callback(sourceNode, data) 9989 // Events (drop support) 9990 dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop 9991 dragOver: $.noop, // Callback(targetNode, data) 9992 dragExpand: $.noop, // Callback(targetNode, data), return false to prevent autoExpand 9993 dragDrop: $.noop, // Callback(targetNode, data) 9994 dragLeave: $.noop, // Callback(targetNode, data) 9995 }, 9996 9997 treeInit: function (ctx) { 9998 var $temp, 9999 tree = ctx.tree, 10000 opts = ctx.options, 10001 glyph = opts.glyph || null, 10002 dndOpts = opts.dnd5; 10003 10004 if ($.inArray("dnd", opts.extensions) >= 0) { 10005 $.error("Extensions 'dnd' and 'dnd5' are mutually exclusive."); 10006 } 10007 if (dndOpts.dragStop) { 10008 $.error( 10009 "dragStop is not used by ext-dnd5. Use dragEnd instead." 10010 ); 10011 } 10012 if (dndOpts.preventRecursiveMoves != null) { 10013 $.error( 10014 "preventRecursiveMoves was renamed to preventRecursion." 10015 ); 10016 } 10017 10018 // Implement `opts.createNode` event to add the 'draggable' attribute 10019 // #680: this must happen before calling super.treeInit() 10020 if (dndOpts.dragStart) { 10021 FT.overrideMethod( 10022 ctx.options, 10023 "createNode", 10024 function (event, data) { 10025 // Default processing if any 10026 this._super.apply(this, arguments); 10027 if (data.node.span) { 10028 data.node.span.draggable = true; 10029 } else { 10030 data.node.warn( 10031 "Cannot add `draggable`: no span tag" 10032 ); 10033 } 10034 } 10035 ); 10036 } 10037 this._superApply(arguments); 10038 10039 this.$container.addClass("fancytree-ext-dnd5"); 10040 10041 // Store the current scroll parent, which may be the tree 10042 // container, any enclosing div, or the document. 10043 // #761: scrollParent() always needs a container child 10044 $temp = $("<span>").appendTo(this.$container); 10045 this.$scrollParent = $temp.scrollParent(); 10046 $temp.remove(); 10047 10048 $dropMarker = $("#fancytree-drop-marker"); 10049 if (!$dropMarker.length) { 10050 $dropMarker = $("<div id='fancytree-drop-marker'></div>") 10051 .hide() 10052 .css({ 10053 "z-index": 1000, 10054 // Drop marker should not steal dragenter/dragover events: 10055 "pointer-events": "none", 10056 }) 10057 .prependTo(dndOpts.dropMarkerParent); 10058 if (glyph) { 10059 FT.setSpanIcon( 10060 $dropMarker[0], 10061 glyph.map._addClass, 10062 glyph.map.dropMarker 10063 ); 10064 } 10065 } 10066 $dropMarker.toggleClass("fancytree-rtl", !!opts.rtl); 10067 10068 // Enable drag support if dragStart() is specified: 10069 if (dndOpts.dragStart) { 10070 // Bind drag event handlers 10071 tree.$container.on( 10072 "dragstart drag dragend", 10073 onDragEvent.bind(tree) 10074 ); 10075 } 10076 // Enable drop support if dragEnter() is specified: 10077 if (dndOpts.dragEnter) { 10078 // Bind drop event handlers 10079 tree.$container.on( 10080 "dragenter dragover dragleave drop", 10081 onDropEvent.bind(tree) 10082 ); 10083 } 10084 }, 10085 }); 10086 // Value returned by `require('jquery.fancytree..')` 10087 return $.ui.fancytree; 10088}); // End of closure 10089 10090/*! 10091 * jquery.fancytree.edit.js 10092 * 10093 * Make node titles editable. 10094 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 10095 * 10096 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 10097 * 10098 * Released under the MIT license 10099 * https://github.com/mar10/fancytree/wiki/LicenseInfo 10100 * 10101 * @version 2.38.3 10102 * @date 2023-02-01T20:52:50Z 10103 */ 10104 10105(function (factory) { 10106 if (typeof define === "function" && define.amd) { 10107 // AMD. Register as an anonymous module. 10108 define(["jquery", "./jquery.fancytree"], factory); 10109 } else if (typeof module === "object" && module.exports) { 10110 // Node/CommonJS 10111 require("./jquery.fancytree"); 10112 module.exports = factory(require("jquery")); 10113 } else { 10114 // Browser globals 10115 factory(jQuery); 10116 } 10117})(function ($) { 10118 "use strict"; 10119 10120 /******************************************************************************* 10121 * Private functions and variables 10122 */ 10123 10124 var isMac = /Mac/.test(navigator.platform), 10125 escapeHtml = $.ui.fancytree.escapeHtml, 10126 trim = $.ui.fancytree.trim, 10127 unescapeHtml = $.ui.fancytree.unescapeHtml; 10128 10129 /** 10130 * [ext-edit] Start inline editing of current node title. 10131 * 10132 * @alias FancytreeNode#editStart 10133 * @requires Fancytree 10134 */ 10135 $.ui.fancytree._FancytreeNodeClass.prototype.editStart = function () { 10136 var $input, 10137 node = this, 10138 tree = this.tree, 10139 local = tree.ext.edit, 10140 instOpts = tree.options.edit, 10141 $title = $(".fancytree-title", node.span), 10142 eventData = { 10143 node: node, 10144 tree: tree, 10145 options: tree.options, 10146 isNew: $(node[tree.statusClassPropName]).hasClass( 10147 "fancytree-edit-new" 10148 ), 10149 orgTitle: node.title, 10150 input: null, 10151 dirty: false, 10152 }; 10153 10154 // beforeEdit may want to modify the title before editing 10155 if ( 10156 instOpts.beforeEdit.call( 10157 node, 10158 { type: "beforeEdit" }, 10159 eventData 10160 ) === false 10161 ) { 10162 return false; 10163 } 10164 $.ui.fancytree.assert(!local.currentNode, "recursive edit"); 10165 local.currentNode = this; 10166 local.eventData = eventData; 10167 10168 // Disable standard Fancytree mouse- and key handling 10169 tree.widget._unbind(); 10170 10171 local.lastDraggableAttrValue = node.span.draggable; 10172 if (local.lastDraggableAttrValue) { 10173 node.span.draggable = false; 10174 } 10175 10176 // #116: ext-dnd prevents the blur event, so we have to catch outer clicks 10177 $(document).on("mousedown.fancytree-edit", function (event) { 10178 if (!$(event.target).hasClass("fancytree-edit-input")) { 10179 node.editEnd(true, event); 10180 } 10181 }); 10182 10183 // Replace node with <input> 10184 $input = $("<input />", { 10185 class: "fancytree-edit-input", 10186 type: "text", 10187 value: tree.options.escapeTitles 10188 ? eventData.orgTitle 10189 : unescapeHtml(eventData.orgTitle), 10190 }); 10191 local.eventData.input = $input; 10192 if (instOpts.adjustWidthOfs != null) { 10193 $input.width($title.width() + instOpts.adjustWidthOfs); 10194 } 10195 if (instOpts.inputCss != null) { 10196 $input.css(instOpts.inputCss); 10197 } 10198 10199 $title.html($input); 10200 10201 // Focus <input> and bind keyboard handler 10202 $input 10203 .focus() 10204 .change(function (event) { 10205 $input.addClass("fancytree-edit-dirty"); 10206 }) 10207 .on("keydown", function (event) { 10208 switch (event.which) { 10209 case $.ui.keyCode.ESCAPE: 10210 node.editEnd(false, event); 10211 break; 10212 case $.ui.keyCode.ENTER: 10213 node.editEnd(true, event); 10214 return false; // so we don't start editmode on Mac 10215 } 10216 event.stopPropagation(); 10217 }) 10218 .blur(function (event) { 10219 return node.editEnd(true, event); 10220 }); 10221 10222 instOpts.edit.call(node, { type: "edit" }, eventData); 10223 }; 10224 10225 /** 10226 * [ext-edit] Stop inline editing. 10227 * @param {Boolean} [applyChanges=false] false: cancel edit, true: save (if modified) 10228 * @alias FancytreeNode#editEnd 10229 * @requires jquery.fancytree.edit.js 10230 */ 10231 $.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function ( 10232 applyChanges, 10233 _event 10234 ) { 10235 var newVal, 10236 node = this, 10237 tree = this.tree, 10238 local = tree.ext.edit, 10239 eventData = local.eventData, 10240 instOpts = tree.options.edit, 10241 $title = $(".fancytree-title", node.span), 10242 $input = $title.find("input.fancytree-edit-input"); 10243 10244 if (instOpts.trim) { 10245 $input.val(trim($input.val())); 10246 } 10247 newVal = $input.val(); 10248 10249 eventData.dirty = newVal !== node.title; 10250 eventData.originalEvent = _event; 10251 10252 // Find out, if saving is required 10253 if (applyChanges === false) { 10254 // If true/false was passed, honor this (except in rename mode, if unchanged) 10255 eventData.save = false; 10256 } else if (eventData.isNew) { 10257 // In create mode, we save everything, except for empty text 10258 eventData.save = newVal !== ""; 10259 } else { 10260 // In rename mode, we save everyting, except for empty or unchanged text 10261 eventData.save = eventData.dirty && newVal !== ""; 10262 } 10263 // Allow to break (keep editor open), modify input, or re-define data.save 10264 if ( 10265 instOpts.beforeClose.call( 10266 node, 10267 { type: "beforeClose" }, 10268 eventData 10269 ) === false 10270 ) { 10271 return false; 10272 } 10273 if ( 10274 eventData.save && 10275 instOpts.save.call(node, { type: "save" }, eventData) === false 10276 ) { 10277 return false; 10278 } 10279 $input.removeClass("fancytree-edit-dirty").off(); 10280 // Unbind outer-click handler 10281 $(document).off(".fancytree-edit"); 10282 10283 if (eventData.save) { 10284 // # 171: escape user input (not required if global escaping is on) 10285 node.setTitle( 10286 tree.options.escapeTitles ? newVal : escapeHtml(newVal) 10287 ); 10288 node.setFocus(); 10289 } else { 10290 if (eventData.isNew) { 10291 node.remove(); 10292 node = eventData.node = null; 10293 local.relatedNode.setFocus(); 10294 } else { 10295 node.renderTitle(); 10296 node.setFocus(); 10297 } 10298 } 10299 local.eventData = null; 10300 local.currentNode = null; 10301 local.relatedNode = null; 10302 // Re-enable mouse and keyboard handling 10303 tree.widget._bind(); 10304 10305 if (node && local.lastDraggableAttrValue) { 10306 node.span.draggable = true; 10307 } 10308 10309 // Set keyboard focus, even if setFocus() claims 'nothing to do' 10310 tree.$container.get(0).focus({ preventScroll: true }); 10311 eventData.input = null; 10312 instOpts.close.call(node, { type: "close" }, eventData); 10313 return true; 10314 }; 10315 10316 /** 10317 * [ext-edit] Create a new child or sibling node and start edit mode. 10318 * 10319 * @param {String} [mode='child'] 'before', 'after', or 'child' 10320 * @param {Object} [init] NodeData (or simple title string) 10321 * @alias FancytreeNode#editCreateNode 10322 * @requires jquery.fancytree.edit.js 10323 * @since 2.4 10324 */ 10325 $.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function ( 10326 mode, 10327 init 10328 ) { 10329 var newNode, 10330 tree = this.tree, 10331 self = this; 10332 10333 mode = mode || "child"; 10334 if (init == null) { 10335 init = { title: "" }; 10336 } else if (typeof init === "string") { 10337 init = { title: init }; 10338 } else { 10339 $.ui.fancytree.assert($.isPlainObject(init)); 10340 } 10341 // Make sure node is expanded (and loaded) in 'child' mode 10342 if ( 10343 mode === "child" && 10344 !this.isExpanded() && 10345 this.hasChildren() !== false 10346 ) { 10347 this.setExpanded().done(function () { 10348 self.editCreateNode(mode, init); 10349 }); 10350 return; 10351 } 10352 newNode = this.addNode(init, mode); 10353 10354 // #644: Don't filter new nodes. 10355 newNode.match = true; 10356 $(newNode[tree.statusClassPropName]) 10357 .removeClass("fancytree-hide") 10358 .addClass("fancytree-match"); 10359 10360 newNode.makeVisible(/*{noAnimation: true}*/).done(function () { 10361 $(newNode[tree.statusClassPropName]).addClass("fancytree-edit-new"); 10362 self.tree.ext.edit.relatedNode = self; 10363 newNode.editStart(); 10364 }); 10365 }; 10366 10367 /** 10368 * [ext-edit] Check if any node in this tree in edit mode. 10369 * 10370 * @returns {FancytreeNode | null} 10371 * @alias Fancytree#isEditing 10372 * @requires jquery.fancytree.edit.js 10373 */ 10374 $.ui.fancytree._FancytreeClass.prototype.isEditing = function () { 10375 return this.ext.edit ? this.ext.edit.currentNode : null; 10376 }; 10377 10378 /** 10379 * [ext-edit] Check if this node is in edit mode. 10380 * @returns {Boolean} true if node is currently beeing edited 10381 * @alias FancytreeNode#isEditing 10382 * @requires jquery.fancytree.edit.js 10383 */ 10384 $.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function () { 10385 return this.tree.ext.edit 10386 ? this.tree.ext.edit.currentNode === this 10387 : false; 10388 }; 10389 10390 /******************************************************************************* 10391 * Extension code 10392 */ 10393 $.ui.fancytree.registerExtension({ 10394 name: "edit", 10395 version: "2.38.3", 10396 // Default options for this extension. 10397 options: { 10398 adjustWidthOfs: 4, // null: don't adjust input size to content 10399 allowEmpty: false, // Prevent empty input 10400 inputCss: { minWidth: "3em" }, 10401 // triggerCancel: ["esc", "tab", "click"], 10402 triggerStart: ["f2", "mac+enter", "shift+click"], 10403 trim: true, // Trim whitespace before save 10404 // Events: 10405 beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available) 10406 beforeEdit: $.noop, // Return false to prevent edit mode 10407 close: $.noop, // Editor was removed 10408 edit: $.noop, // Editor was opened (available as data.input) 10409 // keypress: $.noop, // Not yet implemented 10410 save: $.noop, // Save data.input.val() or return false to keep editor open 10411 }, 10412 // Local attributes 10413 currentNode: null, 10414 10415 treeInit: function (ctx) { 10416 var tree = ctx.tree; 10417 10418 this._superApply(arguments); 10419 10420 this.$container 10421 .addClass("fancytree-ext-edit") 10422 .on("fancytreebeforeupdateviewport", function (event, data) { 10423 var editNode = tree.isEditing(); 10424 // When scrolling, the TR may be re-used by another node, so the 10425 // active cell marker an 10426 if (editNode) { 10427 editNode.info("Cancel edit due to scroll event."); 10428 editNode.editEnd(false, event); 10429 } 10430 }); 10431 }, 10432 nodeClick: function (ctx) { 10433 var eventStr = $.ui.fancytree.eventToString(ctx.originalEvent), 10434 triggerStart = ctx.options.edit.triggerStart; 10435 10436 if ( 10437 eventStr === "shift+click" && 10438 $.inArray("shift+click", triggerStart) >= 0 10439 ) { 10440 if (ctx.originalEvent.shiftKey) { 10441 ctx.node.editStart(); 10442 return false; 10443 } 10444 } 10445 if ( 10446 eventStr === "click" && 10447 $.inArray("clickActive", triggerStart) >= 0 10448 ) { 10449 // Only when click was inside title text (not aynwhere else in the row) 10450 if ( 10451 ctx.node.isActive() && 10452 !ctx.node.isEditing() && 10453 $(ctx.originalEvent.target).hasClass("fancytree-title") 10454 ) { 10455 ctx.node.editStart(); 10456 return false; 10457 } 10458 } 10459 return this._superApply(arguments); 10460 }, 10461 nodeDblclick: function (ctx) { 10462 if ($.inArray("dblclick", ctx.options.edit.triggerStart) >= 0) { 10463 ctx.node.editStart(); 10464 return false; 10465 } 10466 return this._superApply(arguments); 10467 }, 10468 nodeKeydown: function (ctx) { 10469 switch (ctx.originalEvent.which) { 10470 case 113: // [F2] 10471 if ($.inArray("f2", ctx.options.edit.triggerStart) >= 0) { 10472 ctx.node.editStart(); 10473 return false; 10474 } 10475 break; 10476 case $.ui.keyCode.ENTER: 10477 if ( 10478 $.inArray("mac+enter", ctx.options.edit.triggerStart) >= 10479 0 && 10480 isMac 10481 ) { 10482 ctx.node.editStart(); 10483 return false; 10484 } 10485 break; 10486 } 10487 return this._superApply(arguments); 10488 }, 10489 }); 10490 // Value returned by `require('jquery.fancytree..')` 10491 return $.ui.fancytree; 10492}); // End of closure 10493 10494/*! 10495 * jquery.fancytree.filter.js 10496 * 10497 * Remove or highlight tree nodes, based on a filter. 10498 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 10499 * 10500 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 10501 * 10502 * Released under the MIT license 10503 * https://github.com/mar10/fancytree/wiki/LicenseInfo 10504 * 10505 * @version 2.38.3 10506 * @date 2023-02-01T20:52:50Z 10507 */ 10508 10509(function (factory) { 10510 if (typeof define === "function" && define.amd) { 10511 // AMD. Register as an anonymous module. 10512 define(["jquery", "./jquery.fancytree"], factory); 10513 } else if (typeof module === "object" && module.exports) { 10514 // Node/CommonJS 10515 require("./jquery.fancytree"); 10516 module.exports = factory(require("jquery")); 10517 } else { 10518 // Browser globals 10519 factory(jQuery); 10520 } 10521})(function ($) { 10522 "use strict"; 10523 10524 /******************************************************************************* 10525 * Private functions and variables 10526 */ 10527 10528 var KeyNoData = "__not_found__", 10529 escapeHtml = $.ui.fancytree.escapeHtml, 10530 exoticStartChar = "\uFFF7", 10531 exoticEndChar = "\uFFF8"; 10532 function _escapeRegex(str) { 10533 return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 10534 } 10535 10536 function extractHtmlText(s) { 10537 if (s.indexOf(">") >= 0) { 10538 return $("<div/>").html(s).text(); 10539 } 10540 return s; 10541 } 10542 10543 /** 10544 * @description Marks the matching charecters of `text` either by `mark` or 10545 * by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray` 10546 * which is an array of matching groups. 10547 * @param {string} text 10548 * @param {RegExpMatchArray} regexMatchArray 10549 */ 10550 function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) { 10551 // It is extremely infuriating that we can not use `let` or `const` or arrow functions. 10552 // Damn you IE!!! 10553 var matchingIndices = []; 10554 // get the indices of matched characters (Iterate through `RegExpMatchArray`) 10555 for ( 10556 var _matchingArrIdx = 1; 10557 _matchingArrIdx < regexMatchArray.length; 10558 _matchingArrIdx++ 10559 ) { 10560 var _mIdx = 10561 // get matching char index by cumulatively adding 10562 // the matched group length 10563 regexMatchArray[_matchingArrIdx].length + 10564 (_matchingArrIdx === 1 ? 0 : 1) + 10565 (matchingIndices[matchingIndices.length - 1] || 0); 10566 matchingIndices.push(_mIdx); 10567 } 10568 // Map each `text` char to its position and store in `textPoses`. 10569 var textPoses = text.split(""); 10570 if (escapeTitles) { 10571 // If escaping the title, then wrap the matchng char within exotic chars 10572 matchingIndices.forEach(function (v) { 10573 textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar; 10574 }); 10575 } else { 10576 // Otherwise, Wrap the matching chars within `mark`. 10577 matchingIndices.forEach(function (v) { 10578 textPoses[v] = "<mark>" + textPoses[v] + "</mark>"; 10579 }); 10580 } 10581 // Join back the modified `textPoses` to create final highlight markup. 10582 return textPoses.join(""); 10583 } 10584 $.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function ( 10585 filter, 10586 branchMode, 10587 _opts 10588 ) { 10589 var match, 10590 statusNode, 10591 re, 10592 reHighlight, 10593 reExoticStartChar, 10594 reExoticEndChar, 10595 temp, 10596 prevEnableUpdate, 10597 count = 0, 10598 treeOpts = this.options, 10599 escapeTitles = treeOpts.escapeTitles, 10600 prevAutoCollapse = treeOpts.autoCollapse, 10601 opts = $.extend({}, treeOpts.filter, _opts), 10602 hideMode = opts.mode === "hide", 10603 leavesOnly = !!opts.leavesOnly && !branchMode; 10604 10605 // Default to 'match title substring (not case sensitive)' 10606 if (typeof filter === "string") { 10607 if (filter === "") { 10608 this.warn( 10609 "Fancytree passing an empty string as a filter is handled as clearFilter()." 10610 ); 10611 this.clearFilter(); 10612 return; 10613 } 10614 if (opts.fuzzy) { 10615 // See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905 10616 // and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed 10617 // and http://www.dustindiaz.com/autocomplete-fuzzy-matching 10618 match = filter 10619 .split("") 10620 // Escaping the `filter` will not work because, 10621 // it gets further split into individual characters. So, 10622 // escape each character after splitting 10623 .map(_escapeRegex) 10624 .reduce(function (a, b) { 10625 // create capture groups for parts that comes before 10626 // the character 10627 return a + "([^" + b + "]*)" + b; 10628 }, ""); 10629 } else { 10630 match = _escapeRegex(filter); // make sure a '.' is treated literally 10631 } 10632 re = new RegExp(match, "i"); 10633 reHighlight = new RegExp(_escapeRegex(filter), "gi"); 10634 if (escapeTitles) { 10635 reExoticStartChar = new RegExp( 10636 _escapeRegex(exoticStartChar), 10637 "g" 10638 ); 10639 reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g"); 10640 } 10641 filter = function (node) { 10642 if (!node.title) { 10643 return false; 10644 } 10645 var text = escapeTitles 10646 ? node.title 10647 : extractHtmlText(node.title), 10648 // `.match` instead of `.test` to get the capture groups 10649 res = text.match(re); 10650 if (res && opts.highlight) { 10651 if (escapeTitles) { 10652 if (opts.fuzzy) { 10653 temp = _markFuzzyMatchedChars( 10654 text, 10655 res, 10656 escapeTitles 10657 ); 10658 } else { 10659 // #740: we must not apply the marks to escaped entity names, e.g. `"` 10660 // Use some exotic characters to mark matches: 10661 temp = text.replace(reHighlight, function (s) { 10662 return exoticStartChar + s + exoticEndChar; 10663 }); 10664 } 10665 // now we can escape the title... 10666 node.titleWithHighlight = escapeHtml(temp) 10667 // ... and finally insert the desired `<mark>` tags 10668 .replace(reExoticStartChar, "<mark>") 10669 .replace(reExoticEndChar, "</mark>"); 10670 } else { 10671 if (opts.fuzzy) { 10672 node.titleWithHighlight = _markFuzzyMatchedChars( 10673 text, 10674 res 10675 ); 10676 } else { 10677 node.titleWithHighlight = text.replace( 10678 reHighlight, 10679 function (s) { 10680 return "<mark>" + s + "</mark>"; 10681 } 10682 ); 10683 } 10684 } 10685 // node.debug("filter", escapeTitles, text, node.titleWithHighlight); 10686 } 10687 return !!res; 10688 }; 10689 } 10690 10691 this.enableFilter = true; 10692 this.lastFilterArgs = arguments; 10693 10694 prevEnableUpdate = this.enableUpdate(false); 10695 10696 this.$div.addClass("fancytree-ext-filter"); 10697 if (hideMode) { 10698 this.$div.addClass("fancytree-ext-filter-hide"); 10699 } else { 10700 this.$div.addClass("fancytree-ext-filter-dimm"); 10701 } 10702 this.$div.toggleClass( 10703 "fancytree-ext-filter-hide-expanders", 10704 !!opts.hideExpanders 10705 ); 10706 // Reset current filter 10707 this.rootNode.subMatchCount = 0; 10708 this.visit(function (node) { 10709 delete node.match; 10710 delete node.titleWithHighlight; 10711 node.subMatchCount = 0; 10712 }); 10713 statusNode = this.getRootNode()._findDirectChild(KeyNoData); 10714 if (statusNode) { 10715 statusNode.remove(); 10716 } 10717 10718 // Adjust node.hide, .match, and .subMatchCount properties 10719 treeOpts.autoCollapse = false; // #528 10720 10721 this.visit(function (node) { 10722 if (leavesOnly && node.children != null) { 10723 return; 10724 } 10725 var res = filter(node), 10726 matchedByBranch = false; 10727 10728 if (res === "skip") { 10729 node.visit(function (c) { 10730 c.match = false; 10731 }, true); 10732 return "skip"; 10733 } 10734 if (!res && (branchMode || res === "branch") && node.parent.match) { 10735 res = true; 10736 matchedByBranch = true; 10737 } 10738 if (res) { 10739 count++; 10740 node.match = true; 10741 node.visitParents(function (p) { 10742 if (p !== node) { 10743 p.subMatchCount += 1; 10744 } 10745 // Expand match (unless this is no real match, but only a node in a matched branch) 10746 if (opts.autoExpand && !matchedByBranch && !p.expanded) { 10747 p.setExpanded(true, { 10748 noAnimation: true, 10749 noEvents: true, 10750 scrollIntoView: false, 10751 }); 10752 p._filterAutoExpanded = true; 10753 } 10754 }, true); 10755 } 10756 }); 10757 treeOpts.autoCollapse = prevAutoCollapse; 10758 10759 if (count === 0 && opts.nodata && hideMode) { 10760 statusNode = opts.nodata; 10761 if (typeof statusNode === "function") { 10762 statusNode = statusNode(); 10763 } 10764 if (statusNode === true) { 10765 statusNode = {}; 10766 } else if (typeof statusNode === "string") { 10767 statusNode = { title: statusNode }; 10768 } 10769 statusNode = $.extend( 10770 { 10771 statusNodeType: "nodata", 10772 key: KeyNoData, 10773 title: this.options.strings.noData, 10774 }, 10775 statusNode 10776 ); 10777 10778 this.getRootNode().addNode(statusNode).match = true; 10779 } 10780 // Redraw whole tree 10781 this._callHook("treeStructureChanged", this, "applyFilter"); 10782 // this.render(); 10783 this.enableUpdate(prevEnableUpdate); 10784 return count; 10785 }; 10786 10787 /** 10788 * [ext-filter] Dimm or hide nodes. 10789 * 10790 * @param {function | string} filter 10791 * @param {boolean} [opts={autoExpand: false, leavesOnly: false}] 10792 * @returns {integer} count 10793 * @alias Fancytree#filterNodes 10794 * @requires jquery.fancytree.filter.js 10795 */ 10796 $.ui.fancytree._FancytreeClass.prototype.filterNodes = function ( 10797 filter, 10798 opts 10799 ) { 10800 if (typeof opts === "boolean") { 10801 opts = { leavesOnly: opts }; 10802 this.warn( 10803 "Fancytree.filterNodes() leavesOnly option is deprecated since 2.9.0 / 2015-04-19. Use opts.leavesOnly instead." 10804 ); 10805 } 10806 return this._applyFilterImpl(filter, false, opts); 10807 }; 10808 10809 /** 10810 * [ext-filter] Dimm or hide whole branches. 10811 * 10812 * @param {function | string} filter 10813 * @param {boolean} [opts={autoExpand: false}] 10814 * @returns {integer} count 10815 * @alias Fancytree#filterBranches 10816 * @requires jquery.fancytree.filter.js 10817 */ 10818 $.ui.fancytree._FancytreeClass.prototype.filterBranches = function ( 10819 filter, 10820 opts 10821 ) { 10822 return this._applyFilterImpl(filter, true, opts); 10823 }; 10824 10825 /** 10826 * [ext-filter] Re-apply current filter. 10827 * 10828 * @returns {integer} count 10829 * @alias Fancytree#updateFilter 10830 * @requires jquery.fancytree.filter.js 10831 * @since 2.38 10832 */ 10833 $.ui.fancytree._FancytreeClass.prototype.updateFilter = function () { 10834 if ( 10835 this.enableFilter && 10836 this.lastFilterArgs && 10837 this.options.filter.autoApply 10838 ) { 10839 this._applyFilterImpl.apply(this, this.lastFilterArgs); 10840 } else { 10841 this.warn("updateFilter(): no filter active."); 10842 } 10843 }; 10844 10845 /** 10846 * [ext-filter] Reset the filter. 10847 * 10848 * @alias Fancytree#clearFilter 10849 * @requires jquery.fancytree.filter.js 10850 */ 10851 $.ui.fancytree._FancytreeClass.prototype.clearFilter = function () { 10852 var $title, 10853 statusNode = this.getRootNode()._findDirectChild(KeyNoData), 10854 escapeTitles = this.options.escapeTitles, 10855 enhanceTitle = this.options.enhanceTitle, 10856 prevEnableUpdate = this.enableUpdate(false); 10857 10858 if (statusNode) { 10859 statusNode.remove(); 10860 } 10861 // we also counted root node's subMatchCount 10862 delete this.rootNode.match; 10863 delete this.rootNode.subMatchCount; 10864 10865 this.visit(function (node) { 10866 if (node.match && node.span) { 10867 // #491, #601 10868 $title = $(node.span).find(">span.fancytree-title"); 10869 if (escapeTitles) { 10870 $title.text(node.title); 10871 } else { 10872 $title.html(node.title); 10873 } 10874 if (enhanceTitle) { 10875 enhanceTitle( 10876 { type: "enhanceTitle" }, 10877 { node: node, $title: $title } 10878 ); 10879 } 10880 } 10881 delete node.match; 10882 delete node.subMatchCount; 10883 delete node.titleWithHighlight; 10884 if (node.$subMatchBadge) { 10885 node.$subMatchBadge.remove(); 10886 delete node.$subMatchBadge; 10887 } 10888 if (node._filterAutoExpanded && node.expanded) { 10889 node.setExpanded(false, { 10890 noAnimation: true, 10891 noEvents: true, 10892 scrollIntoView: false, 10893 }); 10894 } 10895 delete node._filterAutoExpanded; 10896 }); 10897 this.enableFilter = false; 10898 this.lastFilterArgs = null; 10899 this.$div.removeClass( 10900 "fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide" 10901 ); 10902 this._callHook("treeStructureChanged", this, "clearFilter"); 10903 // this.render(); 10904 this.enableUpdate(prevEnableUpdate); 10905 }; 10906 10907 /** 10908 * [ext-filter] Return true if a filter is currently applied. 10909 * 10910 * @returns {Boolean} 10911 * @alias Fancytree#isFilterActive 10912 * @requires jquery.fancytree.filter.js 10913 * @since 2.13 10914 */ 10915 $.ui.fancytree._FancytreeClass.prototype.isFilterActive = function () { 10916 return !!this.enableFilter; 10917 }; 10918 10919 /** 10920 * [ext-filter] Return true if this node is matched by current filter (or no filter is active). 10921 * 10922 * @returns {Boolean} 10923 * @alias FancytreeNode#isMatched 10924 * @requires jquery.fancytree.filter.js 10925 * @since 2.13 10926 */ 10927 $.ui.fancytree._FancytreeNodeClass.prototype.isMatched = function () { 10928 return !(this.tree.enableFilter && !this.match); 10929 }; 10930 10931 /******************************************************************************* 10932 * Extension code 10933 */ 10934 $.ui.fancytree.registerExtension({ 10935 name: "filter", 10936 version: "2.38.3", 10937 // Default options for this extension. 10938 options: { 10939 autoApply: true, // Re-apply last filter if lazy data is loaded 10940 autoExpand: false, // Expand all branches that contain matches while filtered 10941 counter: true, // Show a badge with number of matching child nodes near parent icons 10942 fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar' 10943 hideExpandedCounter: true, // Hide counter badge if parent is expanded 10944 hideExpanders: false, // Hide expanders if all child nodes are hidden by filter 10945 highlight: true, // Highlight matches by wrapping inside <mark> tags 10946 leavesOnly: false, // Match end nodes only 10947 nodata: true, // Display a 'no data' status node if result is empty 10948 mode: "dimm", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) 10949 }, 10950 nodeLoadChildren: function (ctx, source) { 10951 var tree = ctx.tree; 10952 10953 return this._superApply(arguments).done(function () { 10954 if ( 10955 tree.enableFilter && 10956 tree.lastFilterArgs && 10957 ctx.options.filter.autoApply 10958 ) { 10959 tree._applyFilterImpl.apply(tree, tree.lastFilterArgs); 10960 } 10961 }); 10962 }, 10963 nodeSetExpanded: function (ctx, flag, callOpts) { 10964 var node = ctx.node; 10965 10966 delete node._filterAutoExpanded; 10967 // Make sure counter badge is displayed again, when node is beeing collapsed 10968 if ( 10969 !flag && 10970 ctx.options.filter.hideExpandedCounter && 10971 node.$subMatchBadge 10972 ) { 10973 node.$subMatchBadge.show(); 10974 } 10975 return this._superApply(arguments); 10976 }, 10977 nodeRenderStatus: function (ctx) { 10978 // Set classes for current status 10979 var res, 10980 node = ctx.node, 10981 tree = ctx.tree, 10982 opts = ctx.options.filter, 10983 $title = $(node.span).find("span.fancytree-title"), 10984 $span = $(node[tree.statusClassPropName]), 10985 enhanceTitle = ctx.options.enhanceTitle, 10986 escapeTitles = ctx.options.escapeTitles; 10987 10988 res = this._super(ctx); 10989 // nothing to do, if node was not yet rendered 10990 if (!$span.length || !tree.enableFilter) { 10991 return res; 10992 } 10993 $span 10994 .toggleClass("fancytree-match", !!node.match) 10995 .toggleClass("fancytree-submatch", !!node.subMatchCount) 10996 .toggleClass( 10997 "fancytree-hide", 10998 !(node.match || node.subMatchCount) 10999 ); 11000 // Add/update counter badge 11001 if ( 11002 opts.counter && 11003 node.subMatchCount && 11004 (!node.isExpanded() || !opts.hideExpandedCounter) 11005 ) { 11006 if (!node.$subMatchBadge) { 11007 node.$subMatchBadge = $( 11008 "<span class='fancytree-childcounter'/>" 11009 ); 11010 $( 11011 "span.fancytree-icon, span.fancytree-custom-icon", 11012 node.span 11013 ).append(node.$subMatchBadge); 11014 } 11015 node.$subMatchBadge.show().text(node.subMatchCount); 11016 } else if (node.$subMatchBadge) { 11017 node.$subMatchBadge.hide(); 11018 } 11019 // node.debug("nodeRenderStatus", node.titleWithHighlight, node.title) 11020 // #601: also check for $title.length, because we don't need to render 11021 // if node.span is null (i.e. not rendered) 11022 if (node.span && (!node.isEditing || !node.isEditing.call(node))) { 11023 if (node.titleWithHighlight) { 11024 $title.html(node.titleWithHighlight); 11025 } else if (escapeTitles) { 11026 $title.text(node.title); 11027 } else { 11028 $title.html(node.title); 11029 } 11030 if (enhanceTitle) { 11031 enhanceTitle( 11032 { type: "enhanceTitle" }, 11033 { node: node, $title: $title } 11034 ); 11035 } 11036 } 11037 return res; 11038 }, 11039 }); 11040 // Value returned by `require('jquery.fancytree..')` 11041 return $.ui.fancytree; 11042}); // End of closure 11043 11044/*! 11045 * jquery.fancytree.glyph.js 11046 * 11047 * Use glyph-fonts, ligature-fonts, or SVG icons instead of icon sprites. 11048 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 11049 * 11050 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 11051 * 11052 * Released under the MIT license 11053 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11054 * 11055 * @version 2.38.3 11056 * @date 2023-02-01T20:52:50Z 11057 */ 11058 11059(function (factory) { 11060 if (typeof define === "function" && define.amd) { 11061 // AMD. Register as an anonymous module. 11062 define(["jquery", "./jquery.fancytree"], factory); 11063 } else if (typeof module === "object" && module.exports) { 11064 // Node/CommonJS 11065 require("./jquery.fancytree"); 11066 module.exports = factory(require("jquery")); 11067 } else { 11068 // Browser globals 11069 factory(jQuery); 11070 } 11071})(function ($) { 11072 "use strict"; 11073 11074 /****************************************************************************** 11075 * Private functions and variables 11076 */ 11077 11078 var FT = $.ui.fancytree, 11079 PRESETS = { 11080 awesome3: { 11081 // Outdated! 11082 _addClass: "", 11083 checkbox: "icon-check-empty", 11084 checkboxSelected: "icon-check", 11085 checkboxUnknown: "icon-check icon-muted", 11086 dragHelper: "icon-caret-right", 11087 dropMarker: "icon-caret-right", 11088 error: "icon-exclamation-sign", 11089 expanderClosed: "icon-caret-right", 11090 expanderLazy: "icon-angle-right", 11091 expanderOpen: "icon-caret-down", 11092 loading: "icon-refresh icon-spin", 11093 nodata: "icon-meh", 11094 noExpander: "", 11095 radio: "icon-circle-blank", 11096 radioSelected: "icon-circle", 11097 // radioUnknown: "icon-circle icon-muted", 11098 // Default node icons. 11099 // (Use tree.options.icon callback to define custom icons based on node data) 11100 doc: "icon-file-alt", 11101 docOpen: "icon-file-alt", 11102 folder: "icon-folder-close-alt", 11103 folderOpen: "icon-folder-open-alt", 11104 }, 11105 awesome4: { 11106 _addClass: "fa", 11107 checkbox: "fa-square-o", 11108 checkboxSelected: "fa-check-square-o", 11109 checkboxUnknown: "fa-square fancytree-helper-indeterminate-cb", 11110 dragHelper: "fa-arrow-right", 11111 dropMarker: "fa-long-arrow-right", 11112 error: "fa-warning", 11113 expanderClosed: "fa-caret-right", 11114 expanderLazy: "fa-angle-right", 11115 expanderOpen: "fa-caret-down", 11116 // We may prevent wobbling rotations on FF by creating a separate sub element: 11117 loading: { html: "<span class='fa fa-spinner fa-pulse' />" }, 11118 nodata: "fa-meh-o", 11119 noExpander: "", 11120 radio: "fa-circle-thin", // "fa-circle-o" 11121 radioSelected: "fa-circle", 11122 // radioUnknown: "fa-dot-circle-o", 11123 // Default node icons. 11124 // (Use tree.options.icon callback to define custom icons based on node data) 11125 doc: "fa-file-o", 11126 docOpen: "fa-file-o", 11127 folder: "fa-folder-o", 11128 folderOpen: "fa-folder-open-o", 11129 }, 11130 awesome5: { 11131 // fontawesome 5 have several different base classes 11132 // "far, fas, fal and fab" The rendered svg puts that prefix 11133 // in a different location so we have to keep them separate here 11134 _addClass: "", 11135 checkbox: "far fa-square", 11136 checkboxSelected: "far fa-check-square", 11137 // checkboxUnknown: "far fa-window-close", 11138 checkboxUnknown: 11139 "fas fa-square fancytree-helper-indeterminate-cb", 11140 radio: "far fa-circle", 11141 radioSelected: "fas fa-circle", 11142 radioUnknown: "far fa-dot-circle", 11143 dragHelper: "fas fa-arrow-right", 11144 dropMarker: "fas fa-long-arrow-alt-right", 11145 error: "fas fa-exclamation-triangle", 11146 expanderClosed: "fas fa-caret-right", 11147 expanderLazy: "fas fa-angle-right", 11148 expanderOpen: "fas fa-caret-down", 11149 loading: "fas fa-spinner fa-pulse", 11150 nodata: "far fa-meh", 11151 noExpander: "", 11152 // Default node icons. 11153 // (Use tree.options.icon callback to define custom icons based on node data) 11154 doc: "far fa-file", 11155 docOpen: "far fa-file", 11156 folder: "far fa-folder", 11157 folderOpen: "far fa-folder-open", 11158 }, 11159 bootstrap3: { 11160 _addClass: "glyphicon", 11161 checkbox: "glyphicon-unchecked", 11162 checkboxSelected: "glyphicon-check", 11163 checkboxUnknown: 11164 "glyphicon-expand fancytree-helper-indeterminate-cb", // "glyphicon-share", 11165 dragHelper: "glyphicon-play", 11166 dropMarker: "glyphicon-arrow-right", 11167 error: "glyphicon-warning-sign", 11168 expanderClosed: "glyphicon-menu-right", // glyphicon-plus-sign 11169 expanderLazy: "glyphicon-menu-right", // glyphicon-plus-sign 11170 expanderOpen: "glyphicon-menu-down", // glyphicon-minus-sign 11171 loading: "glyphicon-refresh fancytree-helper-spin", 11172 nodata: "glyphicon-info-sign", 11173 noExpander: "", 11174 radio: "glyphicon-remove-circle", // "glyphicon-unchecked", 11175 radioSelected: "glyphicon-ok-circle", // "glyphicon-check", 11176 // radioUnknown: "glyphicon-ban-circle", 11177 // Default node icons. 11178 // (Use tree.options.icon callback to define custom icons based on node data) 11179 doc: "glyphicon-file", 11180 docOpen: "glyphicon-file", 11181 folder: "glyphicon-folder-close", 11182 folderOpen: "glyphicon-folder-open", 11183 }, 11184 material: { 11185 _addClass: "material-icons", 11186 checkbox: { text: "check_box_outline_blank" }, 11187 checkboxSelected: { text: "check_box" }, 11188 checkboxUnknown: { text: "indeterminate_check_box" }, 11189 dragHelper: { text: "play_arrow" }, 11190 dropMarker: { text: "arrow-forward" }, 11191 error: { text: "warning" }, 11192 expanderClosed: { text: "chevron_right" }, 11193 expanderLazy: { text: "last_page" }, 11194 expanderOpen: { text: "expand_more" }, 11195 loading: { 11196 text: "autorenew", 11197 addClass: "fancytree-helper-spin", 11198 }, 11199 nodata: { text: "info" }, 11200 noExpander: { text: "" }, 11201 radio: { text: "radio_button_unchecked" }, 11202 radioSelected: { text: "radio_button_checked" }, 11203 // Default node icons. 11204 // (Use tree.options.icon callback to define custom icons based on node data) 11205 doc: { text: "insert_drive_file" }, 11206 docOpen: { text: "insert_drive_file" }, 11207 folder: { text: "folder" }, 11208 folderOpen: { text: "folder_open" }, 11209 }, 11210 }; 11211 11212 function setIcon(node, span, baseClass, opts, type) { 11213 var map = opts.map, 11214 icon = map[type], 11215 $span = $(span), 11216 $counter = $span.find(".fancytree-childcounter"), 11217 setClass = baseClass + " " + (map._addClass || ""); 11218 11219 // #871 Allow a callback 11220 if (typeof icon === "function") { 11221 icon = icon.call(this, node, span, type); 11222 } 11223 // node.debug( "setIcon(" + baseClass + ", " + type + "): " + "oldIcon" + " -> " + icon ); 11224 // #871: propsed this, but I am not sure how robust this is, e.g. 11225 // the prefix (fas, far) class changes are not considered? 11226 // if (span.tagName === "svg" && opts.preset === "awesome5") { 11227 // // fa5 script converts <i> to <svg> so call a specific handler. 11228 // var oldIcon = "fa-" + $span.data("icon"); 11229 // // node.debug( "setIcon(" + baseClass + ", " + type + "): " + oldIcon + " -> " + icon ); 11230 // if (typeof oldIcon === "string") { 11231 // $span.removeClass(oldIcon); 11232 // } 11233 // if (typeof icon === "string") { 11234 // $span.addClass(icon); 11235 // } 11236 // return; 11237 // } 11238 if (typeof icon === "string") { 11239 // #883: remove inner html that may be added by prev. mode 11240 span.innerHTML = ""; 11241 $span.attr("class", setClass + " " + icon).append($counter); 11242 } else if (icon) { 11243 if (icon.text) { 11244 span.textContent = "" + icon.text; 11245 } else if (icon.html) { 11246 span.innerHTML = icon.html; 11247 } else { 11248 span.innerHTML = ""; 11249 } 11250 $span 11251 .attr("class", setClass + " " + (icon.addClass || "")) 11252 .append($counter); 11253 } 11254 } 11255 11256 $.ui.fancytree.registerExtension({ 11257 name: "glyph", 11258 version: "2.38.3", 11259 // Default options for this extension. 11260 options: { 11261 preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material' 11262 map: {}, 11263 }, 11264 11265 treeInit: function (ctx) { 11266 var tree = ctx.tree, 11267 opts = ctx.options.glyph; 11268 11269 if (opts.preset) { 11270 FT.assert( 11271 !!PRESETS[opts.preset], 11272 "Invalid value for `options.glyph.preset`: " + opts.preset 11273 ); 11274 opts.map = $.extend({}, PRESETS[opts.preset], opts.map); 11275 } else { 11276 tree.warn("ext-glyph: missing `preset` option."); 11277 } 11278 this._superApply(arguments); 11279 tree.$container.addClass("fancytree-ext-glyph"); 11280 }, 11281 nodeRenderStatus: function (ctx) { 11282 var checkbox, 11283 icon, 11284 res, 11285 span, 11286 node = ctx.node, 11287 $span = $(node.span), 11288 opts = ctx.options.glyph; 11289 11290 res = this._super(ctx); 11291 11292 if (node.isRootNode()) { 11293 return res; 11294 } 11295 span = $span.children(".fancytree-expander").get(0); 11296 if (span) { 11297 // if( node.isLoading() ){ 11298 // icon = "loading"; 11299 if (node.expanded && node.hasChildren()) { 11300 icon = "expanderOpen"; 11301 } else if (node.isUndefined()) { 11302 icon = "expanderLazy"; 11303 } else if (node.hasChildren()) { 11304 icon = "expanderClosed"; 11305 } else { 11306 icon = "noExpander"; 11307 } 11308 // span.className = "fancytree-expander " + map[icon]; 11309 setIcon(node, span, "fancytree-expander", opts, icon); 11310 } 11311 11312 if (node.tr) { 11313 span = $("td", node.tr).find(".fancytree-checkbox").get(0); 11314 } else { 11315 span = $span.children(".fancytree-checkbox").get(0); 11316 } 11317 if (span) { 11318 checkbox = FT.evalOption("checkbox", node, node, opts, false); 11319 if ( 11320 (node.parent && node.parent.radiogroup) || 11321 checkbox === "radio" 11322 ) { 11323 icon = node.selected ? "radioSelected" : "radio"; 11324 setIcon( 11325 node, 11326 span, 11327 "fancytree-checkbox fancytree-radio", 11328 opts, 11329 icon 11330 ); 11331 } else { 11332 // eslint-disable-next-line no-nested-ternary 11333 icon = node.selected 11334 ? "checkboxSelected" 11335 : node.partsel 11336 ? "checkboxUnknown" 11337 : "checkbox"; 11338 // span.className = "fancytree-checkbox " + map[icon]; 11339 setIcon(node, span, "fancytree-checkbox", opts, icon); 11340 } 11341 } 11342 11343 // Standard icon (note that this does not match .fancytree-custom-icon, 11344 // that might be set by opts.icon callbacks) 11345 span = $span.children(".fancytree-icon").get(0); 11346 if (span) { 11347 if (node.statusNodeType) { 11348 icon = node.statusNodeType; // loading, error 11349 } else if (node.folder) { 11350 icon = 11351 node.expanded && node.hasChildren() 11352 ? "folderOpen" 11353 : "folder"; 11354 } else { 11355 icon = node.expanded ? "docOpen" : "doc"; 11356 } 11357 setIcon(node, span, "fancytree-icon", opts, icon); 11358 } 11359 return res; 11360 }, 11361 nodeSetStatus: function (ctx, status, message, details) { 11362 var res, 11363 span, 11364 opts = ctx.options.glyph, 11365 node = ctx.node; 11366 11367 res = this._superApply(arguments); 11368 11369 if ( 11370 status === "error" || 11371 status === "loading" || 11372 status === "nodata" 11373 ) { 11374 if (node.parent) { 11375 span = $(".fancytree-expander", node.span).get(0); 11376 if (span) { 11377 setIcon(node, span, "fancytree-expander", opts, status); 11378 } 11379 } else { 11380 // 11381 span = $( 11382 ".fancytree-statusnode-" + status, 11383 node[this.nodeContainerAttrName] 11384 ) 11385 .find(".fancytree-icon") 11386 .get(0); 11387 if (span) { 11388 setIcon(node, span, "fancytree-icon", opts, status); 11389 } 11390 } 11391 } 11392 return res; 11393 }, 11394 }); 11395 // Value returned by `require('jquery.fancytree..')` 11396 return $.ui.fancytree; 11397}); // End of closure 11398 11399/*! 11400 * jquery.fancytree.gridnav.js 11401 * 11402 * Support keyboard navigation for trees with embedded input controls. 11403 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 11404 * 11405 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 11406 * 11407 * Released under the MIT license 11408 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11409 * 11410 * @version 2.38.3 11411 * @date 2023-02-01T20:52:50Z 11412 */ 11413 11414(function (factory) { 11415 if (typeof define === "function" && define.amd) { 11416 // AMD. Register as an anonymous module. 11417 define([ 11418 "jquery", 11419 "./jquery.fancytree", 11420 "./jquery.fancytree.table", 11421 ], factory); 11422 } else if (typeof module === "object" && module.exports) { 11423 // Node/CommonJS 11424 require("./jquery.fancytree.table"); // core + table 11425 module.exports = factory(require("jquery")); 11426 } else { 11427 // Browser globals 11428 factory(jQuery); 11429 } 11430})(function ($) { 11431 "use strict"; 11432 11433 /******************************************************************************* 11434 * Private functions and variables 11435 */ 11436 11437 // Allow these navigation keys even when input controls are focused 11438 11439 var KC = $.ui.keyCode, 11440 // which keys are *not* handled by embedded control, but passed to tree 11441 // navigation handler: 11442 NAV_KEYS = { 11443 text: [KC.UP, KC.DOWN], 11444 checkbox: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 11445 link: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 11446 radiobutton: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 11447 "select-one": [KC.LEFT, KC.RIGHT], 11448 "select-multiple": [KC.LEFT, KC.RIGHT], 11449 }; 11450 11451 /* Calculate TD column index (considering colspans).*/ 11452 function getColIdx($tr, $td) { 11453 var colspan, 11454 td = $td.get(0), 11455 idx = 0; 11456 11457 $tr.children().each(function () { 11458 if (this === td) { 11459 return false; 11460 } 11461 colspan = $(this).prop("colspan"); 11462 idx += colspan ? colspan : 1; 11463 }); 11464 return idx; 11465 } 11466 11467 /* Find TD at given column index (considering colspans).*/ 11468 function findTdAtColIdx($tr, colIdx) { 11469 var colspan, 11470 res = null, 11471 idx = 0; 11472 11473 $tr.children().each(function () { 11474 if (idx >= colIdx) { 11475 res = $(this); 11476 return false; 11477 } 11478 colspan = $(this).prop("colspan"); 11479 idx += colspan ? colspan : 1; 11480 }); 11481 return res; 11482 } 11483 11484 /* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */ 11485 function findNeighbourTd($target, keyCode) { 11486 var $tr, 11487 colIdx, 11488 $td = $target.closest("td"), 11489 $tdNext = null; 11490 11491 switch (keyCode) { 11492 case KC.LEFT: 11493 $tdNext = $td.prev(); 11494 break; 11495 case KC.RIGHT: 11496 $tdNext = $td.next(); 11497 break; 11498 case KC.UP: 11499 case KC.DOWN: 11500 $tr = $td.parent(); 11501 colIdx = getColIdx($tr, $td); 11502 while (true) { 11503 $tr = keyCode === KC.UP ? $tr.prev() : $tr.next(); 11504 if (!$tr.length) { 11505 break; 11506 } 11507 // Skip hidden rows 11508 if ($tr.is(":hidden")) { 11509 continue; 11510 } 11511 // Find adjacent cell in the same column 11512 $tdNext = findTdAtColIdx($tr, colIdx); 11513 // Skip cells that don't conatain a focusable element 11514 if ($tdNext && $tdNext.find(":input,a").length) { 11515 break; 11516 } 11517 } 11518 break; 11519 } 11520 return $tdNext; 11521 } 11522 11523 /******************************************************************************* 11524 * Extension code 11525 */ 11526 $.ui.fancytree.registerExtension({ 11527 name: "gridnav", 11528 version: "2.38.3", 11529 // Default options for this extension. 11530 options: { 11531 autofocusInput: false, // Focus first embedded input if node gets activated 11532 handleCursorKeys: true, // Allow UP/DOWN in inputs to move to prev/next node 11533 }, 11534 11535 treeInit: function (ctx) { 11536 // gridnav requires the table extension to be loaded before itself 11537 this._requireExtension("table", true, true); 11538 this._superApply(arguments); 11539 11540 this.$container.addClass("fancytree-ext-gridnav"); 11541 11542 // Activate node if embedded input gets focus (due to a click) 11543 this.$container.on("focusin", function (event) { 11544 var ctx2, 11545 node = $.ui.fancytree.getNode(event.target); 11546 11547 if (node && !node.isActive()) { 11548 // Call node.setActive(), but also pass the event 11549 ctx2 = ctx.tree._makeHookContext(node, event); 11550 ctx.tree._callHook("nodeSetActive", ctx2, true); 11551 } 11552 }); 11553 }, 11554 nodeSetActive: function (ctx, flag, callOpts) { 11555 var $outer, 11556 opts = ctx.options.gridnav, 11557 node = ctx.node, 11558 event = ctx.originalEvent || {}, 11559 triggeredByInput = $(event.target).is(":input"); 11560 11561 flag = flag !== false; 11562 11563 this._superApply(arguments); 11564 11565 if (flag) { 11566 if (ctx.options.titlesTabbable) { 11567 if (!triggeredByInput) { 11568 $(node.span).find("span.fancytree-title").focus(); 11569 node.setFocus(); 11570 } 11571 // If one node is tabbable, the container no longer needs to be 11572 ctx.tree.$container.attr("tabindex", "-1"); 11573 // ctx.tree.$container.removeAttr("tabindex"); 11574 } else if (opts.autofocusInput && !triggeredByInput) { 11575 // Set focus to input sub input (if node was clicked, but not 11576 // when TAB was pressed ) 11577 $outer = $(node.tr || node.span); 11578 $outer.find(":input:enabled").first().focus(); 11579 } 11580 } 11581 }, 11582 nodeKeydown: function (ctx) { 11583 var inputType, 11584 handleKeys, 11585 $td, 11586 opts = ctx.options.gridnav, 11587 event = ctx.originalEvent, 11588 $target = $(event.target); 11589 11590 if ($target.is(":input:enabled")) { 11591 inputType = $target.prop("type"); 11592 } else if ($target.is("a")) { 11593 inputType = "link"; 11594 } 11595 // ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType); 11596 11597 if (inputType && opts.handleCursorKeys) { 11598 handleKeys = NAV_KEYS[inputType]; 11599 if (handleKeys && $.inArray(event.which, handleKeys) >= 0) { 11600 $td = findNeighbourTd($target, event.which); 11601 if ($td && $td.length) { 11602 // ctx.node.debug("ignore keydown in input", event.which, handleKeys); 11603 $td.find(":input:enabled,a").focus(); 11604 // Prevent Fancytree default navigation 11605 return false; 11606 } 11607 } 11608 return true; 11609 } 11610 // ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType); 11611 return this._superApply(arguments); 11612 }, 11613 }); 11614 // Value returned by `require('jquery.fancytree..')` 11615 return $.ui.fancytree; 11616}); // End of closure 11617 11618/*! 11619 * jquery.fancytree.multi.js 11620 * 11621 * Allow multiple selection of nodes by mouse or keyboard. 11622 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 11623 * 11624 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 11625 * 11626 * Released under the MIT license 11627 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11628 * 11629 * @version 2.38.3 11630 * @date 2023-02-01T20:52:50Z 11631 */ 11632 11633(function (factory) { 11634 if (typeof define === "function" && define.amd) { 11635 // AMD. Register as an anonymous module. 11636 define(["jquery", "./jquery.fancytree"], factory); 11637 } else if (typeof module === "object" && module.exports) { 11638 // Node/CommonJS 11639 require("./jquery.fancytree"); 11640 module.exports = factory(require("jquery")); 11641 } else { 11642 // Browser globals 11643 factory(jQuery); 11644 } 11645})(function ($) { 11646 "use strict"; 11647 11648 /******************************************************************************* 11649 * Private functions and variables 11650 */ 11651 11652 // var isMac = /Mac/.test(navigator.platform); 11653 11654 /******************************************************************************* 11655 * Extension code 11656 */ 11657 $.ui.fancytree.registerExtension({ 11658 name: "multi", 11659 version: "2.38.3", 11660 // Default options for this extension. 11661 options: { 11662 allowNoSelect: false, // 11663 mode: "sameParent", // 11664 // Events: 11665 // beforeSelect: $.noop // Return false to prevent cancel/save (data.input is available) 11666 }, 11667 11668 treeInit: function (ctx) { 11669 this._superApply(arguments); 11670 this.$container.addClass("fancytree-ext-multi"); 11671 if (ctx.options.selectMode === 1) { 11672 $.error( 11673 "Fancytree ext-multi: selectMode: 1 (single) is not compatible." 11674 ); 11675 } 11676 }, 11677 nodeClick: function (ctx) { 11678 var //pluginOpts = ctx.options.multi, 11679 tree = ctx.tree, 11680 node = ctx.node, 11681 activeNode = tree.getActiveNode() || tree.getFirstChild(), 11682 isCbClick = ctx.targetType === "checkbox", 11683 isExpanderClick = ctx.targetType === "expander", 11684 eventStr = $.ui.fancytree.eventToString(ctx.originalEvent); 11685 11686 switch (eventStr) { 11687 case "click": 11688 if (isExpanderClick) { 11689 break; 11690 } // Default handler will expand/collapse 11691 if (!isCbClick) { 11692 tree.selectAll(false); 11693 // Select clicked node (radio-button mode) 11694 node.setSelected(); 11695 } 11696 // Default handler will toggle checkbox clicks and activate 11697 break; 11698 case "shift+click": 11699 // node.debug("click") 11700 tree.visitRows( 11701 function (n) { 11702 // n.debug("click2", n===node, node) 11703 n.setSelected(); 11704 if (n === node) { 11705 return false; 11706 } 11707 }, 11708 { 11709 start: activeNode, 11710 reverse: activeNode.isBelowOf(node), 11711 } 11712 ); 11713 break; 11714 case "ctrl+click": 11715 case "meta+click": // Mac: [Command] 11716 node.toggleSelected(); 11717 return; 11718 } 11719 return this._superApply(arguments); 11720 }, 11721 nodeKeydown: function (ctx) { 11722 var tree = ctx.tree, 11723 node = ctx.node, 11724 event = ctx.originalEvent, 11725 eventStr = $.ui.fancytree.eventToString(event); 11726 11727 switch (eventStr) { 11728 case "up": 11729 case "down": 11730 tree.selectAll(false); 11731 node.navigate(event.which, true); 11732 tree.getActiveNode().setSelected(); 11733 break; 11734 case "shift+up": 11735 case "shift+down": 11736 node.navigate(event.which, true); 11737 tree.getActiveNode().setSelected(); 11738 break; 11739 } 11740 return this._superApply(arguments); 11741 }, 11742 }); 11743 // Value returned by `require('jquery.fancytree..')` 11744 return $.ui.fancytree; 11745}); // End of closure 11746 11747/*! 11748 * jquery.fancytree.persist.js 11749 * 11750 * Persist tree status in cookiesRemove or highlight tree nodes, based on a filter. 11751 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 11752 * 11753 * @depends: js-cookie or jquery-cookie 11754 * 11755 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 11756 * 11757 * Released under the MIT license 11758 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11759 * 11760 * @version 2.38.3 11761 * @date 2023-02-01T20:52:50Z 11762 */ 11763 11764(function (factory) { 11765 if (typeof define === "function" && define.amd) { 11766 // AMD. Register as an anonymous module. 11767 define(["jquery", "./jquery.fancytree"], factory); 11768 } else if (typeof module === "object" && module.exports) { 11769 // Node/CommonJS 11770 require("./jquery.fancytree"); 11771 module.exports = factory(require("jquery")); 11772 } else { 11773 // Browser globals 11774 factory(jQuery); 11775 } 11776})(function ($) { 11777 "use strict"; 11778 /* global Cookies:false */ 11779 11780 /******************************************************************************* 11781 * Private functions and variables 11782 */ 11783 var cookieStore = null, 11784 localStorageStore = null, 11785 sessionStorageStore = null, 11786 _assert = $.ui.fancytree.assert, 11787 ACTIVE = "active", 11788 EXPANDED = "expanded", 11789 FOCUS = "focus", 11790 SELECTED = "selected"; 11791 11792 // Accessing window.xxxStorage may raise security exceptions (see #1022) 11793 try { 11794 _assert(window.localStorage && window.localStorage.getItem); 11795 localStorageStore = { 11796 get: function (key) { 11797 return window.localStorage.getItem(key); 11798 }, 11799 set: function (key, value) { 11800 window.localStorage.setItem(key, value); 11801 }, 11802 remove: function (key) { 11803 window.localStorage.removeItem(key); 11804 }, 11805 }; 11806 } catch (e) { 11807 $.ui.fancytree.warn("Could not access window.localStorage", e); 11808 } 11809 11810 try { 11811 _assert(window.sessionStorage && window.sessionStorage.getItem); 11812 sessionStorageStore = { 11813 get: function (key) { 11814 return window.sessionStorage.getItem(key); 11815 }, 11816 set: function (key, value) { 11817 window.sessionStorage.setItem(key, value); 11818 }, 11819 remove: function (key) { 11820 window.sessionStorage.removeItem(key); 11821 }, 11822 }; 11823 } catch (e) { 11824 $.ui.fancytree.warn("Could not access window.sessionStorage", e); 11825 } 11826 11827 if (typeof Cookies === "function") { 11828 // Assume https://github.com/js-cookie/js-cookie 11829 cookieStore = { 11830 get: Cookies.get, 11831 set: function (key, value) { 11832 Cookies.set(key, value, this.options.persist.cookie); 11833 }, 11834 remove: Cookies.remove, 11835 }; 11836 } else if ($ && typeof $.cookie === "function") { 11837 // Fall back to https://github.com/carhartl/jquery-cookie 11838 cookieStore = { 11839 get: $.cookie, 11840 set: function (key, value) { 11841 $.cookie(key, value, this.options.persist.cookie); 11842 }, 11843 remove: $.removeCookie, 11844 }; 11845 } 11846 11847 /* Recursively load lazy nodes 11848 * @param {string} mode 'load', 'expand', false 11849 */ 11850 function _loadLazyNodes(tree, local, keyList, mode, dfd) { 11851 var i, 11852 key, 11853 l, 11854 node, 11855 foundOne = false, 11856 expandOpts = tree.options.persist.expandOpts, 11857 deferredList = [], 11858 missingKeyList = []; 11859 11860 keyList = keyList || []; 11861 dfd = dfd || $.Deferred(); 11862 11863 for (i = 0, l = keyList.length; i < l; i++) { 11864 key = keyList[i]; 11865 node = tree.getNodeByKey(key); 11866 if (node) { 11867 if (mode && node.isUndefined()) { 11868 foundOne = true; 11869 tree.debug( 11870 "_loadLazyNodes: " + node + " is lazy: loading..." 11871 ); 11872 if (mode === "expand") { 11873 deferredList.push(node.setExpanded(true, expandOpts)); 11874 } else { 11875 deferredList.push(node.load()); 11876 } 11877 } else { 11878 tree.debug("_loadLazyNodes: " + node + " already loaded."); 11879 node.setExpanded(true, expandOpts); 11880 } 11881 } else { 11882 missingKeyList.push(key); 11883 tree.debug("_loadLazyNodes: " + node + " was not yet found."); 11884 } 11885 } 11886 11887 $.when.apply($, deferredList).always(function () { 11888 // All lazy-expands have finished 11889 if (foundOne && missingKeyList.length > 0) { 11890 // If we read new nodes from server, try to resolve yet-missing keys 11891 _loadLazyNodes(tree, local, missingKeyList, mode, dfd); 11892 } else { 11893 if (missingKeyList.length) { 11894 tree.warn( 11895 "_loadLazyNodes: could not load those keys: ", 11896 missingKeyList 11897 ); 11898 for (i = 0, l = missingKeyList.length; i < l; i++) { 11899 key = keyList[i]; 11900 local._appendKey(EXPANDED, keyList[i], false); 11901 } 11902 } 11903 dfd.resolve(); 11904 } 11905 }); 11906 return dfd; 11907 } 11908 11909 /** 11910 * [ext-persist] Remove persistence data of the given type(s). 11911 * Called like 11912 * $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected"); 11913 * 11914 * @alias Fancytree#clearPersistData 11915 * @requires jquery.fancytree.persist.js 11916 */ 11917 $.ui.fancytree._FancytreeClass.prototype.clearPersistData = function ( 11918 types 11919 ) { 11920 var local = this.ext.persist, 11921 prefix = local.cookiePrefix; 11922 11923 types = types || "active expanded focus selected"; 11924 if (types.indexOf(ACTIVE) >= 0) { 11925 local._data(prefix + ACTIVE, null); 11926 } 11927 if (types.indexOf(EXPANDED) >= 0) { 11928 local._data(prefix + EXPANDED, null); 11929 } 11930 if (types.indexOf(FOCUS) >= 0) { 11931 local._data(prefix + FOCUS, null); 11932 } 11933 if (types.indexOf(SELECTED) >= 0) { 11934 local._data(prefix + SELECTED, null); 11935 } 11936 }; 11937 11938 $.ui.fancytree._FancytreeClass.prototype.clearCookies = function (types) { 11939 this.warn( 11940 "'tree.clearCookies()' is deprecated since v2.27.0: use 'clearPersistData()' instead." 11941 ); 11942 return this.clearPersistData(types); 11943 }; 11944 11945 /** 11946 * [ext-persist] Return persistence information from cookies 11947 * 11948 * Called like 11949 * $.ui.fancytree.getTree("#tree").getPersistData(); 11950 * 11951 * @alias Fancytree#getPersistData 11952 * @requires jquery.fancytree.persist.js 11953 */ 11954 $.ui.fancytree._FancytreeClass.prototype.getPersistData = function () { 11955 var local = this.ext.persist, 11956 prefix = local.cookiePrefix, 11957 delim = local.cookieDelimiter, 11958 res = {}; 11959 11960 res[ACTIVE] = local._data(prefix + ACTIVE); 11961 res[EXPANDED] = (local._data(prefix + EXPANDED) || "").split(delim); 11962 res[SELECTED] = (local._data(prefix + SELECTED) || "").split(delim); 11963 res[FOCUS] = local._data(prefix + FOCUS); 11964 return res; 11965 }; 11966 11967 /****************************************************************************** 11968 * Extension code 11969 */ 11970 $.ui.fancytree.registerExtension({ 11971 name: "persist", 11972 version: "2.38.3", 11973 // Default options for this extension. 11974 options: { 11975 cookieDelimiter: "~", 11976 cookiePrefix: undefined, // 'fancytree-<treeId>-' by default 11977 cookie: { 11978 raw: false, 11979 expires: "", 11980 path: "", 11981 domain: "", 11982 secure: false, 11983 }, 11984 expandLazy: false, // true: recursively expand and load lazy nodes 11985 expandOpts: undefined, // optional `opts` argument passed to setExpanded() 11986 fireActivate: true, // false: suppress `activate` event after active node was restored 11987 overrideSource: true, // true: cookie takes precedence over `source` data attributes. 11988 store: "auto", // 'cookie': force cookie, 'local': force localStore, 'session': force sessionStore 11989 types: "active expanded focus selected", 11990 }, 11991 11992 /* Generic read/write string data to cookie, sessionStorage or localStorage. */ 11993 _data: function (key, value) { 11994 var store = this._local.store; 11995 11996 if (value === undefined) { 11997 return store.get.call(this, key); 11998 } else if (value === null) { 11999 store.remove.call(this, key); 12000 } else { 12001 store.set.call(this, key, value); 12002 } 12003 }, 12004 12005 /* Append `key` to a cookie. */ 12006 _appendKey: function (type, key, flag) { 12007 key = "" + key; // #90 12008 var local = this._local, 12009 instOpts = this.options.persist, 12010 delim = instOpts.cookieDelimiter, 12011 cookieName = local.cookiePrefix + type, 12012 data = local._data(cookieName), 12013 keyList = data ? data.split(delim) : [], 12014 idx = $.inArray(key, keyList); 12015 // Remove, even if we add a key, so the key is always the last entry 12016 if (idx >= 0) { 12017 keyList.splice(idx, 1); 12018 } 12019 // Append key to cookie 12020 if (flag) { 12021 keyList.push(key); 12022 } 12023 local._data(cookieName, keyList.join(delim)); 12024 }, 12025 12026 treeInit: function (ctx) { 12027 var tree = ctx.tree, 12028 opts = ctx.options, 12029 local = this._local, 12030 instOpts = this.options.persist; 12031 12032 // // For 'auto' or 'cookie' mode, the cookie plugin must be available 12033 // _assert((instOpts.store !== "auto" && instOpts.store !== "cookie") || cookieStore, 12034 // "Missing required plugin for 'persist' extension: js.cookie.js or jquery.cookie.js"); 12035 12036 local.cookiePrefix = 12037 instOpts.cookiePrefix || "fancytree-" + tree._id + "-"; 12038 local.storeActive = instOpts.types.indexOf(ACTIVE) >= 0; 12039 local.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0; 12040 local.storeSelected = instOpts.types.indexOf(SELECTED) >= 0; 12041 local.storeFocus = instOpts.types.indexOf(FOCUS) >= 0; 12042 local.store = null; 12043 12044 if (instOpts.store === "auto") { 12045 instOpts.store = localStorageStore ? "local" : "cookie"; 12046 } 12047 if ($.isPlainObject(instOpts.store)) { 12048 local.store = instOpts.store; 12049 } else if (instOpts.store === "cookie") { 12050 local.store = cookieStore; 12051 } else if (instOpts.store === "local") { 12052 local.store = 12053 instOpts.store === "local" 12054 ? localStorageStore 12055 : sessionStorageStore; 12056 } else if (instOpts.store === "session") { 12057 local.store = 12058 instOpts.store === "local" 12059 ? localStorageStore 12060 : sessionStorageStore; 12061 } 12062 _assert(local.store, "Need a valid store."); 12063 12064 // Bind init-handler to apply cookie state 12065 tree.$div.on("fancytreeinit", function (event) { 12066 if ( 12067 tree._triggerTreeEvent("beforeRestore", null, {}) === false 12068 ) { 12069 return; 12070 } 12071 12072 var cookie, 12073 dfd, 12074 i, 12075 keyList, 12076 node, 12077 prevFocus = local._data(local.cookiePrefix + FOCUS), // record this before node.setActive() overrides it; 12078 noEvents = instOpts.fireActivate === false; 12079 12080 // tree.debug("document.cookie:", document.cookie); 12081 12082 cookie = local._data(local.cookiePrefix + EXPANDED); 12083 keyList = cookie && cookie.split(instOpts.cookieDelimiter); 12084 12085 if (local.storeExpanded) { 12086 // Recursively load nested lazy nodes if expandLazy is 'expand' or 'load' 12087 // Also remove expand-cookies for unmatched nodes 12088 dfd = _loadLazyNodes( 12089 tree, 12090 local, 12091 keyList, 12092 instOpts.expandLazy ? "expand" : false, 12093 null 12094 ); 12095 } else { 12096 // nothing to do 12097 dfd = new $.Deferred().resolve(); 12098 } 12099 12100 dfd.done(function () { 12101 if (local.storeSelected) { 12102 cookie = local._data(local.cookiePrefix + SELECTED); 12103 if (cookie) { 12104 keyList = cookie.split(instOpts.cookieDelimiter); 12105 for (i = 0; i < keyList.length; i++) { 12106 node = tree.getNodeByKey(keyList[i]); 12107 if (node) { 12108 if ( 12109 node.selected === undefined || 12110 (instOpts.overrideSource && 12111 node.selected === false) 12112 ) { 12113 // node.setSelected(); 12114 node.selected = true; 12115 node.renderStatus(); 12116 } 12117 } else { 12118 // node is no longer member of the tree: remove from cookie also 12119 local._appendKey( 12120 SELECTED, 12121 keyList[i], 12122 false 12123 ); 12124 } 12125 } 12126 } 12127 // In selectMode 3 we have to fix the child nodes, since we 12128 // only stored the selected *top* nodes 12129 if (tree.options.selectMode === 3) { 12130 tree.visit(function (n) { 12131 if (n.selected) { 12132 n.fixSelection3AfterClick(); 12133 return "skip"; 12134 } 12135 }); 12136 } 12137 } 12138 if (local.storeActive) { 12139 cookie = local._data(local.cookiePrefix + ACTIVE); 12140 if ( 12141 cookie && 12142 (opts.persist.overrideSource || !tree.activeNode) 12143 ) { 12144 node = tree.getNodeByKey(cookie); 12145 if (node) { 12146 node.debug("persist: set active", cookie); 12147 // We only want to set the focus if the container 12148 // had the keyboard focus before 12149 node.setActive(true, { 12150 noFocus: true, 12151 noEvents: noEvents, 12152 }); 12153 } 12154 } 12155 } 12156 if (local.storeFocus && prevFocus) { 12157 node = tree.getNodeByKey(prevFocus); 12158 if (node) { 12159 // node.debug("persist: set focus", cookie); 12160 if (tree.options.titlesTabbable) { 12161 $(node.span).find(".fancytree-title").focus(); 12162 } else { 12163 $(tree.$container).focus(); 12164 } 12165 // node.setFocus(); 12166 } 12167 } 12168 tree._triggerTreeEvent("restore", null, {}); 12169 }); 12170 }); 12171 // Init the tree 12172 return this._superApply(arguments); 12173 }, 12174 nodeSetActive: function (ctx, flag, callOpts) { 12175 var res, 12176 local = this._local; 12177 12178 flag = flag !== false; 12179 res = this._superApply(arguments); 12180 12181 if (local.storeActive) { 12182 local._data( 12183 local.cookiePrefix + ACTIVE, 12184 this.activeNode ? this.activeNode.key : null 12185 ); 12186 } 12187 return res; 12188 }, 12189 nodeSetExpanded: function (ctx, flag, callOpts) { 12190 var res, 12191 node = ctx.node, 12192 local = this._local; 12193 12194 flag = flag !== false; 12195 res = this._superApply(arguments); 12196 12197 if (local.storeExpanded) { 12198 local._appendKey(EXPANDED, node.key, flag); 12199 } 12200 return res; 12201 }, 12202 nodeSetFocus: function (ctx, flag) { 12203 var res, 12204 local = this._local; 12205 12206 flag = flag !== false; 12207 res = this._superApply(arguments); 12208 12209 if (local.storeFocus) { 12210 local._data( 12211 local.cookiePrefix + FOCUS, 12212 this.focusNode ? this.focusNode.key : null 12213 ); 12214 } 12215 return res; 12216 }, 12217 nodeSetSelected: function (ctx, flag, callOpts) { 12218 var res, 12219 selNodes, 12220 tree = ctx.tree, 12221 node = ctx.node, 12222 local = this._local; 12223 12224 flag = flag !== false; 12225 res = this._superApply(arguments); 12226 12227 if (local.storeSelected) { 12228 if (tree.options.selectMode === 3) { 12229 // In selectMode 3 we only store the the selected *top* nodes. 12230 // De-selecting a node may also de-select some parents, so we 12231 // calculate the current status again 12232 selNodes = $.map(tree.getSelectedNodes(true), function (n) { 12233 return n.key; 12234 }); 12235 selNodes = selNodes.join( 12236 ctx.options.persist.cookieDelimiter 12237 ); 12238 local._data(local.cookiePrefix + SELECTED, selNodes); 12239 } else { 12240 // beforeSelect can prevent the change - flag doesn't reflect the node.selected state 12241 local._appendKey(SELECTED, node.key, node.selected); 12242 } 12243 } 12244 return res; 12245 }, 12246 }); 12247 // Value returned by `require('jquery.fancytree..')` 12248 return $.ui.fancytree; 12249}); // End of closure 12250 12251/*! 12252 * jquery.fancytree.table.js 12253 * 12254 * Render tree as table (aka 'tree grid', 'table tree'). 12255 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12256 * 12257 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12258 * 12259 * Released under the MIT license 12260 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12261 * 12262 * @version 2.38.3 12263 * @date 2023-02-01T20:52:50Z 12264 */ 12265 12266(function (factory) { 12267 if (typeof define === "function" && define.amd) { 12268 // AMD. Register as an anonymous module. 12269 define(["jquery", "./jquery.fancytree"], factory); 12270 } else if (typeof module === "object" && module.exports) { 12271 // Node/CommonJS 12272 require("./jquery.fancytree"); 12273 module.exports = factory(require("jquery")); 12274 } else { 12275 // Browser globals 12276 factory(jQuery); 12277 } 12278})(function ($) { 12279 "use strict"; 12280 12281 /****************************************************************************** 12282 * Private functions and variables 12283 */ 12284 var _assert = $.ui.fancytree.assert; 12285 12286 function insertFirstChild(referenceNode, newNode) { 12287 referenceNode.insertBefore(newNode, referenceNode.firstChild); 12288 } 12289 12290 function insertSiblingAfter(referenceNode, newNode) { 12291 referenceNode.parentNode.insertBefore( 12292 newNode, 12293 referenceNode.nextSibling 12294 ); 12295 } 12296 12297 /* Show/hide all rows that are structural descendants of `parent`. */ 12298 function setChildRowVisibility(parent, flag) { 12299 parent.visit(function (node) { 12300 var tr = node.tr; 12301 // currentFlag = node.hide ? false : flag; // fix for ext-filter 12302 if (tr) { 12303 tr.style.display = node.hide || !flag ? "none" : ""; 12304 } 12305 if (!node.expanded) { 12306 return "skip"; 12307 } 12308 }); 12309 } 12310 12311 /* Find node that is rendered in previous row. */ 12312 function findPrevRowNode(node) { 12313 var i, 12314 last, 12315 prev, 12316 parent = node.parent, 12317 siblings = parent ? parent.children : null; 12318 12319 if (siblings && siblings.length > 1 && siblings[0] !== node) { 12320 // use the lowest descendant of the preceeding sibling 12321 i = $.inArray(node, siblings); 12322 prev = siblings[i - 1]; 12323 _assert(prev.tr); 12324 // descend to lowest child (with a <tr> tag) 12325 while (prev.children && prev.children.length) { 12326 last = prev.children[prev.children.length - 1]; 12327 if (!last.tr) { 12328 break; 12329 } 12330 prev = last; 12331 } 12332 } else { 12333 // if there is no preceding sibling, use the direct parent 12334 prev = parent; 12335 } 12336 return prev; 12337 } 12338 12339 $.ui.fancytree.registerExtension({ 12340 name: "table", 12341 version: "2.38.3", 12342 // Default options for this extension. 12343 options: { 12344 checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) 12345 indentation: 16, // indent every node level by 16px 12346 mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR 12347 nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0) 12348 }, 12349 // Overide virtual methods for this extension. 12350 // `this` : is this extension object 12351 // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) 12352 treeInit: function (ctx) { 12353 var i, 12354 n, 12355 $row, 12356 $tbody, 12357 tree = ctx.tree, 12358 opts = ctx.options, 12359 tableOpts = opts.table, 12360 $table = tree.widget.element; 12361 12362 if (tableOpts.customStatus != null) { 12363 if (opts.renderStatusColumns == null) { 12364 tree.warn( 12365 "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead." 12366 ); 12367 opts.renderStatusColumns = tableOpts.customStatus; 12368 } else { 12369 $.error( 12370 "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead." 12371 ); 12372 } 12373 } 12374 if (opts.renderStatusColumns) { 12375 if (opts.renderStatusColumns === true) { 12376 opts.renderStatusColumns = opts.renderColumns; 12377 // } else if( opts.renderStatusColumns === "wide" ) { 12378 // opts.renderStatusColumns = _renderStatusNodeWide; 12379 } 12380 } 12381 12382 $table.addClass("fancytree-container fancytree-ext-table"); 12383 $tbody = $table.find(">tbody"); 12384 if (!$tbody.length) { 12385 // TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s: 12386 if ($table.find(">tr").length) { 12387 $.error( 12388 "Expected table > tbody > tr. If you see this please open an issue." 12389 ); 12390 } 12391 $tbody = $("<tbody>").appendTo($table); 12392 } 12393 12394 tree.tbody = $tbody[0]; 12395 12396 // Prepare row templates: 12397 // Determine column count from table header if any 12398 tree.columnCount = $("thead >tr", $table) 12399 .last() 12400 .find(">th", $table).length; 12401 // Read TR templates from tbody if any 12402 $row = $tbody.children("tr").first(); 12403 if ($row.length) { 12404 n = $row.children("td").length; 12405 if (tree.columnCount && n !== tree.columnCount) { 12406 tree.warn( 12407 "Column count mismatch between thead (" + 12408 tree.columnCount + 12409 ") and tbody (" + 12410 n + 12411 "): using tbody." 12412 ); 12413 tree.columnCount = n; 12414 } 12415 $row = $row.clone(); 12416 } else { 12417 // Only thead is defined: create default row markup 12418 _assert( 12419 tree.columnCount >= 1, 12420 "Need either <thead> or <tbody> with <td> elements to determine column count." 12421 ); 12422 $row = $("<tr />"); 12423 for (i = 0; i < tree.columnCount; i++) { 12424 $row.append("<td />"); 12425 } 12426 } 12427 $row.find(">td") 12428 .eq(tableOpts.nodeColumnIdx) 12429 .html("<span class='fancytree-node' />"); 12430 if (opts.aria) { 12431 $row.attr("role", "row"); 12432 $row.find("td").attr("role", "gridcell"); 12433 } 12434 tree.rowFragment = document.createDocumentFragment(); 12435 tree.rowFragment.appendChild($row.get(0)); 12436 12437 // // If tbody contains a second row, use this as status node template 12438 // $row = $tbody.children("tr").eq(1); 12439 // if( $row.length === 0 ) { 12440 // tree.statusRowFragment = tree.rowFragment; 12441 // } else { 12442 // $row = $row.clone(); 12443 // tree.statusRowFragment = document.createDocumentFragment(); 12444 // tree.statusRowFragment.appendChild($row.get(0)); 12445 // } 12446 // 12447 $tbody.empty(); 12448 12449 // Make sure that status classes are set on the node's <tr> elements 12450 tree.statusClassPropName = "tr"; 12451 tree.ariaPropName = "tr"; 12452 this.nodeContainerAttrName = "tr"; 12453 12454 // #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table 12455 tree.$container = $table; 12456 12457 this._superApply(arguments); 12458 12459 // standard Fancytree created a root UL 12460 $(tree.rootNode.ul).remove(); 12461 tree.rootNode.ul = null; 12462 12463 // Add container to the TAB chain 12464 // #577: Allow to set tabindex to "0", "-1" and "" 12465 this.$container.attr("tabindex", opts.tabindex); 12466 // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1"); 12467 if (opts.aria) { 12468 tree.$container 12469 .attr("role", "treegrid") 12470 .attr("aria-readonly", true); 12471 } 12472 }, 12473 nodeRemoveChildMarkup: function (ctx) { 12474 var node = ctx.node; 12475 // node.debug("nodeRemoveChildMarkup()"); 12476 node.visit(function (n) { 12477 if (n.tr) { 12478 $(n.tr).remove(); 12479 n.tr = null; 12480 } 12481 }); 12482 }, 12483 nodeRemoveMarkup: function (ctx) { 12484 var node = ctx.node; 12485 // node.debug("nodeRemoveMarkup()"); 12486 if (node.tr) { 12487 $(node.tr).remove(); 12488 node.tr = null; 12489 } 12490 this.nodeRemoveChildMarkup(ctx); 12491 }, 12492 /* Override standard render. */ 12493 nodeRender: function (ctx, force, deep, collapsed, _recursive) { 12494 var children, 12495 firstTr, 12496 i, 12497 l, 12498 newRow, 12499 prevNode, 12500 prevTr, 12501 subCtx, 12502 tree = ctx.tree, 12503 node = ctx.node, 12504 opts = ctx.options, 12505 isRootNode = !node.parent; 12506 12507 if (tree._enableUpdate === false) { 12508 // $.ui.fancytree.debug("*** nodeRender _enableUpdate: false"); 12509 return; 12510 } 12511 if (!_recursive) { 12512 ctx.hasCollapsedParents = node.parent && !node.parent.expanded; 12513 } 12514 // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); 12515 if (!isRootNode) { 12516 if (node.tr && force) { 12517 this.nodeRemoveMarkup(ctx); 12518 } 12519 if (node.tr) { 12520 if (force) { 12521 // Set icon, link, and title (normally this is only required on initial render) 12522 this.nodeRenderTitle(ctx); // triggers renderColumns() 12523 } else { 12524 // Update element classes according to node state 12525 this.nodeRenderStatus(ctx); 12526 } 12527 } else { 12528 if (ctx.hasCollapsedParents && !deep) { 12529 // #166: we assume that the parent will be (recursively) rendered 12530 // later anyway. 12531 // node.debug("nodeRender ignored due to unrendered parent"); 12532 return; 12533 } 12534 // Create new <tr> after previous row 12535 // if( node.isStatusNode() ) { 12536 // newRow = tree.statusRowFragment.firstChild.cloneNode(true); 12537 // } else { 12538 newRow = tree.rowFragment.firstChild.cloneNode(true); 12539 // } 12540 prevNode = findPrevRowNode(node); 12541 // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key); 12542 _assert(prevNode); 12543 if (collapsed === true && _recursive) { 12544 // hide all child rows, so we can use an animation to show it later 12545 newRow.style.display = "none"; 12546 } else if (deep && ctx.hasCollapsedParents) { 12547 // also hide this row if deep === true but any parent is collapsed 12548 newRow.style.display = "none"; 12549 // newRow.style.color = "red"; 12550 } 12551 if (prevNode.tr) { 12552 insertSiblingAfter(prevNode.tr, newRow); 12553 } else { 12554 _assert( 12555 !prevNode.parent, 12556 "prev. row must have a tr, or be system root" 12557 ); 12558 // tree.tbody.appendChild(newRow); 12559 insertFirstChild(tree.tbody, newRow); // #675 12560 } 12561 node.tr = newRow; 12562 if (node.key && opts.generateIds) { 12563 node.tr.id = opts.idPrefix + node.key; 12564 } 12565 node.tr.ftnode = node; 12566 // if(opts.aria){ 12567 // $(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key); 12568 // } 12569 node.span = $("span.fancytree-node", node.tr).get(0); 12570 // Set icon, link, and title (normally this is only required on initial render) 12571 this.nodeRenderTitle(ctx); 12572 // Allow tweaking, binding, after node was created for the first time 12573 // tree._triggerNodeEvent("createNode", ctx); 12574 if (opts.createNode) { 12575 opts.createNode.call(tree, { type: "createNode" }, ctx); 12576 } 12577 } 12578 } 12579 // Allow tweaking after node state was rendered 12580 // tree._triggerNodeEvent("renderNode", ctx); 12581 if (opts.renderNode) { 12582 opts.renderNode.call(tree, { type: "renderNode" }, ctx); 12583 } 12584 // Visit child nodes 12585 // Add child markup 12586 children = node.children; 12587 if (children && (isRootNode || deep || node.expanded)) { 12588 for (i = 0, l = children.length; i < l; i++) { 12589 subCtx = $.extend({}, ctx, { node: children[i] }); 12590 subCtx.hasCollapsedParents = 12591 subCtx.hasCollapsedParents || !node.expanded; 12592 this.nodeRender(subCtx, force, deep, collapsed, true); 12593 } 12594 } 12595 // Make sure, that <tr> order matches node.children order. 12596 if (children && !_recursive) { 12597 // we only have to do it once, for the root branch 12598 prevTr = node.tr || null; 12599 firstTr = tree.tbody.firstChild; 12600 // Iterate over all descendants 12601 node.visit(function (n) { 12602 if (n.tr) { 12603 if ( 12604 !n.parent.expanded && 12605 n.tr.style.display !== "none" 12606 ) { 12607 // fix after a node was dropped over a collapsed 12608 n.tr.style.display = "none"; 12609 setChildRowVisibility(n, false); 12610 } 12611 if (n.tr.previousSibling !== prevTr) { 12612 node.debug("_fixOrder: mismatch at node: " + n); 12613 var nextTr = prevTr ? prevTr.nextSibling : firstTr; 12614 tree.tbody.insertBefore(n.tr, nextTr); 12615 } 12616 prevTr = n.tr; 12617 } 12618 }); 12619 } 12620 // Update element classes according to node state 12621 // if(!isRootNode){ 12622 // this.nodeRenderStatus(ctx); 12623 // } 12624 }, 12625 nodeRenderTitle: function (ctx, title) { 12626 var $cb, 12627 res, 12628 tree = ctx.tree, 12629 node = ctx.node, 12630 opts = ctx.options, 12631 isStatusNode = node.isStatusNode(); 12632 12633 res = this._super(ctx, title); 12634 12635 if (node.isRootNode()) { 12636 return res; 12637 } 12638 // Move checkbox to custom column 12639 if ( 12640 opts.checkbox && 12641 !isStatusNode && 12642 opts.table.checkboxColumnIdx != null 12643 ) { 12644 $cb = $("span.fancytree-checkbox", node.span); //.detach(); 12645 $(node.tr) 12646 .find("td") 12647 .eq(+opts.table.checkboxColumnIdx) 12648 .html($cb); 12649 } 12650 // Update element classes according to node state 12651 this.nodeRenderStatus(ctx); 12652 12653 if (isStatusNode) { 12654 if (opts.renderStatusColumns) { 12655 // Let user code write column content 12656 opts.renderStatusColumns.call( 12657 tree, 12658 { type: "renderStatusColumns" }, 12659 ctx 12660 ); 12661 } else if (opts.table.mergeStatusColumns && node.isTopLevel()) { 12662 $(node.tr) 12663 .find(">td") 12664 .eq(0) 12665 .prop("colspan", tree.columnCount) 12666 .text(node.title) 12667 .addClass("fancytree-status-merged") 12668 .nextAll() 12669 .remove(); 12670 } // else: default rendering for status node: leave other cells empty 12671 } else if (opts.renderColumns) { 12672 opts.renderColumns.call(tree, { type: "renderColumns" }, ctx); 12673 } 12674 return res; 12675 }, 12676 nodeRenderStatus: function (ctx) { 12677 var indent, 12678 node = ctx.node, 12679 opts = ctx.options; 12680 12681 this._super(ctx); 12682 12683 $(node.tr).removeClass("fancytree-node"); 12684 // indent 12685 indent = (node.getLevel() - 1) * opts.table.indentation; 12686 if (opts.rtl) { 12687 $(node.span).css({ paddingRight: indent + "px" }); 12688 } else { 12689 $(node.span).css({ paddingLeft: indent + "px" }); 12690 } 12691 }, 12692 /* Expand node, return Deferred.promise. */ 12693 nodeSetExpanded: function (ctx, flag, callOpts) { 12694 // flag defaults to true 12695 flag = flag !== false; 12696 12697 if ((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) { 12698 // Expanded state isn't changed - just call base implementation 12699 return this._superApply(arguments); 12700 } 12701 12702 var dfd = new $.Deferred(), 12703 subOpts = $.extend({}, callOpts, { 12704 noEvents: true, 12705 noAnimation: true, 12706 }); 12707 12708 callOpts = callOpts || {}; 12709 12710 function _afterExpand(ok, args) { 12711 // ctx.tree.info("ok:" + ok, args); 12712 if (ok) { 12713 // #1108 minExpandLevel: 2 together with table extension does not work 12714 // don't call when 'ok' is false: 12715 setChildRowVisibility(ctx.node, flag); 12716 if ( 12717 flag && 12718 ctx.options.autoScroll && 12719 !callOpts.noAnimation && 12720 ctx.node.hasChildren() 12721 ) { 12722 // Scroll down to last child, but keep current node visible 12723 ctx.node 12724 .getLastChild() 12725 .scrollIntoView(true, { topNode: ctx.node }) 12726 .always(function () { 12727 if (!callOpts.noEvents) { 12728 ctx.tree._triggerNodeEvent( 12729 flag ? "expand" : "collapse", 12730 ctx 12731 ); 12732 } 12733 dfd.resolveWith(ctx.node); 12734 }); 12735 } else { 12736 if (!callOpts.noEvents) { 12737 ctx.tree._triggerNodeEvent( 12738 flag ? "expand" : "collapse", 12739 ctx 12740 ); 12741 } 12742 dfd.resolveWith(ctx.node); 12743 } 12744 } else { 12745 if (!callOpts.noEvents) { 12746 ctx.tree._triggerNodeEvent( 12747 flag ? "expand" : "collapse", 12748 ctx 12749 ); 12750 } 12751 dfd.rejectWith(ctx.node); 12752 } 12753 } 12754 // Call base-expand with disabled events and animation 12755 this._super(ctx, flag, subOpts) 12756 .done(function () { 12757 _afterExpand(true, arguments); 12758 }) 12759 .fail(function () { 12760 _afterExpand(false, arguments); 12761 }); 12762 return dfd.promise(); 12763 }, 12764 nodeSetStatus: function (ctx, status, message, details) { 12765 if (status === "ok") { 12766 var node = ctx.node, 12767 firstChild = node.children ? node.children[0] : null; 12768 if (firstChild && firstChild.isStatusNode()) { 12769 $(firstChild.tr).remove(); 12770 } 12771 } 12772 return this._superApply(arguments); 12773 }, 12774 treeClear: function (ctx) { 12775 this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); 12776 return this._superApply(arguments); 12777 }, 12778 treeDestroy: function (ctx) { 12779 this.$container.find("tbody").empty(); 12780 if (this.$source) { 12781 this.$source.removeClass("fancytree-helper-hidden"); 12782 } 12783 return this._superApply(arguments); 12784 }, 12785 /*, 12786 treeSetFocus: function(ctx, flag) { 12787// alert("treeSetFocus" + ctx.tree.$container); 12788 ctx.tree.$container.focus(); 12789 $.ui.fancytree.focusTree = ctx.tree; 12790 }*/ 12791 }); 12792 // Value returned by `require('jquery.fancytree..')` 12793 return $.ui.fancytree; 12794}); // End of closure 12795 12796/*! 12797 * jquery.fancytree.themeroller.js 12798 * 12799 * Enable jQuery UI ThemeRoller styles. 12800 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12801 * 12802 * @see http://jqueryui.com/themeroller/ 12803 * 12804 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12805 * 12806 * Released under the MIT license 12807 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12808 * 12809 * @version 2.38.3 12810 * @date 2023-02-01T20:52:50Z 12811 */ 12812 12813(function (factory) { 12814 if (typeof define === "function" && define.amd) { 12815 // AMD. Register as an anonymous module. 12816 define(["jquery", "./jquery.fancytree"], factory); 12817 } else if (typeof module === "object" && module.exports) { 12818 // Node/CommonJS 12819 require("./jquery.fancytree"); 12820 module.exports = factory(require("jquery")); 12821 } else { 12822 // Browser globals 12823 factory(jQuery); 12824 } 12825})(function ($) { 12826 "use strict"; 12827 12828 /******************************************************************************* 12829 * Extension code 12830 */ 12831 $.ui.fancytree.registerExtension({ 12832 name: "themeroller", 12833 version: "2.38.3", 12834 // Default options for this extension. 12835 options: { 12836 activeClass: "ui-state-active", // Class added to active node 12837 // activeClass: "ui-state-highlight", 12838 addClass: "ui-corner-all", // Class added to all nodes 12839 focusClass: "ui-state-focus", // Class added to focused node 12840 hoverClass: "ui-state-hover", // Class added to hovered node 12841 selectedClass: "ui-state-highlight", // Class added to selected nodes 12842 // selectedClass: "ui-state-active" 12843 }, 12844 12845 treeInit: function (ctx) { 12846 var $el = ctx.widget.element, 12847 opts = ctx.options.themeroller; 12848 12849 this._superApply(arguments); 12850 12851 if ($el[0].nodeName === "TABLE") { 12852 $el.addClass("ui-widget ui-corner-all"); 12853 $el.find(">thead tr").addClass("ui-widget-header"); 12854 $el.find(">tbody").addClass("ui-widget-conent"); 12855 } else { 12856 $el.addClass("ui-widget ui-widget-content ui-corner-all"); 12857 } 12858 12859 $el.on( 12860 "mouseenter mouseleave", 12861 ".fancytree-node", 12862 function (event) { 12863 var node = $.ui.fancytree.getNode(event.target), 12864 flag = event.type === "mouseenter"; 12865 12866 $(node.tr ? node.tr : node.span).toggleClass( 12867 opts.hoverClass + " " + opts.addClass, 12868 flag 12869 ); 12870 } 12871 ); 12872 }, 12873 treeDestroy: function (ctx) { 12874 this._superApply(arguments); 12875 ctx.widget.element.removeClass( 12876 "ui-widget ui-widget-content ui-corner-all" 12877 ); 12878 }, 12879 nodeRenderStatus: function (ctx) { 12880 var classes = {}, 12881 node = ctx.node, 12882 $el = $(node.tr ? node.tr : node.span), 12883 opts = ctx.options.themeroller; 12884 12885 this._super(ctx); 12886 /* 12887 .ui-state-highlight: Class to be applied to highlighted or selected elements. Applies "highlight" container styles to an element and its child text, links, and icons. 12888 .ui-state-error: Class to be applied to error messaging container elements. Applies "error" container styles to an element and its child text, links, and icons. 12889 .ui-state-error-text: An additional class that applies just the error text color without background. Can be used on form labels for instance. Also applies error icon color to child icons. 12890 12891 .ui-state-default: Class to be applied to clickable button-like elements. Applies "clickable default" container styles to an element and its child text, links, and icons. 12892 .ui-state-hover: Class to be applied on mouseover to clickable button-like elements. Applies "clickable hover" container styles to an element and its child text, links, and icons. 12893 .ui-state-focus: Class to be applied on keyboard focus to clickable button-like elements. Applies "clickable hover" container styles to an element and its child text, links, and icons. 12894 .ui-state-active: Class to be applied on mousedown to clickable button-like elements. Applies "clickable active" container styles to an element and its child text, links, and icons. 12895*/ 12896 // Set ui-state-* class (handle the case that the same class is assigned 12897 // to different states) 12898 classes[opts.activeClass] = false; 12899 classes[opts.focusClass] = false; 12900 classes[opts.selectedClass] = false; 12901 if (node.isActive()) { 12902 classes[opts.activeClass] = true; 12903 } 12904 if (node.hasFocus()) { 12905 classes[opts.focusClass] = true; 12906 } 12907 // activeClass takes precedence before selectedClass: 12908 if (node.isSelected() && !node.isActive()) { 12909 classes[opts.selectedClass] = true; 12910 } 12911 $el.toggleClass(opts.activeClass, classes[opts.activeClass]); 12912 $el.toggleClass(opts.focusClass, classes[opts.focusClass]); 12913 $el.toggleClass(opts.selectedClass, classes[opts.selectedClass]); 12914 // Additional classes (e.g. 'ui-corner-all') 12915 $el.addClass(opts.addClass); 12916 }, 12917 }); 12918 // Value returned by `require('jquery.fancytree..')` 12919 return $.ui.fancytree; 12920}); // End of closure 12921 12922/*! 12923 * jquery.fancytree.wide.js 12924 * Support for 100% wide selection bars. 12925 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12926 * 12927 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12928 * 12929 * Released under the MIT license 12930 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12931 * 12932 * @version 2.38.3 12933 * @date 2023-02-01T20:52:50Z 12934 */ 12935 12936(function (factory) { 12937 if (typeof define === "function" && define.amd) { 12938 // AMD. Register as an anonymous module. 12939 define(["jquery", "./jquery.fancytree"], factory); 12940 } else if (typeof module === "object" && module.exports) { 12941 // Node/CommonJS 12942 require("./jquery.fancytree"); 12943 module.exports = factory(require("jquery")); 12944 } else { 12945 // Browser globals 12946 factory(jQuery); 12947 } 12948})(function ($) { 12949 "use strict"; 12950 12951 var reNumUnit = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/; // split "1.5em" to ["1.5", "em"] 12952 12953 /******************************************************************************* 12954 * Private functions and variables 12955 */ 12956 // var _assert = $.ui.fancytree.assert; 12957 12958 /* Calculate inner width without scrollbar */ 12959 // function realInnerWidth($el) { 12960 // // http://blog.jquery.com/2012/08/16/jquery-1-8-box-sizing-width-csswidth-and-outerwidth/ 12961 // // inst.contWidth = parseFloat(this.$container.css("width"), 10); 12962 // // 'Client width without scrollbar' - 'padding' 12963 // return $el[0].clientWidth - ($el.innerWidth() - parseFloat($el.css("width"), 10)); 12964 // } 12965 12966 /* Create a global embedded CSS style for the tree. */ 12967 function defineHeadStyleElement(id, cssText) { 12968 id = "fancytree-style-" + id; 12969 var $headStyle = $("#" + id); 12970 12971 if (!cssText) { 12972 $headStyle.remove(); 12973 return null; 12974 } 12975 if (!$headStyle.length) { 12976 $headStyle = $("<style />") 12977 .attr("id", id) 12978 .addClass("fancytree-style") 12979 .prop("type", "text/css") 12980 .appendTo("head"); 12981 } 12982 try { 12983 $headStyle.html(cssText); 12984 } catch (e) { 12985 // fix for IE 6-8 12986 $headStyle[0].styleSheet.cssText = cssText; 12987 } 12988 return $headStyle; 12989 } 12990 12991 /* Calculate the CSS rules that indent title spans. */ 12992 function renderLevelCss( 12993 containerId, 12994 depth, 12995 levelOfs, 12996 lineOfs, 12997 labelOfs, 12998 measureUnit 12999 ) { 13000 var i, 13001 prefix = "#" + containerId + " span.fancytree-level-", 13002 rules = []; 13003 13004 for (i = 0; i < depth; i++) { 13005 rules.push( 13006 prefix + 13007 (i + 1) + 13008 " span.fancytree-title { padding-left: " + 13009 (i * levelOfs + lineOfs) + 13010 measureUnit + 13011 "; }" 13012 ); 13013 } 13014 // Some UI animations wrap the UL inside a DIV and set position:relative on both. 13015 // This breaks the left:0 and padding-left:nn settings of the title 13016 rules.push( 13017 "#" + 13018 containerId + 13019 " div.ui-effects-wrapper ul li span.fancytree-title, " + 13020 "#" + 13021 containerId + 13022 " li.fancytree-animating span.fancytree-title " + // #716 13023 "{ padding-left: " + 13024 labelOfs + 13025 measureUnit + 13026 "; position: static; width: auto; }" 13027 ); 13028 return rules.join("\n"); 13029 } 13030 13031 // /** 13032 // * [ext-wide] Recalculate the width of the selection bar after the tree container 13033 // * was resized.<br> 13034 // * May be called explicitly on container resize, since there is no resize event 13035 // * for DIV tags. 13036 // * 13037 // * @alias Fancytree#wideUpdate 13038 // * @requires jquery.fancytree.wide.js 13039 // */ 13040 // $.ui.fancytree._FancytreeClass.prototype.wideUpdate = function(){ 13041 // var inst = this.ext.wide, 13042 // prevCw = inst.contWidth, 13043 // prevLo = inst.lineOfs; 13044 13045 // inst.contWidth = realInnerWidth(this.$container); 13046 // // Each title is precceeded by 2 or 3 icons (16px + 3 margin) 13047 // // + 1px title border and 3px title padding 13048 // // TODO: use code from treeInit() below 13049 // inst.lineOfs = (this.options.checkbox ? 3 : 2) * 19; 13050 // if( prevCw !== inst.contWidth || prevLo !== inst.lineOfs ) { 13051 // this.debug("wideUpdate: " + inst.contWidth); 13052 // this.visit(function(node){ 13053 // node.tree._callHook("nodeRenderTitle", node); 13054 // }); 13055 // } 13056 // }; 13057 13058 /******************************************************************************* 13059 * Extension code 13060 */ 13061 $.ui.fancytree.registerExtension({ 13062 name: "wide", 13063 version: "2.38.3", 13064 // Default options for this extension. 13065 options: { 13066 iconWidth: null, // Adjust this if @fancy-icon-width != "16px" 13067 iconSpacing: null, // Adjust this if @fancy-icon-spacing != "3px" 13068 labelSpacing: null, // Adjust this if padding between icon and label != "3px" 13069 levelOfs: null, // Adjust this if ul padding != "16px" 13070 }, 13071 13072 treeCreate: function (ctx) { 13073 this._superApply(arguments); 13074 this.$container.addClass("fancytree-ext-wide"); 13075 13076 var containerId, 13077 cssText, 13078 iconSpacingUnit, 13079 labelSpacingUnit, 13080 iconWidthUnit, 13081 levelOfsUnit, 13082 instOpts = ctx.options.wide, 13083 // css sniffing 13084 $dummyLI = $( 13085 "<li id='fancytreeTemp'><span class='fancytree-node'><span class='fancytree-icon' /><span class='fancytree-title' /></span><ul />" 13086 ).appendTo(ctx.tree.$container), 13087 $dummyIcon = $dummyLI.find(".fancytree-icon"), 13088 $dummyUL = $dummyLI.find("ul"), 13089 // $dummyTitle = $dummyLI.find(".fancytree-title"), 13090 iconSpacing = 13091 instOpts.iconSpacing || $dummyIcon.css("margin-left"), 13092 iconWidth = instOpts.iconWidth || $dummyIcon.css("width"), 13093 labelSpacing = instOpts.labelSpacing || "3px", 13094 levelOfs = instOpts.levelOfs || $dummyUL.css("padding-left"); 13095 13096 $dummyLI.remove(); 13097 13098 iconSpacingUnit = iconSpacing.match(reNumUnit)[2]; 13099 iconSpacing = parseFloat(iconSpacing, 10); 13100 labelSpacingUnit = labelSpacing.match(reNumUnit)[2]; 13101 labelSpacing = parseFloat(labelSpacing, 10); 13102 iconWidthUnit = iconWidth.match(reNumUnit)[2]; 13103 iconWidth = parseFloat(iconWidth, 10); 13104 levelOfsUnit = levelOfs.match(reNumUnit)[2]; 13105 if ( 13106 iconSpacingUnit !== iconWidthUnit || 13107 levelOfsUnit !== iconWidthUnit || 13108 labelSpacingUnit !== iconWidthUnit 13109 ) { 13110 $.error( 13111 "iconWidth, iconSpacing, and levelOfs must have the same css measure unit" 13112 ); 13113 } 13114 this._local.measureUnit = iconWidthUnit; 13115 this._local.levelOfs = parseFloat(levelOfs); 13116 this._local.lineOfs = 13117 (1 + 13118 (ctx.options.checkbox ? 1 : 0) + 13119 (ctx.options.icon === false ? 0 : 1)) * 13120 (iconWidth + iconSpacing) + 13121 iconSpacing; 13122 this._local.labelOfs = labelSpacing; 13123 this._local.maxDepth = 10; 13124 13125 // Get/Set a unique Id on the container (if not already exists) 13126 containerId = this.$container.uniqueId().attr("id"); 13127 // Generated css rules for some levels (extended on demand) 13128 cssText = renderLevelCss( 13129 containerId, 13130 this._local.maxDepth, 13131 this._local.levelOfs, 13132 this._local.lineOfs, 13133 this._local.labelOfs, 13134 this._local.measureUnit 13135 ); 13136 defineHeadStyleElement(containerId, cssText); 13137 }, 13138 treeDestroy: function (ctx) { 13139 // Remove generated css rules 13140 defineHeadStyleElement(this.$container.attr("id"), null); 13141 return this._superApply(arguments); 13142 }, 13143 nodeRenderStatus: function (ctx) { 13144 var containerId, 13145 cssText, 13146 res, 13147 node = ctx.node, 13148 level = node.getLevel(); 13149 13150 res = this._super(ctx); 13151 // Generate some more level-n rules if required 13152 if (level > this._local.maxDepth) { 13153 containerId = this.$container.attr("id"); 13154 this._local.maxDepth *= 2; 13155 node.debug( 13156 "Define global ext-wide css up to level " + 13157 this._local.maxDepth 13158 ); 13159 cssText = renderLevelCss( 13160 containerId, 13161 this._local.maxDepth, 13162 this._local.levelOfs, 13163 this._local.lineOfs, 13164 this._local.labelSpacing, 13165 this._local.measureUnit 13166 ); 13167 defineHeadStyleElement(containerId, cssText); 13168 } 13169 // Add level-n class to apply indentation padding. 13170 // (Setting element style would not work, since it cannot easily be 13171 // overriden while animations run) 13172 $(node.span).addClass("fancytree-level-" + level); 13173 return res; 13174 }, 13175 }); 13176 // Value returned by `require('jquery.fancytree..')` 13177 return $.ui.fancytree; 13178}); // End of closure 13179