1/*! jQuery Fancytree Plugin - 2.38.3 - 2023-02-01T20:52:50Z 2 * https://github.com/mar10/fancytree 3 * Copyright (c) 2023 Martin Wendt; Licensed MIT 4 */ 5/*! jQuery UI - v1.13.0 - 2021-11-09 6* http://jqueryui.com 7* Includes: widget.js, position.js, jquery-patch.js, keycode.js, scroll-parent.js, unique-id.js 8* Copyright jQuery Foundation and other contributors; Licensed MIT */ 9 10/* 11 NOTE: Original jQuery UI wrapper was replaced with a simple IIFE. 12 See README-Fancytree.md 13*/ 14(function( $ ) { 15 16 $.ui = $.ui || {}; 17 18 var version = $.ui.version = "1.13.2"; 19 20 21 /*! 22 * jQuery UI Widget 1.13.2 23 * http://jqueryui.com 24 * 25 * Copyright jQuery Foundation and other contributors 26 * Released under the MIT license. 27 * http://jquery.org/license 28 */ 29 30 //>>label: Widget 31 //>>group: Core 32 //>>description: Provides a factory for creating stateful widgets with a common API. 33 //>>docs: http://api.jqueryui.com/jQuery.widget/ 34 //>>demos: http://jqueryui.com/widget/ 35 36 37 var widgetUuid = 0; 38 var widgetHasOwnProperty = Array.prototype.hasOwnProperty; 39 var widgetSlice = Array.prototype.slice; 40 41 $.cleanData = ( function( orig ) { 42 return function( elems ) { 43 var events, elem, i; 44 for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) { 45 46 // Only trigger remove when necessary to save time 47 events = $._data( elem, "events" ); 48 if ( events && events.remove ) { 49 $( elem ).triggerHandler( "remove" ); 50 } 51 } 52 orig( elems ); 53 }; 54 } )( $.cleanData ); 55 56 $.widget = function( name, base, prototype ) { 57 var existingConstructor, constructor, basePrototype; 58 59 // ProxiedPrototype allows the provided prototype to remain unmodified 60 // so that it can be used as a mixin for multiple widgets (#8876) 61 var proxiedPrototype = {}; 62 63 var namespace = name.split( "." )[ 0 ]; 64 name = name.split( "." )[ 1 ]; 65 var fullName = namespace + "-" + name; 66 67 if ( !prototype ) { 68 prototype = base; 69 base = $.Widget; 70 } 71 72 if ( Array.isArray( prototype ) ) { 73 prototype = $.extend.apply( null, [ {} ].concat( prototype ) ); 74 } 75 76 // Create selector for plugin 77 $.expr.pseudos[ fullName.toLowerCase() ] = function( elem ) { 78 return !!$.data( elem, fullName ); 79 }; 80 81 $[ namespace ] = $[ namespace ] || {}; 82 existingConstructor = $[ namespace ][ name ]; 83 constructor = $[ namespace ][ name ] = function( options, element ) { 84 85 // Allow instantiation without "new" keyword 86 if ( !this || !this._createWidget ) { 87 return new constructor( options, element ); 88 } 89 90 // Allow instantiation without initializing for simple inheritance 91 // must use "new" keyword (the code above always passes args) 92 if ( arguments.length ) { 93 this._createWidget( options, element ); 94 } 95 }; 96 97 // Extend with the existing constructor to carry over any static properties 98 $.extend( constructor, existingConstructor, { 99 version: prototype.version, 100 101 // Copy the object used to create the prototype in case we need to 102 // redefine the widget later 103 _proto: $.extend( {}, prototype ), 104 105 // Track widgets that inherit from this widget in case this widget is 106 // redefined after a widget inherits from it 107 _childConstructors: [] 108 } ); 109 110 basePrototype = new base(); 111 112 // We need to make the options hash a property directly on the new instance 113 // otherwise we'll modify the options hash on the prototype that we're 114 // inheriting from 115 basePrototype.options = $.widget.extend( {}, basePrototype.options ); 116 $.each( prototype, function( prop, value ) { 117 if ( typeof value !== "function" ) { 118 proxiedPrototype[ prop ] = value; 119 return; 120 } 121 proxiedPrototype[ prop ] = ( function() { 122 function _super() { 123 return base.prototype[ prop ].apply( this, arguments ); 124 } 125 126 function _superApply( args ) { 127 return base.prototype[ prop ].apply( this, args ); 128 } 129 130 return function() { 131 var __super = this._super; 132 var __superApply = this._superApply; 133 var returnValue; 134 135 this._super = _super; 136 this._superApply = _superApply; 137 138 returnValue = value.apply( this, arguments ); 139 140 this._super = __super; 141 this._superApply = __superApply; 142 143 return returnValue; 144 }; 145 } )(); 146 } ); 147 constructor.prototype = $.widget.extend( basePrototype, { 148 149 // TODO: remove support for widgetEventPrefix 150 // always use the name + a colon as the prefix, e.g., draggable:start 151 // don't prefix for widgets that aren't DOM-based 152 widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name 153 }, proxiedPrototype, { 154 constructor: constructor, 155 namespace: namespace, 156 widgetName: name, 157 widgetFullName: fullName 158 } ); 159 160 // If this widget is being redefined then we need to find all widgets that 161 // are inheriting from it and redefine all of them so that they inherit from 162 // the new version of this widget. We're essentially trying to replace one 163 // level in the prototype chain. 164 if ( existingConstructor ) { 165 $.each( existingConstructor._childConstructors, function( i, child ) { 166 var childPrototype = child.prototype; 167 168 // Redefine the child widget using the same prototype that was 169 // originally used, but inherit from the new version of the base 170 $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, 171 child._proto ); 172 } ); 173 174 // Remove the list of existing child constructors from the old constructor 175 // so the old child constructors can be garbage collected 176 delete existingConstructor._childConstructors; 177 } else { 178 base._childConstructors.push( constructor ); 179 } 180 181 $.widget.bridge( name, constructor ); 182 183 return constructor; 184 }; 185 186 $.widget.extend = function( target ) { 187 var input = widgetSlice.call( arguments, 1 ); 188 var inputIndex = 0; 189 var inputLength = input.length; 190 var key; 191 var value; 192 193 for ( ; inputIndex < inputLength; inputIndex++ ) { 194 for ( key in input[ inputIndex ] ) { 195 value = input[ inputIndex ][ key ]; 196 if ( widgetHasOwnProperty.call( input[ inputIndex ], key ) && value !== undefined ) { 197 198 // Clone objects 199 if ( $.isPlainObject( value ) ) { 200 target[ key ] = $.isPlainObject( target[ key ] ) ? 201 $.widget.extend( {}, target[ key ], value ) : 202 203 // Don't extend strings, arrays, etc. with objects 204 $.widget.extend( {}, value ); 205 206 // Copy everything else by reference 207 } else { 208 target[ key ] = value; 209 } 210 } 211 } 212 } 213 return target; 214 }; 215 216 $.widget.bridge = function( name, object ) { 217 var fullName = object.prototype.widgetFullName || name; 218 $.fn[ name ] = function( options ) { 219 var isMethodCall = typeof options === "string"; 220 var args = widgetSlice.call( arguments, 1 ); 221 var returnValue = this; 222 223 if ( isMethodCall ) { 224 225 // If this is an empty collection, we need to have the instance method 226 // return undefined instead of the jQuery instance 227 if ( !this.length && options === "instance" ) { 228 returnValue = undefined; 229 } else { 230 this.each( function() { 231 var methodValue; 232 var instance = $.data( this, fullName ); 233 234 if ( options === "instance" ) { 235 returnValue = instance; 236 return false; 237 } 238 239 if ( !instance ) { 240 return $.error( "cannot call methods on " + name + 241 " prior to initialization; " + 242 "attempted to call method '" + options + "'" ); 243 } 244 245 if ( typeof instance[ options ] !== "function" || 246 options.charAt( 0 ) === "_" ) { 247 return $.error( "no such method '" + options + "' for " + name + 248 " widget instance" ); 249 } 250 251 methodValue = instance[ options ].apply( instance, args ); 252 253 if ( methodValue !== instance && methodValue !== undefined ) { 254 returnValue = methodValue && methodValue.jquery ? 255 returnValue.pushStack( methodValue.get() ) : 256 methodValue; 257 return false; 258 } 259 } ); 260 } 261 } else { 262 263 // Allow multiple hashes to be passed on init 264 if ( args.length ) { 265 options = $.widget.extend.apply( null, [ options ].concat( args ) ); 266 } 267 268 this.each( function() { 269 var instance = $.data( this, fullName ); 270 if ( instance ) { 271 instance.option( options || {} ); 272 if ( instance._init ) { 273 instance._init(); 274 } 275 } else { 276 $.data( this, fullName, new object( options, this ) ); 277 } 278 } ); 279 } 280 281 return returnValue; 282 }; 283 }; 284 285 $.Widget = function( /* options, element */ ) {}; 286 $.Widget._childConstructors = []; 287 288 $.Widget.prototype = { 289 widgetName: "widget", 290 widgetEventPrefix: "", 291 defaultElement: "<div>", 292 293 options: { 294 classes: {}, 295 disabled: false, 296 297 // Callbacks 298 create: null 299 }, 300 301 _createWidget: function( options, element ) { 302 element = $( element || this.defaultElement || this )[ 0 ]; 303 this.element = $( element ); 304 this.uuid = widgetUuid++; 305 this.eventNamespace = "." + this.widgetName + this.uuid; 306 307 this.bindings = $(); 308 this.hoverable = $(); 309 this.focusable = $(); 310 this.classesElementLookup = {}; 311 312 if ( element !== this ) { 313 $.data( element, this.widgetFullName, this ); 314 this._on( true, this.element, { 315 remove: function( event ) { 316 if ( event.target === element ) { 317 this.destroy(); 318 } 319 } 320 } ); 321 this.document = $( element.style ? 322 323 // Element within the document 324 element.ownerDocument : 325 326 // Element is window or document 327 element.document || element ); 328 this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow ); 329 } 330 331 this.options = $.widget.extend( {}, 332 this.options, 333 this._getCreateOptions(), 334 options ); 335 336 this._create(); 337 338 if ( this.options.disabled ) { 339 this._setOptionDisabled( this.options.disabled ); 340 } 341 342 this._trigger( "create", null, this._getCreateEventData() ); 343 this._init(); 344 }, 345 346 _getCreateOptions: function() { 347 return {}; 348 }, 349 350 _getCreateEventData: $.noop, 351 352 _create: $.noop, 353 354 _init: $.noop, 355 356 destroy: function() { 357 var that = this; 358 359 this._destroy(); 360 $.each( this.classesElementLookup, function( key, value ) { 361 that._removeClass( value, key ); 362 } ); 363 364 // We can probably remove the unbind calls in 2.0 365 // all event bindings should go through this._on() 366 this.element 367 .off( this.eventNamespace ) 368 .removeData( this.widgetFullName ); 369 this.widget() 370 .off( this.eventNamespace ) 371 .removeAttr( "aria-disabled" ); 372 373 // Clean up events and states 374 this.bindings.off( this.eventNamespace ); 375 }, 376 377 _destroy: $.noop, 378 379 widget: function() { 380 return this.element; 381 }, 382 383 option: function( key, value ) { 384 var options = key; 385 var parts; 386 var curOption; 387 var i; 388 389 if ( arguments.length === 0 ) { 390 391 // Don't return a reference to the internal hash 392 return $.widget.extend( {}, this.options ); 393 } 394 395 if ( typeof key === "string" ) { 396 397 // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } 398 options = {}; 399 parts = key.split( "." ); 400 key = parts.shift(); 401 if ( parts.length ) { 402 curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); 403 for ( i = 0; i < parts.length - 1; i++ ) { 404 curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; 405 curOption = curOption[ parts[ i ] ]; 406 } 407 key = parts.pop(); 408 if ( arguments.length === 1 ) { 409 return curOption[ key ] === undefined ? null : curOption[ key ]; 410 } 411 curOption[ key ] = value; 412 } else { 413 if ( arguments.length === 1 ) { 414 return this.options[ key ] === undefined ? null : this.options[ key ]; 415 } 416 options[ key ] = value; 417 } 418 } 419 420 this._setOptions( options ); 421 422 return this; 423 }, 424 425 _setOptions: function( options ) { 426 var key; 427 428 for ( key in options ) { 429 this._setOption( key, options[ key ] ); 430 } 431 432 return this; 433 }, 434 435 _setOption: function( key, value ) { 436 if ( key === "classes" ) { 437 this._setOptionClasses( value ); 438 } 439 440 this.options[ key ] = value; 441 442 if ( key === "disabled" ) { 443 this._setOptionDisabled( value ); 444 } 445 446 return this; 447 }, 448 449 _setOptionClasses: function( value ) { 450 var classKey, elements, currentElements; 451 452 for ( classKey in value ) { 453 currentElements = this.classesElementLookup[ classKey ]; 454 if ( value[ classKey ] === this.options.classes[ classKey ] || 455 !currentElements || 456 !currentElements.length ) { 457 continue; 458 } 459 460 // We are doing this to create a new jQuery object because the _removeClass() call 461 // on the next line is going to destroy the reference to the current elements being 462 // tracked. We need to save a copy of this collection so that we can add the new classes 463 // below. 464 elements = $( currentElements.get() ); 465 this._removeClass( currentElements, classKey ); 466 467 // We don't use _addClass() here, because that uses this.options.classes 468 // for generating the string of classes. We want to use the value passed in from 469 // _setOption(), this is the new value of the classes option which was passed to 470 // _setOption(). We pass this value directly to _classes(). 471 elements.addClass( this._classes( { 472 element: elements, 473 keys: classKey, 474 classes: value, 475 add: true 476 } ) ); 477 } 478 }, 479 480 _setOptionDisabled: function( value ) { 481 this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value ); 482 483 // If the widget is becoming disabled, then nothing is interactive 484 if ( value ) { 485 this._removeClass( this.hoverable, null, "ui-state-hover" ); 486 this._removeClass( this.focusable, null, "ui-state-focus" ); 487 } 488 }, 489 490 enable: function() { 491 return this._setOptions( { disabled: false } ); 492 }, 493 494 disable: function() { 495 return this._setOptions( { disabled: true } ); 496 }, 497 498 _classes: function( options ) { 499 var full = []; 500 var that = this; 501 502 options = $.extend( { 503 element: this.element, 504 classes: this.options.classes || {} 505 }, options ); 506 507 function bindRemoveEvent() { 508 var nodesToBind = []; 509 510 options.element.each( function( _, element ) { 511 var isTracked = $.map( that.classesElementLookup, function( elements ) { 512 return elements; 513 } ) 514 .some( function( elements ) { 515 return elements.is( element ); 516 } ); 517 518 if ( !isTracked ) { 519 nodesToBind.push( element ); 520 } 521 } ); 522 523 that._on( $( nodesToBind ), { 524 remove: "_untrackClassesElement" 525 } ); 526 } 527 528 function processClassString( classes, checkOption ) { 529 var current, i; 530 for ( i = 0; i < classes.length; i++ ) { 531 current = that.classesElementLookup[ classes[ i ] ] || $(); 532 if ( options.add ) { 533 bindRemoveEvent(); 534 current = $( $.uniqueSort( current.get().concat( options.element.get() ) ) ); 535 } else { 536 current = $( current.not( options.element ).get() ); 537 } 538 that.classesElementLookup[ classes[ i ] ] = current; 539 full.push( classes[ i ] ); 540 if ( checkOption && options.classes[ classes[ i ] ] ) { 541 full.push( options.classes[ classes[ i ] ] ); 542 } 543 } 544 } 545 546 if ( options.keys ) { 547 processClassString( options.keys.match( /\S+/g ) || [], true ); 548 } 549 if ( options.extra ) { 550 processClassString( options.extra.match( /\S+/g ) || [] ); 551 } 552 553 return full.join( " " ); 554 }, 555 556 _untrackClassesElement: function( event ) { 557 var that = this; 558 $.each( that.classesElementLookup, function( key, value ) { 559 if ( $.inArray( event.target, value ) !== -1 ) { 560 that.classesElementLookup[ key ] = $( value.not( event.target ).get() ); 561 } 562 } ); 563 564 this._off( $( event.target ) ); 565 }, 566 567 _removeClass: function( element, keys, extra ) { 568 return this._toggleClass( element, keys, extra, false ); 569 }, 570 571 _addClass: function( element, keys, extra ) { 572 return this._toggleClass( element, keys, extra, true ); 573 }, 574 575 _toggleClass: function( element, keys, extra, add ) { 576 add = ( typeof add === "boolean" ) ? add : extra; 577 var shift = ( typeof element === "string" || element === null ), 578 options = { 579 extra: shift ? keys : extra, 580 keys: shift ? element : keys, 581 element: shift ? this.element : element, 582 add: add 583 }; 584 options.element.toggleClass( this._classes( options ), add ); 585 return this; 586 }, 587 588 _on: function( suppressDisabledCheck, element, handlers ) { 589 var delegateElement; 590 var instance = this; 591 592 // No suppressDisabledCheck flag, shuffle arguments 593 if ( typeof suppressDisabledCheck !== "boolean" ) { 594 handlers = element; 595 element = suppressDisabledCheck; 596 suppressDisabledCheck = false; 597 } 598 599 // No element argument, shuffle and use this.element 600 if ( !handlers ) { 601 handlers = element; 602 element = this.element; 603 delegateElement = this.widget(); 604 } else { 605 element = delegateElement = $( element ); 606 this.bindings = this.bindings.add( element ); 607 } 608 609 $.each( handlers, function( event, handler ) { 610 function handlerProxy() { 611 612 // Allow widgets to customize the disabled handling 613 // - disabled as an array instead of boolean 614 // - disabled class as method for disabling individual parts 615 if ( !suppressDisabledCheck && 616 ( instance.options.disabled === true || 617 $( this ).hasClass( "ui-state-disabled" ) ) ) { 618 return; 619 } 620 return ( typeof handler === "string" ? instance[ handler ] : handler ) 621 .apply( instance, arguments ); 622 } 623 624 // Copy the guid so direct unbinding works 625 if ( typeof handler !== "string" ) { 626 handlerProxy.guid = handler.guid = 627 handler.guid || handlerProxy.guid || $.guid++; 628 } 629 630 var match = event.match( /^([\w:-]*)\s*(.*)$/ ); 631 var eventName = match[ 1 ] + instance.eventNamespace; 632 var selector = match[ 2 ]; 633 634 if ( selector ) { 635 delegateElement.on( eventName, selector, handlerProxy ); 636 } else { 637 element.on( eventName, handlerProxy ); 638 } 639 } ); 640 }, 641 642 _off: function( element, eventName ) { 643 eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) + 644 this.eventNamespace; 645 element.off( eventName ); 646 647 // Clear the stack to avoid memory leaks (#10056) 648 this.bindings = $( this.bindings.not( element ).get() ); 649 this.focusable = $( this.focusable.not( element ).get() ); 650 this.hoverable = $( this.hoverable.not( element ).get() ); 651 }, 652 653 _delay: function( handler, delay ) { 654 function handlerProxy() { 655 return ( typeof handler === "string" ? instance[ handler ] : handler ) 656 .apply( instance, arguments ); 657 } 658 var instance = this; 659 return setTimeout( handlerProxy, delay || 0 ); 660 }, 661 662 _hoverable: function( element ) { 663 this.hoverable = this.hoverable.add( element ); 664 this._on( element, { 665 mouseenter: function( event ) { 666 this._addClass( $( event.currentTarget ), null, "ui-state-hover" ); 667 }, 668 mouseleave: function( event ) { 669 this._removeClass( $( event.currentTarget ), null, "ui-state-hover" ); 670 } 671 } ); 672 }, 673 674 _focusable: function( element ) { 675 this.focusable = this.focusable.add( element ); 676 this._on( element, { 677 focusin: function( event ) { 678 this._addClass( $( event.currentTarget ), null, "ui-state-focus" ); 679 }, 680 focusout: function( event ) { 681 this._removeClass( $( event.currentTarget ), null, "ui-state-focus" ); 682 } 683 } ); 684 }, 685 686 _trigger: function( type, event, data ) { 687 var prop, orig; 688 var callback = this.options[ type ]; 689 690 data = data || {}; 691 event = $.Event( event ); 692 event.type = ( type === this.widgetEventPrefix ? 693 type : 694 this.widgetEventPrefix + type ).toLowerCase(); 695 696 // The original event may come from any element 697 // so we need to reset the target on the new event 698 event.target = this.element[ 0 ]; 699 700 // Copy original event properties over to the new event 701 orig = event.originalEvent; 702 if ( orig ) { 703 for ( prop in orig ) { 704 if ( !( prop in event ) ) { 705 event[ prop ] = orig[ prop ]; 706 } 707 } 708 } 709 710 this.element.trigger( event, data ); 711 return !( typeof callback === "function" && 712 callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false || 713 event.isDefaultPrevented() ); 714 } 715 }; 716 717 $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { 718 $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { 719 if ( typeof options === "string" ) { 720 options = { effect: options }; 721 } 722 723 var hasOptions; 724 var effectName = !options ? 725 method : 726 options === true || typeof options === "number" ? 727 defaultEffect : 728 options.effect || defaultEffect; 729 730 options = options || {}; 731 if ( typeof options === "number" ) { 732 options = { duration: options }; 733 } else if ( options === true ) { 734 options = {}; 735 } 736 737 hasOptions = !$.isEmptyObject( options ); 738 options.complete = callback; 739 740 if ( options.delay ) { 741 element.delay( options.delay ); 742 } 743 744 if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { 745 element[ method ]( options ); 746 } else if ( effectName !== method && element[ effectName ] ) { 747 element[ effectName ]( options.duration, options.easing, callback ); 748 } else { 749 element.queue( function( next ) { 750 $( this )[ method ](); 751 if ( callback ) { 752 callback.call( element[ 0 ] ); 753 } 754 next(); 755 } ); 756 } 757 }; 758 } ); 759 760 var widget = $.widget; 761 762 763 /*! 764 * jQuery UI Position 1.13.2 765 * http://jqueryui.com 766 * 767 * Copyright jQuery Foundation and other contributors 768 * Released under the MIT license. 769 * http://jquery.org/license 770 * 771 * http://api.jqueryui.com/position/ 772 */ 773 774 //>>label: Position 775 //>>group: Core 776 //>>description: Positions elements relative to other elements. 777 //>>docs: http://api.jqueryui.com/position/ 778 //>>demos: http://jqueryui.com/position/ 779 780 781 ( function() { 782 var cachedScrollbarWidth, 783 max = Math.max, 784 abs = Math.abs, 785 rhorizontal = /left|center|right/, 786 rvertical = /top|center|bottom/, 787 roffset = /[\+\-]\d+(\.[\d]+)?%?/, 788 rposition = /^\w+/, 789 rpercent = /%$/, 790 _position = $.fn.position; 791 792 function getOffsets( offsets, width, height ) { 793 return [ 794 parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), 795 parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) 796 ]; 797 } 798 799 function parseCss( element, property ) { 800 return parseInt( $.css( element, property ), 10 ) || 0; 801 } 802 803 function isWindow( obj ) { 804 return obj != null && obj === obj.window; 805 } 806 807 function getDimensions( elem ) { 808 var raw = elem[ 0 ]; 809 if ( raw.nodeType === 9 ) { 810 return { 811 width: elem.width(), 812 height: elem.height(), 813 offset: { top: 0, left: 0 } 814 }; 815 } 816 if ( isWindow( raw ) ) { 817 return { 818 width: elem.width(), 819 height: elem.height(), 820 offset: { top: elem.scrollTop(), left: elem.scrollLeft() } 821 }; 822 } 823 if ( raw.preventDefault ) { 824 return { 825 width: 0, 826 height: 0, 827 offset: { top: raw.pageY, left: raw.pageX } 828 }; 829 } 830 return { 831 width: elem.outerWidth(), 832 height: elem.outerHeight(), 833 offset: elem.offset() 834 }; 835 } 836 837 $.position = { 838 scrollbarWidth: function() { 839 if ( cachedScrollbarWidth !== undefined ) { 840 return cachedScrollbarWidth; 841 } 842 var w1, w2, 843 div = $( "<div style=" + 844 "'display:block;position:absolute;width:200px;height:200px;overflow:hidden;'>" + 845 "<div style='height:300px;width:auto;'></div></div>" ), 846 innerDiv = div.children()[ 0 ]; 847 848 $( "body" ).append( div ); 849 w1 = innerDiv.offsetWidth; 850 div.css( "overflow", "scroll" ); 851 852 w2 = innerDiv.offsetWidth; 853 854 if ( w1 === w2 ) { 855 w2 = div[ 0 ].clientWidth; 856 } 857 858 div.remove(); 859 860 return ( cachedScrollbarWidth = w1 - w2 ); 861 }, 862 getScrollInfo: function( within ) { 863 var overflowX = within.isWindow || within.isDocument ? "" : 864 within.element.css( "overflow-x" ), 865 overflowY = within.isWindow || within.isDocument ? "" : 866 within.element.css( "overflow-y" ), 867 hasOverflowX = overflowX === "scroll" || 868 ( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ), 869 hasOverflowY = overflowY === "scroll" || 870 ( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight ); 871 return { 872 width: hasOverflowY ? $.position.scrollbarWidth() : 0, 873 height: hasOverflowX ? $.position.scrollbarWidth() : 0 874 }; 875 }, 876 getWithinInfo: function( element ) { 877 var withinElement = $( element || window ), 878 isElemWindow = isWindow( withinElement[ 0 ] ), 879 isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9, 880 hasOffset = !isElemWindow && !isDocument; 881 return { 882 element: withinElement, 883 isWindow: isElemWindow, 884 isDocument: isDocument, 885 offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 }, 886 scrollLeft: withinElement.scrollLeft(), 887 scrollTop: withinElement.scrollTop(), 888 width: withinElement.outerWidth(), 889 height: withinElement.outerHeight() 890 }; 891 } 892 }; 893 894 $.fn.position = function( options ) { 895 if ( !options || !options.of ) { 896 return _position.apply( this, arguments ); 897 } 898 899 // Make a copy, we don't want to modify arguments 900 options = $.extend( {}, options ); 901 902 var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions, 903 904 // Make sure string options are treated as CSS selectors 905 target = typeof options.of === "string" ? 906 $( document ).find( options.of ) : 907 $( options.of ), 908 909 within = $.position.getWithinInfo( options.within ), 910 scrollInfo = $.position.getScrollInfo( within ), 911 collision = ( options.collision || "flip" ).split( " " ), 912 offsets = {}; 913 914 dimensions = getDimensions( target ); 915 if ( target[ 0 ].preventDefault ) { 916 917 // Force left top to allow flipping 918 options.at = "left top"; 919 } 920 targetWidth = dimensions.width; 921 targetHeight = dimensions.height; 922 targetOffset = dimensions.offset; 923 924 // Clone to reuse original targetOffset later 925 basePosition = $.extend( {}, targetOffset ); 926 927 // Force my and at to have valid horizontal and vertical positions 928 // if a value is missing or invalid, it will be converted to center 929 $.each( [ "my", "at" ], function() { 930 var pos = ( options[ this ] || "" ).split( " " ), 931 horizontalOffset, 932 verticalOffset; 933 934 if ( pos.length === 1 ) { 935 pos = rhorizontal.test( pos[ 0 ] ) ? 936 pos.concat( [ "center" ] ) : 937 rvertical.test( pos[ 0 ] ) ? 938 [ "center" ].concat( pos ) : 939 [ "center", "center" ]; 940 } 941 pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; 942 pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; 943 944 // Calculate offsets 945 horizontalOffset = roffset.exec( pos[ 0 ] ); 946 verticalOffset = roffset.exec( pos[ 1 ] ); 947 offsets[ this ] = [ 948 horizontalOffset ? horizontalOffset[ 0 ] : 0, 949 verticalOffset ? verticalOffset[ 0 ] : 0 950 ]; 951 952 // Reduce to just the positions without the offsets 953 options[ this ] = [ 954 rposition.exec( pos[ 0 ] )[ 0 ], 955 rposition.exec( pos[ 1 ] )[ 0 ] 956 ]; 957 } ); 958 959 // Normalize collision option 960 if ( collision.length === 1 ) { 961 collision[ 1 ] = collision[ 0 ]; 962 } 963 964 if ( options.at[ 0 ] === "right" ) { 965 basePosition.left += targetWidth; 966 } else if ( options.at[ 0 ] === "center" ) { 967 basePosition.left += targetWidth / 2; 968 } 969 970 if ( options.at[ 1 ] === "bottom" ) { 971 basePosition.top += targetHeight; 972 } else if ( options.at[ 1 ] === "center" ) { 973 basePosition.top += targetHeight / 2; 974 } 975 976 atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); 977 basePosition.left += atOffset[ 0 ]; 978 basePosition.top += atOffset[ 1 ]; 979 980 return this.each( function() { 981 var collisionPosition, using, 982 elem = $( this ), 983 elemWidth = elem.outerWidth(), 984 elemHeight = elem.outerHeight(), 985 marginLeft = parseCss( this, "marginLeft" ), 986 marginTop = parseCss( this, "marginTop" ), 987 collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + 988 scrollInfo.width, 989 collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + 990 scrollInfo.height, 991 position = $.extend( {}, basePosition ), 992 myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); 993 994 if ( options.my[ 0 ] === "right" ) { 995 position.left -= elemWidth; 996 } else if ( options.my[ 0 ] === "center" ) { 997 position.left -= elemWidth / 2; 998 } 999 1000 if ( options.my[ 1 ] === "bottom" ) { 1001 position.top -= elemHeight; 1002 } else if ( options.my[ 1 ] === "center" ) { 1003 position.top -= elemHeight / 2; 1004 } 1005 1006 position.left += myOffset[ 0 ]; 1007 position.top += myOffset[ 1 ]; 1008 1009 collisionPosition = { 1010 marginLeft: marginLeft, 1011 marginTop: marginTop 1012 }; 1013 1014 $.each( [ "left", "top" ], function( i, dir ) { 1015 if ( $.ui.position[ collision[ i ] ] ) { 1016 $.ui.position[ collision[ i ] ][ dir ]( position, { 1017 targetWidth: targetWidth, 1018 targetHeight: targetHeight, 1019 elemWidth: elemWidth, 1020 elemHeight: elemHeight, 1021 collisionPosition: collisionPosition, 1022 collisionWidth: collisionWidth, 1023 collisionHeight: collisionHeight, 1024 offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], 1025 my: options.my, 1026 at: options.at, 1027 within: within, 1028 elem: elem 1029 } ); 1030 } 1031 } ); 1032 1033 if ( options.using ) { 1034 1035 // Adds feedback as second argument to using callback, if present 1036 using = function( props ) { 1037 var left = targetOffset.left - position.left, 1038 right = left + targetWidth - elemWidth, 1039 top = targetOffset.top - position.top, 1040 bottom = top + targetHeight - elemHeight, 1041 feedback = { 1042 target: { 1043 element: target, 1044 left: targetOffset.left, 1045 top: targetOffset.top, 1046 width: targetWidth, 1047 height: targetHeight 1048 }, 1049 element: { 1050 element: elem, 1051 left: position.left, 1052 top: position.top, 1053 width: elemWidth, 1054 height: elemHeight 1055 }, 1056 horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", 1057 vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" 1058 }; 1059 if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { 1060 feedback.horizontal = "center"; 1061 } 1062 if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { 1063 feedback.vertical = "middle"; 1064 } 1065 if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { 1066 feedback.important = "horizontal"; 1067 } else { 1068 feedback.important = "vertical"; 1069 } 1070 options.using.call( this, props, feedback ); 1071 }; 1072 } 1073 1074 elem.offset( $.extend( position, { using: using } ) ); 1075 } ); 1076 }; 1077 1078 $.ui.position = { 1079 fit: { 1080 left: function( position, data ) { 1081 var within = data.within, 1082 withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, 1083 outerWidth = within.width, 1084 collisionPosLeft = position.left - data.collisionPosition.marginLeft, 1085 overLeft = withinOffset - collisionPosLeft, 1086 overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, 1087 newOverRight; 1088 1089 // Element is wider than within 1090 if ( data.collisionWidth > outerWidth ) { 1091 1092 // Element is initially over the left side of within 1093 if ( overLeft > 0 && overRight <= 0 ) { 1094 newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - 1095 withinOffset; 1096 position.left += overLeft - newOverRight; 1097 1098 // Element is initially over right side of within 1099 } else if ( overRight > 0 && overLeft <= 0 ) { 1100 position.left = withinOffset; 1101 1102 // Element is initially over both left and right sides of within 1103 } else { 1104 if ( overLeft > overRight ) { 1105 position.left = withinOffset + outerWidth - data.collisionWidth; 1106 } else { 1107 position.left = withinOffset; 1108 } 1109 } 1110 1111 // Too far left -> align with left edge 1112 } else if ( overLeft > 0 ) { 1113 position.left += overLeft; 1114 1115 // Too far right -> align with right edge 1116 } else if ( overRight > 0 ) { 1117 position.left -= overRight; 1118 1119 // Adjust based on position and margin 1120 } else { 1121 position.left = max( position.left - collisionPosLeft, position.left ); 1122 } 1123 }, 1124 top: function( position, data ) { 1125 var within = data.within, 1126 withinOffset = within.isWindow ? within.scrollTop : within.offset.top, 1127 outerHeight = data.within.height, 1128 collisionPosTop = position.top - data.collisionPosition.marginTop, 1129 overTop = withinOffset - collisionPosTop, 1130 overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, 1131 newOverBottom; 1132 1133 // Element is taller than within 1134 if ( data.collisionHeight > outerHeight ) { 1135 1136 // Element is initially over the top of within 1137 if ( overTop > 0 && overBottom <= 0 ) { 1138 newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - 1139 withinOffset; 1140 position.top += overTop - newOverBottom; 1141 1142 // Element is initially over bottom of within 1143 } else if ( overBottom > 0 && overTop <= 0 ) { 1144 position.top = withinOffset; 1145 1146 // Element is initially over both top and bottom of within 1147 } else { 1148 if ( overTop > overBottom ) { 1149 position.top = withinOffset + outerHeight - data.collisionHeight; 1150 } else { 1151 position.top = withinOffset; 1152 } 1153 } 1154 1155 // Too far up -> align with top 1156 } else if ( overTop > 0 ) { 1157 position.top += overTop; 1158 1159 // Too far down -> align with bottom edge 1160 } else if ( overBottom > 0 ) { 1161 position.top -= overBottom; 1162 1163 // Adjust based on position and margin 1164 } else { 1165 position.top = max( position.top - collisionPosTop, position.top ); 1166 } 1167 } 1168 }, 1169 flip: { 1170 left: function( position, data ) { 1171 var within = data.within, 1172 withinOffset = within.offset.left + within.scrollLeft, 1173 outerWidth = within.width, 1174 offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, 1175 collisionPosLeft = position.left - data.collisionPosition.marginLeft, 1176 overLeft = collisionPosLeft - offsetLeft, 1177 overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, 1178 myOffset = data.my[ 0 ] === "left" ? 1179 -data.elemWidth : 1180 data.my[ 0 ] === "right" ? 1181 data.elemWidth : 1182 0, 1183 atOffset = data.at[ 0 ] === "left" ? 1184 data.targetWidth : 1185 data.at[ 0 ] === "right" ? 1186 -data.targetWidth : 1187 0, 1188 offset = -2 * data.offset[ 0 ], 1189 newOverRight, 1190 newOverLeft; 1191 1192 if ( overLeft < 0 ) { 1193 newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - 1194 outerWidth - withinOffset; 1195 if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { 1196 position.left += myOffset + atOffset + offset; 1197 } 1198 } else if ( overRight > 0 ) { 1199 newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + 1200 atOffset + offset - offsetLeft; 1201 if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { 1202 position.left += myOffset + atOffset + offset; 1203 } 1204 } 1205 }, 1206 top: function( position, data ) { 1207 var within = data.within, 1208 withinOffset = within.offset.top + within.scrollTop, 1209 outerHeight = within.height, 1210 offsetTop = within.isWindow ? within.scrollTop : within.offset.top, 1211 collisionPosTop = position.top - data.collisionPosition.marginTop, 1212 overTop = collisionPosTop - offsetTop, 1213 overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, 1214 top = data.my[ 1 ] === "top", 1215 myOffset = top ? 1216 -data.elemHeight : 1217 data.my[ 1 ] === "bottom" ? 1218 data.elemHeight : 1219 0, 1220 atOffset = data.at[ 1 ] === "top" ? 1221 data.targetHeight : 1222 data.at[ 1 ] === "bottom" ? 1223 -data.targetHeight : 1224 0, 1225 offset = -2 * data.offset[ 1 ], 1226 newOverTop, 1227 newOverBottom; 1228 if ( overTop < 0 ) { 1229 newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - 1230 outerHeight - withinOffset; 1231 if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) { 1232 position.top += myOffset + atOffset + offset; 1233 } 1234 } else if ( overBottom > 0 ) { 1235 newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + 1236 offset - offsetTop; 1237 if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) { 1238 position.top += myOffset + atOffset + offset; 1239 } 1240 } 1241 } 1242 }, 1243 flipfit: { 1244 left: function() { 1245 $.ui.position.flip.left.apply( this, arguments ); 1246 $.ui.position.fit.left.apply( this, arguments ); 1247 }, 1248 top: function() { 1249 $.ui.position.flip.top.apply( this, arguments ); 1250 $.ui.position.fit.top.apply( this, arguments ); 1251 } 1252 } 1253 }; 1254 1255 } )(); 1256 1257 var position = $.ui.position; 1258 1259 1260 /*! 1261 * jQuery UI Support for jQuery core 1.8.x and newer 1.13.2 1262 * http://jqueryui.com 1263 * 1264 * Copyright jQuery Foundation and other contributors 1265 * Released under the MIT license. 1266 * http://jquery.org/license 1267 * 1268 */ 1269 1270 //>>label: jQuery 1.8+ Support 1271 //>>group: Core 1272 //>>description: Support version 1.8.x and newer of jQuery core 1273 1274 1275 // Support: jQuery 1.9.x or older 1276 // $.expr[ ":" ] is deprecated. 1277 if ( !$.expr.pseudos ) { 1278 $.expr.pseudos = $.expr[ ":" ]; 1279 } 1280 1281 // Support: jQuery 1.11.x or older 1282 // $.unique has been renamed to $.uniqueSort 1283 if ( !$.uniqueSort ) { 1284 $.uniqueSort = $.unique; 1285 } 1286 1287 // Support: jQuery 2.2.x or older. 1288 // This method has been defined in jQuery 3.0.0. 1289 // Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js 1290 if ( !$.escapeSelector ) { 1291 1292 // CSS string/identifier serialization 1293 // https://drafts.csswg.org/cssom/#common-serializing-idioms 1294 var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g; 1295 1296 var fcssescape = function( ch, asCodePoint ) { 1297 if ( asCodePoint ) { 1298 1299 // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER 1300 if ( ch === "\0" ) { 1301 return "\uFFFD"; 1302 } 1303 1304 // Control characters and (dependent upon position) numbers get escaped as code points 1305 return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " "; 1306 } 1307 1308 // Other potentially-special ASCII characters get backslash-escaped 1309 return "\\" + ch; 1310 }; 1311 1312 $.escapeSelector = function( sel ) { 1313 return ( sel + "" ).replace( rcssescape, fcssescape ); 1314 }; 1315 } 1316 1317 // Support: jQuery 3.4.x or older 1318 // These methods have been defined in jQuery 3.5.0. 1319 if ( !$.fn.even || !$.fn.odd ) { 1320 $.fn.extend( { 1321 even: function() { 1322 return this.filter( function( i ) { 1323 return i % 2 === 0; 1324 } ); 1325 }, 1326 odd: function() { 1327 return this.filter( function( i ) { 1328 return i % 2 === 1; 1329 } ); 1330 } 1331 } ); 1332 } 1333 1334 ; 1335 /*! 1336 * jQuery UI Keycode 1.13.2 1337 * http://jqueryui.com 1338 * 1339 * Copyright jQuery Foundation and other contributors 1340 * Released under the MIT license. 1341 * http://jquery.org/license 1342 */ 1343 1344 //>>label: Keycode 1345 //>>group: Core 1346 //>>description: Provide keycodes as keynames 1347 //>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/ 1348 1349 1350 var keycode = $.ui.keyCode = { 1351 BACKSPACE: 8, 1352 COMMA: 188, 1353 DELETE: 46, 1354 DOWN: 40, 1355 END: 35, 1356 ENTER: 13, 1357 ESCAPE: 27, 1358 HOME: 36, 1359 LEFT: 37, 1360 PAGE_DOWN: 34, 1361 PAGE_UP: 33, 1362 PERIOD: 190, 1363 RIGHT: 39, 1364 SPACE: 32, 1365 TAB: 9, 1366 UP: 38 1367 }; 1368 1369 1370 /*! 1371 * jQuery UI Scroll Parent 1.13.2 1372 * http://jqueryui.com 1373 * 1374 * Copyright jQuery Foundation and other contributors 1375 * Released under the MIT license. 1376 * http://jquery.org/license 1377 */ 1378 1379 //>>label: scrollParent 1380 //>>group: Core 1381 //>>description: Get the closest ancestor element that is scrollable. 1382 //>>docs: http://api.jqueryui.com/scrollParent/ 1383 1384 1385 var scrollParent = $.fn.scrollParent = function( includeHidden ) { 1386 var position = this.css( "position" ), 1387 excludeStaticParent = position === "absolute", 1388 overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, 1389 scrollParent = this.parents().filter( function() { 1390 var parent = $( this ); 1391 if ( excludeStaticParent && parent.css( "position" ) === "static" ) { 1392 return false; 1393 } 1394 return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + 1395 parent.css( "overflow-x" ) ); 1396 } ).eq( 0 ); 1397 1398 return position === "fixed" || !scrollParent.length ? 1399 $( this[ 0 ].ownerDocument || document ) : 1400 scrollParent; 1401 }; 1402 1403 1404 /*! 1405 * jQuery UI Unique ID 1.13.2 1406 * http://jqueryui.com 1407 * 1408 * Copyright jQuery Foundation and other contributors 1409 * Released under the MIT license. 1410 * http://jquery.org/license 1411 */ 1412 1413 //>>label: uniqueId 1414 //>>group: Core 1415 //>>description: Functions to generate and remove uniqueId's 1416 //>>docs: http://api.jqueryui.com/uniqueId/ 1417 1418 1419 var uniqueId = $.fn.extend( { 1420 uniqueId: ( function() { 1421 var uuid = 0; 1422 1423 return function() { 1424 return this.each( function() { 1425 if ( !this.id ) { 1426 this.id = "ui-id-" + ( ++uuid ); 1427 } 1428 } ); 1429 }; 1430 } )(), 1431 1432 removeUniqueId: function() { 1433 return this.each( function() { 1434 if ( /^ui-id-\d+$/.test( this.id ) ) { 1435 $( this ).removeAttr( "id" ); 1436 } 1437 } ); 1438 } 1439 } ); 1440 1441 1442 1443 1444// NOTE: Original jQuery UI wrapper was replaced. See README-Fancytree.md 1445// })); 1446})(jQuery); 1447 1448(function( factory ) { 1449 if ( typeof define === "function" && define.amd ) { 1450 // AMD. Register as an anonymous module. 1451 define( [ "jquery" ], factory ); 1452 } else if ( typeof module === "object" && module.exports ) { 1453 // Node/CommonJS 1454 module.exports = factory(require("jquery")); 1455 } else { 1456 // Browser globals 1457 factory( jQuery ); 1458 } 1459}(function( $ ) { 1460 1461 1462/*! Fancytree Core *//*! 1463 * jquery.fancytree.js 1464 * Tree view control with support for lazy loading and much more. 1465 * https://github.com/mar10/fancytree/ 1466 * 1467 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 1468 * Released under the MIT license 1469 * https://github.com/mar10/fancytree/wiki/LicenseInfo 1470 * 1471 * @version 2.38.3 1472 * @date 2023-02-01T20:52:50Z 1473 */ 1474 1475/** Core Fancytree module. 1476 */ 1477 1478// UMD wrapper for the Fancytree core module 1479(function (factory) { 1480 if (typeof define === "function" && define.amd) { 1481 // AMD. Register as an anonymous module. 1482 define(["jquery", "./jquery.fancytree.ui-deps"], factory); 1483 } else if (typeof module === "object" && module.exports) { 1484 // Node/CommonJS 1485 require("./jquery.fancytree.ui-deps"); 1486 module.exports = factory(require("jquery")); 1487 } else { 1488 // Browser globals 1489 factory(jQuery); 1490 } 1491})(function ($) { 1492 "use strict"; 1493 1494 // prevent duplicate loading 1495 if ($.ui && $.ui.fancytree) { 1496 $.ui.fancytree.warn("Fancytree: ignored duplicate include"); 1497 return; 1498 } 1499 1500 /****************************************************************************** 1501 * Private functions and variables 1502 */ 1503 1504 var i, 1505 attr, 1506 FT = null, // initialized below 1507 TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/' 1508 REX_HTML = /[&<>"'/]/g, // Escape those characters 1509 REX_TOOLTIP = /[<>"'/]/g, // Don't escape `&` in tooltips 1510 RECURSIVE_REQUEST_ERROR = "$recursive_request", 1511 INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid", 1512 ENTITY_MAP = { 1513 "&": "&", 1514 "<": "<", 1515 ">": ">", 1516 '"': """, 1517 "'": "'", 1518 "/": "/", 1519 }, 1520 IGNORE_KEYCODES = { 16: true, 17: true, 18: true }, 1521 SPECIAL_KEYCODES = { 1522 8: "backspace", 1523 9: "tab", 1524 10: "return", 1525 13: "return", 1526 // 16: null, 17: null, 18: null, // ignore shift, ctrl, alt 1527 19: "pause", 1528 20: "capslock", 1529 27: "esc", 1530 32: "space", 1531 33: "pageup", 1532 34: "pagedown", 1533 35: "end", 1534 36: "home", 1535 37: "left", 1536 38: "up", 1537 39: "right", 1538 40: "down", 1539 45: "insert", 1540 46: "del", 1541 59: ";", 1542 61: "=", 1543 // 91: null, 93: null, // ignore left and right meta 1544 96: "0", 1545 97: "1", 1546 98: "2", 1547 99: "3", 1548 100: "4", 1549 101: "5", 1550 102: "6", 1551 103: "7", 1552 104: "8", 1553 105: "9", 1554 106: "*", 1555 107: "+", 1556 109: "-", 1557 110: ".", 1558 111: "/", 1559 112: "f1", 1560 113: "f2", 1561 114: "f3", 1562 115: "f4", 1563 116: "f5", 1564 117: "f6", 1565 118: "f7", 1566 119: "f8", 1567 120: "f9", 1568 121: "f10", 1569 122: "f11", 1570 123: "f12", 1571 144: "numlock", 1572 145: "scroll", 1573 173: "-", 1574 186: ";", 1575 187: "=", 1576 188: ",", 1577 189: "-", 1578 190: ".", 1579 191: "/", 1580 192: "`", 1581 219: "[", 1582 220: "\\", 1583 221: "]", 1584 222: "'", 1585 }, 1586 MODIFIERS = { 1587 16: "shift", 1588 17: "ctrl", 1589 18: "alt", 1590 91: "meta", 1591 93: "meta", 1592 }, 1593 MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" }, 1594 // Boolean attributes that can be set with equivalent class names in the LI tags 1595 // Note: v2.23: checkbox and hideCheckbox are *not* in this list 1596 CLASS_ATTRS = 1597 "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split( 1598 " " 1599 ), 1600 CLASS_ATTR_MAP = {}, 1601 // Top-level Fancytree attributes, that can be set by dict 1602 TREE_ATTRS = "columns types".split(" "), 1603 // TREE_ATTR_MAP = {}, 1604 // Top-level FancytreeNode attributes, that can be set by dict 1605 NODE_ATTRS = 1606 "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type unselectable unselectableIgnore unselectableStatus".split( 1607 " " 1608 ), 1609 NODE_ATTR_MAP = {}, 1610 // Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase) 1611 NODE_ATTR_LOWERCASE_MAP = {}, 1612 // Attribute names that should NOT be added to node.data 1613 NONE_NODE_DATA_MAP = { 1614 active: true, 1615 children: true, 1616 data: true, 1617 focus: true, 1618 }; 1619 1620 for (i = 0; i < CLASS_ATTRS.length; i++) { 1621 CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true; 1622 } 1623 for (i = 0; i < NODE_ATTRS.length; i++) { 1624 attr = NODE_ATTRS[i]; 1625 NODE_ATTR_MAP[attr] = true; 1626 if (attr !== attr.toLowerCase()) { 1627 NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr; 1628 } 1629 } 1630 // for(i=0; i<TREE_ATTRS.length; i++) { 1631 // TREE_ATTR_MAP[TREE_ATTRS[i]] = true; 1632 // } 1633 1634 function _assert(cond, msg) { 1635 // TODO: see qunit.js extractStacktrace() 1636 if (!cond) { 1637 msg = msg ? ": " + msg : ""; 1638 msg = "Fancytree assertion failed" + msg; 1639 1640 // consoleApply("assert", [!!cond, msg]); 1641 1642 // #1041: Raised exceptions may not be visible in the browser 1643 // console if inside promise chains, so we also print directly: 1644 $.ui.fancytree.error(msg); 1645 1646 // Throw exception: 1647 $.error(msg); 1648 } 1649 } 1650 1651 function _hasProp(object, property) { 1652 return Object.prototype.hasOwnProperty.call(object, property); 1653 } 1654 1655 /* Replacement for the deprecated `jQuery.isFunction()`. */ 1656 function _isFunction(obj) { 1657 return typeof obj === "function"; 1658 } 1659 1660 /* Replacement for the deprecated `jQuery.trim()`. */ 1661 function _trim(text) { 1662 return text == null ? "" : text.trim(); 1663 } 1664 1665 /* Replacement for the deprecated `jQuery.isArray()`. */ 1666 var _isArray = Array.isArray; 1667 1668 _assert($.ui, "Fancytree requires jQuery UI (http://jqueryui.com)"); 1669 1670 function consoleApply(method, args) { 1671 var i, 1672 s, 1673 fn = window.console ? window.console[method] : null; 1674 1675 if (fn) { 1676 try { 1677 fn.apply(window.console, args); 1678 } catch (e) { 1679 // IE 8? 1680 s = ""; 1681 for (i = 0; i < args.length; i++) { 1682 s += args[i]; 1683 } 1684 fn(s); 1685 } 1686 } 1687 } 1688 1689 /* support: IE8 Polyfil for Date.now() */ 1690 if (!Date.now) { 1691 Date.now = function now() { 1692 return new Date().getTime(); 1693 }; 1694 } 1695 1696 /*Return true if x is a FancytreeNode.*/ 1697 function _isNode(x) { 1698 return !!(x.tree && x.statusNodeType !== undefined); 1699 } 1700 1701 /** Return true if dotted version string is equal or higher than requested version. 1702 * 1703 * See http://jsfiddle.net/mar10/FjSAN/ 1704 */ 1705 function isVersionAtLeast(dottedVersion, major, minor, patch) { 1706 var i, 1707 v, 1708 t, 1709 verParts = $.map(_trim(dottedVersion).split("."), function (e) { 1710 return parseInt(e, 10); 1711 }), 1712 testParts = $.map( 1713 Array.prototype.slice.call(arguments, 1), 1714 function (e) { 1715 return parseInt(e, 10); 1716 } 1717 ); 1718 1719 for (i = 0; i < testParts.length; i++) { 1720 v = verParts[i] || 0; 1721 t = testParts[i] || 0; 1722 if (v !== t) { 1723 return v > t; 1724 } 1725 } 1726 return true; 1727 } 1728 1729 /** 1730 * Deep-merge a list of objects (but replace array-type options). 1731 * 1732 * jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays. 1733 * This variant is used to merge extension defaults with user options, and should 1734 * merge objects, but override arrays (for example the `triggerStart: [...]` option 1735 * of ext-edit). Also `null` values are copied over and not skipped. 1736 * 1737 * See issue #876 1738 * 1739 * Example: 1740 * _simpleDeepMerge({}, o1, o2); 1741 */ 1742 function _simpleDeepMerge() { 1743 var options, 1744 name, 1745 src, 1746 copy, 1747 clone, 1748 target = arguments[0] || {}, 1749 i = 1, 1750 length = arguments.length; 1751 1752 // Handle case when target is a string or something (possible in deep copy) 1753 if (typeof target !== "object" && !_isFunction(target)) { 1754 target = {}; 1755 } 1756 if (i === length) { 1757 throw Error("need at least two args"); 1758 } 1759 for (; i < length; i++) { 1760 // Only deal with non-null/undefined values 1761 if ((options = arguments[i]) != null) { 1762 // Extend the base object 1763 for (name in options) { 1764 if (_hasProp(options, name)) { 1765 src = target[name]; 1766 copy = options[name]; 1767 // Prevent never-ending loop 1768 if (target === copy) { 1769 continue; 1770 } 1771 // Recurse if we're merging plain objects 1772 // (NOTE: unlike $.extend, we don't merge arrays, but replace them) 1773 if (copy && $.isPlainObject(copy)) { 1774 clone = src && $.isPlainObject(src) ? src : {}; 1775 // Never move original objects, clone them 1776 target[name] = _simpleDeepMerge(clone, copy); 1777 // Don't bring in undefined values 1778 } else if (copy !== undefined) { 1779 target[name] = copy; 1780 } 1781 } 1782 } 1783 } 1784 } 1785 // Return the modified object 1786 return target; 1787 } 1788 1789 /** Return a wrapper that calls sub.methodName() and exposes 1790 * this : tree 1791 * this._local : tree.ext.EXTNAME 1792 * this._super : base.methodName.call() 1793 * this._superApply : base.methodName.apply() 1794 */ 1795 function _makeVirtualFunction(methodName, tree, base, extension, extName) { 1796 // $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName); 1797 // if(rexTestSuper && !rexTestSuper.test(func)){ 1798 // // extension.methodName() doesn't call _super(), so no wrapper required 1799 // return func; 1800 // } 1801 // Use an immediate function as closure 1802 var proxy = (function () { 1803 var prevFunc = tree[methodName], // org. tree method or prev. proxy 1804 baseFunc = extension[methodName], // 1805 _local = tree.ext[extName], 1806 _super = function () { 1807 return prevFunc.apply(tree, arguments); 1808 }, 1809 _superApply = function (args) { 1810 return prevFunc.apply(tree, args); 1811 }; 1812 1813 // Return the wrapper function 1814 return function () { 1815 var prevLocal = tree._local, 1816 prevSuper = tree._super, 1817 prevSuperApply = tree._superApply; 1818 1819 try { 1820 tree._local = _local; 1821 tree._super = _super; 1822 tree._superApply = _superApply; 1823 return baseFunc.apply(tree, arguments); 1824 } finally { 1825 tree._local = prevLocal; 1826 tree._super = prevSuper; 1827 tree._superApply = prevSuperApply; 1828 } 1829 }; 1830 })(); // end of Immediate Function 1831 return proxy; 1832 } 1833 1834 /** 1835 * Subclass `base` by creating proxy functions 1836 */ 1837 function _subclassObject(tree, base, extension, extName) { 1838 // $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName); 1839 for (var attrName in extension) { 1840 if (typeof extension[attrName] === "function") { 1841 if (typeof tree[attrName] === "function") { 1842 // override existing method 1843 tree[attrName] = _makeVirtualFunction( 1844 attrName, 1845 tree, 1846 base, 1847 extension, 1848 extName 1849 ); 1850 } else if (attrName.charAt(0) === "_") { 1851 // Create private methods in tree.ext.EXTENSION namespace 1852 tree.ext[extName][attrName] = _makeVirtualFunction( 1853 attrName, 1854 tree, 1855 base, 1856 extension, 1857 extName 1858 ); 1859 } else { 1860 $.error( 1861 "Could not override tree." + 1862 attrName + 1863 ". Use prefix '_' to create tree." + 1864 extName + 1865 "._" + 1866 attrName 1867 ); 1868 } 1869 } else { 1870 // Create member variables in tree.ext.EXTENSION namespace 1871 if (attrName !== "options") { 1872 tree.ext[extName][attrName] = extension[attrName]; 1873 } 1874 } 1875 } 1876 } 1877 1878 function _getResolvedPromise(context, argArray) { 1879 if (context === undefined) { 1880 return $.Deferred(function () { 1881 this.resolve(); 1882 }).promise(); 1883 } 1884 return $.Deferred(function () { 1885 this.resolveWith(context, argArray); 1886 }).promise(); 1887 } 1888 1889 function _getRejectedPromise(context, argArray) { 1890 if (context === undefined) { 1891 return $.Deferred(function () { 1892 this.reject(); 1893 }).promise(); 1894 } 1895 return $.Deferred(function () { 1896 this.rejectWith(context, argArray); 1897 }).promise(); 1898 } 1899 1900 function _makeResolveFunc(deferred, context) { 1901 return function () { 1902 deferred.resolveWith(context); 1903 }; 1904 } 1905 1906 function _getElementDataAsDict($el) { 1907 // Evaluate 'data-NAME' attributes with special treatment for 'data-json'. 1908 var d = $.extend({}, $el.data()), 1909 json = d.json; 1910 1911 delete d.fancytree; // added to container by widget factory (old jQuery UI) 1912 delete d.uiFancytree; // added to container by widget factory 1913 1914 if (json) { 1915 delete d.json; 1916 // <li data-json='...'> is already returned as object (http://api.jquery.com/data/#data-html5) 1917 d = $.extend(d, json); 1918 } 1919 return d; 1920 } 1921 1922 function _escapeTooltip(s) { 1923 return ("" + s).replace(REX_TOOLTIP, function (s) { 1924 return ENTITY_MAP[s]; 1925 }); 1926 } 1927 1928 // TODO: use currying 1929 function _makeNodeTitleMatcher(s) { 1930 s = s.toLowerCase(); 1931 return function (node) { 1932 return node.title.toLowerCase().indexOf(s) >= 0; 1933 }; 1934 } 1935 1936 function _makeNodeTitleStartMatcher(s) { 1937 var reMatch = new RegExp("^" + s, "i"); 1938 return function (node) { 1939 return reMatch.test(node.title); 1940 }; 1941 } 1942 1943 /****************************************************************************** 1944 * FancytreeNode 1945 */ 1946 1947 /** 1948 * Creates a new FancytreeNode 1949 * 1950 * @class FancytreeNode 1951 * @classdesc A FancytreeNode represents the hierarchical data model and operations. 1952 * 1953 * @param {FancytreeNode} parent 1954 * @param {NodeData} obj 1955 * 1956 * @property {Fancytree} tree The tree instance 1957 * @property {FancytreeNode} parent The parent node 1958 * @property {string} key Node id (must be unique inside the tree) 1959 * @property {string} title Display name (may contain HTML) 1960 * @property {object} data Contains all extra data that was passed on node creation 1961 * @property {FancytreeNode[] | null | undefined} children Array of child nodes.<br> 1962 * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array 1963 * to define a node that has no children. 1964 * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property. 1965 * @property {string} extraClasses Additional CSS classes, added to the node's `<span>`.<br> 1966 * Note: use `node.add/remove/toggleClass()` to modify. 1967 * @property {boolean} folder Folder nodes have different default icons and click behavior.<br> 1968 * Note: Also non-folders may have children. 1969 * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'. 1970 * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion. 1971 * @property {boolean} selected Use isSelected(), setSelected() to access this property. 1972 * @property {string} tooltip Alternative description used as hover popup 1973 * @property {string} iconTooltip Description used as hover popup for icon. @since 2.27 1974 * @property {string} type Node type, used with tree.types map. @since 2.27 1975 */ 1976 function FancytreeNode(parent, obj) { 1977 var i, l, name, cl; 1978 1979 this.parent = parent; 1980 this.tree = parent.tree; 1981 this.ul = null; 1982 this.li = null; // <li id='key' ftnode=this> tag 1983 this.statusNodeType = null; // if this is a temp. node to display the status of its parent 1984 this._isLoading = false; // if this node itself is loading 1985 this._error = null; // {message: '...'} if a load error occurred 1986 this.data = {}; 1987 1988 // TODO: merge this code with node.toDict() 1989 // copy attributes from obj object 1990 for (i = 0, l = NODE_ATTRS.length; i < l; i++) { 1991 name = NODE_ATTRS[i]; 1992 this[name] = obj[name]; 1993 } 1994 // unselectableIgnore and unselectableStatus imply unselectable 1995 if ( 1996 this.unselectableIgnore != null || 1997 this.unselectableStatus != null 1998 ) { 1999 this.unselectable = true; 2000 } 2001 if (obj.hideCheckbox) { 2002 $.error( 2003 "'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'" 2004 ); 2005 } 2006 // node.data += obj.data 2007 if (obj.data) { 2008 $.extend(this.data, obj.data); 2009 } 2010 // Copy all other attributes to this.data.NAME 2011 for (name in obj) { 2012 if ( 2013 !NODE_ATTR_MAP[name] && 2014 (this.tree.options.copyFunctionsToData || 2015 !_isFunction(obj[name])) && 2016 !NONE_NODE_DATA_MAP[name] 2017 ) { 2018 // node.data.NAME = obj.NAME 2019 this.data[name] = obj[name]; 2020 } 2021 } 2022 2023 // Fix missing key 2024 if (this.key == null) { 2025 // test for null OR undefined 2026 if (this.tree.options.defaultKey) { 2027 this.key = "" + this.tree.options.defaultKey(this); 2028 _assert(this.key, "defaultKey() must return a unique key"); 2029 } else { 2030 this.key = "_" + FT._nextNodeKey++; 2031 } 2032 } else { 2033 this.key = "" + this.key; // Convert to string (#217) 2034 } 2035 2036 // Fix tree.activeNode 2037 // TODO: not elegant: we use obj.active as marker to set tree.activeNode 2038 // when loading from a dictionary. 2039 if (obj.active) { 2040 _assert( 2041 this.tree.activeNode === null, 2042 "only one active node allowed" 2043 ); 2044 this.tree.activeNode = this; 2045 } 2046 if (obj.selected) { 2047 // #186 2048 this.tree.lastSelectedNode = this; 2049 } 2050 // TODO: handle obj.focus = true 2051 2052 // Create child nodes 2053 cl = obj.children; 2054 if (cl) { 2055 if (cl.length) { 2056 this._setChildren(cl); 2057 } else { 2058 // if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded' 2059 this.children = this.lazy ? [] : null; 2060 } 2061 } else { 2062 this.children = null; 2063 } 2064 // Add to key/ref map (except for root node) 2065 // if( parent ) { 2066 this.tree._callHook("treeRegisterNode", this.tree, true, this); 2067 // } 2068 } 2069 2070 FancytreeNode.prototype = /** @lends FancytreeNode# */ { 2071 /* Return the direct child FancytreeNode with a given key, index. */ 2072 _findDirectChild: function (ptr) { 2073 var i, 2074 l, 2075 cl = this.children; 2076 2077 if (cl) { 2078 if (typeof ptr === "string") { 2079 for (i = 0, l = cl.length; i < l; i++) { 2080 if (cl[i].key === ptr) { 2081 return cl[i]; 2082 } 2083 } 2084 } else if (typeof ptr === "number") { 2085 return this.children[ptr]; 2086 } else if (ptr.parent === this) { 2087 return ptr; 2088 } 2089 } 2090 return null; 2091 }, 2092 // TODO: activate() 2093 // TODO: activateSilently() 2094 /* Internal helper called in recursive addChildren sequence.*/ 2095 _setChildren: function (children) { 2096 _assert( 2097 children && (!this.children || this.children.length === 0), 2098 "only init supported" 2099 ); 2100 this.children = []; 2101 for (var i = 0, l = children.length; i < l; i++) { 2102 this.children.push(new FancytreeNode(this, children[i])); 2103 } 2104 this.tree._callHook( 2105 "treeStructureChanged", 2106 this.tree, 2107 "setChildren" 2108 ); 2109 }, 2110 /** 2111 * Append (or insert) a list of child nodes. 2112 * 2113 * @param {NodeData[]} children array of child node definitions (also single child accepted) 2114 * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such). 2115 * If omitted, the new children are appended. 2116 * @returns {FancytreeNode} first child added 2117 * 2118 * @see FancytreeNode#applyPatch 2119 */ 2120 addChildren: function (children, insertBefore) { 2121 var i, 2122 l, 2123 pos, 2124 origFirstChild = this.getFirstChild(), 2125 origLastChild = this.getLastChild(), 2126 firstNode = null, 2127 nodeList = []; 2128 2129 if ($.isPlainObject(children)) { 2130 children = [children]; 2131 } 2132 if (!this.children) { 2133 this.children = []; 2134 } 2135 for (i = 0, l = children.length; i < l; i++) { 2136 nodeList.push(new FancytreeNode(this, children[i])); 2137 } 2138 firstNode = nodeList[0]; 2139 if (insertBefore == null) { 2140 this.children = this.children.concat(nodeList); 2141 } else { 2142 // Returns null if insertBefore is not a direct child: 2143 insertBefore = this._findDirectChild(insertBefore); 2144 pos = $.inArray(insertBefore, this.children); 2145 _assert(pos >= 0, "insertBefore must be an existing child"); 2146 // insert nodeList after children[pos] 2147 this.children.splice.apply( 2148 this.children, 2149 [pos, 0].concat(nodeList) 2150 ); 2151 } 2152 if (origFirstChild && !insertBefore) { 2153 // #708: Fast path -- don't render every child of root, just the new ones! 2154 // #723, #729: but only if it's appended to an existing child list 2155 for (i = 0, l = nodeList.length; i < l; i++) { 2156 nodeList[i].render(); // New nodes were never rendered before 2157 } 2158 // Adjust classes where status may have changed 2159 // Has a first child 2160 if (origFirstChild !== this.getFirstChild()) { 2161 // Different first child -- recompute classes 2162 origFirstChild.renderStatus(); 2163 } 2164 if (origLastChild !== this.getLastChild()) { 2165 // Different last child -- recompute classes 2166 origLastChild.renderStatus(); 2167 } 2168 } else if (!this.parent || this.parent.ul || this.tr) { 2169 // render if the parent was rendered (or this is a root node) 2170 this.render(); 2171 } 2172 if (this.tree.options.selectMode === 3) { 2173 this.fixSelection3FromEndNodes(); 2174 } 2175 this.triggerModifyChild( 2176 "add", 2177 nodeList.length === 1 ? nodeList[0] : null 2178 ); 2179 return firstNode; 2180 }, 2181 /** 2182 * Add class to node's span tag and to .extraClasses. 2183 * 2184 * @param {string} className class name 2185 * 2186 * @since 2.17 2187 */ 2188 addClass: function (className) { 2189 return this.toggleClass(className, true); 2190 }, 2191 /** 2192 * Append or prepend a node, or append a child node. 2193 * 2194 * This a convenience function that calls addChildren() 2195 * 2196 * @param {NodeData} node node definition 2197 * @param {string} [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child') 2198 * @returns {FancytreeNode} new node 2199 */ 2200 addNode: function (node, mode) { 2201 if (mode === undefined || mode === "over") { 2202 mode = "child"; 2203 } 2204 switch (mode) { 2205 case "after": 2206 return this.getParent().addChildren( 2207 node, 2208 this.getNextSibling() 2209 ); 2210 case "before": 2211 return this.getParent().addChildren(node, this); 2212 case "firstChild": 2213 // Insert before the first child if any 2214 var insertBefore = this.children ? this.children[0] : null; 2215 return this.addChildren(node, insertBefore); 2216 case "child": 2217 case "over": 2218 return this.addChildren(node); 2219 } 2220 _assert(false, "Invalid mode: " + mode); 2221 }, 2222 /**Add child status nodes that indicate 'More...', etc. 2223 * 2224 * This also maintains the node's `partload` property. 2225 * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes. 2226 * @param {string} [mode='child'] 'child'|firstChild' 2227 * @since 2.15 2228 */ 2229 addPagingNode: function (node, mode) { 2230 var i, n; 2231 2232 mode = mode || "child"; 2233 if (node === false) { 2234 for (i = this.children.length - 1; i >= 0; i--) { 2235 n = this.children[i]; 2236 if (n.statusNodeType === "paging") { 2237 this.removeChild(n); 2238 } 2239 } 2240 this.partload = false; 2241 return; 2242 } 2243 node = $.extend( 2244 { 2245 title: this.tree.options.strings.moreData, 2246 statusNodeType: "paging", 2247 icon: false, 2248 }, 2249 node 2250 ); 2251 this.partload = true; 2252 return this.addNode(node, mode); 2253 }, 2254 /** 2255 * Append new node after this. 2256 * 2257 * This a convenience function that calls addNode(node, 'after') 2258 * 2259 * @param {NodeData} node node definition 2260 * @returns {FancytreeNode} new node 2261 */ 2262 appendSibling: function (node) { 2263 return this.addNode(node, "after"); 2264 }, 2265 /** 2266 * (experimental) Apply a modification (or navigation) operation. 2267 * 2268 * @param {string} cmd 2269 * @param {object} [opts] 2270 * @see Fancytree#applyCommand 2271 * @since 2.32 2272 */ 2273 applyCommand: function (cmd, opts) { 2274 return this.tree.applyCommand(cmd, this, opts); 2275 }, 2276 /** 2277 * Modify existing child nodes. 2278 * 2279 * @param {NodePatch} patch 2280 * @returns {$.Promise} 2281 * @see FancytreeNode#addChildren 2282 */ 2283 applyPatch: function (patch) { 2284 // patch [key, null] means 'remove' 2285 if (patch === null) { 2286 this.remove(); 2287 return _getResolvedPromise(this); 2288 } 2289 // TODO: make sure that root node is not collapsed or modified 2290 // copy (most) attributes to node.ATTR or node.data.ATTR 2291 var name, 2292 promise, 2293 v, 2294 IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global 2295 2296 for (name in patch) { 2297 if (_hasProp(patch, name)) { 2298 v = patch[name]; 2299 if (!IGNORE_MAP[name] && !_isFunction(v)) { 2300 if (NODE_ATTR_MAP[name]) { 2301 this[name] = v; 2302 } else { 2303 this.data[name] = v; 2304 } 2305 } 2306 } 2307 } 2308 // Remove and/or create children 2309 if (_hasProp(patch, "children")) { 2310 this.removeChildren(); 2311 if (patch.children) { 2312 // only if not null and not empty list 2313 // TODO: addChildren instead? 2314 this._setChildren(patch.children); 2315 } 2316 // TODO: how can we APPEND or INSERT child nodes? 2317 } 2318 if (this.isVisible()) { 2319 this.renderTitle(); 2320 this.renderStatus(); 2321 } 2322 // Expand collapse (final step, since this may be async) 2323 if (_hasProp(patch, "expanded")) { 2324 promise = this.setExpanded(patch.expanded); 2325 } else { 2326 promise = _getResolvedPromise(this); 2327 } 2328 return promise; 2329 }, 2330 /** Collapse all sibling nodes. 2331 * @returns {$.Promise} 2332 */ 2333 collapseSiblings: function () { 2334 return this.tree._callHook("nodeCollapseSiblings", this); 2335 }, 2336 /** Copy this node as sibling or child of `node`. 2337 * 2338 * @param {FancytreeNode} node source node 2339 * @param {string} [mode=child] 'before' | 'after' | 'child' 2340 * @param {Function} [map] callback function(NodeData, FancytreeNode) that could modify the new node 2341 * @returns {FancytreeNode} new 2342 */ 2343 copyTo: function (node, mode, map) { 2344 return node.addNode(this.toDict(true, map), mode); 2345 }, 2346 /** Count direct and indirect children. 2347 * 2348 * @param {boolean} [deep=true] pass 'false' to only count direct children 2349 * @returns {int} number of child nodes 2350 */ 2351 countChildren: function (deep) { 2352 var cl = this.children, 2353 i, 2354 l, 2355 n; 2356 if (!cl) { 2357 return 0; 2358 } 2359 n = cl.length; 2360 if (deep !== false) { 2361 for (i = 0, l = n; i < l; i++) { 2362 n += cl[i].countChildren(); 2363 } 2364 } 2365 return n; 2366 }, 2367 // TODO: deactivate() 2368 /** Write to browser console if debugLevel >= 4 (prepending node info) 2369 * 2370 * @param {*} msg string or object or array of such 2371 */ 2372 debug: function (msg) { 2373 if (this.tree.options.debugLevel >= 4) { 2374 Array.prototype.unshift.call(arguments, this.toString()); 2375 consoleApply("log", arguments); 2376 } 2377 }, 2378 /** Deprecated. 2379 * @deprecated since 2014-02-16. Use resetLazy() instead. 2380 */ 2381 discard: function () { 2382 this.warn( 2383 "FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead." 2384 ); 2385 return this.resetLazy(); 2386 }, 2387 /** Remove DOM elements for all descendents. May be called on .collapse event 2388 * to keep the DOM small. 2389 * @param {boolean} [includeSelf=false] 2390 */ 2391 discardMarkup: function (includeSelf) { 2392 var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup"; 2393 this.tree._callHook(fn, this); 2394 }, 2395 /** Write error to browser console if debugLevel >= 1 (prepending tree info) 2396 * 2397 * @param {*} msg string or object or array of such 2398 */ 2399 error: function (msg) { 2400 if (this.tree.options.debugLevel >= 1) { 2401 Array.prototype.unshift.call(arguments, this.toString()); 2402 consoleApply("error", arguments); 2403 } 2404 }, 2405 /**Find all nodes that match condition (excluding self). 2406 * 2407 * @param {string | function(node)} match title string to search for, or a 2408 * callback function that returns `true` if a node is matched. 2409 * @returns {FancytreeNode[]} array of nodes (may be empty) 2410 */ 2411 findAll: function (match) { 2412 match = _isFunction(match) ? match : _makeNodeTitleMatcher(match); 2413 var res = []; 2414 this.visit(function (n) { 2415 if (match(n)) { 2416 res.push(n); 2417 } 2418 }); 2419 return res; 2420 }, 2421 /**Find first node that matches condition (excluding self). 2422 * 2423 * @param {string | function(node)} match title string to search for, or a 2424 * callback function that returns `true` if a node is matched. 2425 * @returns {FancytreeNode} matching node or null 2426 * @see FancytreeNode#findAll 2427 */ 2428 findFirst: function (match) { 2429 match = _isFunction(match) ? match : _makeNodeTitleMatcher(match); 2430 var res = null; 2431 this.visit(function (n) { 2432 if (match(n)) { 2433 res = n; 2434 return false; 2435 } 2436 }); 2437 return res; 2438 }, 2439 /** Find a node relative to self. 2440 * 2441 * @param {number|string} where The keyCode that would normally trigger this move, 2442 * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up'). 2443 * @returns {FancytreeNode} 2444 * @since v2.31 2445 */ 2446 findRelatedNode: function (where, includeHidden) { 2447 return this.tree.findRelatedNode(this, where, includeHidden); 2448 }, 2449 /* Apply selection state (internal use only) */ 2450 _changeSelectStatusAttrs: function (state) { 2451 var changed = false, 2452 opts = this.tree.options, 2453 unselectable = FT.evalOption( 2454 "unselectable", 2455 this, 2456 this, 2457 opts, 2458 false 2459 ), 2460 unselectableStatus = FT.evalOption( 2461 "unselectableStatus", 2462 this, 2463 this, 2464 opts, 2465 undefined 2466 ); 2467 2468 if (unselectable && unselectableStatus != null) { 2469 state = unselectableStatus; 2470 } 2471 switch (state) { 2472 case false: 2473 changed = this.selected || this.partsel; 2474 this.selected = false; 2475 this.partsel = false; 2476 break; 2477 case true: 2478 changed = !this.selected || !this.partsel; 2479 this.selected = true; 2480 this.partsel = true; 2481 break; 2482 case undefined: 2483 changed = this.selected || !this.partsel; 2484 this.selected = false; 2485 this.partsel = true; 2486 break; 2487 default: 2488 _assert(false, "invalid state: " + state); 2489 } 2490 // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed); 2491 if (changed) { 2492 this.renderStatus(); 2493 } 2494 return changed; 2495 }, 2496 /** 2497 * Fix selection status, after this node was (de)selected in multi-hier mode. 2498 * This includes (de)selecting all children. 2499 */ 2500 fixSelection3AfterClick: function (callOpts) { 2501 var flag = this.isSelected(); 2502 2503 // this.debug("fixSelection3AfterClick()"); 2504 2505 this.visit(function (node) { 2506 node._changeSelectStatusAttrs(flag); 2507 if (node.radiogroup) { 2508 // #931: don't (de)select this branch 2509 return "skip"; 2510 } 2511 }); 2512 this.fixSelection3FromEndNodes(callOpts); 2513 }, 2514 /** 2515 * Fix selection status for multi-hier mode. 2516 * Only end-nodes are considered to update the descendants branch and parents. 2517 * Should be called after this node has loaded new children or after 2518 * children have been modified using the API. 2519 */ 2520 fixSelection3FromEndNodes: function (callOpts) { 2521 var opts = this.tree.options; 2522 2523 // this.debug("fixSelection3FromEndNodes()"); 2524 _assert(opts.selectMode === 3, "expected selectMode 3"); 2525 2526 // Visit all end nodes and adjust their parent's `selected` and `partsel` 2527 // attributes. Return selection state true, false, or undefined. 2528 function _walk(node) { 2529 var i, 2530 l, 2531 child, 2532 s, 2533 state, 2534 allSelected, 2535 someSelected, 2536 unselIgnore, 2537 unselState, 2538 children = node.children; 2539 2540 if (children && children.length) { 2541 // check all children recursively 2542 allSelected = true; 2543 someSelected = false; 2544 2545 for (i = 0, l = children.length; i < l; i++) { 2546 child = children[i]; 2547 // the selection state of a node is not relevant; we need the end-nodes 2548 s = _walk(child); 2549 // if( !child.unselectableIgnore ) { 2550 unselIgnore = FT.evalOption( 2551 "unselectableIgnore", 2552 child, 2553 child, 2554 opts, 2555 false 2556 ); 2557 if (!unselIgnore) { 2558 if (s !== false) { 2559 someSelected = true; 2560 } 2561 if (s !== true) { 2562 allSelected = false; 2563 } 2564 } 2565 } 2566 // eslint-disable-next-line no-nested-ternary 2567 state = allSelected 2568 ? true 2569 : someSelected 2570 ? undefined 2571 : false; 2572 } else { 2573 // This is an end-node: simply report the status 2574 unselState = FT.evalOption( 2575 "unselectableStatus", 2576 node, 2577 node, 2578 opts, 2579 undefined 2580 ); 2581 state = unselState == null ? !!node.selected : !!unselState; 2582 } 2583 // #939: Keep a `partsel` flag that was explicitly set on a lazy node 2584 if ( 2585 node.partsel && 2586 !node.selected && 2587 node.lazy && 2588 node.children == null 2589 ) { 2590 state = undefined; 2591 } 2592 node._changeSelectStatusAttrs(state); 2593 return state; 2594 } 2595 _walk(this); 2596 2597 // Update parent's state 2598 this.visitParents(function (node) { 2599 var i, 2600 l, 2601 child, 2602 state, 2603 unselIgnore, 2604 unselState, 2605 children = node.children, 2606 allSelected = true, 2607 someSelected = false; 2608 2609 for (i = 0, l = children.length; i < l; i++) { 2610 child = children[i]; 2611 unselIgnore = FT.evalOption( 2612 "unselectableIgnore", 2613 child, 2614 child, 2615 opts, 2616 false 2617 ); 2618 if (!unselIgnore) { 2619 unselState = FT.evalOption( 2620 "unselectableStatus", 2621 child, 2622 child, 2623 opts, 2624 undefined 2625 ); 2626 state = 2627 unselState == null 2628 ? !!child.selected 2629 : !!unselState; 2630 // When fixing the parents, we trust the sibling status (i.e. 2631 // we don't recurse) 2632 if (state || child.partsel) { 2633 someSelected = true; 2634 } 2635 if (!state) { 2636 allSelected = false; 2637 } 2638 } 2639 } 2640 // eslint-disable-next-line no-nested-ternary 2641 state = allSelected ? true : someSelected ? undefined : false; 2642 node._changeSelectStatusAttrs(state); 2643 }); 2644 }, 2645 // TODO: focus() 2646 /** 2647 * Update node data. If dict contains 'children', then also replace 2648 * the hole sub tree. 2649 * @param {NodeData} dict 2650 * 2651 * @see FancytreeNode#addChildren 2652 * @see FancytreeNode#applyPatch 2653 */ 2654 fromDict: function (dict) { 2655 // copy all other attributes to this.data.xxx 2656 for (var name in dict) { 2657 if (NODE_ATTR_MAP[name]) { 2658 // node.NAME = dict.NAME 2659 this[name] = dict[name]; 2660 } else if (name === "data") { 2661 // node.data += dict.data 2662 $.extend(this.data, dict.data); 2663 } else if ( 2664 !_isFunction(dict[name]) && 2665 !NONE_NODE_DATA_MAP[name] 2666 ) { 2667 // node.data.NAME = dict.NAME 2668 this.data[name] = dict[name]; 2669 } 2670 } 2671 if (dict.children) { 2672 // recursively set children and render 2673 this.removeChildren(); 2674 this.addChildren(dict.children); 2675 } 2676 this.renderTitle(); 2677 /* 2678 var children = dict.children; 2679 if(children === undefined){ 2680 this.data = $.extend(this.data, dict); 2681 this.render(); 2682 return; 2683 } 2684 dict = $.extend({}, dict); 2685 dict.children = undefined; 2686 this.data = $.extend(this.data, dict); 2687 this.removeChildren(); 2688 this.addChild(children); 2689 */ 2690 }, 2691 /** Return the list of child nodes (undefined for unexpanded lazy nodes). 2692 * @returns {FancytreeNode[] | undefined} 2693 */ 2694 getChildren: function () { 2695 if (this.hasChildren() === undefined) { 2696 // TODO: only required for lazy nodes? 2697 return undefined; // Lazy node: unloaded, currently loading, or load error 2698 } 2699 return this.children; 2700 }, 2701 /** Return the first child node or null. 2702 * @returns {FancytreeNode | null} 2703 */ 2704 getFirstChild: function () { 2705 return this.children ? this.children[0] : null; 2706 }, 2707 /** Return the 0-based child index. 2708 * @returns {int} 2709 */ 2710 getIndex: function () { 2711 // return this.parent.children.indexOf(this); 2712 return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7 2713 }, 2714 /** Return the hierarchical child index (1-based, e.g. '3.2.4'). 2715 * @param {string} [separator="."] 2716 * @param {int} [digits=1] 2717 * @returns {string} 2718 */ 2719 getIndexHier: function (separator, digits) { 2720 separator = separator || "."; 2721 var s, 2722 res = []; 2723 $.each(this.getParentList(false, true), function (i, o) { 2724 s = "" + (o.getIndex() + 1); 2725 if (digits) { 2726 // prepend leading zeroes 2727 s = ("0000000" + s).substr(-digits); 2728 } 2729 res.push(s); 2730 }); 2731 return res.join(separator); 2732 }, 2733 /** Return the parent keys separated by options.keyPathSeparator, e.g. "/id_1/id_17/id_32". 2734 * 2735 * (Unlike `node.getPath()`, this method prepends a "/" and inverts the first argument.) 2736 * 2737 * @see FancytreeNode#getPath 2738 * @param {boolean} [excludeSelf=false] 2739 * @returns {string} 2740 */ 2741 getKeyPath: function (excludeSelf) { 2742 var sep = this.tree.options.keyPathSeparator; 2743 2744 return sep + this.getPath(!excludeSelf, "key", sep); 2745 }, 2746 /** Return the last child of this node or null. 2747 * @returns {FancytreeNode | null} 2748 */ 2749 getLastChild: function () { 2750 return this.children 2751 ? this.children[this.children.length - 1] 2752 : null; 2753 }, 2754 /** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... . 2755 * @returns {int} 2756 */ 2757 getLevel: function () { 2758 var level = 0, 2759 dtn = this.parent; 2760 while (dtn) { 2761 level++; 2762 dtn = dtn.parent; 2763 } 2764 return level; 2765 }, 2766 /** Return the successor node (under the same parent) or null. 2767 * @returns {FancytreeNode | null} 2768 */ 2769 getNextSibling: function () { 2770 // TODO: use indexOf, if available: (not in IE6) 2771 if (this.parent) { 2772 var i, 2773 l, 2774 ac = this.parent.children; 2775 2776 for (i = 0, l = ac.length - 1; i < l; i++) { 2777 // up to length-2, so next(last) = null 2778 if (ac[i] === this) { 2779 return ac[i + 1]; 2780 } 2781 } 2782 } 2783 return null; 2784 }, 2785 /** Return the parent node (null for the system root node). 2786 * @returns {FancytreeNode | null} 2787 */ 2788 getParent: function () { 2789 // TODO: return null for top-level nodes? 2790 return this.parent; 2791 }, 2792 /** Return an array of all parent nodes (top-down). 2793 * @param {boolean} [includeRoot=false] Include the invisible system root node. 2794 * @param {boolean} [includeSelf=false] Include the node itself. 2795 * @returns {FancytreeNode[]} 2796 */ 2797 getParentList: function (includeRoot, includeSelf) { 2798 var l = [], 2799 dtn = includeSelf ? this : this.parent; 2800 while (dtn) { 2801 if (includeRoot || dtn.parent) { 2802 l.unshift(dtn); 2803 } 2804 dtn = dtn.parent; 2805 } 2806 return l; 2807 }, 2808 /** Return a string representing the hierachical node path, e.g. "a/b/c". 2809 * @param {boolean} [includeSelf=true] 2810 * @param {string | function} [part="title"] node property name or callback 2811 * @param {string} [separator="/"] 2812 * @returns {string} 2813 * @since v2.31 2814 */ 2815 getPath: function (includeSelf, part, separator) { 2816 includeSelf = includeSelf !== false; 2817 part = part || "title"; 2818 separator = separator || "/"; 2819 2820 var val, 2821 path = [], 2822 isFunc = _isFunction(part); 2823 2824 this.visitParents(function (n) { 2825 if (n.parent) { 2826 val = isFunc ? part(n) : n[part]; 2827 path.unshift(val); 2828 } 2829 }, includeSelf); 2830 return path.join(separator); 2831 }, 2832 /** Return the predecessor node (under the same parent) or null. 2833 * @returns {FancytreeNode | null} 2834 */ 2835 getPrevSibling: function () { 2836 if (this.parent) { 2837 var i, 2838 l, 2839 ac = this.parent.children; 2840 2841 for (i = 1, l = ac.length; i < l; i++) { 2842 // start with 1, so prev(first) = null 2843 if (ac[i] === this) { 2844 return ac[i - 1]; 2845 } 2846 } 2847 } 2848 return null; 2849 }, 2850 /** 2851 * Return an array of selected descendant nodes. 2852 * @param {boolean} [stopOnParents=false] only return the topmost selected 2853 * node (useful with selectMode 3) 2854 * @returns {FancytreeNode[]} 2855 */ 2856 getSelectedNodes: function (stopOnParents) { 2857 var nodeList = []; 2858 this.visit(function (node) { 2859 if (node.selected) { 2860 nodeList.push(node); 2861 if (stopOnParents === true) { 2862 return "skip"; // stop processing this branch 2863 } 2864 } 2865 }); 2866 return nodeList; 2867 }, 2868 /** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded). 2869 * @returns {boolean | undefined} 2870 */ 2871 hasChildren: function () { 2872 if (this.lazy) { 2873 if (this.children == null) { 2874 // null or undefined: Not yet loaded 2875 return undefined; 2876 } else if (this.children.length === 0) { 2877 // Loaded, but response was empty 2878 return false; 2879 } else if ( 2880 this.children.length === 1 && 2881 this.children[0].isStatusNode() 2882 ) { 2883 // Currently loading or load error 2884 return undefined; 2885 } 2886 return true; 2887 } 2888 return !!(this.children && this.children.length); 2889 }, 2890 /** 2891 * Return true if node has `className` defined in .extraClasses. 2892 * 2893 * @param {string} className class name (separate multiple classes by space) 2894 * @returns {boolean} 2895 * 2896 * @since 2.32 2897 */ 2898 hasClass: function (className) { 2899 return ( 2900 (" " + (this.extraClasses || "") + " ").indexOf( 2901 " " + className + " " 2902 ) >= 0 2903 ); 2904 }, 2905 /** Return true if node has keyboard focus. 2906 * @returns {boolean} 2907 */ 2908 hasFocus: function () { 2909 return this.tree.hasFocus() && this.tree.focusNode === this; 2910 }, 2911 /** Write to browser console if debugLevel >= 3 (prepending node info) 2912 * 2913 * @param {*} msg string or object or array of such 2914 */ 2915 info: function (msg) { 2916 if (this.tree.options.debugLevel >= 3) { 2917 Array.prototype.unshift.call(arguments, this.toString()); 2918 consoleApply("info", arguments); 2919 } 2920 }, 2921 /** Return true if node is active (see also FancytreeNode#isSelected). 2922 * @returns {boolean} 2923 */ 2924 isActive: function () { 2925 return this.tree.activeNode === this; 2926 }, 2927 /** Return true if node is vertically below `otherNode`, i.e. rendered in a subsequent row. 2928 * @param {FancytreeNode} otherNode 2929 * @returns {boolean} 2930 * @since 2.28 2931 */ 2932 isBelowOf: function (otherNode) { 2933 return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5); 2934 }, 2935 /** Return true if node is a direct child of otherNode. 2936 * @param {FancytreeNode} otherNode 2937 * @returns {boolean} 2938 */ 2939 isChildOf: function (otherNode) { 2940 return this.parent && this.parent === otherNode; 2941 }, 2942 /** Return true, if node is a direct or indirect sub node of otherNode. 2943 * @param {FancytreeNode} otherNode 2944 * @returns {boolean} 2945 */ 2946 isDescendantOf: function (otherNode) { 2947 if (!otherNode || otherNode.tree !== this.tree) { 2948 return false; 2949 } 2950 var p = this.parent; 2951 while (p) { 2952 if (p === otherNode) { 2953 return true; 2954 } 2955 if (p === p.parent) { 2956 $.error("Recursive parent link: " + p); 2957 } 2958 p = p.parent; 2959 } 2960 return false; 2961 }, 2962 /** Return true if node is expanded. 2963 * @returns {boolean} 2964 */ 2965 isExpanded: function () { 2966 return !!this.expanded; 2967 }, 2968 /** Return true if node is the first node of its parent's children. 2969 * @returns {boolean} 2970 */ 2971 isFirstSibling: function () { 2972 var p = this.parent; 2973 return !p || p.children[0] === this; 2974 }, 2975 /** Return true if node is a folder, i.e. has the node.folder attribute set. 2976 * @returns {boolean} 2977 */ 2978 isFolder: function () { 2979 return !!this.folder; 2980 }, 2981 /** Return true if node is the last node of its parent's children. 2982 * @returns {boolean} 2983 */ 2984 isLastSibling: function () { 2985 var p = this.parent; 2986 return !p || p.children[p.children.length - 1] === this; 2987 }, 2988 /** Return true if node is lazy (even if data was already loaded) 2989 * @returns {boolean} 2990 */ 2991 isLazy: function () { 2992 return !!this.lazy; 2993 }, 2994 /** Return true if node is lazy and loaded. For non-lazy nodes always return true. 2995 * @returns {boolean} 2996 */ 2997 isLoaded: function () { 2998 return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node 2999 }, 3000 /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending. 3001 * @returns {boolean} 3002 */ 3003 isLoading: function () { 3004 return !!this._isLoading; 3005 }, 3006 /* 3007 * @deprecated since v2.4.0: Use isRootNode() instead 3008 */ 3009 isRoot: function () { 3010 return this.isRootNode(); 3011 }, 3012 /** Return true if node is partially selected (tri-state). 3013 * @returns {boolean} 3014 * @since 2.23 3015 */ 3016 isPartsel: function () { 3017 return !this.selected && !!this.partsel; 3018 }, 3019 /** (experimental) Return true if this is partially loaded. 3020 * @returns {boolean} 3021 * @since 2.15 3022 */ 3023 isPartload: function () { 3024 return !!this.partload; 3025 }, 3026 /** Return true if this is the (invisible) system root node. 3027 * @returns {boolean} 3028 * @since 2.4 3029 */ 3030 isRootNode: function () { 3031 return this.tree.rootNode === this; 3032 }, 3033 /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive). 3034 * @returns {boolean} 3035 */ 3036 isSelected: function () { 3037 return !!this.selected; 3038 }, 3039 /** Return true if this node is a temporarily generated system node like 3040 * 'loading', 'paging', or 'error' (node.statusNodeType contains the type). 3041 * @returns {boolean} 3042 */ 3043 isStatusNode: function () { 3044 return !!this.statusNodeType; 3045 }, 3046 /** Return true if this node is a status node of type 'paging'. 3047 * @returns {boolean} 3048 * @since 2.15 3049 */ 3050 isPagingNode: function () { 3051 return this.statusNodeType === "paging"; 3052 }, 3053 /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node. 3054 * @returns {boolean} 3055 * @since 2.4 3056 */ 3057 isTopLevel: function () { 3058 return this.tree.rootNode === this.parent; 3059 }, 3060 /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false. 3061 * @returns {boolean} 3062 */ 3063 isUndefined: function () { 3064 return this.hasChildren() === undefined; // also checks if the only child is a status node 3065 }, 3066 /** Return true if all parent nodes are expanded. Note: this does not check 3067 * whether the node is scrolled into the visible part of the screen. 3068 * @returns {boolean} 3069 */ 3070 isVisible: function () { 3071 var i, 3072 l, 3073 n, 3074 hasFilter = this.tree.enableFilter, 3075 parents = this.getParentList(false, false); 3076 3077 // TODO: check $(n.span).is(":visible") 3078 // i.e. return false for nodes (but not parents) that are hidden 3079 // by a filter 3080 if (hasFilter && !this.match && !this.subMatchCount) { 3081 // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" ); 3082 return false; 3083 } 3084 3085 for (i = 0, l = parents.length; i < l; i++) { 3086 n = parents[i]; 3087 3088 if (!n.expanded) { 3089 // this.debug("isVisible: HIDDEN (parent collapsed)"); 3090 return false; 3091 } 3092 // if (hasFilter && !n.match && !n.subMatchCount) { 3093 // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")"); 3094 // return false; 3095 // } 3096 } 3097 // this.debug("isVisible: VISIBLE"); 3098 return true; 3099 }, 3100 /** Deprecated. 3101 * @deprecated since 2014-02-16: use load() instead. 3102 */ 3103 lazyLoad: function (discard) { 3104 $.error( 3105 "FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead." 3106 ); 3107 }, 3108 /** 3109 * Load all children of a lazy node if neccessary. The <i>expanded</i> state is maintained. 3110 * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded. 3111 * @returns {$.Promise} 3112 */ 3113 load: function (forceReload) { 3114 var res, 3115 source, 3116 self = this, 3117 wasExpanded = this.isExpanded(); 3118 3119 _assert(this.isLazy(), "load() requires a lazy node"); 3120 // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" ); 3121 if (!forceReload && !this.isUndefined()) { 3122 return _getResolvedPromise(this); 3123 } 3124 if (this.isLoaded()) { 3125 this.resetLazy(); // also collapses 3126 } 3127 // This method is also called by setExpanded() and loadKeyPath(), so we 3128 // have to avoid recursion. 3129 source = this.tree._triggerNodeEvent("lazyLoad", this); 3130 if (source === false) { 3131 // #69 3132 return _getResolvedPromise(this); 3133 } 3134 _assert( 3135 typeof source !== "boolean", 3136 "lazyLoad event must return source in data.result" 3137 ); 3138 res = this.tree._callHook("nodeLoadChildren", this, source); 3139 if (wasExpanded) { 3140 this.expanded = true; 3141 res.always(function () { 3142 self.render(); 3143 }); 3144 } else { 3145 res.always(function () { 3146 self.renderStatus(); // fix expander icon to 'loaded' 3147 }); 3148 } 3149 return res; 3150 }, 3151 /** Expand all parents and optionally scroll into visible area as neccessary. 3152 * Promise is resolved, when lazy loading and animations are done. 3153 * @param {object} [opts] passed to `setExpanded()`. 3154 * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} 3155 * @returns {$.Promise} 3156 */ 3157 makeVisible: function (opts) { 3158 var i, 3159 self = this, 3160 deferreds = [], 3161 dfd = new $.Deferred(), 3162 parents = this.getParentList(false, false), 3163 len = parents.length, 3164 effects = !(opts && opts.noAnimation === true), 3165 scroll = !(opts && opts.scrollIntoView === false); 3166 3167 // Expand bottom-up, so only the top node is animated 3168 for (i = len - 1; i >= 0; i--) { 3169 // self.debug("pushexpand" + parents[i]); 3170 deferreds.push(parents[i].setExpanded(true, opts)); 3171 } 3172 $.when.apply($, deferreds).done(function () { 3173 // All expands have finished 3174 // self.debug("expand DONE", scroll); 3175 if (scroll) { 3176 self.scrollIntoView(effects).done(function () { 3177 // self.debug("scroll DONE"); 3178 dfd.resolve(); 3179 }); 3180 } else { 3181 dfd.resolve(); 3182 } 3183 }); 3184 return dfd.promise(); 3185 }, 3186 /** Move this node to targetNode. 3187 * @param {FancytreeNode} targetNode 3188 * @param {string} mode <pre> 3189 * 'child': append this node as last child of targetNode. 3190 * This is the default. To be compatble with the D'n'd 3191 * hitMode, we also accept 'over'. 3192 * 'firstChild': add this node as first child of targetNode. 3193 * 'before': add this node as sibling before targetNode. 3194 * 'after': add this node as sibling after targetNode.</pre> 3195 * @param {function} [map] optional callback(FancytreeNode) to allow modifcations 3196 */ 3197 moveTo: function (targetNode, mode, map) { 3198 if (mode === undefined || mode === "over") { 3199 mode = "child"; 3200 } else if (mode === "firstChild") { 3201 if (targetNode.children && targetNode.children.length) { 3202 mode = "before"; 3203 targetNode = targetNode.children[0]; 3204 } else { 3205 mode = "child"; 3206 } 3207 } 3208 var pos, 3209 tree = this.tree, 3210 prevParent = this.parent, 3211 targetParent = 3212 mode === "child" ? targetNode : targetNode.parent; 3213 3214 if (this === targetNode) { 3215 return; 3216 } else if (!this.parent) { 3217 $.error("Cannot move system root"); 3218 } else if (targetParent.isDescendantOf(this)) { 3219 $.error("Cannot move a node to its own descendant"); 3220 } 3221 if (targetParent !== prevParent) { 3222 prevParent.triggerModifyChild("remove", this); 3223 } 3224 // Unlink this node from current parent 3225 if (this.parent.children.length === 1) { 3226 if (this.parent === targetParent) { 3227 return; // #258 3228 } 3229 this.parent.children = this.parent.lazy ? [] : null; 3230 this.parent.expanded = false; 3231 } else { 3232 pos = $.inArray(this, this.parent.children); 3233 _assert(pos >= 0, "invalid source parent"); 3234 this.parent.children.splice(pos, 1); 3235 } 3236 // Remove from source DOM parent 3237 // if(this.parent.ul){ 3238 // this.parent.ul.removeChild(this.li); 3239 // } 3240 3241 // Insert this node to target parent's child list 3242 this.parent = targetParent; 3243 if (targetParent.hasChildren()) { 3244 switch (mode) { 3245 case "child": 3246 // Append to existing target children 3247 targetParent.children.push(this); 3248 break; 3249 case "before": 3250 // Insert this node before target node 3251 pos = $.inArray(targetNode, targetParent.children); 3252 _assert(pos >= 0, "invalid target parent"); 3253 targetParent.children.splice(pos, 0, this); 3254 break; 3255 case "after": 3256 // Insert this node after target node 3257 pos = $.inArray(targetNode, targetParent.children); 3258 _assert(pos >= 0, "invalid target parent"); 3259 targetParent.children.splice(pos + 1, 0, this); 3260 break; 3261 default: 3262 $.error("Invalid mode " + mode); 3263 } 3264 } else { 3265 targetParent.children = [this]; 3266 } 3267 // Parent has no <ul> tag yet: 3268 // if( !targetParent.ul ) { 3269 // // This is the parent's first child: create UL tag 3270 // // (Hidden, because it will be 3271 // targetParent.ul = document.createElement("ul"); 3272 // targetParent.ul.style.display = "none"; 3273 // targetParent.li.appendChild(targetParent.ul); 3274 // } 3275 // // Issue 319: Add to target DOM parent (only if node was already rendered(expanded)) 3276 // if(this.li){ 3277 // targetParent.ul.appendChild(this.li); 3278 // } 3279 3280 // Let caller modify the nodes 3281 if (map) { 3282 targetNode.visit(map, true); 3283 } 3284 if (targetParent === prevParent) { 3285 targetParent.triggerModifyChild("move", this); 3286 } else { 3287 // prevParent.triggerModifyChild("remove", this); 3288 targetParent.triggerModifyChild("add", this); 3289 } 3290 // Handle cross-tree moves 3291 if (tree !== targetNode.tree) { 3292 // Fix node.tree for all source nodes 3293 // _assert(false, "Cross-tree move is not yet implemented."); 3294 this.warn("Cross-tree moveTo is experimental!"); 3295 this.visit(function (n) { 3296 // TODO: fix selection state and activation, ... 3297 n.tree = targetNode.tree; 3298 }, true); 3299 } 3300 3301 // A collaposed node won't re-render children, so we have to remove it manually 3302 // if( !targetParent.expanded ){ 3303 // prevParent.ul.removeChild(this.li); 3304 // } 3305 tree._callHook("treeStructureChanged", tree, "moveTo"); 3306 3307 // Update HTML markup 3308 if (!prevParent.isDescendantOf(targetParent)) { 3309 prevParent.render(); 3310 } 3311 if ( 3312 !targetParent.isDescendantOf(prevParent) && 3313 targetParent !== prevParent 3314 ) { 3315 targetParent.render(); 3316 } 3317 // TODO: fix selection state 3318 // TODO: fix active state 3319 3320 /* 3321 var tree = this.tree; 3322 var opts = tree.options; 3323 var pers = tree.persistence; 3324 3325 // Always expand, if it's below minExpandLevel 3326 // tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel()); 3327 if ( opts.minExpandLevel >= ftnode.getLevel() ) { 3328 // tree.logDebug ("Force expand for %o", ftnode); 3329 this.bExpanded = true; 3330 } 3331 3332 // In multi-hier mode, update the parents selection state 3333 // DT issue #82: only if not initializing, because the children may not exist yet 3334 // if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing ) 3335 // ftnode._fixSelectionState(); 3336 3337 // In multi-hier mode, update the parents selection state 3338 if( ftnode.bSelected && opts.selectMode==3 ) { 3339 var p = this; 3340 while( p ) { 3341 if( !p.hasSubSel ) 3342 p._setSubSel(true); 3343 p = p.parent; 3344 } 3345 } 3346 // render this node and the new child 3347 if ( tree.bEnableUpdate ) 3348 this.render(); 3349 return ftnode; 3350 */ 3351 }, 3352 /** Set focus relative to this node and optionally activate. 3353 * 3354 * 'left' collapses the node if it is expanded, or move to the parent 3355 * otherwise. 3356 * 'right' expands the node if it is collapsed, or move to the first 3357 * child otherwise. 3358 * 3359 * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. 3360 * (Alternatively the keyCode that would normally trigger this move, 3361 * e.g. `$.ui.keyCode.LEFT` = 'left'. 3362 * @param {boolean} [activate=true] 3363 * @returns {$.Promise} 3364 */ 3365 navigate: function (where, activate) { 3366 var node, 3367 KC = $.ui.keyCode; 3368 3369 // Handle optional expand/collapse action for LEFT/RIGHT 3370 switch (where) { 3371 case "left": 3372 case KC.LEFT: 3373 if (this.expanded) { 3374 return this.setExpanded(false); 3375 } 3376 break; 3377 case "right": 3378 case KC.RIGHT: 3379 if (!this.expanded && (this.children || this.lazy)) { 3380 return this.setExpanded(); 3381 } 3382 break; 3383 } 3384 // Otherwise activate or focus the related node 3385 node = this.findRelatedNode(where); 3386 if (node) { 3387 // setFocus/setActive will scroll later (if autoScroll is specified) 3388 try { 3389 node.makeVisible({ scrollIntoView: false }); 3390 } catch (e) {} // #272 3391 if (activate === false) { 3392 node.setFocus(); 3393 return _getResolvedPromise(); 3394 } 3395 return node.setActive(); 3396 } 3397 this.warn("Could not find related node '" + where + "'."); 3398 return _getResolvedPromise(); 3399 }, 3400 /** 3401 * Remove this node (not allowed for system root). 3402 */ 3403 remove: function () { 3404 return this.parent.removeChild(this); 3405 }, 3406 /** 3407 * Remove childNode from list of direct children. 3408 * @param {FancytreeNode} childNode 3409 */ 3410 removeChild: function (childNode) { 3411 return this.tree._callHook("nodeRemoveChild", this, childNode); 3412 }, 3413 /** 3414 * Remove all child nodes and descendents. This converts the node into a leaf.<br> 3415 * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy() 3416 * in order to trigger lazyLoad on next expand. 3417 */ 3418 removeChildren: function () { 3419 return this.tree._callHook("nodeRemoveChildren", this); 3420 }, 3421 /** 3422 * Remove class from node's span tag and .extraClasses. 3423 * 3424 * @param {string} className class name 3425 * 3426 * @since 2.17 3427 */ 3428 removeClass: function (className) { 3429 return this.toggleClass(className, false); 3430 }, 3431 /** 3432 * This method renders and updates all HTML markup that is required 3433 * to display this node in its current state.<br> 3434 * Note: 3435 * <ul> 3436 * <li>It should only be neccessary to call this method after the node object 3437 * was modified by direct access to its properties, because the common 3438 * API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...) 3439 * already handle this. 3440 * <li> {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus} 3441 * are implied. If changes are more local, calling only renderTitle() or 3442 * renderStatus() may be sufficient and faster. 3443 * </ul> 3444 * 3445 * @param {boolean} [force=false] re-render, even if html markup was already created 3446 * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed 3447 */ 3448 render: function (force, deep) { 3449 return this.tree._callHook("nodeRender", this, force, deep); 3450 }, 3451 /** Create HTML markup for the node's outer `<span>` (expander, checkbox, icon, and title). 3452 * Implies {@link FancytreeNode#renderStatus}. 3453 * @see Fancytree_Hooks#nodeRenderTitle 3454 */ 3455 renderTitle: function () { 3456 return this.tree._callHook("nodeRenderTitle", this); 3457 }, 3458 /** Update element's CSS classes according to node state. 3459 * @see Fancytree_Hooks#nodeRenderStatus 3460 */ 3461 renderStatus: function () { 3462 return this.tree._callHook("nodeRenderStatus", this); 3463 }, 3464 /** 3465 * (experimental) Replace this node with `source`. 3466 * (Currently only available for paging nodes.) 3467 * @param {NodeData[]} source List of child node definitions 3468 * @since 2.15 3469 */ 3470 replaceWith: function (source) { 3471 var res, 3472 parent = this.parent, 3473 pos = $.inArray(this, parent.children), 3474 self = this; 3475 3476 _assert( 3477 this.isPagingNode(), 3478 "replaceWith() currently requires a paging status node" 3479 ); 3480 3481 res = this.tree._callHook("nodeLoadChildren", this, source); 3482 res.done(function (data) { 3483 // New nodes are currently children of `this`. 3484 var children = self.children; 3485 // Prepend newly loaded child nodes to `this` 3486 // Move new children after self 3487 for (i = 0; i < children.length; i++) { 3488 children[i].parent = parent; 3489 } 3490 parent.children.splice.apply( 3491 parent.children, 3492 [pos + 1, 0].concat(children) 3493 ); 3494 3495 // Remove self 3496 self.children = null; 3497 self.remove(); 3498 // Redraw new nodes 3499 parent.render(); 3500 // TODO: set node.partload = false if this was tha last paging node? 3501 // parent.addPagingNode(false); 3502 }).fail(function () { 3503 self.setExpanded(); 3504 }); 3505 return res; 3506 // $.error("Not implemented: replaceWith()"); 3507 }, 3508 /** 3509 * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad 3510 * event is triggered on next expand. 3511 */ 3512 resetLazy: function () { 3513 this.removeChildren(); 3514 this.expanded = false; 3515 this.lazy = true; 3516 this.children = undefined; 3517 this.renderStatus(); 3518 }, 3519 /** Schedule activity for delayed execution (cancel any pending request). 3520 * scheduleAction('cancel') will only cancel a pending request (if any). 3521 * @param {string} mode 3522 * @param {number} ms 3523 */ 3524 scheduleAction: function (mode, ms) { 3525 if (this.tree.timer) { 3526 clearTimeout(this.tree.timer); 3527 this.tree.debug("clearTimeout(%o)", this.tree.timer); 3528 } 3529 this.tree.timer = null; 3530 var self = this; // required for closures 3531 switch (mode) { 3532 case "cancel": 3533 // Simply made sure that timer was cleared 3534 break; 3535 case "expand": 3536 this.tree.timer = setTimeout(function () { 3537 self.tree.debug("setTimeout: trigger expand"); 3538 self.setExpanded(true); 3539 }, ms); 3540 break; 3541 case "activate": 3542 this.tree.timer = setTimeout(function () { 3543 self.tree.debug("setTimeout: trigger activate"); 3544 self.setActive(true); 3545 }, ms); 3546 break; 3547 default: 3548 $.error("Invalid mode " + mode); 3549 } 3550 // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer); 3551 }, 3552 /** 3553 * 3554 * @param {boolean | PlainObject} [effects=false] animation options. 3555 * @param {object} [options=null] {topNode: null, effects: ..., parent: ...} this node will remain visible in 3556 * any case, even if `this` is outside the scroll pane. 3557 * @returns {$.Promise} 3558 */ 3559 scrollIntoView: function (effects, options) { 3560 if (options !== undefined && _isNode(options)) { 3561 throw Error( 3562 "scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead." 3563 ); 3564 } 3565 // The scroll parent is typically the plain tree's <UL> container. 3566 // For ext-table, we choose the nearest parent that has `position: relative` 3567 // and `overflow` set. 3568 // (This default can be overridden by the local or global `scrollParent` option.) 3569 var opts = $.extend( 3570 { 3571 effects: 3572 effects === true 3573 ? { duration: 200, queue: false } 3574 : effects, 3575 scrollOfs: this.tree.options.scrollOfs, 3576 scrollParent: this.tree.options.scrollParent, 3577 topNode: null, 3578 }, 3579 options 3580 ), 3581 $scrollParent = opts.scrollParent, 3582 $container = this.tree.$container, 3583 overflowY = $container.css("overflow-y"); 3584 3585 if (!$scrollParent) { 3586 if (this.tree.tbody) { 3587 $scrollParent = $container.scrollParent(); 3588 } else if (overflowY === "scroll" || overflowY === "auto") { 3589 $scrollParent = $container; 3590 } else { 3591 // #922 plain tree in a non-fixed-sized UL scrolls inside its parent 3592 $scrollParent = $container.scrollParent(); 3593 } 3594 } else if (!$scrollParent.jquery) { 3595 // Make sure we have a jQuery object 3596 $scrollParent = $($scrollParent); 3597 } 3598 if ( 3599 $scrollParent[0] === document || 3600 $scrollParent[0] === document.body 3601 ) { 3602 // `document` may be returned by $().scrollParent(), if nothing is found, 3603 // but would not work: (see #894) 3604 this.debug( 3605 "scrollIntoView(): normalizing scrollParent to 'window':", 3606 $scrollParent[0] 3607 ); 3608 $scrollParent = $(window); 3609 } 3610 // eslint-disable-next-line one-var 3611 var topNodeY, 3612 nodeY, 3613 horzScrollbarHeight, 3614 containerOffsetTop, 3615 dfd = new $.Deferred(), 3616 self = this, 3617 nodeHeight = $(this.span).height(), 3618 topOfs = opts.scrollOfs.top || 0, 3619 bottomOfs = opts.scrollOfs.bottom || 0, 3620 containerHeight = $scrollParent.height(), 3621 scrollTop = $scrollParent.scrollTop(), 3622 $animateTarget = $scrollParent, 3623 isParentWindow = $scrollParent[0] === window, 3624 topNode = opts.topNode || null, 3625 newScrollTop = null; 3626 3627 // this.debug("scrollIntoView(), scrollTop=" + scrollTop, opts.scrollOfs); 3628 // _assert($(this.span).is(":visible"), "scrollIntoView node is invisible"); // otherwise we cannot calc offsets 3629 if (this.isRootNode() || !this.isVisible()) { 3630 // We cannot calc offsets for hidden elements 3631 this.info("scrollIntoView(): node is invisible."); 3632 return _getResolvedPromise(); 3633 } 3634 if (isParentWindow) { 3635 nodeY = $(this.span).offset().top; 3636 topNodeY = 3637 topNode && topNode.span ? $(topNode.span).offset().top : 0; 3638 $animateTarget = $("html,body"); 3639 } else { 3640 _assert( 3641 $scrollParent[0] !== document && 3642 $scrollParent[0] !== document.body, 3643 "scrollParent should be a simple element or `window`, not document or body." 3644 ); 3645 3646 containerOffsetTop = $scrollParent.offset().top; 3647 nodeY = 3648 $(this.span).offset().top - containerOffsetTop + scrollTop; // relative to scroll parent 3649 topNodeY = topNode 3650 ? $(topNode.span).offset().top - 3651 containerOffsetTop + 3652 scrollTop 3653 : 0; 3654 horzScrollbarHeight = Math.max( 3655 0, 3656 $scrollParent.innerHeight() - $scrollParent[0].clientHeight 3657 ); 3658 containerHeight -= horzScrollbarHeight; 3659 } 3660 3661 // this.debug(" scrollIntoView(), nodeY=" + nodeY + ", containerHeight=" + containerHeight); 3662 if (nodeY < scrollTop + topOfs) { 3663 // Node is above visible container area 3664 newScrollTop = nodeY - topOfs; 3665 // this.debug(" scrollIntoView(), UPPER newScrollTop=" + newScrollTop); 3666 } else if ( 3667 nodeY + nodeHeight > 3668 scrollTop + containerHeight - bottomOfs 3669 ) { 3670 newScrollTop = nodeY + nodeHeight - containerHeight + bottomOfs; 3671 // this.debug(" scrollIntoView(), LOWER newScrollTop=" + newScrollTop); 3672 // If a topNode was passed, make sure that it is never scrolled 3673 // outside the upper border 3674 if (topNode) { 3675 _assert( 3676 topNode.isRootNode() || topNode.isVisible(), 3677 "topNode must be visible" 3678 ); 3679 if (topNodeY < newScrollTop) { 3680 newScrollTop = topNodeY - topOfs; 3681 // this.debug(" scrollIntoView(), TOP newScrollTop=" + newScrollTop); 3682 } 3683 } 3684 } 3685 3686 if (newScrollTop === null) { 3687 dfd.resolveWith(this); 3688 } else { 3689 // this.debug(" scrollIntoView(), SET newScrollTop=" + newScrollTop); 3690 if (opts.effects) { 3691 opts.effects.complete = function () { 3692 dfd.resolveWith(self); 3693 }; 3694 $animateTarget.stop(true).animate( 3695 { 3696 scrollTop: newScrollTop, 3697 }, 3698 opts.effects 3699 ); 3700 } else { 3701 $animateTarget[0].scrollTop = newScrollTop; 3702 dfd.resolveWith(this); 3703 } 3704 } 3705 return dfd.promise(); 3706 }, 3707 3708 /**Activate this node. 3709 * 3710 * The `cell` option requires the ext-table and ext-ariagrid extensions. 3711 * 3712 * @param {boolean} [flag=true] pass false to deactivate 3713 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false, cell: null} 3714 * @returns {$.Promise} 3715 */ 3716 setActive: function (flag, opts) { 3717 return this.tree._callHook("nodeSetActive", this, flag, opts); 3718 }, 3719 /**Expand or collapse this node. Promise is resolved, when lazy loading and animations are done. 3720 * @param {boolean} [flag=true] pass false to collapse 3721 * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false} 3722 * @returns {$.Promise} 3723 */ 3724 setExpanded: function (flag, opts) { 3725 return this.tree._callHook("nodeSetExpanded", this, flag, opts); 3726 }, 3727 /**Set keyboard focus to this node. 3728 * @param {boolean} [flag=true] pass false to blur 3729 * @see Fancytree#setFocus 3730 */ 3731 setFocus: function (flag) { 3732 return this.tree._callHook("nodeSetFocus", this, flag); 3733 }, 3734 /**Select this node, i.e. check the checkbox. 3735 * @param {boolean} [flag=true] pass false to deselect 3736 * @param {object} [opts] additional options. Defaults to {noEvents: false, p 3737 * propagateDown: null, propagateUp: null, callback: null } 3738 */ 3739 setSelected: function (flag, opts) { 3740 return this.tree._callHook("nodeSetSelected", this, flag, opts); 3741 }, 3742 /**Mark a lazy node as 'error', 'loading', 'nodata', or 'ok'. 3743 * @param {string} status 'error'|'loading'|'nodata'|'ok' 3744 * @param {string} [message] 3745 * @param {string} [details] 3746 */ 3747 setStatus: function (status, message, details) { 3748 return this.tree._callHook( 3749 "nodeSetStatus", 3750 this, 3751 status, 3752 message, 3753 details 3754 ); 3755 }, 3756 /**Rename this node. 3757 * @param {string} title 3758 */ 3759 setTitle: function (title) { 3760 this.title = title; 3761 this.renderTitle(); 3762 this.triggerModify("rename"); 3763 }, 3764 /**Sort child list by title. 3765 * @param {function} [cmp] custom compare function(a, b) that returns -1, 0, or 1 (defaults to sort by title). 3766 * @param {boolean} [deep=false] pass true to sort all descendant nodes 3767 */ 3768 sortChildren: function (cmp, deep) { 3769 var i, 3770 l, 3771 cl = this.children; 3772 3773 if (!cl) { 3774 return; 3775 } 3776 cmp = 3777 cmp || 3778 function (a, b) { 3779 var x = a.title.toLowerCase(), 3780 y = b.title.toLowerCase(); 3781 3782 // eslint-disable-next-line no-nested-ternary 3783 return x === y ? 0 : x > y ? 1 : -1; 3784 }; 3785 cl.sort(cmp); 3786 if (deep) { 3787 for (i = 0, l = cl.length; i < l; i++) { 3788 if (cl[i].children) { 3789 cl[i].sortChildren(cmp, "$norender$"); 3790 } 3791 } 3792 } 3793 if (deep !== "$norender$") { 3794 this.render(); 3795 } 3796 this.triggerModifyChild("sort"); 3797 }, 3798 /** Convert node (or whole branch) into a plain object. 3799 * 3800 * The result is compatible with node.addChildren(). 3801 * 3802 * @param {boolean} [recursive=false] include child nodes 3803 * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications. 3804 * Return `false` to ignore this node or `"skip"` to include this node without its children. 3805 * @returns {NodeData} 3806 */ 3807 toDict: function (recursive, callback) { 3808 var i, 3809 l, 3810 node, 3811 res, 3812 dict = {}, 3813 self = this; 3814 3815 $.each(NODE_ATTRS, function (i, a) { 3816 if (self[a] || self[a] === false) { 3817 dict[a] = self[a]; 3818 } 3819 }); 3820 if (!$.isEmptyObject(this.data)) { 3821 dict.data = $.extend({}, this.data); 3822 if ($.isEmptyObject(dict.data)) { 3823 delete dict.data; 3824 } 3825 } 3826 if (callback) { 3827 res = callback(dict, self); 3828 if (res === false) { 3829 return false; // Don't include this node nor its children 3830 } 3831 if (res === "skip") { 3832 recursive = false; // Include this node, but not the children 3833 } 3834 } 3835 if (recursive) { 3836 if (_isArray(this.children)) { 3837 dict.children = []; 3838 for (i = 0, l = this.children.length; i < l; i++) { 3839 node = this.children[i]; 3840 if (!node.isStatusNode()) { 3841 res = node.toDict(true, callback); 3842 if (res !== false) { 3843 dict.children.push(res); 3844 } 3845 } 3846 } 3847 } 3848 } 3849 return dict; 3850 }, 3851 /** 3852 * Set, clear, or toggle class of node's span tag and .extraClasses. 3853 * 3854 * @param {string} className class name (separate multiple classes by space) 3855 * @param {boolean} [flag] true/false to add/remove class. If omitted, class is toggled. 3856 * @returns {boolean} true if a class was added 3857 * 3858 * @since 2.17 3859 */ 3860 toggleClass: function (value, flag) { 3861 var className, 3862 hasClass, 3863 rnotwhite = /\S+/g, 3864 classNames = value.match(rnotwhite) || [], 3865 i = 0, 3866 wasAdded = false, 3867 statusElem = this[this.tree.statusClassPropName], 3868 curClasses = " " + (this.extraClasses || "") + " "; 3869 3870 // this.info("toggleClass('" + value + "', " + flag + ")", curClasses); 3871 // Modify DOM element directly if it already exists 3872 if (statusElem) { 3873 $(statusElem).toggleClass(value, flag); 3874 } 3875 // Modify node.extraClasses to make this change persistent 3876 // Toggle if flag was not passed 3877 while ((className = classNames[i++])) { 3878 hasClass = curClasses.indexOf(" " + className + " ") >= 0; 3879 flag = flag === undefined ? !hasClass : !!flag; 3880 if (flag) { 3881 if (!hasClass) { 3882 curClasses += className + " "; 3883 wasAdded = true; 3884 } 3885 } else { 3886 while (curClasses.indexOf(" " + className + " ") > -1) { 3887 curClasses = curClasses.replace( 3888 " " + className + " ", 3889 " " 3890 ); 3891 } 3892 } 3893 } 3894 this.extraClasses = _trim(curClasses); 3895 // this.info("-> toggleClass('" + value + "', " + flag + "): '" + this.extraClasses + "'"); 3896 return wasAdded; 3897 }, 3898 /** Flip expanded status. */ 3899 toggleExpanded: function () { 3900 return this.tree._callHook("nodeToggleExpanded", this); 3901 }, 3902 /** Flip selection status. */ 3903 toggleSelected: function () { 3904 return this.tree._callHook("nodeToggleSelected", this); 3905 }, 3906 toString: function () { 3907 return "FancytreeNode@" + this.key + "[title='" + this.title + "']"; 3908 // return "<FancytreeNode(#" + this.key + ", '" + this.title + "')>"; 3909 }, 3910 /** 3911 * Trigger `modifyChild` event on a parent to signal that a child was modified. 3912 * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... 3913 * @param {FancytreeNode} [childNode] 3914 * @param {object} [extra] 3915 */ 3916 triggerModifyChild: function (operation, childNode, extra) { 3917 var data, 3918 modifyChild = this.tree.options.modifyChild; 3919 3920 if (modifyChild) { 3921 if (childNode && childNode.parent !== this) { 3922 $.error( 3923 "childNode " + childNode + " is not a child of " + this 3924 ); 3925 } 3926 data = { 3927 node: this, 3928 tree: this.tree, 3929 operation: operation, 3930 childNode: childNode || null, 3931 }; 3932 if (extra) { 3933 $.extend(data, extra); 3934 } 3935 modifyChild({ type: "modifyChild" }, data); 3936 } 3937 }, 3938 /** 3939 * Trigger `modifyChild` event on node.parent(!). 3940 * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ... 3941 * @param {object} [extra] 3942 */ 3943 triggerModify: function (operation, extra) { 3944 this.parent.triggerModifyChild(operation, this, extra); 3945 }, 3946 /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br> 3947 * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br> 3948 * Return false if iteration was stopped. 3949 * 3950 * @param {function} fn the callback function. 3951 * Return false to stop iteration, return "skip" to skip this node and 3952 * its children only. 3953 * @param {boolean} [includeSelf=false] 3954 * @returns {boolean} 3955 */ 3956 visit: function (fn, includeSelf) { 3957 var i, 3958 l, 3959 res = true, 3960 children = this.children; 3961 3962 if (includeSelf === true) { 3963 res = fn(this); 3964 if (res === false || res === "skip") { 3965 return res; 3966 } 3967 } 3968 if (children) { 3969 for (i = 0, l = children.length; i < l; i++) { 3970 res = children[i].visit(fn, true); 3971 if (res === false) { 3972 break; 3973 } 3974 } 3975 } 3976 return res; 3977 }, 3978 /** Call fn(node) for all child nodes and recursively load lazy children.<br> 3979 * <b>Note:</b> If you need this method, you probably should consider to review 3980 * your architecture! Recursivley loading nodes is a perfect way for lazy 3981 * programmers to flood the server with requests ;-) 3982 * 3983 * @param {function} [fn] optional callback function. 3984 * Return false to stop iteration, return "skip" to skip this node and 3985 * its children only. 3986 * @param {boolean} [includeSelf=false] 3987 * @returns {$.Promise} 3988 * @since 2.4 3989 */ 3990 visitAndLoad: function (fn, includeSelf, _recursion) { 3991 var dfd, 3992 res, 3993 loaders, 3994 node = this; 3995 3996 // node.debug("visitAndLoad"); 3997 if (fn && includeSelf === true) { 3998 res = fn(node); 3999 if (res === false || res === "skip") { 4000 return _recursion ? res : _getResolvedPromise(); 4001 } 4002 } 4003 if (!node.children && !node.lazy) { 4004 return _getResolvedPromise(); 4005 } 4006 dfd = new $.Deferred(); 4007 loaders = []; 4008 // node.debug("load()..."); 4009 node.load().done(function () { 4010 // node.debug("load()... done."); 4011 for (var i = 0, l = node.children.length; i < l; i++) { 4012 res = node.children[i].visitAndLoad(fn, true, true); 4013 if (res === false) { 4014 dfd.reject(); 4015 break; 4016 } else if (res !== "skip") { 4017 loaders.push(res); // Add promise to the list 4018 } 4019 } 4020 $.when.apply(this, loaders).then(function () { 4021 dfd.resolve(); 4022 }); 4023 }); 4024 return dfd.promise(); 4025 }, 4026 /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br> 4027 * Stop iteration, if fn() returns false.<br> 4028 * Return false if iteration was stopped. 4029 * 4030 * @param {function} fn the callback function. 4031 * Return false to stop iteration, return "skip" to skip this node and children only. 4032 * @param {boolean} [includeSelf=false] 4033 * @returns {boolean} 4034 */ 4035 visitParents: function (fn, includeSelf) { 4036 // Visit parent nodes (bottom up) 4037 if (includeSelf && fn(this) === false) { 4038 return false; 4039 } 4040 var p = this.parent; 4041 while (p) { 4042 if (fn(p) === false) { 4043 return false; 4044 } 4045 p = p.parent; 4046 } 4047 return true; 4048 }, 4049 /** Call fn(node) for all sibling nodes.<br> 4050 * Stop iteration, if fn() returns false.<br> 4051 * Return false if iteration was stopped. 4052 * 4053 * @param {function} fn the callback function. 4054 * Return false to stop iteration. 4055 * @param {boolean} [includeSelf=false] 4056 * @returns {boolean} 4057 */ 4058 visitSiblings: function (fn, includeSelf) { 4059 var i, 4060 l, 4061 n, 4062 ac = this.parent.children; 4063 4064 for (i = 0, l = ac.length; i < l; i++) { 4065 n = ac[i]; 4066 if (includeSelf || n !== this) { 4067 if (fn(n) === false) { 4068 return false; 4069 } 4070 } 4071 } 4072 return true; 4073 }, 4074 /** Write warning to browser console if debugLevel >= 2 (prepending node info) 4075 * 4076 * @param {*} msg string or object or array of such 4077 */ 4078 warn: function (msg) { 4079 if (this.tree.options.debugLevel >= 2) { 4080 Array.prototype.unshift.call(arguments, this.toString()); 4081 consoleApply("warn", arguments); 4082 } 4083 }, 4084 }; 4085 4086 /****************************************************************************** 4087 * Fancytree 4088 */ 4089 /** 4090 * Construct a new tree object. 4091 * 4092 * @class Fancytree 4093 * @classdesc The controller behind a fancytree. 4094 * This class also contains 'hook methods': see {@link Fancytree_Hooks}. 4095 * 4096 * @param {Widget} widget 4097 * 4098 * @property {string} _id Automatically generated unique tree instance ID, e.g. "1". 4099 * @property {string} _ns Automatically generated unique tree namespace, e.g. ".fancytree-1". 4100 * @property {FancytreeNode} activeNode Currently active node or null. 4101 * @property {string} ariaPropName Property name of FancytreeNode that contains the element which will receive the aria attributes. 4102 * Typically "li", but "tr" for table extension. 4103 * @property {jQueryObject} $container Outer `<ul>` element (or `<table>` element for ext-table). 4104 * @property {jQueryObject} $div A jQuery object containing the element used to instantiate the tree widget (`widget.element`) 4105 * @property {object|array} columns Recommended place to store shared column meta data. @since 2.27 4106 * @property {object} data Metadata, i.e. properties that may be passed to `source` in addition to a children array. 4107 * @property {object} ext Hash of all active plugin instances. 4108 * @property {FancytreeNode} focusNode Currently focused node or null. 4109 * @property {FancytreeNode} lastSelectedNode Used to implement selectMode 1 (single select) 4110 * @property {string} nodeContainerAttrName Property name of FancytreeNode that contains the outer element of single nodes. 4111 * Typically "li", but "tr" for table extension. 4112 * @property {FancytreeOptions} options Current options, i.e. default options + options passed to constructor. 4113 * @property {FancytreeNode} rootNode Invisible system root node. 4114 * @property {string} statusClassPropName Property name of FancytreeNode that contains the element which will receive the status classes. 4115 * Typically "span", but "tr" for table extension. 4116 * @property {object} types Map for shared type specific meta data, used with node.type attribute. @since 2.27 4117 * @property {object} viewport See ext-vieport. @since v2.31 4118 * @property {object} widget Base widget instance. 4119 */ 4120 function Fancytree(widget) { 4121 this.widget = widget; 4122 this.$div = widget.element; 4123 this.options = widget.options; 4124 if (this.options) { 4125 if (this.options.lazyload !== undefined) { 4126 $.error( 4127 "The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead." 4128 ); 4129 } 4130 if (this.options.loaderror !== undefined) { 4131 $.error( 4132 "The 'loaderror' event was renamed since 2014-07-03. Use 'loadError' (with uppercase E) instead." 4133 ); 4134 } 4135 if (this.options.fx !== undefined) { 4136 $.error( 4137 "The 'fx' option was replaced by 'toggleEffect' since 2014-11-30." 4138 ); 4139 } 4140 if (this.options.removeNode !== undefined) { 4141 $.error( 4142 "The 'removeNode' event was replaced by 'modifyChild' since 2.20 (2016-09-10)." 4143 ); 4144 } 4145 } 4146 this.ext = {}; // Active extension instances 4147 this.types = {}; 4148 this.columns = {}; 4149 // allow to init tree.data.foo from <div data-foo=''> 4150 this.data = _getElementDataAsDict(this.$div); 4151 // TODO: use widget.uuid instead? 4152 this._id = "" + (this.options.treeId || $.ui.fancytree._nextId++); 4153 // TODO: use widget.eventNamespace instead? 4154 this._ns = ".fancytree-" + this._id; // append for namespaced events 4155 this.activeNode = null; 4156 this.focusNode = null; 4157 this._hasFocus = null; 4158 this._tempCache = {}; 4159 this._lastMousedownNode = null; 4160 this._enableUpdate = true; 4161 this.lastSelectedNode = null; 4162 this.systemFocusElement = null; 4163 this.lastQuicksearchTerm = ""; 4164 this.lastQuicksearchTime = 0; 4165 this.viewport = null; // ext-grid 4166 4167 this.statusClassPropName = "span"; 4168 this.ariaPropName = "li"; 4169 this.nodeContainerAttrName = "li"; 4170 4171 // Remove previous markup if any 4172 this.$div.find(">ul.fancytree-container").remove(); 4173 4174 // Create a node without parent. 4175 var fakeParent = { tree: this }, 4176 $ul; 4177 this.rootNode = new FancytreeNode(fakeParent, { 4178 title: "root", 4179 key: "root_" + this._id, 4180 children: null, 4181 expanded: true, 4182 }); 4183 this.rootNode.parent = null; 4184 4185 // Create root markup 4186 $ul = $("<ul>", { 4187 id: "ft-id-" + this._id, 4188 class: "ui-fancytree fancytree-container fancytree-plain", 4189 }).appendTo(this.$div); 4190 this.$container = $ul; 4191 this.rootNode.ul = $ul[0]; 4192 4193 if (this.options.debugLevel == null) { 4194 this.options.debugLevel = FT.debugLevel; 4195 } 4196 // // Add container to the TAB chain 4197 // // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant 4198 // // #577: Allow to set tabindex to "0", "-1" and "" 4199 // this.$container.attr("tabindex", this.options.tabindex); 4200 4201 // if( this.options.rtl ) { 4202 // this.$container.attr("DIR", "RTL").addClass("fancytree-rtl"); 4203 // // }else{ 4204 // // this.$container.attr("DIR", null).removeClass("fancytree-rtl"); 4205 // } 4206 // if(this.options.aria){ 4207 // this.$container.attr("role", "tree"); 4208 // if( this.options.selectMode !== 1 ) { 4209 // this.$container.attr("aria-multiselectable", true); 4210 // } 4211 // } 4212 } 4213 4214 Fancytree.prototype = /** @lends Fancytree# */ { 4215 /* Return a context object that can be re-used for _callHook(). 4216 * @param {Fancytree | FancytreeNode | EventData} obj 4217 * @param {Event} originalEvent 4218 * @param {Object} extra 4219 * @returns {EventData} 4220 */ 4221 _makeHookContext: function (obj, originalEvent, extra) { 4222 var ctx, tree; 4223 if (obj.node !== undefined) { 4224 // obj is already a context object 4225 if (originalEvent && obj.originalEvent !== originalEvent) { 4226 $.error("invalid args"); 4227 } 4228 ctx = obj; 4229 } else if (obj.tree) { 4230 // obj is a FancytreeNode 4231 tree = obj.tree; 4232 ctx = { 4233 node: obj, 4234 tree: tree, 4235 widget: tree.widget, 4236 options: tree.widget.options, 4237 originalEvent: originalEvent, 4238 typeInfo: tree.types[obj.type] || {}, 4239 }; 4240 } else if (obj.widget) { 4241 // obj is a Fancytree 4242 ctx = { 4243 node: null, 4244 tree: obj, 4245 widget: obj.widget, 4246 options: obj.widget.options, 4247 originalEvent: originalEvent, 4248 }; 4249 } else { 4250 $.error("invalid args"); 4251 } 4252 if (extra) { 4253 $.extend(ctx, extra); 4254 } 4255 return ctx; 4256 }, 4257 /* Trigger a hook function: funcName(ctx, [...]). 4258 * 4259 * @param {string} funcName 4260 * @param {Fancytree|FancytreeNode|EventData} contextObject 4261 * @param {any} [_extraArgs] optional additional arguments 4262 * @returns {any} 4263 */ 4264 _callHook: function (funcName, contextObject, _extraArgs) { 4265 var ctx = this._makeHookContext(contextObject), 4266 fn = this[funcName], 4267 args = Array.prototype.slice.call(arguments, 2); 4268 if (!_isFunction(fn)) { 4269 $.error("_callHook('" + funcName + "') is not a function"); 4270 } 4271 args.unshift(ctx); 4272 // this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args); 4273 return fn.apply(this, args); 4274 }, 4275 _setExpiringValue: function (key, value, ms) { 4276 this._tempCache[key] = { 4277 value: value, 4278 expire: Date.now() + (+ms || 50), 4279 }; 4280 }, 4281 _getExpiringValue: function (key) { 4282 var entry = this._tempCache[key]; 4283 if (entry && entry.expire > Date.now()) { 4284 return entry.value; 4285 } 4286 delete this._tempCache[key]; 4287 return null; 4288 }, 4289 /* Check if this tree has extension `name` enabled. 4290 * 4291 * @param {string} name name of the required extension 4292 */ 4293 _usesExtension: function (name) { 4294 return $.inArray(name, this.options.extensions) >= 0; 4295 }, 4296 /* Check if current extensions dependencies are met and throw an error if not. 4297 * 4298 * This method may be called inside the `treeInit` hook for custom extensions. 4299 * 4300 * @param {string} name name of the required extension 4301 * @param {boolean} [required=true] pass `false` if the extension is optional, but we want to check for order if it is present 4302 * @param {boolean} [before] `true` if `name` must be included before this, `false` otherwise (use `null` if order doesn't matter) 4303 * @param {string} [message] optional error message (defaults to a descriptve error message) 4304 */ 4305 _requireExtension: function (name, required, before, message) { 4306 if (before != null) { 4307 before = !!before; 4308 } 4309 var thisName = this._local.name, 4310 extList = this.options.extensions, 4311 isBefore = 4312 $.inArray(name, extList) < $.inArray(thisName, extList), 4313 isMissing = required && this.ext[name] == null, 4314 badOrder = !isMissing && before != null && before !== isBefore; 4315 4316 _assert( 4317 thisName && thisName !== name, 4318 "invalid or same name '" + thisName + "' (require yourself?)" 4319 ); 4320 4321 if (isMissing || badOrder) { 4322 if (!message) { 4323 if (isMissing || required) { 4324 message = 4325 "'" + 4326 thisName + 4327 "' extension requires '" + 4328 name + 4329 "'"; 4330 if (badOrder) { 4331 message += 4332 " to be registered " + 4333 (before ? "before" : "after") + 4334 " itself"; 4335 } 4336 } else { 4337 message = 4338 "If used together, `" + 4339 name + 4340 "` must be registered " + 4341 (before ? "before" : "after") + 4342 " `" + 4343 thisName + 4344 "`"; 4345 } 4346 } 4347 $.error(message); 4348 return false; 4349 } 4350 return true; 4351 }, 4352 /** Activate node with a given key and fire focus and activate events. 4353 * 4354 * A previously activated node will be deactivated. 4355 * If activeVisible option is set, all parents will be expanded as necessary. 4356 * Pass key = false, to deactivate the current node only. 4357 * @param {string} key 4358 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false} 4359 * @returns {FancytreeNode} activated node (null, if not found) 4360 */ 4361 activateKey: function (key, opts) { 4362 var node = this.getNodeByKey(key); 4363 if (node) { 4364 node.setActive(true, opts); 4365 } else if (this.activeNode) { 4366 this.activeNode.setActive(false, opts); 4367 } 4368 return node; 4369 }, 4370 /** (experimental) Add child status nodes that indicate 'More...', .... 4371 * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes. 4372 * @param {string} [mode='append'] 'child'|firstChild' 4373 * @since 2.15 4374 */ 4375 addPagingNode: function (node, mode) { 4376 return this.rootNode.addPagingNode(node, mode); 4377 }, 4378 /** 4379 * (experimental) Apply a modification (or navigation) operation. 4380 * 4381 * Valid commands: 4382 * - 'moveUp', 'moveDown' 4383 * - 'indent', 'outdent' 4384 * - 'remove' 4385 * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension) 4386 * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard') 4387 * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate 4388 * 4389 * @param {string} cmd 4390 * @param {FancytreeNode} [node=active_node] 4391 * @param {object} [opts] Currently unused 4392 * 4393 * @since 2.32 4394 */ 4395 applyCommand: function (cmd, node, opts_) { 4396 var // clipboard, 4397 refNode; 4398 // opts = $.extend( 4399 // { setActive: true, clipboard: CLIPBOARD }, 4400 // opts_ 4401 // ); 4402 4403 node = node || this.getActiveNode(); 4404 // clipboard = opts.clipboard; 4405 4406 switch (cmd) { 4407 // Sorting and indentation: 4408 case "moveUp": 4409 refNode = node.getPrevSibling(); 4410 if (refNode) { 4411 node.moveTo(refNode, "before"); 4412 node.setActive(); 4413 } 4414 break; 4415 case "moveDown": 4416 refNode = node.getNextSibling(); 4417 if (refNode) { 4418 node.moveTo(refNode, "after"); 4419 node.setActive(); 4420 } 4421 break; 4422 case "indent": 4423 refNode = node.getPrevSibling(); 4424 if (refNode) { 4425 node.moveTo(refNode, "child"); 4426 refNode.setExpanded(); 4427 node.setActive(); 4428 } 4429 break; 4430 case "outdent": 4431 if (!node.isTopLevel()) { 4432 node.moveTo(node.getParent(), "after"); 4433 node.setActive(); 4434 } 4435 break; 4436 // Remove: 4437 case "remove": 4438 refNode = node.getPrevSibling() || node.getParent(); 4439 node.remove(); 4440 if (refNode) { 4441 refNode.setActive(); 4442 } 4443 break; 4444 // Add, edit (requires ext-edit): 4445 case "addChild": 4446 node.editCreateNode("child", ""); 4447 break; 4448 case "addSibling": 4449 node.editCreateNode("after", ""); 4450 break; 4451 case "rename": 4452 node.editStart(); 4453 break; 4454 // Simple clipboard simulation: 4455 // case "cut": 4456 // clipboard = { mode: cmd, data: node }; 4457 // break; 4458 // case "copy": 4459 // clipboard = { 4460 // mode: cmd, 4461 // data: node.toDict(function(d, n) { 4462 // delete d.key; 4463 // }), 4464 // }; 4465 // break; 4466 // case "clear": 4467 // clipboard = null; 4468 // break; 4469 // case "paste": 4470 // if (clipboard.mode === "cut") { 4471 // // refNode = node.getPrevSibling(); 4472 // clipboard.data.moveTo(node, "child"); 4473 // clipboard.data.setActive(); 4474 // } else if (clipboard.mode === "copy") { 4475 // node.addChildren(clipboard.data).setActive(); 4476 // } 4477 // break; 4478 // Navigation commands: 4479 case "down": 4480 case "first": 4481 case "last": 4482 case "left": 4483 case "parent": 4484 case "right": 4485 case "up": 4486 return node.navigate(cmd); 4487 default: 4488 $.error("Unhandled command: '" + cmd + "'"); 4489 } 4490 }, 4491 /** (experimental) Modify existing data model. 4492 * 4493 * @param {Array} patchList array of [key, NodePatch] arrays 4494 * @returns {$.Promise} resolved, when all patches have been applied 4495 * @see TreePatch 4496 */ 4497 applyPatch: function (patchList) { 4498 var dfd, 4499 i, 4500 p2, 4501 key, 4502 patch, 4503 node, 4504 patchCount = patchList.length, 4505 deferredList = []; 4506 4507 for (i = 0; i < patchCount; i++) { 4508 p2 = patchList[i]; 4509 _assert( 4510 p2.length === 2, 4511 "patchList must be an array of length-2-arrays" 4512 ); 4513 key = p2[0]; 4514 patch = p2[1]; 4515 node = key === null ? this.rootNode : this.getNodeByKey(key); 4516 if (node) { 4517 dfd = new $.Deferred(); 4518 deferredList.push(dfd); 4519 node.applyPatch(patch).always(_makeResolveFunc(dfd, node)); 4520 } else { 4521 this.warn("could not find node with key '" + key + "'"); 4522 } 4523 } 4524 // Return a promise that is resolved, when ALL patches were applied 4525 return $.when.apply($, deferredList).promise(); 4526 }, 4527 /* TODO: implement in dnd extension 4528 cancelDrag: function() { 4529 var dd = $.ui.ddmanager.current; 4530 if(dd){ 4531 dd.cancel(); 4532 } 4533 }, 4534 */ 4535 /** Remove all nodes. 4536 * @since 2.14 4537 */ 4538 clear: function (source) { 4539 this._callHook("treeClear", this); 4540 }, 4541 /** Return the number of nodes. 4542 * @returns {integer} 4543 */ 4544 count: function () { 4545 return this.rootNode.countChildren(); 4546 }, 4547 /** Write to browser console if debugLevel >= 4 (prepending tree name) 4548 * 4549 * @param {*} msg string or object or array of such 4550 */ 4551 debug: function (msg) { 4552 if (this.options.debugLevel >= 4) { 4553 Array.prototype.unshift.call(arguments, this.toString()); 4554 consoleApply("log", arguments); 4555 } 4556 }, 4557 /** Destroy this widget, restore previous markup and cleanup resources. 4558 * 4559 * @since 2.34 4560 */ 4561 destroy: function () { 4562 this.widget.destroy(); 4563 }, 4564 /** Enable (or disable) the tree control. 4565 * 4566 * @param {boolean} [flag=true] pass false to disable 4567 * @since 2.30 4568 */ 4569 enable: function (flag) { 4570 if (flag === false) { 4571 this.widget.disable(); 4572 } else { 4573 this.widget.enable(); 4574 } 4575 }, 4576 /** Temporarily suppress rendering to improve performance on bulk-updates. 4577 * 4578 * @param {boolean} flag 4579 * @returns {boolean} previous status 4580 * @since 2.19 4581 */ 4582 enableUpdate: function (flag) { 4583 flag = flag !== false; 4584 if (!!this._enableUpdate === !!flag) { 4585 return flag; 4586 } 4587 this._enableUpdate = flag; 4588 if (flag) { 4589 this.debug("enableUpdate(true): redraw "); //, this._dirtyRoots); 4590 this._callHook("treeStructureChanged", this, "enableUpdate"); 4591 this.render(); 4592 } else { 4593 // this._dirtyRoots = null; 4594 this.debug("enableUpdate(false)..."); 4595 } 4596 return !flag; // return previous value 4597 }, 4598 /** Write error to browser console if debugLevel >= 1 (prepending tree info) 4599 * 4600 * @param {*} msg string or object or array of such 4601 */ 4602 error: function (msg) { 4603 if (this.options.debugLevel >= 1) { 4604 Array.prototype.unshift.call(arguments, this.toString()); 4605 consoleApply("error", arguments); 4606 } 4607 }, 4608 /** Expand (or collapse) all parent nodes. 4609 * 4610 * This convenience method uses `tree.visit()` and `tree.setExpanded()` 4611 * internally. 4612 * 4613 * @param {boolean} [flag=true] pass false to collapse 4614 * @param {object} [opts] passed to setExpanded() 4615 * @since 2.30 4616 */ 4617 expandAll: function (flag, opts) { 4618 var prev = this.enableUpdate(false); 4619 4620 flag = flag !== false; 4621 this.visit(function (node) { 4622 if ( 4623 node.hasChildren() !== false && 4624 node.isExpanded() !== flag 4625 ) { 4626 node.setExpanded(flag, opts); 4627 } 4628 }); 4629 this.enableUpdate(prev); 4630 }, 4631 /**Find all nodes that matches condition. 4632 * 4633 * @param {string | function(node)} match title string to search for, or a 4634 * callback function that returns `true` if a node is matched. 4635 * @returns {FancytreeNode[]} array of nodes (may be empty) 4636 * @see FancytreeNode#findAll 4637 * @since 2.12 4638 */ 4639 findAll: function (match) { 4640 return this.rootNode.findAll(match); 4641 }, 4642 /**Find first node that matches condition. 4643 * 4644 * @param {string | function(node)} match title string to search for, or a 4645 * callback function that returns `true` if a node is matched. 4646 * @returns {FancytreeNode} matching node or null 4647 * @see FancytreeNode#findFirst 4648 * @since 2.12 4649 */ 4650 findFirst: function (match) { 4651 return this.rootNode.findFirst(match); 4652 }, 4653 /** Find the next visible node that starts with `match`, starting at `startNode` 4654 * and wrap-around at the end. 4655 * 4656 * @param {string|function} match 4657 * @param {FancytreeNode} [startNode] defaults to first node 4658 * @returns {FancytreeNode} matching node or null 4659 */ 4660 findNextNode: function (match, startNode) { 4661 //, visibleOnly) { 4662 var res = null, 4663 firstNode = this.getFirstChild(); 4664 4665 match = 4666 typeof match === "string" 4667 ? _makeNodeTitleStartMatcher(match) 4668 : match; 4669 startNode = startNode || firstNode; 4670 4671 function _checkNode(n) { 4672 // console.log("_check " + n) 4673 if (match(n)) { 4674 res = n; 4675 } 4676 if (res || n === startNode) { 4677 return false; 4678 } 4679 } 4680 this.visitRows(_checkNode, { 4681 start: startNode, 4682 includeSelf: false, 4683 }); 4684 // Wrap around search 4685 if (!res && startNode !== firstNode) { 4686 this.visitRows(_checkNode, { 4687 start: firstNode, 4688 includeSelf: true, 4689 }); 4690 } 4691 return res; 4692 }, 4693 /** Find a node relative to another node. 4694 * 4695 * @param {FancytreeNode} node 4696 * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'. 4697 * (Alternatively the keyCode that would normally trigger this move, 4698 * e.g. `$.ui.keyCode.LEFT` = 'left'. 4699 * @param {boolean} [includeHidden=false] Not yet implemented 4700 * @returns {FancytreeNode|null} 4701 * @since v2.31 4702 */ 4703 findRelatedNode: function (node, where, includeHidden) { 4704 var res = null, 4705 KC = $.ui.keyCode; 4706 4707 switch (where) { 4708 case "parent": 4709 case KC.BACKSPACE: 4710 if (node.parent && node.parent.parent) { 4711 res = node.parent; 4712 } 4713 break; 4714 case "first": 4715 case KC.HOME: 4716 // First visible node 4717 this.visit(function (n) { 4718 if (n.isVisible()) { 4719 res = n; 4720 return false; 4721 } 4722 }); 4723 break; 4724 case "last": 4725 case KC.END: 4726 this.visit(function (n) { 4727 // last visible node 4728 if (n.isVisible()) { 4729 res = n; 4730 } 4731 }); 4732 break; 4733 case "left": 4734 case KC.LEFT: 4735 if (node.expanded) { 4736 node.setExpanded(false); 4737 } else if (node.parent && node.parent.parent) { 4738 res = node.parent; 4739 } 4740 break; 4741 case "right": 4742 case KC.RIGHT: 4743 if (!node.expanded && (node.children || node.lazy)) { 4744 node.setExpanded(); 4745 res = node; 4746 } else if (node.children && node.children.length) { 4747 res = node.children[0]; 4748 } 4749 break; 4750 case "up": 4751 case KC.UP: 4752 this.visitRows( 4753 function (n) { 4754 res = n; 4755 return false; 4756 }, 4757 { start: node, reverse: true, includeSelf: false } 4758 ); 4759 break; 4760 case "down": 4761 case KC.DOWN: 4762 this.visitRows( 4763 function (n) { 4764 res = n; 4765 return false; 4766 }, 4767 { start: node, includeSelf: false } 4768 ); 4769 break; 4770 default: 4771 this.tree.warn("Unknown relation '" + where + "'."); 4772 } 4773 return res; 4774 }, 4775 // TODO: fromDict 4776 /** 4777 * Generate INPUT elements that can be submitted with html forms. 4778 * 4779 * In selectMode 3 only the topmost selected nodes are considered, unless 4780 * `opts.stopOnParents: false` is passed. 4781 * 4782 * @example 4783 * // Generate input elements for active and selected nodes 4784 * tree.generateFormElements(); 4785 * // Generate input elements selected nodes, using a custom `name` attribute 4786 * tree.generateFormElements("cust_sel", false); 4787 * // Generate input elements using a custom filter 4788 * tree.generateFormElements(true, true, { filter: function(node) { 4789 * return node.isSelected() && node.data.yes; 4790 * }}); 4791 * 4792 * @param {boolean | string} [selected=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID[]') 4793 * @param {boolean | string} [active=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID_active') 4794 * @param {object} [opts] default { filter: null, stopOnParents: true } 4795 */ 4796 generateFormElements: function (selected, active, opts) { 4797 opts = opts || {}; 4798 4799 var nodeList, 4800 selectedName = 4801 typeof selected === "string" 4802 ? selected 4803 : "ft_" + this._id + "[]", 4804 activeName = 4805 typeof active === "string" 4806 ? active 4807 : "ft_" + this._id + "_active", 4808 id = "fancytree_result_" + this._id, 4809 $result = $("#" + id), 4810 stopOnParents = 4811 this.options.selectMode === 3 && 4812 opts.stopOnParents !== false; 4813 4814 if ($result.length) { 4815 $result.empty(); 4816 } else { 4817 $result = $("<div>", { 4818 id: id, 4819 }) 4820 .hide() 4821 .insertAfter(this.$container); 4822 } 4823 if (active !== false && this.activeNode) { 4824 $result.append( 4825 $("<input>", { 4826 type: "radio", 4827 name: activeName, 4828 value: this.activeNode.key, 4829 checked: true, 4830 }) 4831 ); 4832 } 4833 function _appender(node) { 4834 $result.append( 4835 $("<input>", { 4836 type: "checkbox", 4837 name: selectedName, 4838 value: node.key, 4839 checked: true, 4840 }) 4841 ); 4842 } 4843 if (opts.filter) { 4844 this.visit(function (node) { 4845 var res = opts.filter(node); 4846 if (res === "skip") { 4847 return res; 4848 } 4849 if (res !== false) { 4850 _appender(node); 4851 } 4852 }); 4853 } else if (selected !== false) { 4854 nodeList = this.getSelectedNodes(stopOnParents); 4855 $.each(nodeList, function (idx, node) { 4856 _appender(node); 4857 }); 4858 } 4859 }, 4860 /** 4861 * Return the currently active node or null. 4862 * @returns {FancytreeNode} 4863 */ 4864 getActiveNode: function () { 4865 return this.activeNode; 4866 }, 4867 /** Return the first top level node if any (not the invisible root node). 4868 * @returns {FancytreeNode | null} 4869 */ 4870 getFirstChild: function () { 4871 return this.rootNode.getFirstChild(); 4872 }, 4873 /** 4874 * Return node that has keyboard focus or null. 4875 * @returns {FancytreeNode} 4876 */ 4877 getFocusNode: function () { 4878 return this.focusNode; 4879 }, 4880 /** 4881 * Return current option value. 4882 * (Note: this is the preferred variant of `$().fancytree("option", "KEY")`) 4883 * 4884 * @param {string} name option name (may contain '.') 4885 * @returns {any} 4886 */ 4887 getOption: function (optionName) { 4888 return this.widget.option(optionName); 4889 }, 4890 /** 4891 * Return node with a given key or null if not found. 4892 * 4893 * @param {string} key 4894 * @param {FancytreeNode} [searchRoot] only search below this node 4895 * @returns {FancytreeNode | null} 4896 */ 4897 getNodeByKey: function (key, searchRoot) { 4898 // Search the DOM by element ID (assuming this is faster than traversing all nodes). 4899 var el, match; 4900 // TODO: use tree.keyMap if available 4901 // TODO: check opts.generateIds === true 4902 if (!searchRoot) { 4903 el = document.getElementById(this.options.idPrefix + key); 4904 if (el) { 4905 return el.ftnode ? el.ftnode : null; 4906 } 4907 } 4908 // Not found in the DOM, but still may be in an unrendered part of tree 4909 searchRoot = searchRoot || this.rootNode; 4910 match = null; 4911 key = "" + key; // Convert to string (#1005) 4912 searchRoot.visit(function (node) { 4913 if (node.key === key) { 4914 match = node; 4915 return false; // Stop iteration 4916 } 4917 }, true); 4918 return match; 4919 }, 4920 /** Return the invisible system root node. 4921 * @returns {FancytreeNode} 4922 */ 4923 getRootNode: function () { 4924 return this.rootNode; 4925 }, 4926 /** 4927 * Return an array of selected nodes. 4928 * 4929 * Note: you cannot send this result via Ajax directly. Instead the 4930 * node object need to be converted to plain objects, for example 4931 * by using `$.map()` and `node.toDict()`. 4932 * @param {boolean} [stopOnParents=false] only return the topmost selected 4933 * node (useful with selectMode 3) 4934 * @returns {FancytreeNode[]} 4935 */ 4936 getSelectedNodes: function (stopOnParents) { 4937 return this.rootNode.getSelectedNodes(stopOnParents); 4938 }, 4939 /** Return true if the tree control has keyboard focus 4940 * @returns {boolean} 4941 */ 4942 hasFocus: function () { 4943 // var ae = document.activeElement, 4944 // hasFocus = !!( 4945 // ae && $(ae).closest(".fancytree-container").length 4946 // ); 4947 4948 // if (hasFocus !== !!this._hasFocus) { 4949 // this.warn( 4950 // "hasFocus(): fix inconsistent container state, now: " + 4951 // hasFocus 4952 // ); 4953 // this._hasFocus = hasFocus; 4954 // this.$container.toggleClass("fancytree-treefocus", hasFocus); 4955 // } 4956 // return hasFocus; 4957 return !!this._hasFocus; 4958 }, 4959 /** Write to browser console if debugLevel >= 3 (prepending tree name) 4960 * @param {*} msg string or object or array of such 4961 */ 4962 info: function (msg) { 4963 if (this.options.debugLevel >= 3) { 4964 Array.prototype.unshift.call(arguments, this.toString()); 4965 consoleApply("info", arguments); 4966 } 4967 }, 4968 /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending. 4969 * @returns {boolean} 4970 * @since 2.32 4971 */ 4972 isLoading: function () { 4973 var res = false; 4974 4975 this.rootNode.visit(function (n) { 4976 // also visit rootNode 4977 if (n._isLoading || n._requestId) { 4978 res = true; 4979 return false; 4980 } 4981 }, true); 4982 return res; 4983 }, 4984 /* 4985 TODO: isInitializing: function() { 4986 return ( this.phase=="init" || this.phase=="postInit" ); 4987 }, 4988 TODO: isReloading: function() { 4989 return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound; 4990 }, 4991 TODO: isUserEvent: function() { 4992 return ( this.phase=="userEvent" ); 4993 }, 4994 */ 4995 4996 /** 4997 * Make sure that a node with a given ID is loaded, by traversing - and 4998 * loading - its parents. This method is meant for lazy hierarchies. 4999 * A callback is executed for every node as we go. 5000 * @example 5001 * // Resolve using node.key: 5002 * tree.loadKeyPath("/_3/_23/_26/_27", function(node, status){ 5003 * if(status === "loaded") { 5004 * console.log("loaded intermediate node " + node); 5005 * }else if(status === "ok") { 5006 * node.activate(); 5007 * } 5008 * }); 5009 * // Use deferred promise: 5010 * tree.loadKeyPath("/_3/_23/_26/_27").progress(function(data){ 5011 * if(data.status === "loaded") { 5012 * console.log("loaded intermediate node " + data.node); 5013 * }else if(data.status === "ok") { 5014 * node.activate(); 5015 * } 5016 * }).done(function(){ 5017 * ... 5018 * }); 5019 * // Custom path segment resolver: 5020 * tree.loadKeyPath("/321/431/21/2", { 5021 * matchKey: function(node, key){ 5022 * return node.data.refKey === key; 5023 * }, 5024 * callback: function(node, status){ 5025 * if(status === "loaded") { 5026 * console.log("loaded intermediate node " + node); 5027 * }else if(status === "ok") { 5028 * node.activate(); 5029 * } 5030 * } 5031 * }); 5032 * @param {string | string[]} keyPathList one or more key paths (e.g. '/3/2_1/7') 5033 * @param {function | object} optsOrCallback callback(node, status) is called for every visited node ('loading', 'loaded', 'ok', 'error'). 5034 * Pass an object to define custom key matchers for the path segments: {callback: function, matchKey: function}. 5035 * @returns {$.Promise} 5036 */ 5037 loadKeyPath: function (keyPathList, optsOrCallback) { 5038 var callback, 5039 i, 5040 path, 5041 self = this, 5042 dfd = new $.Deferred(), 5043 parent = this.getRootNode(), 5044 sep = this.options.keyPathSeparator, 5045 pathSegList = [], 5046 opts = $.extend({}, optsOrCallback); 5047 5048 // Prepare options 5049 if (typeof optsOrCallback === "function") { 5050 callback = optsOrCallback; 5051 } else if (optsOrCallback && optsOrCallback.callback) { 5052 callback = optsOrCallback.callback; 5053 } 5054 opts.callback = function (ctx, node, status) { 5055 if (callback) { 5056 callback.call(ctx, node, status); 5057 } 5058 dfd.notifyWith(ctx, [{ node: node, status: status }]); 5059 }; 5060 if (opts.matchKey == null) { 5061 opts.matchKey = function (node, key) { 5062 return node.key === key; 5063 }; 5064 } 5065 // Convert array of path strings to array of segment arrays 5066 if (!_isArray(keyPathList)) { 5067 keyPathList = [keyPathList]; 5068 } 5069 for (i = 0; i < keyPathList.length; i++) { 5070 path = keyPathList[i]; 5071 // strip leading slash 5072 if (path.charAt(0) === sep) { 5073 path = path.substr(1); 5074 } 5075 // segListMap[path] = { parent: parent, segList: path.split(sep) }; 5076 pathSegList.push(path.split(sep)); 5077 // targetList.push({ parent: parent, segList: path.split(sep)/* , path: path*/}); 5078 } 5079 // The timeout forces async behavior always (even if nodes are all loaded) 5080 // This way a potential progress() event will fire. 5081 setTimeout(function () { 5082 self._loadKeyPathImpl(dfd, opts, parent, pathSegList).done( 5083 function () { 5084 dfd.resolve(); 5085 } 5086 ); 5087 }, 0); 5088 return dfd.promise(); 5089 }, 5090 /* 5091 * Resolve a list of paths, relative to one parent node. 5092 */ 5093 _loadKeyPathImpl: function (dfd, opts, parent, pathSegList) { 5094 var deferredList, 5095 i, 5096 key, 5097 node, 5098 nodeKey, 5099 remain, 5100 remainMap, 5101 tmpParent, 5102 segList, 5103 subDfd, 5104 self = this; 5105 5106 function __findChild(parent, key) { 5107 // console.log("__findChild", key, parent); 5108 var i, 5109 l, 5110 cl = parent.children; 5111 5112 if (cl) { 5113 for (i = 0, l = cl.length; i < l; i++) { 5114 if (opts.matchKey(cl[i], key)) { 5115 return cl[i]; 5116 } 5117 } 5118 } 5119 return null; 5120 } 5121 5122 // console.log("_loadKeyPathImpl, parent=", parent, ", pathSegList=", pathSegList); 5123 5124 // Pass 1: 5125 // Handle all path segments for nodes that are already loaded. 5126 // Collect distinct top-most lazy nodes in a map. 5127 // Note that we can use node.key to de-dupe entries, even if a custom matcher would 5128 // look for other node attributes. 5129 // map[node.key] => {node: node, pathList: [list of remaining rest-paths]} 5130 remainMap = {}; 5131 5132 for (i = 0; i < pathSegList.length; i++) { 5133 segList = pathSegList[i]; 5134 // target = targetList[i]; 5135 5136 // Traverse and pop path segments (i.e. keys), until we hit a lazy, unloaded node 5137 tmpParent = parent; 5138 while (segList.length) { 5139 key = segList.shift(); 5140 node = __findChild(tmpParent, key); 5141 if (!node) { 5142 this.warn( 5143 "loadKeyPath: key not found: " + 5144 key + 5145 " (parent: " + 5146 tmpParent + 5147 ")" 5148 ); 5149 opts.callback(this, key, "error"); 5150 break; 5151 } else if (segList.length === 0) { 5152 opts.callback(this, node, "ok"); 5153 break; 5154 } else if (!node.lazy || node.hasChildren() !== undefined) { 5155 opts.callback(this, node, "loaded"); 5156 tmpParent = node; 5157 } else { 5158 opts.callback(this, node, "loaded"); 5159 key = node.key; //target.segList.join(sep); 5160 if (remainMap[key]) { 5161 remainMap[key].pathSegList.push(segList); 5162 } else { 5163 remainMap[key] = { 5164 parent: node, 5165 pathSegList: [segList], 5166 }; 5167 } 5168 break; 5169 } 5170 } 5171 } 5172 // console.log("_loadKeyPathImpl AFTER pass 1, remainMap=", remainMap); 5173 5174 // Now load all lazy nodes and continue iteration for remaining paths 5175 deferredList = []; 5176 5177 // Avoid jshint warning 'Don't make functions within a loop.': 5178 function __lazyload(dfd, parent, pathSegList) { 5179 // console.log("__lazyload", parent, "pathSegList=", pathSegList); 5180 opts.callback(self, parent, "loading"); 5181 parent 5182 .load() 5183 .done(function () { 5184 self._loadKeyPathImpl 5185 .call(self, dfd, opts, parent, pathSegList) 5186 .always(_makeResolveFunc(dfd, self)); 5187 }) 5188 .fail(function (errMsg) { 5189 self.warn("loadKeyPath: error loading lazy " + parent); 5190 opts.callback(self, node, "error"); 5191 dfd.rejectWith(self); 5192 }); 5193 } 5194 // remainMap contains parent nodes, each with a list of relative sub-paths. 5195 // We start loading all of them now, and pass the the list to each loader. 5196 for (nodeKey in remainMap) { 5197 if (_hasProp(remainMap, nodeKey)) { 5198 remain = remainMap[nodeKey]; 5199 // console.log("for(): remain=", remain, "remainMap=", remainMap); 5200 // key = remain.segList.shift(); 5201 // node = __findChild(remain.parent, key); 5202 // if (node == null) { // #576 5203 // // Issue #576, refactored for v2.27: 5204 // // The root cause was, that sometimes the wrong parent was used here 5205 // // to find the next segment. 5206 // // Falling back to getNodeByKey() was a hack that no longer works if a custom 5207 // // matcher is used, because we cannot assume that a single segment-key is unique 5208 // // throughout the tree. 5209 // self.error("loadKeyPath: error loading child by key '" + key + "' (parent: " + target.parent + ")", target); 5210 // // node = self.getNodeByKey(key); 5211 // continue; 5212 // } 5213 subDfd = new $.Deferred(); 5214 deferredList.push(subDfd); 5215 __lazyload(subDfd, remain.parent, remain.pathSegList); 5216 } 5217 } 5218 // Return a promise that is resolved, when ALL paths were loaded 5219 return $.when.apply($, deferredList).promise(); 5220 }, 5221 /** Re-fire beforeActivate, activate, and (optional) focus events. 5222 * Calling this method in the `init` event, will activate the node that 5223 * was marked 'active' in the source data, and optionally set the keyboard 5224 * focus. 5225 * @param [setFocus=false] 5226 */ 5227 reactivate: function (setFocus) { 5228 var res, 5229 node = this.activeNode; 5230 5231 if (!node) { 5232 return _getResolvedPromise(); 5233 } 5234 this.activeNode = null; // Force re-activating 5235 res = node.setActive(true, { noFocus: true }); 5236 if (setFocus) { 5237 node.setFocus(); 5238 } 5239 return res; 5240 }, 5241 /** Reload tree from source and return a promise. 5242 * @param [source] optional new source (defaults to initial source data) 5243 * @returns {$.Promise} 5244 */ 5245 reload: function (source) { 5246 this._callHook("treeClear", this); 5247 return this._callHook("treeLoad", this, source); 5248 }, 5249 /**Render tree (i.e. create DOM elements for all top-level nodes). 5250 * @param {boolean} [force=false] create DOM elemnts, even if parent is collapsed 5251 * @param {boolean} [deep=false] 5252 */ 5253 render: function (force, deep) { 5254 return this.rootNode.render(force, deep); 5255 }, 5256 /**(De)select all nodes. 5257 * @param {boolean} [flag=true] 5258 * @since 2.28 5259 */ 5260 selectAll: function (flag) { 5261 this.visit(function (node) { 5262 node.setSelected(flag); 5263 }); 5264 }, 5265 // TODO: selectKey: function(key, select) 5266 // TODO: serializeArray: function(stopOnParents) 5267 /** 5268 * @param {boolean} [flag=true] 5269 */ 5270 setFocus: function (flag) { 5271 return this._callHook("treeSetFocus", this, flag); 5272 }, 5273 /** 5274 * Set current option value. 5275 * (Note: this is the preferred variant of `$().fancytree("option", "KEY", VALUE)`) 5276 * @param {string} name option name (may contain '.') 5277 * @param {any} new value 5278 */ 5279 setOption: function (optionName, value) { 5280 return this.widget.option(optionName, value); 5281 }, 5282 /** 5283 * Call console.time() when in debug mode (verbose >= 4). 5284 * 5285 * @param {string} label 5286 */ 5287 debugTime: function (label) { 5288 if (this.options.debugLevel >= 4) { 5289 window.console.time(this + " - " + label); 5290 } 5291 }, 5292 /** 5293 * Call console.timeEnd() when in debug mode (verbose >= 4). 5294 * 5295 * @param {string} label 5296 */ 5297 debugTimeEnd: function (label) { 5298 if (this.options.debugLevel >= 4) { 5299 window.console.timeEnd(this + " - " + label); 5300 } 5301 }, 5302 /** 5303 * Return all nodes as nested list of {@link NodeData}. 5304 * 5305 * @param {boolean} [includeRoot=false] Returns the hidden system root node (and its children) 5306 * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications. 5307 * Return `false` to ignore this node or "skip" to include this node without its children. 5308 * @returns {Array | object} 5309 * @see FancytreeNode#toDict 5310 */ 5311 toDict: function (includeRoot, callback) { 5312 var res = this.rootNode.toDict(true, callback); 5313 return includeRoot ? res : res.children; 5314 }, 5315 /* Implicitly called for string conversions. 5316 * @returns {string} 5317 */ 5318 toString: function () { 5319 return "Fancytree@" + this._id; 5320 // return "<Fancytree(#" + this._id + ")>"; 5321 }, 5322 /* _trigger a widget event with additional node ctx. 5323 * @see EventData 5324 */ 5325 _triggerNodeEvent: function (type, node, originalEvent, extra) { 5326 // this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx); 5327 var ctx = this._makeHookContext(node, originalEvent, extra), 5328 res = this.widget._trigger(type, originalEvent, ctx); 5329 if (res !== false && ctx.result !== undefined) { 5330 return ctx.result; 5331 } 5332 return res; 5333 }, 5334 /* _trigger a widget event with additional tree data. */ 5335 _triggerTreeEvent: function (type, originalEvent, extra) { 5336 // this.debug("_trigger(" + type + ")", ctx); 5337 var ctx = this._makeHookContext(this, originalEvent, extra), 5338 res = this.widget._trigger(type, originalEvent, ctx); 5339 5340 if (res !== false && ctx.result !== undefined) { 5341 return ctx.result; 5342 } 5343 return res; 5344 }, 5345 /** Call fn(node) for all nodes in hierarchical order (depth-first). 5346 * 5347 * @param {function} fn the callback function. 5348 * Return false to stop iteration, return "skip" to skip this node and children only. 5349 * @returns {boolean} false, if the iterator was stopped. 5350 */ 5351 visit: function (fn) { 5352 return this.rootNode.visit(fn, false); 5353 }, 5354 /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br> 5355 * Stop iteration, if fn() returns false.<br> 5356 * Return false if iteration was stopped. 5357 * 5358 * @param {function} fn the callback function. 5359 * Return false to stop iteration, return "skip" to skip this node and children only. 5360 * @param {object} [options] 5361 * Defaults: 5362 * {start: First top node, reverse: false, includeSelf: true, includeHidden: false} 5363 * @returns {boolean} false if iteration was cancelled 5364 * @since 2.28 5365 */ 5366 visitRows: function (fn, opts) { 5367 if (!this.rootNode.hasChildren()) { 5368 return false; 5369 } 5370 if (opts && opts.reverse) { 5371 delete opts.reverse; 5372 return this._visitRowsUp(fn, opts); 5373 } 5374 opts = opts || {}; 5375 5376 var i, 5377 nextIdx, 5378 parent, 5379 res, 5380 siblings, 5381 siblingOfs = 0, 5382 skipFirstNode = opts.includeSelf === false, 5383 includeHidden = !!opts.includeHidden, 5384 checkFilter = !includeHidden && this.enableFilter, 5385 node = opts.start || this.rootNode.children[0]; 5386 5387 parent = node.parent; 5388 while (parent) { 5389 // visit siblings 5390 siblings = parent.children; 5391 nextIdx = siblings.indexOf(node) + siblingOfs; 5392 _assert( 5393 nextIdx >= 0, 5394 "Could not find " + 5395 node + 5396 " in parent's children: " + 5397 parent 5398 ); 5399 5400 for (i = nextIdx; i < siblings.length; i++) { 5401 node = siblings[i]; 5402 if (checkFilter && !node.match && !node.subMatchCount) { 5403 continue; 5404 } 5405 if (!skipFirstNode && fn(node) === false) { 5406 return false; 5407 } 5408 skipFirstNode = false; 5409 // Dive into node's child nodes 5410 if ( 5411 node.children && 5412 node.children.length && 5413 (includeHidden || node.expanded) 5414 ) { 5415 // Disable warning: Functions declared within loops referencing an outer 5416 // scoped variable may lead to confusing semantics: 5417 /*jshint -W083 */ 5418 res = node.visit(function (n) { 5419 if (checkFilter && !n.match && !n.subMatchCount) { 5420 return "skip"; 5421 } 5422 if (fn(n) === false) { 5423 return false; 5424 } 5425 if (!includeHidden && n.children && !n.expanded) { 5426 return "skip"; 5427 } 5428 }, false); 5429 /*jshint +W083 */ 5430 if (res === false) { 5431 return false; 5432 } 5433 } 5434 } 5435 // Visit parent nodes (bottom up) 5436 node = parent; 5437 parent = parent.parent; 5438 siblingOfs = 1; // 5439 } 5440 return true; 5441 }, 5442 /* Call fn(node) for all nodes in vertical order, bottom up. 5443 */ 5444 _visitRowsUp: function (fn, opts) { 5445 var children, 5446 idx, 5447 parent, 5448 includeHidden = !!opts.includeHidden, 5449 node = opts.start || this.rootNode.children[0]; 5450 5451 while (true) { 5452 parent = node.parent; 5453 children = parent.children; 5454 5455 if (children[0] === node) { 5456 // If this is already the first sibling, goto parent 5457 node = parent; 5458 if (!node.parent) { 5459 break; // first node of the tree 5460 } 5461 children = parent.children; 5462 } else { 5463 // Otherwise, goto prev. sibling 5464 idx = children.indexOf(node); 5465 node = children[idx - 1]; 5466 // If the prev. sibling has children, follow down to last descendant 5467 while ( 5468 // See: https://github.com/eslint/eslint/issues/11302 5469 // eslint-disable-next-line no-unmodified-loop-condition 5470 (includeHidden || node.expanded) && 5471 node.children && 5472 node.children.length 5473 ) { 5474 children = node.children; 5475 parent = node; 5476 node = children[children.length - 1]; 5477 } 5478 } 5479 // Skip invisible 5480 if (!includeHidden && !node.isVisible()) { 5481 continue; 5482 } 5483 if (fn(node) === false) { 5484 return false; 5485 } 5486 } 5487 }, 5488 /** Write warning to browser console if debugLevel >= 2 (prepending tree info) 5489 * 5490 * @param {*} msg string or object or array of such 5491 */ 5492 warn: function (msg) { 5493 if (this.options.debugLevel >= 2) { 5494 Array.prototype.unshift.call(arguments, this.toString()); 5495 consoleApply("warn", arguments); 5496 } 5497 }, 5498 }; 5499 5500 /** 5501 * These additional methods of the {@link Fancytree} class are 'hook functions' 5502 * that can be used and overloaded by extensions. 5503 * 5504 * @see [writing extensions](https://github.com/mar10/fancytree/wiki/TutorialExtensions) 5505 * @mixin Fancytree_Hooks 5506 */ 5507 $.extend( 5508 Fancytree.prototype, 5509 /** @lends Fancytree_Hooks# */ 5510 { 5511 /** Default handling for mouse click events. 5512 * 5513 * @param {EventData} ctx 5514 */ 5515 nodeClick: function (ctx) { 5516 var activate, 5517 expand, 5518 // event = ctx.originalEvent, 5519 targetType = ctx.targetType, 5520 node = ctx.node; 5521 5522 // this.debug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which, ctx); 5523 // TODO: use switch 5524 // TODO: make sure clicks on embedded <input> doesn't steal focus (see table sample) 5525 if (targetType === "expander") { 5526 if (node.isLoading()) { 5527 // #495: we probably got a click event while a lazy load is pending. 5528 // The 'expanded' state is not yet set, so 'toggle' would expand 5529 // and trigger lazyLoad again. 5530 // It would be better to allow to collapse/expand the status node 5531 // while loading (instead of ignoring), but that would require some 5532 // more work. 5533 node.debug("Got 2nd click while loading: ignored"); 5534 return; 5535 } 5536 // Clicking the expander icon always expands/collapses 5537 this._callHook("nodeToggleExpanded", ctx); 5538 } else if (targetType === "checkbox") { 5539 // Clicking the checkbox always (de)selects 5540 this._callHook("nodeToggleSelected", ctx); 5541 if (ctx.options.focusOnSelect) { 5542 // #358 5543 this._callHook("nodeSetFocus", ctx, true); 5544 } 5545 } else { 5546 // Honor `clickFolderMode` for 5547 expand = false; 5548 activate = true; 5549 if (node.folder) { 5550 switch (ctx.options.clickFolderMode) { 5551 case 2: // expand only 5552 expand = true; 5553 activate = false; 5554 break; 5555 case 3: // expand and activate 5556 activate = true; 5557 expand = true; //!node.isExpanded(); 5558 break; 5559 // else 1 or 4: just activate 5560 } 5561 } 5562 if (activate) { 5563 this.nodeSetFocus(ctx); 5564 this._callHook("nodeSetActive", ctx, true); 5565 } 5566 if (expand) { 5567 if (!activate) { 5568 // this._callHook("nodeSetFocus", ctx); 5569 } 5570 // this._callHook("nodeSetExpanded", ctx, true); 5571 this._callHook("nodeToggleExpanded", ctx); 5572 } 5573 } 5574 // Make sure that clicks stop, otherwise <a href='#'> jumps to the top 5575 // if(event.target.localName === "a" && event.target.className === "fancytree-title"){ 5576 // event.preventDefault(); 5577 // } 5578 // TODO: return promise? 5579 }, 5580 /** Collapse all other children of same parent. 5581 * 5582 * @param {EventData} ctx 5583 * @param {object} callOpts 5584 */ 5585 nodeCollapseSiblings: function (ctx, callOpts) { 5586 // TODO: return promise? 5587 var ac, 5588 i, 5589 l, 5590 node = ctx.node; 5591 5592 if (node.parent) { 5593 ac = node.parent.children; 5594 for (i = 0, l = ac.length; i < l; i++) { 5595 if (ac[i] !== node && ac[i].expanded) { 5596 this._callHook( 5597 "nodeSetExpanded", 5598 ac[i], 5599 false, 5600 callOpts 5601 ); 5602 } 5603 } 5604 } 5605 }, 5606 /** Default handling for mouse douleclick events. 5607 * @param {EventData} ctx 5608 */ 5609 nodeDblclick: function (ctx) { 5610 // TODO: return promise? 5611 if ( 5612 ctx.targetType === "title" && 5613 ctx.options.clickFolderMode === 4 5614 ) { 5615 // this.nodeSetFocus(ctx); 5616 // this._callHook("nodeSetActive", ctx, true); 5617 this._callHook("nodeToggleExpanded", ctx); 5618 } 5619 // TODO: prevent text selection on dblclicks 5620 if (ctx.targetType === "title") { 5621 ctx.originalEvent.preventDefault(); 5622 } 5623 }, 5624 /** Default handling for mouse keydown events. 5625 * 5626 * NOTE: this may be called with node == null if tree (but no node) has focus. 5627 * @param {EventData} ctx 5628 */ 5629 nodeKeydown: function (ctx) { 5630 // TODO: return promise? 5631 var matchNode, 5632 stamp, 5633 _res, 5634 focusNode, 5635 event = ctx.originalEvent, 5636 node = ctx.node, 5637 tree = ctx.tree, 5638 opts = ctx.options, 5639 which = event.which, 5640 // #909: Use event.key, to get unicode characters. 5641 // We can't use `/\w/.test(key)`, because that would 5642 // only detect plain ascii alpha-numerics. But we still need 5643 // to ignore modifier-only, whitespace, cursor-keys, etc. 5644 key = event.key || String.fromCharCode(which), 5645 specialModifiers = !!( 5646 event.altKey || 5647 event.ctrlKey || 5648 event.metaKey 5649 ), 5650 isAlnum = 5651 !MODIFIERS[which] && 5652 !SPECIAL_KEYCODES[which] && 5653 !specialModifiers, 5654 $target = $(event.target), 5655 handled = true, 5656 activate = !(event.ctrlKey || !opts.autoActivate); 5657 5658 // (node || FT).debug("ftnode.nodeKeydown(" + event.type + "): ftnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which); 5659 // FT.debug( "eventToString(): " + FT.eventToString(event) + ", key='" + key + "', isAlnum: " + isAlnum ); 5660 5661 // Set focus to active (or first node) if no other node has the focus yet 5662 if (!node) { 5663 focusNode = this.getActiveNode() || this.getFirstChild(); 5664 if (focusNode) { 5665 focusNode.setFocus(); 5666 node = ctx.node = this.focusNode; 5667 node.debug("Keydown force focus on active node"); 5668 } 5669 } 5670 5671 if ( 5672 opts.quicksearch && 5673 isAlnum && 5674 !$target.is(":input:enabled") 5675 ) { 5676 // Allow to search for longer streaks if typed in quickly 5677 stamp = Date.now(); 5678 if (stamp - tree.lastQuicksearchTime > 500) { 5679 tree.lastQuicksearchTerm = ""; 5680 } 5681 tree.lastQuicksearchTime = stamp; 5682 tree.lastQuicksearchTerm += key; 5683 // tree.debug("quicksearch find", tree.lastQuicksearchTerm); 5684 matchNode = tree.findNextNode( 5685 tree.lastQuicksearchTerm, 5686 tree.getActiveNode() 5687 ); 5688 if (matchNode) { 5689 matchNode.setActive(); 5690 } 5691 event.preventDefault(); 5692 return; 5693 } 5694 switch (FT.eventToString(event)) { 5695 case "+": 5696 case "=": // 187: '+' @ Chrome, Safari 5697 tree.nodeSetExpanded(ctx, true); 5698 break; 5699 case "-": 5700 tree.nodeSetExpanded(ctx, false); 5701 break; 5702 case "space": 5703 if (node.isPagingNode()) { 5704 tree._triggerNodeEvent("clickPaging", ctx, event); 5705 } else if ( 5706 FT.evalOption("checkbox", node, node, opts, false) 5707 ) { 5708 // #768 5709 tree.nodeToggleSelected(ctx); 5710 } else { 5711 tree.nodeSetActive(ctx, true); 5712 } 5713 break; 5714 case "return": 5715 tree.nodeSetActive(ctx, true); 5716 break; 5717 case "home": 5718 case "end": 5719 case "backspace": 5720 case "left": 5721 case "right": 5722 case "up": 5723 case "down": 5724 _res = node.navigate(event.which, activate); 5725 break; 5726 default: 5727 handled = false; 5728 } 5729 if (handled) { 5730 event.preventDefault(); 5731 } 5732 }, 5733 5734 // /** Default handling for mouse keypress events. */ 5735 // nodeKeypress: function(ctx) { 5736 // var event = ctx.originalEvent; 5737 // }, 5738 5739 // /** Trigger lazyLoad event (async). */ 5740 // nodeLazyLoad: function(ctx) { 5741 // var node = ctx.node; 5742 // if(this._triggerNodeEvent()) 5743 // }, 5744 /** Load child nodes (async). 5745 * 5746 * @param {EventData} ctx 5747 * @param {object[]|object|string|$.Promise|function} source 5748 * @returns {$.Promise} The deferred will be resolved as soon as the (ajax) 5749 * data was rendered. 5750 */ 5751 nodeLoadChildren: function (ctx, source) { 5752 var ajax, 5753 delay, 5754 ajaxDfd = null, 5755 resultDfd, 5756 isAsync = true, 5757 tree = ctx.tree, 5758 node = ctx.node, 5759 nodePrevParent = node.parent, 5760 tag = "nodeLoadChildren", 5761 requestId = Date.now(); 5762 5763 // `source` is a callback: use the returned result instead: 5764 if (_isFunction(source)) { 5765 source = source.call(tree, { type: "source" }, ctx); 5766 _assert( 5767 !_isFunction(source), 5768 "source callback must not return another function" 5769 ); 5770 } 5771 // `source` is already a promise: 5772 if (_isFunction(source.then)) { 5773 // _assert(_isFunction(source.always), "Expected jQuery?"); 5774 ajaxDfd = source; 5775 } else if (source.url) { 5776 // `source` is an Ajax options object 5777 ajax = $.extend({}, ctx.options.ajax, source); 5778 if (ajax.debugDelay) { 5779 // Simulate a slow server 5780 delay = ajax.debugDelay; 5781 delete ajax.debugDelay; // remove debug option 5782 if (_isArray(delay)) { 5783 // random delay range [min..max] 5784 delay = 5785 delay[0] + 5786 Math.random() * (delay[1] - delay[0]); 5787 } 5788 node.warn( 5789 "nodeLoadChildren waiting debugDelay " + 5790 Math.round(delay) + 5791 " ms ..." 5792 ); 5793 ajaxDfd = $.Deferred(function (ajaxDfd) { 5794 setTimeout(function () { 5795 $.ajax(ajax) 5796 .done(function () { 5797 ajaxDfd.resolveWith(this, arguments); 5798 }) 5799 .fail(function () { 5800 ajaxDfd.rejectWith(this, arguments); 5801 }); 5802 }, delay); 5803 }); 5804 } else { 5805 ajaxDfd = $.ajax(ajax); 5806 } 5807 } else if ($.isPlainObject(source) || _isArray(source)) { 5808 // `source` is already a constant dict or list, but we convert 5809 // to a thenable for unified processing. 5810 // 2020-01-03: refactored. 5811 // `ajaxDfd = $.when(source)` would do the trick, but the returned 5812 // promise will resolve async, which broke some tests and 5813 // would probably also break current implementations out there. 5814 // So we mock-up a thenable that resolves synchronously: 5815 ajaxDfd = { 5816 then: function (resolve, reject) { 5817 resolve(source, null, null); 5818 }, 5819 }; 5820 isAsync = false; 5821 } else { 5822 $.error("Invalid source type: " + source); 5823 } 5824 5825 // Check for overlapping requests 5826 if (node._requestId) { 5827 node.warn( 5828 "Recursive load request #" + 5829 requestId + 5830 " while #" + 5831 node._requestId + 5832 " is pending." 5833 ); 5834 node._requestId = requestId; 5835 // node.debug("Send load request #" + requestId); 5836 } 5837 5838 if (isAsync) { 5839 tree.debugTime(tag); 5840 tree.nodeSetStatus(ctx, "loading"); 5841 } 5842 5843 // The async Ajax request has now started... 5844 // Defer the deferred: 5845 // we want to be able to reject invalid responses, even if 5846 // the raw HTTP Ajax XHR resolved as Ok. 5847 // We use the ajaxDfd.then() syntax here, which is compatible with 5848 // jQuery and ECMA6. 5849 // However resultDfd is a jQuery deferred, which is currently the 5850 // expected result type of nodeLoadChildren() 5851 resultDfd = new $.Deferred(); 5852 ajaxDfd.then( 5853 function (data, textStatus, jqXHR) { 5854 // ajaxDfd was resolved, but we reject or resolve resultDfd 5855 // depending on the response data 5856 var errorObj, res; 5857 5858 if ( 5859 (source.dataType === "json" || 5860 source.dataType === "jsonp") && 5861 typeof data === "string" 5862 ) { 5863 $.error( 5864 "Ajax request returned a string (did you get the JSON dataType wrong?)." 5865 ); 5866 } 5867 if (node._requestId && node._requestId > requestId) { 5868 // The expected request time stamp is later than `requestId` 5869 // (which was kept as as closure variable to this handler function) 5870 // node.warn("Ignored load response for obsolete request #" + requestId + " (expected #" + node._requestId + ")"); 5871 resultDfd.rejectWith(this, [ 5872 RECURSIVE_REQUEST_ERROR, 5873 ]); 5874 return; 5875 // } else { 5876 // node.debug("Response returned for load request #" + requestId); 5877 } 5878 if (node.parent === null && nodePrevParent !== null) { 5879 resultDfd.rejectWith(this, [ 5880 INVALID_REQUEST_TARGET_ERROR, 5881 ]); 5882 return; 5883 } 5884 // Allow to adjust the received response data in the `postProcess` event. 5885 if (ctx.options.postProcess) { 5886 // The handler may either 5887 // - modify `ctx.response` in-place (and leave `ctx.result` undefined) 5888 // => res = undefined 5889 // - return a replacement in `ctx.result` 5890 // => res = <new data> 5891 // If res contains an `error` property, an error status is displayed 5892 try { 5893 res = tree._triggerNodeEvent( 5894 "postProcess", 5895 ctx, 5896 ctx.originalEvent, 5897 { 5898 response: data, 5899 error: null, 5900 dataType: source.dataType, 5901 } 5902 ); 5903 if (res.error) { 5904 tree.warn( 5905 "postProcess returned error:", 5906 res 5907 ); 5908 } 5909 } catch (e) { 5910 res = { 5911 error: e, 5912 message: "" + e, 5913 details: "postProcess failed", 5914 }; 5915 } 5916 if (res.error) { 5917 // Either postProcess failed with an exception, or the returned 5918 // result object has an 'error' property attached: 5919 errorObj = $.isPlainObject(res.error) 5920 ? res.error 5921 : { message: res.error }; 5922 errorObj = tree._makeHookContext( 5923 node, 5924 null, 5925 errorObj 5926 ); 5927 resultDfd.rejectWith(this, [errorObj]); 5928 return; 5929 } 5930 if ( 5931 _isArray(res) || 5932 ($.isPlainObject(res) && _isArray(res.children)) 5933 ) { 5934 // Use `ctx.result` if valid 5935 // (otherwise use existing data, which may have been modified in-place) 5936 data = res; 5937 } 5938 } else if ( 5939 data && 5940 _hasProp(data, "d") && 5941 ctx.options.enableAspx 5942 ) { 5943 // Process ASPX WebMethod JSON object inside "d" property 5944 // (only if no postProcess event was defined) 5945 if (ctx.options.enableAspx === 42) { 5946 tree.warn( 5947 "The default for enableAspx will change to `false` in the fututure. " + 5948 "Pass `enableAspx: true` or implement postProcess to silence this warning." 5949 ); 5950 } 5951 data = 5952 typeof data.d === "string" 5953 ? $.parseJSON(data.d) 5954 : data.d; 5955 } 5956 resultDfd.resolveWith(this, [data]); 5957 }, 5958 function (jqXHR, textStatus, errorThrown) { 5959 // ajaxDfd was rejected, so we reject resultDfd as well 5960 var errorObj = tree._makeHookContext(node, null, { 5961 error: jqXHR, 5962 args: Array.prototype.slice.call(arguments), 5963 message: errorThrown, 5964 details: jqXHR.status + ": " + errorThrown, 5965 }); 5966 resultDfd.rejectWith(this, [errorObj]); 5967 } 5968 ); 5969 5970 // The async Ajax request has now started. 5971 // resultDfd will be resolved/rejected after the response arrived, 5972 // was postProcessed, and checked. 5973 // Now we implement the UI update and add the data to the tree. 5974 // We also return this promise to the caller. 5975 resultDfd 5976 .done(function (data) { 5977 tree.nodeSetStatus(ctx, "ok"); 5978 var children, metaData, noDataRes; 5979 5980 if ($.isPlainObject(data)) { 5981 // We got {foo: 'abc', children: [...]} 5982 // Copy extra properties to tree.data.foo 5983 _assert( 5984 node.isRootNode(), 5985 "source may only be an object for root nodes (expecting an array of child objects otherwise)" 5986 ); 5987 _assert( 5988 _isArray(data.children), 5989 "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')" 5990 ); 5991 metaData = data; 5992 children = data.children; 5993 delete metaData.children; 5994 // Copy some attributes to tree.data 5995 $.each(TREE_ATTRS, function (i, attr) { 5996 if (metaData[attr] !== undefined) { 5997 tree[attr] = metaData[attr]; 5998 delete metaData[attr]; 5999 } 6000 }); 6001 // Copy all other attributes to tree.data.NAME 6002 $.extend(tree.data, metaData); 6003 } else { 6004 children = data; 6005 } 6006 _assert( 6007 _isArray(children), 6008 "expected array of children" 6009 ); 6010 node._setChildren(children); 6011 6012 if (tree.options.nodata && children.length === 0) { 6013 if (_isFunction(tree.options.nodata)) { 6014 noDataRes = tree.options.nodata.call( 6015 tree, 6016 { type: "nodata" }, 6017 ctx 6018 ); 6019 } else if ( 6020 tree.options.nodata === true && 6021 node.isRootNode() 6022 ) { 6023 noDataRes = tree.options.strings.noData; 6024 } else if ( 6025 typeof tree.options.nodata === "string" && 6026 node.isRootNode() 6027 ) { 6028 noDataRes = tree.options.nodata; 6029 } 6030 if (noDataRes) { 6031 node.setStatus("nodata", noDataRes); 6032 } 6033 } 6034 // trigger fancytreeloadchildren 6035 tree._triggerNodeEvent("loadChildren", node); 6036 }) 6037 .fail(function (error) { 6038 var ctxErr; 6039 6040 if (error === RECURSIVE_REQUEST_ERROR) { 6041 node.warn( 6042 "Ignored response for obsolete load request #" + 6043 requestId + 6044 " (expected #" + 6045 node._requestId + 6046 ")" 6047 ); 6048 return; 6049 } else if (error === INVALID_REQUEST_TARGET_ERROR) { 6050 node.warn( 6051 "Lazy parent node was removed while loading: discarding response." 6052 ); 6053 return; 6054 } else if (error.node && error.error && error.message) { 6055 // error is already a context object 6056 ctxErr = error; 6057 } else { 6058 ctxErr = tree._makeHookContext(node, null, { 6059 error: error, // it can be jqXHR or any custom error 6060 args: Array.prototype.slice.call(arguments), 6061 message: error 6062 ? error.message || error.toString() 6063 : "", 6064 }); 6065 if (ctxErr.message === "[object Object]") { 6066 ctxErr.message = ""; 6067 } 6068 } 6069 node.warn( 6070 "Load children failed (" + ctxErr.message + ")", 6071 ctxErr 6072 ); 6073 if ( 6074 tree._triggerNodeEvent( 6075 "loadError", 6076 ctxErr, 6077 null 6078 ) !== false 6079 ) { 6080 tree.nodeSetStatus( 6081 ctx, 6082 "error", 6083 ctxErr.message, 6084 ctxErr.details 6085 ); 6086 } 6087 }) 6088 .always(function () { 6089 node._requestId = null; 6090 if (isAsync) { 6091 tree.debugTimeEnd(tag); 6092 } 6093 }); 6094 6095 return resultDfd.promise(); 6096 }, 6097 /** [Not Implemented] */ 6098 nodeLoadKeyPath: function (ctx, keyPathList) { 6099 // TODO: implement and improve 6100 // http://code.google.com/p/dynatree/issues/detail?id=222 6101 }, 6102 /** 6103 * Remove a single direct child of ctx.node. 6104 * @param {EventData} ctx 6105 * @param {FancytreeNode} childNode dircect child of ctx.node 6106 */ 6107 nodeRemoveChild: function (ctx, childNode) { 6108 var idx, 6109 node = ctx.node, 6110 // opts = ctx.options, 6111 subCtx = $.extend({}, ctx, { node: childNode }), 6112 children = node.children; 6113 6114 // FT.debug("nodeRemoveChild()", node.toString(), childNode.toString()); 6115 6116 if (children.length === 1) { 6117 _assert(childNode === children[0], "invalid single child"); 6118 return this.nodeRemoveChildren(ctx); 6119 } 6120 if ( 6121 this.activeNode && 6122 (childNode === this.activeNode || 6123 this.activeNode.isDescendantOf(childNode)) 6124 ) { 6125 this.activeNode.setActive(false); // TODO: don't fire events 6126 } 6127 if ( 6128 this.focusNode && 6129 (childNode === this.focusNode || 6130 this.focusNode.isDescendantOf(childNode)) 6131 ) { 6132 this.focusNode = null; 6133 } 6134 // TODO: persist must take care to clear select and expand cookies 6135 this.nodeRemoveMarkup(subCtx); 6136 this.nodeRemoveChildren(subCtx); 6137 idx = $.inArray(childNode, children); 6138 _assert(idx >= 0, "invalid child"); 6139 // Notify listeners 6140 node.triggerModifyChild("remove", childNode); 6141 // Unlink to support GC 6142 childNode.visit(function (n) { 6143 n.parent = null; 6144 }, true); 6145 this._callHook("treeRegisterNode", this, false, childNode); 6146 // remove from child list 6147 children.splice(idx, 1); 6148 }, 6149 /**Remove HTML markup for all descendents of ctx.node. 6150 * @param {EventData} ctx 6151 */ 6152 nodeRemoveChildMarkup: function (ctx) { 6153 var node = ctx.node; 6154 6155 // FT.debug("nodeRemoveChildMarkup()", node.toString()); 6156 // TODO: Unlink attr.ftnode to support GC 6157 if (node.ul) { 6158 if (node.isRootNode()) { 6159 $(node.ul).empty(); 6160 } else { 6161 $(node.ul).remove(); 6162 node.ul = null; 6163 } 6164 node.visit(function (n) { 6165 n.li = n.ul = null; 6166 }); 6167 } 6168 }, 6169 /**Remove all descendants of ctx.node. 6170 * @param {EventData} ctx 6171 */ 6172 nodeRemoveChildren: function (ctx) { 6173 var //subCtx, 6174 tree = ctx.tree, 6175 node = ctx.node, 6176 children = node.children; 6177 // opts = ctx.options; 6178 6179 // FT.debug("nodeRemoveChildren()", node.toString()); 6180 if (!children) { 6181 return; 6182 } 6183 if (this.activeNode && this.activeNode.isDescendantOf(node)) { 6184 this.activeNode.setActive(false); // TODO: don't fire events 6185 } 6186 if (this.focusNode && this.focusNode.isDescendantOf(node)) { 6187 this.focusNode = null; 6188 } 6189 // TODO: persist must take care to clear select and expand cookies 6190 this.nodeRemoveChildMarkup(ctx); 6191 // Unlink children to support GC 6192 // TODO: also delete this.children (not possible using visit()) 6193 // subCtx = $.extend({}, ctx); 6194 node.triggerModifyChild("remove", null); 6195 node.visit(function (n) { 6196 n.parent = null; 6197 tree._callHook("treeRegisterNode", tree, false, n); 6198 }); 6199 if (node.lazy) { 6200 // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes 6201 node.children = []; 6202 } else { 6203 node.children = null; 6204 } 6205 if (!node.isRootNode()) { 6206 node.expanded = false; // #449, #459 6207 } 6208 this.nodeRenderStatus(ctx); 6209 }, 6210 /**Remove HTML markup for ctx.node and all its descendents. 6211 * @param {EventData} ctx 6212 */ 6213 nodeRemoveMarkup: function (ctx) { 6214 var node = ctx.node; 6215 // FT.debug("nodeRemoveMarkup()", node.toString()); 6216 // TODO: Unlink attr.ftnode to support GC 6217 if (node.li) { 6218 $(node.li).remove(); 6219 node.li = null; 6220 } 6221 this.nodeRemoveChildMarkup(ctx); 6222 }, 6223 /** 6224 * Create `<li><span>..</span> .. </li>` tags for this node. 6225 * 6226 * This method takes care that all HTML markup is created that is required 6227 * to display this node in its current state. 6228 * 6229 * Call this method to create new nodes, or after the strucuture 6230 * was changed (e.g. after moving this node or adding/removing children) 6231 * nodeRenderTitle() and nodeRenderStatus() are implied. 6232 * 6233 * ```html 6234 * <li id='KEY' ftnode=NODE> 6235 * <span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'> 6236 * <span class="fancytree-expander"></span> 6237 * <span class="fancytree-checkbox"></span> // only present in checkbox mode 6238 * <span class="fancytree-icon"></span> 6239 * <a href="#" class="fancytree-title"> Node 1 </a> 6240 * </span> 6241 * <ul> // only present if node has children 6242 * <li id='KEY' ftnode=NODE> child1 ... </li> 6243 * <li id='KEY' ftnode=NODE> child2 ... </li> 6244 * </ul> 6245 * </li> 6246 * ``` 6247 * 6248 * @param {EventData} ctx 6249 * @param {boolean} [force=false] re-render, even if html markup was already created 6250 * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed 6251 * @param {boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later 6252 */ 6253 nodeRender: function (ctx, force, deep, collapsed, _recursive) { 6254 /* This method must take care of all cases where the current data mode 6255 * (i.e. node hierarchy) does not match the current markup. 6256 * 6257 * - node was not yet rendered: 6258 * create markup 6259 * - node was rendered: exit fast 6260 * - children have been added 6261 * - children have been removed 6262 */ 6263 var childLI, 6264 childNode1, 6265 childNode2, 6266 i, 6267 l, 6268 next, 6269 subCtx, 6270 node = ctx.node, 6271 tree = ctx.tree, 6272 opts = ctx.options, 6273 aria = opts.aria, 6274 firstTime = false, 6275 parent = node.parent, 6276 isRootNode = !parent, 6277 children = node.children, 6278 successorLi = null; 6279 // FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString()); 6280 6281 if (tree._enableUpdate === false) { 6282 // tree.debug("no render", tree._enableUpdate); 6283 return; 6284 } 6285 if (!isRootNode && !parent.ul) { 6286 // Calling node.collapse on a deep, unrendered node 6287 return; 6288 } 6289 _assert(isRootNode || parent.ul, "parent UL must exist"); 6290 6291 // Render the node 6292 if (!isRootNode) { 6293 // Discard markup on force-mode, or if it is not linked to parent <ul> 6294 if ( 6295 node.li && 6296 (force || node.li.parentNode !== node.parent.ul) 6297 ) { 6298 if (node.li.parentNode === node.parent.ul) { 6299 // #486: store following node, so we can insert the new markup there later 6300 successorLi = node.li.nextSibling; 6301 } else { 6302 // May happen, when a top-level node was dropped over another 6303 this.debug( 6304 "Unlinking " + 6305 node + 6306 " (must be child of " + 6307 node.parent + 6308 ")" 6309 ); 6310 } 6311 // this.debug("nodeRemoveMarkup..."); 6312 this.nodeRemoveMarkup(ctx); 6313 } 6314 // Create <li><span /> </li> 6315 // node.debug("render..."); 6316 if (node.li) { 6317 // this.nodeRenderTitle(ctx); 6318 this.nodeRenderStatus(ctx); 6319 } else { 6320 // node.debug("render... really"); 6321 firstTime = true; 6322 node.li = document.createElement("li"); 6323 node.li.ftnode = node; 6324 6325 if (node.key && opts.generateIds) { 6326 node.li.id = opts.idPrefix + node.key; 6327 } 6328 node.span = document.createElement("span"); 6329 node.span.className = "fancytree-node"; 6330 if (aria && !node.tr) { 6331 $(node.li).attr("role", "treeitem"); 6332 } 6333 node.li.appendChild(node.span); 6334 6335 // Create inner HTML for the <span> (expander, checkbox, icon, and title) 6336 this.nodeRenderTitle(ctx); 6337 6338 // Allow tweaking and binding, after node was created for the first time 6339 if (opts.createNode) { 6340 opts.createNode.call( 6341 tree, 6342 { type: "createNode" }, 6343 ctx 6344 ); 6345 } 6346 } 6347 // Allow tweaking after node state was rendered 6348 if (opts.renderNode) { 6349 opts.renderNode.call(tree, { type: "renderNode" }, ctx); 6350 } 6351 } 6352 6353 // Visit child nodes 6354 if (children) { 6355 if (isRootNode || node.expanded || deep === true) { 6356 // Create a UL to hold the children 6357 if (!node.ul) { 6358 node.ul = document.createElement("ul"); 6359 if ( 6360 (collapsed === true && !_recursive) || 6361 !node.expanded 6362 ) { 6363 // hide top UL, so we can use an animation to show it later 6364 node.ul.style.display = "none"; 6365 } 6366 if (aria) { 6367 $(node.ul).attr("role", "group"); 6368 } 6369 if (node.li) { 6370 // issue #67 6371 node.li.appendChild(node.ul); 6372 } else { 6373 node.tree.$div.append(node.ul); 6374 } 6375 } 6376 // Add child markup 6377 for (i = 0, l = children.length; i < l; i++) { 6378 subCtx = $.extend({}, ctx, { node: children[i] }); 6379 this.nodeRender(subCtx, force, deep, false, true); 6380 } 6381 // Remove <li> if nodes have moved to another parent 6382 childLI = node.ul.firstChild; 6383 while (childLI) { 6384 childNode2 = childLI.ftnode; 6385 if (childNode2 && childNode2.parent !== node) { 6386 node.debug( 6387 "_fixParent: remove missing " + childNode2, 6388 childLI 6389 ); 6390 next = childLI.nextSibling; 6391 childLI.parentNode.removeChild(childLI); 6392 childLI = next; 6393 } else { 6394 childLI = childLI.nextSibling; 6395 } 6396 } 6397 // Make sure, that <li> order matches node.children order. 6398 childLI = node.ul.firstChild; 6399 for (i = 0, l = children.length - 1; i < l; i++) { 6400 childNode1 = children[i]; 6401 childNode2 = childLI.ftnode; 6402 if (childNode1 === childNode2) { 6403 childLI = childLI.nextSibling; 6404 } else { 6405 // node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2); 6406 node.ul.insertBefore( 6407 childNode1.li, 6408 childNode2.li 6409 ); 6410 } 6411 } 6412 } 6413 } else { 6414 // No children: remove markup if any 6415 if (node.ul) { 6416 // alert("remove child markup for " + node); 6417 this.warn("remove child markup for " + node); 6418 this.nodeRemoveChildMarkup(ctx); 6419 } 6420 } 6421 if (!isRootNode) { 6422 // Update element classes according to node state 6423 // this.nodeRenderStatus(ctx); 6424 // Finally add the whole structure to the DOM, so the browser can render 6425 if (firstTime) { 6426 // #486: successorLi is set, if we re-rendered (i.e. discarded) 6427 // existing markup, which we want to insert at the same position. 6428 // (null is equivalent to append) 6429 // parent.ul.appendChild(node.li); 6430 parent.ul.insertBefore(node.li, successorLi); 6431 } 6432 } 6433 }, 6434 /** Create HTML inside the node's outer `<span>` (i.e. expander, checkbox, 6435 * icon, and title). 6436 * 6437 * nodeRenderStatus() is implied. 6438 * @param {EventData} ctx 6439 * @param {string} [title] optinal new title 6440 */ 6441 nodeRenderTitle: function (ctx, title) { 6442 // set node connector images, links and text 6443 var checkbox, 6444 className, 6445 icon, 6446 nodeTitle, 6447 role, 6448 tabindex, 6449 tooltip, 6450 iconTooltip, 6451 node = ctx.node, 6452 tree = ctx.tree, 6453 opts = ctx.options, 6454 aria = opts.aria, 6455 level = node.getLevel(), 6456 ares = []; 6457 6458 if (title !== undefined) { 6459 node.title = title; 6460 } 6461 if (!node.span || tree._enableUpdate === false) { 6462 // Silently bail out if node was not rendered yet, assuming 6463 // node.render() will be called as the node becomes visible 6464 return; 6465 } 6466 // Connector (expanded, expandable or simple) 6467 role = 6468 aria && node.hasChildren() !== false 6469 ? " role='button'" 6470 : ""; 6471 if (level < opts.minExpandLevel) { 6472 if (!node.lazy) { 6473 node.expanded = true; 6474 } 6475 if (level > 1) { 6476 ares.push( 6477 "<span " + 6478 role + 6479 " class='fancytree-expander fancytree-expander-fixed'></span>" 6480 ); 6481 } 6482 // .. else (i.e. for root level) skip expander/connector alltogether 6483 } else { 6484 ares.push( 6485 "<span " + role + " class='fancytree-expander'></span>" 6486 ); 6487 } 6488 // Checkbox mode 6489 checkbox = FT.evalOption("checkbox", node, node, opts, false); 6490 6491 if (checkbox && !node.isStatusNode()) { 6492 role = aria ? " role='checkbox'" : ""; 6493 className = "fancytree-checkbox"; 6494 if ( 6495 checkbox === "radio" || 6496 (node.parent && node.parent.radiogroup) 6497 ) { 6498 className += " fancytree-radio"; 6499 } 6500 ares.push( 6501 "<span " + role + " class='" + className + "'></span>" 6502 ); 6503 } 6504 // Folder or doctype icon 6505 if (node.data.iconClass !== undefined) { 6506 // 2015-11-16 6507 // Handle / warn about backward compatibility 6508 if (node.icon) { 6509 $.error( 6510 "'iconClass' node option is deprecated since v2.14.0: use 'icon' only instead" 6511 ); 6512 } else { 6513 node.warn( 6514 "'iconClass' node option is deprecated since v2.14.0: use 'icon' instead" 6515 ); 6516 node.icon = node.data.iconClass; 6517 } 6518 } 6519 // If opts.icon is a callback and returns something other than undefined, use that 6520 // else if node.icon is a boolean or string, use that 6521 // else if opts.icon is a boolean or string, use that 6522 // else show standard icon (which may be different for folders or documents) 6523 icon = FT.evalOption("icon", node, node, opts, true); 6524 // if( typeof icon !== "boolean" ) { 6525 // // icon is defined, but not true/false: must be a string 6526 // icon = "" + icon; 6527 // } 6528 if (icon !== false) { 6529 role = aria ? " role='presentation'" : ""; 6530 6531 iconTooltip = FT.evalOption( 6532 "iconTooltip", 6533 node, 6534 node, 6535 opts, 6536 null 6537 ); 6538 iconTooltip = iconTooltip 6539 ? " title='" + _escapeTooltip(iconTooltip) + "'" 6540 : ""; 6541 6542 if (typeof icon === "string") { 6543 if (TEST_IMG.test(icon)) { 6544 // node.icon is an image url. Prepend imagePath 6545 icon = 6546 icon.charAt(0) === "/" 6547 ? icon 6548 : (opts.imagePath || "") + icon; 6549 ares.push( 6550 "<img src='" + 6551 icon + 6552 "' class='fancytree-icon'" + 6553 iconTooltip + 6554 " alt='' />" 6555 ); 6556 } else { 6557 ares.push( 6558 "<span " + 6559 role + 6560 " class='fancytree-custom-icon " + 6561 icon + 6562 "'" + 6563 iconTooltip + 6564 "></span>" 6565 ); 6566 } 6567 } else if (icon.text) { 6568 ares.push( 6569 "<span " + 6570 role + 6571 " class='fancytree-custom-icon " + 6572 (icon.addClass || "") + 6573 "'" + 6574 iconTooltip + 6575 ">" + 6576 FT.escapeHtml(icon.text) + 6577 "</span>" 6578 ); 6579 } else if (icon.html) { 6580 ares.push( 6581 "<span " + 6582 role + 6583 " class='fancytree-custom-icon " + 6584 (icon.addClass || "") + 6585 "'" + 6586 iconTooltip + 6587 ">" + 6588 icon.html + 6589 "</span>" 6590 ); 6591 } else { 6592 // standard icon: theme css will take care of this 6593 ares.push( 6594 "<span " + 6595 role + 6596 " class='fancytree-icon'" + 6597 iconTooltip + 6598 "></span>" 6599 ); 6600 } 6601 } 6602 // Node title 6603 nodeTitle = ""; 6604 if (opts.renderTitle) { 6605 nodeTitle = 6606 opts.renderTitle.call( 6607 tree, 6608 { type: "renderTitle" }, 6609 ctx 6610 ) || ""; 6611 } 6612 if (!nodeTitle) { 6613 tooltip = FT.evalOption("tooltip", node, node, opts, null); 6614 if (tooltip === true) { 6615 tooltip = node.title; 6616 } 6617 // if( node.tooltip ) { 6618 // tooltip = node.tooltip; 6619 // } else if ( opts.tooltip ) { 6620 // tooltip = opts.tooltip === true ? node.title : opts.tooltip.call(tree, node); 6621 // } 6622 tooltip = tooltip 6623 ? " title='" + _escapeTooltip(tooltip) + "'" 6624 : ""; 6625 tabindex = opts.titlesTabbable ? " tabindex='0'" : ""; 6626 6627 nodeTitle = 6628 "<span class='fancytree-title'" + 6629 tooltip + 6630 tabindex + 6631 ">" + 6632 (opts.escapeTitles 6633 ? FT.escapeHtml(node.title) 6634 : node.title) + 6635 "</span>"; 6636 } 6637 ares.push(nodeTitle); 6638 // Note: this will trigger focusout, if node had the focus 6639 //$(node.span).html(ares.join("")); // it will cleanup the jQuery data currently associated with SPAN (if any), but it executes more slowly 6640 node.span.innerHTML = ares.join(""); 6641 // Update CSS classes 6642 this.nodeRenderStatus(ctx); 6643 if (opts.enhanceTitle) { 6644 ctx.$title = $(">span.fancytree-title", node.span); 6645 nodeTitle = 6646 opts.enhanceTitle.call( 6647 tree, 6648 { type: "enhanceTitle" }, 6649 ctx 6650 ) || ""; 6651 } 6652 }, 6653 /** Update element classes according to node state. 6654 * @param {EventData} ctx 6655 */ 6656 nodeRenderStatus: function (ctx) { 6657 // Set classes for current status 6658 var $ariaElem, 6659 node = ctx.node, 6660 tree = ctx.tree, 6661 opts = ctx.options, 6662 // nodeContainer = node[tree.nodeContainerAttrName], 6663 hasChildren = node.hasChildren(), 6664 isLastSib = node.isLastSibling(), 6665 aria = opts.aria, 6666 cn = opts._classNames, 6667 cnList = [], 6668 statusElem = node[tree.statusClassPropName]; 6669 6670 if (!statusElem || tree._enableUpdate === false) { 6671 // if this function is called for an unrendered node, ignore it (will be updated on nect render anyway) 6672 return; 6673 } 6674 if (aria) { 6675 $ariaElem = $(node.tr || node.li); 6676 } 6677 // Build a list of class names that we will add to the node <span> 6678 cnList.push(cn.node); 6679 if (tree.activeNode === node) { 6680 cnList.push(cn.active); 6681 // $(">span.fancytree-title", statusElem).attr("tabindex", "0"); 6682 // tree.$container.removeAttr("tabindex"); 6683 // }else{ 6684 // $(">span.fancytree-title", statusElem).removeAttr("tabindex"); 6685 // tree.$container.attr("tabindex", "0"); 6686 } 6687 if (tree.focusNode === node) { 6688 cnList.push(cn.focused); 6689 } 6690 if (node.expanded) { 6691 cnList.push(cn.expanded); 6692 } 6693 if (aria) { 6694 if (hasChildren === false) { 6695 $ariaElem.removeAttr("aria-expanded"); 6696 } else { 6697 $ariaElem.attr("aria-expanded", Boolean(node.expanded)); 6698 } 6699 } 6700 if (node.folder) { 6701 cnList.push(cn.folder); 6702 } 6703 if (hasChildren !== false) { 6704 cnList.push(cn.hasChildren); 6705 } 6706 // TODO: required? 6707 if (isLastSib) { 6708 cnList.push(cn.lastsib); 6709 } 6710 if (node.lazy && node.children == null) { 6711 cnList.push(cn.lazy); 6712 } 6713 if (node.partload) { 6714 cnList.push(cn.partload); 6715 } 6716 if (node.partsel) { 6717 cnList.push(cn.partsel); 6718 } 6719 if (FT.evalOption("unselectable", node, node, opts, false)) { 6720 cnList.push(cn.unselectable); 6721 } 6722 if (node._isLoading) { 6723 cnList.push(cn.loading); 6724 } 6725 if (node._error) { 6726 cnList.push(cn.error); 6727 } 6728 if (node.statusNodeType) { 6729 cnList.push(cn.statusNodePrefix + node.statusNodeType); 6730 } 6731 if (node.selected) { 6732 cnList.push(cn.selected); 6733 if (aria) { 6734 $ariaElem.attr("aria-selected", true); 6735 } 6736 } else if (aria) { 6737 $ariaElem.attr("aria-selected", false); 6738 } 6739 if (node.extraClasses) { 6740 cnList.push(node.extraClasses); 6741 } 6742 // IE6 doesn't correctly evaluate multiple class names, 6743 // so we create combined class names that can be used in the CSS 6744 if (hasChildren === false) { 6745 cnList.push( 6746 cn.combinedExpanderPrefix + "n" + (isLastSib ? "l" : "") 6747 ); 6748 } else { 6749 cnList.push( 6750 cn.combinedExpanderPrefix + 6751 (node.expanded ? "e" : "c") + 6752 (node.lazy && node.children == null ? "d" : "") + 6753 (isLastSib ? "l" : "") 6754 ); 6755 } 6756 cnList.push( 6757 cn.combinedIconPrefix + 6758 (node.expanded ? "e" : "c") + 6759 (node.folder ? "f" : "") 6760 ); 6761 // node.span.className = cnList.join(" "); 6762 statusElem.className = cnList.join(" "); 6763 6764 // TODO: we should not set this in the <span> tag also, if we set it here: 6765 // Maybe most (all) of the classes should be set in LI instead of SPAN? 6766 if (node.li) { 6767 // #719: we have to consider that there may be already other classes: 6768 $(node.li).toggleClass(cn.lastsib, isLastSib); 6769 } 6770 }, 6771 /** Activate node. 6772 * flag defaults to true. 6773 * If flag is true, the node is activated (must be a synchronous operation) 6774 * If flag is false, the node is deactivated (must be a synchronous operation) 6775 * @param {EventData} ctx 6776 * @param {boolean} [flag=true] 6777 * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false} 6778 * @returns {$.Promise} 6779 */ 6780 nodeSetActive: function (ctx, flag, callOpts) { 6781 // Handle user click / [space] / [enter], according to clickFolderMode. 6782 callOpts = callOpts || {}; 6783 var subCtx, 6784 node = ctx.node, 6785 tree = ctx.tree, 6786 opts = ctx.options, 6787 noEvents = callOpts.noEvents === true, 6788 noFocus = callOpts.noFocus === true, 6789 scroll = callOpts.scrollIntoView !== false, 6790 isActive = node === tree.activeNode; 6791 6792 // flag defaults to true 6793 flag = flag !== false; 6794 // node.debug("nodeSetActive", flag); 6795 6796 if (isActive === flag) { 6797 // Nothing to do 6798 return _getResolvedPromise(node); 6799 } 6800 // #1042: don't scroll between mousedown/-up when clicking an embedded link 6801 if ( 6802 scroll && 6803 ctx.originalEvent && 6804 $(ctx.originalEvent.target).is("a,:checkbox") 6805 ) { 6806 node.info("Not scrolling while clicking an embedded link."); 6807 scroll = false; 6808 } 6809 if ( 6810 flag && 6811 !noEvents && 6812 this._triggerNodeEvent( 6813 "beforeActivate", 6814 node, 6815 ctx.originalEvent 6816 ) === false 6817 ) { 6818 // Callback returned false 6819 return _getRejectedPromise(node, ["rejected"]); 6820 } 6821 if (flag) { 6822 if (tree.activeNode) { 6823 _assert( 6824 tree.activeNode !== node, 6825 "node was active (inconsistency)" 6826 ); 6827 subCtx = $.extend({}, ctx, { node: tree.activeNode }); 6828 tree.nodeSetActive(subCtx, false); 6829 _assert( 6830 tree.activeNode === null, 6831 "deactivate was out of sync?" 6832 ); 6833 } 6834 6835 if (opts.activeVisible) { 6836 // If no focus is set (noFocus: true) and there is no focused node, this node is made visible. 6837 // scroll = noFocus && tree.focusNode == null; 6838 // #863: scroll by default (unless `scrollIntoView: false` was passed) 6839 node.makeVisible({ scrollIntoView: scroll }); 6840 } 6841 tree.activeNode = node; 6842 tree.nodeRenderStatus(ctx); 6843 if (!noFocus) { 6844 tree.nodeSetFocus(ctx); 6845 } 6846 if (!noEvents) { 6847 tree._triggerNodeEvent( 6848 "activate", 6849 node, 6850 ctx.originalEvent 6851 ); 6852 } 6853 } else { 6854 _assert( 6855 tree.activeNode === node, 6856 "node was not active (inconsistency)" 6857 ); 6858 tree.activeNode = null; 6859 this.nodeRenderStatus(ctx); 6860 if (!noEvents) { 6861 ctx.tree._triggerNodeEvent( 6862 "deactivate", 6863 node, 6864 ctx.originalEvent 6865 ); 6866 } 6867 } 6868 return _getResolvedPromise(node); 6869 }, 6870 /** Expand or collapse node, return Deferred.promise. 6871 * 6872 * @param {EventData} ctx 6873 * @param {boolean} [flag=true] 6874 * @param {object} [opts] additional options. Defaults to `{noAnimation: false, noEvents: false}` 6875 * @returns {$.Promise} The deferred will be resolved as soon as the (lazy) 6876 * data was retrieved, rendered, and the expand animation finished. 6877 */ 6878 nodeSetExpanded: function (ctx, flag, callOpts) { 6879 callOpts = callOpts || {}; 6880 var _afterLoad, 6881 dfd, 6882 i, 6883 l, 6884 parents, 6885 prevAC, 6886 node = ctx.node, 6887 tree = ctx.tree, 6888 opts = ctx.options, 6889 noAnimation = callOpts.noAnimation === true, 6890 noEvents = callOpts.noEvents === true; 6891 6892 // flag defaults to true 6893 flag = flag !== false; 6894 6895 // node.debug("nodeSetExpanded(" + flag + ")"); 6896 6897 if ($(node.li).hasClass(opts._classNames.animating)) { 6898 node.warn( 6899 "setExpanded(" + flag + ") while animating: ignored." 6900 ); 6901 return _getRejectedPromise(node, ["recursion"]); 6902 } 6903 6904 if ((node.expanded && flag) || (!node.expanded && !flag)) { 6905 // Nothing to do 6906 // node.debug("nodeSetExpanded(" + flag + "): nothing to do"); 6907 return _getResolvedPromise(node); 6908 } else if (flag && !node.lazy && !node.hasChildren()) { 6909 // Prevent expanding of empty nodes 6910 // return _getRejectedPromise(node, ["empty"]); 6911 return _getResolvedPromise(node); 6912 } else if (!flag && node.getLevel() < opts.minExpandLevel) { 6913 // Prevent collapsing locked levels 6914 return _getRejectedPromise(node, ["locked"]); 6915 } else if ( 6916 !noEvents && 6917 this._triggerNodeEvent( 6918 "beforeExpand", 6919 node, 6920 ctx.originalEvent 6921 ) === false 6922 ) { 6923 // Callback returned false 6924 return _getRejectedPromise(node, ["rejected"]); 6925 } 6926 // If this node inside a collpased node, no animation and scrolling is needed 6927 if (!noAnimation && !node.isVisible()) { 6928 noAnimation = callOpts.noAnimation = true; 6929 } 6930 6931 dfd = new $.Deferred(); 6932 6933 // Auto-collapse mode: collapse all siblings 6934 if (flag && !node.expanded && opts.autoCollapse) { 6935 parents = node.getParentList(false, true); 6936 prevAC = opts.autoCollapse; 6937 try { 6938 opts.autoCollapse = false; 6939 for (i = 0, l = parents.length; i < l; i++) { 6940 // TODO: should return promise? 6941 this._callHook( 6942 "nodeCollapseSiblings", 6943 parents[i], 6944 callOpts 6945 ); 6946 } 6947 } finally { 6948 opts.autoCollapse = prevAC; 6949 } 6950 } 6951 // Trigger expand/collapse after expanding 6952 dfd.done(function () { 6953 var lastChild = node.getLastChild(); 6954 6955 if ( 6956 flag && 6957 opts.autoScroll && 6958 !noAnimation && 6959 lastChild && 6960 tree._enableUpdate 6961 ) { 6962 // Scroll down to last child, but keep current node visible 6963 lastChild 6964 .scrollIntoView(true, { topNode: node }) 6965 .always(function () { 6966 if (!noEvents) { 6967 ctx.tree._triggerNodeEvent( 6968 flag ? "expand" : "collapse", 6969 ctx 6970 ); 6971 } 6972 }); 6973 } else { 6974 if (!noEvents) { 6975 ctx.tree._triggerNodeEvent( 6976 flag ? "expand" : "collapse", 6977 ctx 6978 ); 6979 } 6980 } 6981 }); 6982 // vvv Code below is executed after loading finished: 6983 _afterLoad = function (callback) { 6984 var cn = opts._classNames, 6985 isVisible, 6986 isExpanded, 6987 effect = opts.toggleEffect; 6988 6989 node.expanded = flag; 6990 tree._callHook( 6991 "treeStructureChanged", 6992 ctx, 6993 flag ? "expand" : "collapse" 6994 ); 6995 // Create required markup, but make sure the top UL is hidden, so we 6996 // can animate later 6997 tree._callHook("nodeRender", ctx, false, false, true); 6998 6999 // Hide children, if node is collapsed 7000 if (node.ul) { 7001 isVisible = node.ul.style.display !== "none"; 7002 isExpanded = !!node.expanded; 7003 if (isVisible === isExpanded) { 7004 node.warn( 7005 "nodeSetExpanded: UL.style.display already set" 7006 ); 7007 } else if (!effect || noAnimation) { 7008 node.ul.style.display = 7009 node.expanded || !parent ? "" : "none"; 7010 } else { 7011 // The UI toggle() effect works with the ext-wide extension, 7012 // while jQuery.animate() has problems when the title span 7013 // has position: absolute. 7014 // Since jQuery UI 1.12, the blind effect requires the parent 7015 // element to have 'position: relative'. 7016 // See #716, #717 7017 $(node.li).addClass(cn.animating); // #717 7018 7019 if (_isFunction($(node.ul)[effect.effect])) { 7020 // tree.debug( "use jquery." + effect.effect + " method" ); 7021 $(node.ul)[effect.effect]({ 7022 duration: effect.duration, 7023 always: function () { 7024 // node.debug("fancytree-animating end: " + node.li.className); 7025 $(this).removeClass(cn.animating); // #716 7026 $(node.li).removeClass(cn.animating); // #717 7027 callback(); 7028 }, 7029 }); 7030 } else { 7031 // The UI toggle() effect works with the ext-wide extension, 7032 // while jQuery.animate() has problems when the title span 7033 // has positon: absolute. 7034 // Since jQuery UI 1.12, the blind effect requires the parent 7035 // element to have 'position: relative'. 7036 // See #716, #717 7037 // tree.debug("use specified effect (" + effect.effect + ") with the jqueryui.toggle method"); 7038 7039 // try to stop an animation that might be already in progress 7040 $(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" 7041 7042 // dirty fix to remove a defunct animation (effect: "blind") after resetLazy has been called 7043 $(node.ul) 7044 .parent() 7045 .find(".ui-effects-placeholder") 7046 .remove(); 7047 7048 $(node.ul).toggle( 7049 effect.effect, 7050 effect.options, 7051 effect.duration, 7052 function () { 7053 // node.debug("fancytree-animating end: " + node.li.className); 7054 $(this).removeClass(cn.animating); // #716 7055 $(node.li).removeClass(cn.animating); // #717 7056 callback(); 7057 } 7058 ); 7059 } 7060 return; 7061 } 7062 } 7063 callback(); 7064 }; 7065 // ^^^ Code above is executed after loading finshed. 7066 7067 // Load lazy nodes, if any. Then continue with _afterLoad() 7068 if (flag && node.lazy && node.hasChildren() === undefined) { 7069 // node.debug("nodeSetExpanded: load start..."); 7070 node.load() 7071 .done(function () { 7072 // node.debug("nodeSetExpanded: load done"); 7073 if (dfd.notifyWith) { 7074 // requires jQuery 1.6+ 7075 dfd.notifyWith(node, ["loaded"]); 7076 } 7077 _afterLoad(function () { 7078 dfd.resolveWith(node); 7079 }); 7080 }) 7081 .fail(function (errMsg) { 7082 _afterLoad(function () { 7083 dfd.rejectWith(node, [ 7084 "load failed (" + errMsg + ")", 7085 ]); 7086 }); 7087 }); 7088 /* 7089 var source = tree._triggerNodeEvent("lazyLoad", node, ctx.originalEvent); 7090 _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result"); 7091 node.debug("nodeSetExpanded: load start..."); 7092 this._callHook("nodeLoadChildren", ctx, source).done(function(){ 7093 node.debug("nodeSetExpanded: load done"); 7094 if(dfd.notifyWith){ // requires jQuery 1.6+ 7095 dfd.notifyWith(node, ["loaded"]); 7096 } 7097 _afterLoad.call(tree); 7098 }).fail(function(errMsg){ 7099 dfd.rejectWith(node, ["load failed (" + errMsg + ")"]); 7100 }); 7101 */ 7102 } else { 7103 _afterLoad(function () { 7104 dfd.resolveWith(node); 7105 }); 7106 } 7107 // node.debug("nodeSetExpanded: returns"); 7108 return dfd.promise(); 7109 }, 7110 /** Focus or blur this node. 7111 * @param {EventData} ctx 7112 * @param {boolean} [flag=true] 7113 */ 7114 nodeSetFocus: function (ctx, flag) { 7115 // ctx.node.debug("nodeSetFocus(" + flag + ")"); 7116 var ctx2, 7117 tree = ctx.tree, 7118 node = ctx.node, 7119 opts = tree.options, 7120 // et = ctx.originalEvent && ctx.originalEvent.type, 7121 isInput = ctx.originalEvent 7122 ? $(ctx.originalEvent.target).is(":input") 7123 : false; 7124 7125 flag = flag !== false; 7126 7127 // (node || tree).debug("nodeSetFocus(" + flag + "), event: " + et + ", isInput: "+ isInput); 7128 // Blur previous node if any 7129 if (tree.focusNode) { 7130 if (tree.focusNode === node && flag) { 7131 // node.debug("nodeSetFocus(" + flag + "): nothing to do"); 7132 return; 7133 } 7134 ctx2 = $.extend({}, ctx, { node: tree.focusNode }); 7135 tree.focusNode = null; 7136 this._triggerNodeEvent("blur", ctx2); 7137 this._callHook("nodeRenderStatus", ctx2); 7138 } 7139 // Set focus to container and node 7140 if (flag) { 7141 if (!this.hasFocus()) { 7142 node.debug("nodeSetFocus: forcing container focus"); 7143 this._callHook("treeSetFocus", ctx, true, { 7144 calledByNode: true, 7145 }); 7146 } 7147 node.makeVisible({ scrollIntoView: false }); 7148 tree.focusNode = node; 7149 if (opts.titlesTabbable) { 7150 if (!isInput) { 7151 // #621 7152 $(node.span).find(".fancytree-title").focus(); 7153 } 7154 } 7155 if (opts.aria) { 7156 // Set active descendant to node's span ID (create one, if needed) 7157 $(tree.$container).attr( 7158 "aria-activedescendant", 7159 $(node.tr || node.li) 7160 .uniqueId() 7161 .attr("id") 7162 ); 7163 // "ftal_" + opts.idPrefix + node.key); 7164 } 7165 // $(node.span).find(".fancytree-title").focus(); 7166 this._triggerNodeEvent("focus", ctx); 7167 7168 // determine if we have focus on or inside tree container 7169 var hasFancytreeFocus = 7170 document.activeElement === tree.$container.get(0) || 7171 $(document.activeElement, tree.$container).length >= 1; 7172 7173 if (!hasFancytreeFocus) { 7174 // We cannot set KB focus to a node, so use the tree container 7175 // #563, #570: IE scrolls on every call to .focus(), if the container 7176 // is partially outside the viewport. So do it only, when absolutely 7177 // necessary. 7178 $(tree.$container).focus(); 7179 } 7180 7181 // if( opts.autoActivate ){ 7182 // tree.nodeSetActive(ctx, true); 7183 // } 7184 if (opts.autoScroll) { 7185 node.scrollIntoView(); 7186 } 7187 this._callHook("nodeRenderStatus", ctx); 7188 } 7189 }, 7190 /** (De)Select node, return new status (sync). 7191 * 7192 * @param {EventData} ctx 7193 * @param {boolean} [flag=true] 7194 * @param {object} [opts] additional options. Defaults to {noEvents: false, 7195 * propagateDown: null, propagateUp: null, 7196 * callback: null, 7197 * } 7198 * @returns {boolean} previous status 7199 */ 7200 nodeSetSelected: function (ctx, flag, callOpts) { 7201 callOpts = callOpts || {}; 7202 var node = ctx.node, 7203 tree = ctx.tree, 7204 opts = ctx.options, 7205 noEvents = callOpts.noEvents === true, 7206 parent = node.parent; 7207 7208 // flag defaults to true 7209 flag = flag !== false; 7210 7211 // node.debug("nodeSetSelected(" + flag + ")", ctx); 7212 7213 // Cannot (de)select unselectable nodes directly (only by propagation or 7214 // by setting the `.selected` property) 7215 if (FT.evalOption("unselectable", node, node, opts, false)) { 7216 return; 7217 } 7218 7219 // Remember the user's intent, in case down -> up propagation prevents 7220 // applying it to node.selected 7221 node._lastSelectIntent = flag; // Confusing use of '!' 7222 7223 // Nothing to do? 7224 if (!!node.selected === flag) { 7225 if (opts.selectMode === 3 && node.partsel && !flag) { 7226 // If propagation prevented selecting this node last time, we still 7227 // want to allow to apply setSelected(false) now 7228 } else { 7229 return flag; 7230 } 7231 } 7232 7233 if ( 7234 !noEvents && 7235 this._triggerNodeEvent( 7236 "beforeSelect", 7237 node, 7238 ctx.originalEvent 7239 ) === false 7240 ) { 7241 return !!node.selected; 7242 } 7243 if (flag && opts.selectMode === 1) { 7244 // single selection mode (we don't uncheck all tree nodes, for performance reasons) 7245 if (tree.lastSelectedNode) { 7246 tree.lastSelectedNode.setSelected(false); 7247 } 7248 node.selected = flag; 7249 } else if ( 7250 opts.selectMode === 3 && 7251 parent && 7252 !parent.radiogroup && 7253 !node.radiogroup 7254 ) { 7255 // multi-hierarchical selection mode 7256 node.selected = flag; 7257 node.fixSelection3AfterClick(callOpts); 7258 } else if (parent && parent.radiogroup) { 7259 node.visitSiblings(function (n) { 7260 n._changeSelectStatusAttrs(flag && n === node); 7261 }, true); 7262 } else { 7263 // default: selectMode: 2, multi selection mode 7264 node.selected = flag; 7265 } 7266 this.nodeRenderStatus(ctx); 7267 tree.lastSelectedNode = flag ? node : null; 7268 if (!noEvents) { 7269 tree._triggerNodeEvent("select", ctx); 7270 } 7271 }, 7272 /** Show node status (ok, loading, error, nodata) using styles and a dummy child node. 7273 * 7274 * @param {EventData} ctx 7275 * @param status 7276 * @param message 7277 * @param details 7278 * @since 2.3 7279 */ 7280 nodeSetStatus: function (ctx, status, message, details) { 7281 var node = ctx.node, 7282 tree = ctx.tree; 7283 7284 function _clearStatusNode() { 7285 // Remove dedicated dummy node, if any 7286 var firstChild = node.children ? node.children[0] : null; 7287 if (firstChild && firstChild.isStatusNode()) { 7288 try { 7289 // I've seen exceptions here with loadKeyPath... 7290 if (node.ul) { 7291 node.ul.removeChild(firstChild.li); 7292 firstChild.li = null; // avoid leaks (DT issue 215) 7293 } 7294 } catch (e) {} 7295 if (node.children.length === 1) { 7296 node.children = []; 7297 } else { 7298 node.children.shift(); 7299 } 7300 tree._callHook( 7301 "treeStructureChanged", 7302 ctx, 7303 "clearStatusNode" 7304 ); 7305 } 7306 } 7307 function _setStatusNode(data, type) { 7308 // Create/modify the dedicated dummy node for 'loading...' or 7309 // 'error!' status. (only called for direct child of the invisible 7310 // system root) 7311 var firstChild = node.children ? node.children[0] : null; 7312 if (firstChild && firstChild.isStatusNode()) { 7313 $.extend(firstChild, data); 7314 firstChild.statusNodeType = type; 7315 tree._callHook("nodeRenderTitle", firstChild); 7316 } else { 7317 node._setChildren([data]); 7318 tree._callHook( 7319 "treeStructureChanged", 7320 ctx, 7321 "setStatusNode" 7322 ); 7323 node.children[0].statusNodeType = type; 7324 tree.render(); 7325 } 7326 return node.children[0]; 7327 } 7328 7329 switch (status) { 7330 case "ok": 7331 _clearStatusNode(); 7332 node._isLoading = false; 7333 node._error = null; 7334 node.renderStatus(); 7335 break; 7336 case "loading": 7337 if (!node.parent) { 7338 _setStatusNode( 7339 { 7340 title: 7341 tree.options.strings.loading + 7342 (message ? " (" + message + ")" : ""), 7343 // icon: true, // needed for 'loding' icon 7344 checkbox: false, 7345 tooltip: details, 7346 }, 7347 status 7348 ); 7349 } 7350 node._isLoading = true; 7351 node._error = null; 7352 node.renderStatus(); 7353 break; 7354 case "error": 7355 _setStatusNode( 7356 { 7357 title: 7358 tree.options.strings.loadError + 7359 (message ? " (" + message + ")" : ""), 7360 // icon: false, 7361 checkbox: false, 7362 tooltip: details, 7363 }, 7364 status 7365 ); 7366 node._isLoading = false; 7367 node._error = { message: message, details: details }; 7368 node.renderStatus(); 7369 break; 7370 case "nodata": 7371 _setStatusNode( 7372 { 7373 title: message || tree.options.strings.noData, 7374 // icon: false, 7375 checkbox: false, 7376 tooltip: details, 7377 }, 7378 status 7379 ); 7380 node._isLoading = false; 7381 node._error = null; 7382 node.renderStatus(); 7383 break; 7384 default: 7385 $.error("invalid node status " + status); 7386 } 7387 }, 7388 /** 7389 * 7390 * @param {EventData} ctx 7391 */ 7392 nodeToggleExpanded: function (ctx) { 7393 return this.nodeSetExpanded(ctx, !ctx.node.expanded); 7394 }, 7395 /** 7396 * @param {EventData} ctx 7397 */ 7398 nodeToggleSelected: function (ctx) { 7399 var node = ctx.node, 7400 flag = !node.selected; 7401 7402 // In selectMode: 3 this node may be unselected+partsel, even if 7403 // setSelected(true) was called before, due to `unselectable` children. 7404 // In this case, we now toggle as `setSelected(false)` 7405 if ( 7406 node.partsel && 7407 !node.selected && 7408 node._lastSelectIntent === true 7409 ) { 7410 flag = false; 7411 node.selected = true; // so it is not considered 'nothing to do' 7412 } 7413 node._lastSelectIntent = flag; 7414 return this.nodeSetSelected(ctx, flag); 7415 }, 7416 /** Remove all nodes. 7417 * @param {EventData} ctx 7418 */ 7419 treeClear: function (ctx) { 7420 var tree = ctx.tree; 7421 tree.activeNode = null; 7422 tree.focusNode = null; 7423 tree.$div.find(">ul.fancytree-container").empty(); 7424 // TODO: call destructors and remove reference loops 7425 tree.rootNode.children = null; 7426 tree._callHook("treeStructureChanged", ctx, "clear"); 7427 }, 7428 /** Widget was created (called only once, even it re-initialized). 7429 * @param {EventData} ctx 7430 */ 7431 treeCreate: function (ctx) {}, 7432 /** Widget was destroyed. 7433 * @param {EventData} ctx 7434 */ 7435 treeDestroy: function (ctx) { 7436 this.$div.find(">ul.fancytree-container").remove(); 7437 if (this.$source) { 7438 this.$source.removeClass("fancytree-helper-hidden"); 7439 } 7440 }, 7441 /** Widget was (re-)initialized. 7442 * @param {EventData} ctx 7443 */ 7444 treeInit: function (ctx) { 7445 var tree = ctx.tree, 7446 opts = tree.options; 7447 7448 //this.debug("Fancytree.treeInit()"); 7449 // Add container to the TAB chain 7450 // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant 7451 // #577: Allow to set tabindex to "0", "-1" and "" 7452 tree.$container.attr("tabindex", opts.tabindex); 7453 7454 // Copy some attributes to tree.data 7455 $.each(TREE_ATTRS, function (i, attr) { 7456 if (opts[attr] !== undefined) { 7457 tree.info("Move option " + attr + " to tree"); 7458 tree[attr] = opts[attr]; 7459 delete opts[attr]; 7460 } 7461 }); 7462 7463 if (opts.checkboxAutoHide) { 7464 tree.$container.addClass("fancytree-checkbox-auto-hide"); 7465 } 7466 if (opts.rtl) { 7467 tree.$container 7468 .attr("DIR", "RTL") 7469 .addClass("fancytree-rtl"); 7470 } else { 7471 tree.$container 7472 .removeAttr("DIR") 7473 .removeClass("fancytree-rtl"); 7474 } 7475 if (opts.aria) { 7476 tree.$container.attr("role", "tree"); 7477 if (opts.selectMode !== 1) { 7478 tree.$container.attr("aria-multiselectable", true); 7479 } 7480 } 7481 this.treeLoad(ctx); 7482 }, 7483 /** Parse Fancytree from source, as configured in the options. 7484 * @param {EventData} ctx 7485 * @param {object} [source] optional new source (use last data otherwise) 7486 */ 7487 treeLoad: function (ctx, source) { 7488 var metaData, 7489 type, 7490 $ul, 7491 tree = ctx.tree, 7492 $container = ctx.widget.element, 7493 dfd, 7494 // calling context for root node 7495 rootCtx = $.extend({}, ctx, { node: this.rootNode }); 7496 7497 if (tree.rootNode.children) { 7498 this.treeClear(ctx); 7499 } 7500 source = source || this.options.source; 7501 7502 if (!source) { 7503 type = $container.data("type") || "html"; 7504 switch (type) { 7505 case "html": 7506 // There should be an embedded `<ul>` with initial nodes, 7507 // but another `<ul class='fancytree-container'>` is appended 7508 // to the tree's <div> on startup anyway. 7509 $ul = $container 7510 .find(">ul") 7511 .not(".fancytree-container") 7512 .first(); 7513 7514 if ($ul.length) { 7515 $ul.addClass( 7516 "ui-fancytree-source fancytree-helper-hidden" 7517 ); 7518 source = $.ui.fancytree.parseHtml($ul); 7519 // allow to init tree.data.foo from <ul data-foo=''> 7520 this.data = $.extend( 7521 this.data, 7522 _getElementDataAsDict($ul) 7523 ); 7524 } else { 7525 FT.warn( 7526 "No `source` option was passed and container does not contain `<ul>`: assuming `source: []`." 7527 ); 7528 source = []; 7529 } 7530 break; 7531 case "json": 7532 source = $.parseJSON($container.text()); 7533 // $container already contains the <ul>, but we remove the plain (json) text 7534 // $container.empty(); 7535 $container 7536 .contents() 7537 .filter(function () { 7538 return this.nodeType === 3; 7539 }) 7540 .remove(); 7541 if ($.isPlainObject(source)) { 7542 // We got {foo: 'abc', children: [...]} 7543 _assert( 7544 _isArray(source.children), 7545 "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')" 7546 ); 7547 metaData = source; 7548 source = source.children; 7549 delete metaData.children; 7550 // Copy some attributes to tree.data 7551 $.each(TREE_ATTRS, function (i, attr) { 7552 if (metaData[attr] !== undefined) { 7553 tree[attr] = metaData[attr]; 7554 delete metaData[attr]; 7555 } 7556 }); 7557 // Copy extra properties to tree.data.foo 7558 $.extend(tree.data, metaData); 7559 } 7560 break; 7561 default: 7562 $.error("Invalid data-type: " + type); 7563 } 7564 } else if (typeof source === "string") { 7565 // TODO: source is an element ID 7566 $.error("Not implemented"); 7567 } 7568 7569 // preInit is fired when the widget markup is created, but nodes 7570 // not yet loaded 7571 tree._triggerTreeEvent("preInit", null); 7572 7573 // Trigger fancytreeinit after nodes have been loaded 7574 dfd = this.nodeLoadChildren(rootCtx, source) 7575 .done(function () { 7576 tree._callHook( 7577 "treeStructureChanged", 7578 ctx, 7579 "loadChildren" 7580 ); 7581 tree.render(); 7582 if (ctx.options.selectMode === 3) { 7583 tree.rootNode.fixSelection3FromEndNodes(); 7584 } 7585 if (tree.activeNode && tree.options.activeVisible) { 7586 tree.activeNode.makeVisible(); 7587 } 7588 tree._triggerTreeEvent("init", null, { status: true }); 7589 }) 7590 .fail(function () { 7591 tree.render(); 7592 tree._triggerTreeEvent("init", null, { status: false }); 7593 }); 7594 return dfd; 7595 }, 7596 /** Node was inserted into or removed from the tree. 7597 * @param {EventData} ctx 7598 * @param {boolean} add 7599 * @param {FancytreeNode} node 7600 */ 7601 treeRegisterNode: function (ctx, add, node) { 7602 ctx.tree._callHook( 7603 "treeStructureChanged", 7604 ctx, 7605 add ? "addNode" : "removeNode" 7606 ); 7607 }, 7608 /** Widget got focus. 7609 * @param {EventData} ctx 7610 * @param {boolean} [flag=true] 7611 */ 7612 treeSetFocus: function (ctx, flag, callOpts) { 7613 var targetNode; 7614 7615 flag = flag !== false; 7616 7617 // this.debug("treeSetFocus(" + flag + "), callOpts: ", callOpts, this.hasFocus()); 7618 // this.debug(" focusNode: " + this.focusNode); 7619 // this.debug(" activeNode: " + this.activeNode); 7620 if (flag !== this.hasFocus()) { 7621 this._hasFocus = flag; 7622 if (!flag && this.focusNode) { 7623 // Node also looses focus if widget blurs 7624 this.focusNode.setFocus(false); 7625 } else if (flag && (!callOpts || !callOpts.calledByNode)) { 7626 $(this.$container).focus(); 7627 } 7628 this.$container.toggleClass("fancytree-treefocus", flag); 7629 this._triggerTreeEvent(flag ? "focusTree" : "blurTree"); 7630 if (flag && !this.activeNode) { 7631 // #712: Use last mousedowned node ('click' event fires after focusin) 7632 targetNode = 7633 this._lastMousedownNode || this.getFirstChild(); 7634 if (targetNode) { 7635 targetNode.setFocus(); 7636 } 7637 } 7638 } 7639 }, 7640 /** Widget option was set using `$().fancytree("option", "KEY", VALUE)`. 7641 * 7642 * Note: `key` may reference a nested option, e.g. 'dnd5.scroll'. 7643 * In this case `value`contains the complete, modified `dnd5` option hash. 7644 * We can check for changed values like 7645 * if( value.scroll !== tree.options.dnd5.scroll ) {...} 7646 * 7647 * @param {EventData} ctx 7648 * @param {string} key option name 7649 * @param {any} value option value 7650 */ 7651 treeSetOption: function (ctx, key, value) { 7652 var tree = ctx.tree, 7653 callDefault = true, 7654 callCreate = false, 7655 callRender = false; 7656 7657 switch (key) { 7658 case "aria": 7659 case "checkbox": 7660 case "icon": 7661 case "minExpandLevel": 7662 case "tabindex": 7663 // tree._callHook("treeCreate", tree); 7664 callCreate = true; 7665 callRender = true; 7666 break; 7667 case "checkboxAutoHide": 7668 tree.$container.toggleClass( 7669 "fancytree-checkbox-auto-hide", 7670 !!value 7671 ); 7672 break; 7673 case "escapeTitles": 7674 case "tooltip": 7675 callRender = true; 7676 break; 7677 case "rtl": 7678 if (value === false) { 7679 tree.$container 7680 .removeAttr("DIR") 7681 .removeClass("fancytree-rtl"); 7682 } else { 7683 tree.$container 7684 .attr("DIR", "RTL") 7685 .addClass("fancytree-rtl"); 7686 } 7687 callRender = true; 7688 break; 7689 case "source": 7690 callDefault = false; 7691 tree._callHook("treeLoad", tree, value); 7692 callRender = true; 7693 break; 7694 } 7695 tree.debug( 7696 "set option " + 7697 key + 7698 "=" + 7699 value + 7700 " <" + 7701 typeof value + 7702 ">" 7703 ); 7704 if (callDefault) { 7705 if (this.widget._super) { 7706 // jQuery UI 1.9+ 7707 this.widget._super.call(this.widget, key, value); 7708 } else { 7709 // jQuery UI <= 1.8, we have to manually invoke the _setOption method from the base widget 7710 $.Widget.prototype._setOption.call( 7711 this.widget, 7712 key, 7713 value 7714 ); 7715 } 7716 } 7717 if (callCreate) { 7718 tree._callHook("treeCreate", tree); 7719 } 7720 if (callRender) { 7721 tree.render(true, false); // force, not-deep 7722 } 7723 }, 7724 /** A Node was added, removed, moved, or it's visibility changed. 7725 * @param {EventData} ctx 7726 */ 7727 treeStructureChanged: function (ctx, type) {}, 7728 } 7729 ); 7730 7731 /******************************************************************************* 7732 * jQuery UI widget boilerplate 7733 */ 7734 7735 /** 7736 * The plugin (derrived from [jQuery.Widget](http://api.jqueryui.com/jQuery.widget/)). 7737 * 7738 * **Note:** 7739 * These methods implement the standard jQuery UI widget API. 7740 * It is recommended to use methods of the {Fancytree} instance instead 7741 * 7742 * @example 7743 * // DEPRECATED: Access jQuery UI widget methods and members: 7744 * var tree = $("#tree").fancytree("getTree"); 7745 * var node = $("#tree").fancytree("getActiveNode"); 7746 * 7747 * // RECOMMENDED: Use the Fancytree object API 7748 * var tree = $.ui.fancytree.getTree("#tree"); 7749 * var node = tree.getActiveNode(); 7750 * 7751 * // or you may already have stored the tree instance upon creation: 7752 * import {createTree, version} from 'jquery.fancytree' 7753 * const tree = createTree('#tree', { ... }); 7754 * var node = tree.getActiveNode(); 7755 * 7756 * @see {Fancytree_Static#getTree} 7757 * @deprecated Use methods of the {Fancytree} instance instead 7758 * @mixin Fancytree_Widget 7759 */ 7760 7761 $.widget( 7762 "ui.fancytree", 7763 /** @lends Fancytree_Widget# */ 7764 { 7765 /**These options will be used as defaults 7766 * @type {FancytreeOptions} 7767 */ 7768 options: { 7769 activeVisible: true, 7770 ajax: { 7771 type: "GET", 7772 cache: false, // false: Append random '_' argument to the request url to prevent caching. 7773 // timeout: 0, // >0: Make sure we get an ajax error if server is unreachable 7774 dataType: "json", // Expect json format and pass json object to callbacks. 7775 }, 7776 aria: true, 7777 autoActivate: true, 7778 autoCollapse: false, 7779 autoScroll: false, 7780 checkbox: false, 7781 clickFolderMode: 4, 7782 copyFunctionsToData: false, 7783 debugLevel: null, // 0..4 (null: use global setting $.ui.fancytree.debugLevel) 7784 disabled: false, // TODO: required anymore? 7785 enableAspx: 42, // TODO: this is truethy, but distinguishable from true: default will change to false in the future 7786 escapeTitles: false, 7787 extensions: [], 7788 focusOnSelect: false, 7789 generateIds: false, 7790 icon: true, 7791 idPrefix: "ft_", 7792 keyboard: true, 7793 keyPathSeparator: "/", 7794 minExpandLevel: 1, 7795 nodata: true, // (bool, string, or callback) display message, when no data available 7796 quicksearch: false, 7797 rtl: false, 7798 scrollOfs: { top: 0, bottom: 0 }, 7799 scrollParent: null, 7800 selectMode: 2, 7801 strings: { 7802 loading: "Loading...", // … would be escaped when escapeTitles is true 7803 loadError: "Load error!", 7804 moreData: "More...", 7805 noData: "No data.", 7806 }, 7807 tabindex: "0", 7808 titlesTabbable: false, 7809 toggleEffect: { effect: "slideToggle", duration: 200 }, //< "toggle" or "slideToggle" to use jQuery instead of jQueryUI for toggleEffect animation 7810 tooltip: false, 7811 treeId: null, 7812 _classNames: { 7813 active: "fancytree-active", 7814 animating: "fancytree-animating", 7815 combinedExpanderPrefix: "fancytree-exp-", 7816 combinedIconPrefix: "fancytree-ico-", 7817 error: "fancytree-error", 7818 expanded: "fancytree-expanded", 7819 focused: "fancytree-focused", 7820 folder: "fancytree-folder", 7821 hasChildren: "fancytree-has-children", 7822 lastsib: "fancytree-lastsib", 7823 lazy: "fancytree-lazy", 7824 loading: "fancytree-loading", 7825 node: "fancytree-node", 7826 partload: "fancytree-partload", 7827 partsel: "fancytree-partsel", 7828 radio: "fancytree-radio", 7829 selected: "fancytree-selected", 7830 statusNodePrefix: "fancytree-statusnode-", 7831 unselectable: "fancytree-unselectable", 7832 }, 7833 // events 7834 lazyLoad: null, 7835 postProcess: null, 7836 }, 7837 _deprecationWarning: function (name) { 7838 var tree = this.tree; 7839 7840 if (tree && tree.options.debugLevel >= 3) { 7841 tree.warn( 7842 "$().fancytree('" + 7843 name + 7844 "') is deprecated (see https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Widget.html" 7845 ); 7846 } 7847 }, 7848 /* Set up the widget, Called on first $().fancytree() */ 7849 _create: function () { 7850 this.tree = new Fancytree(this); 7851 7852 this.$source = 7853 this.source || this.element.data("type") === "json" 7854 ? this.element 7855 : this.element.find(">ul").first(); 7856 // Subclass Fancytree instance with all enabled extensions 7857 var extension, 7858 extName, 7859 i, 7860 opts = this.options, 7861 extensions = opts.extensions, 7862 base = this.tree; 7863 7864 for (i = 0; i < extensions.length; i++) { 7865 extName = extensions[i]; 7866 extension = $.ui.fancytree._extensions[extName]; 7867 if (!extension) { 7868 $.error( 7869 "Could not apply extension '" + 7870 extName + 7871 "' (it is not registered, did you forget to include it?)" 7872 ); 7873 } 7874 // Add extension options as tree.options.EXTENSION 7875 // _assert(!this.tree.options[extName], "Extension name must not exist as option name: " + extName); 7876 7877 // console.info("extend " + extName, extension.options, this.tree.options[extName]) 7878 // issue #876: we want to replace custom array-options, not merge them 7879 this.tree.options[extName] = _simpleDeepMerge( 7880 {}, 7881 extension.options, 7882 this.tree.options[extName] 7883 ); 7884 // this.tree.options[extName] = $.extend(true, {}, extension.options, this.tree.options[extName]); 7885 7886 // console.info("extend " + extName + " =>", this.tree.options[extName]) 7887 // console.info("extend " + extName + " org default =>", extension.options) 7888 7889 // Add a namespace tree.ext.EXTENSION, to hold instance data 7890 _assert( 7891 this.tree.ext[extName] === undefined, 7892 "Extension name must not exist as Fancytree.ext attribute: '" + 7893 extName + 7894 "'" 7895 ); 7896 // this.tree[extName] = extension; 7897 this.tree.ext[extName] = {}; 7898 // Subclass Fancytree methods using proxies. 7899 _subclassObject(this.tree, base, extension, extName); 7900 // current extension becomes base for the next extension 7901 base = extension; 7902 } 7903 // 7904 if (opts.icons !== undefined) { 7905 // 2015-11-16 7906 if (opts.icon === true) { 7907 this.tree.warn( 7908 "'icons' tree option is deprecated since v2.14.0: use 'icon' instead" 7909 ); 7910 opts.icon = opts.icons; 7911 } else { 7912 $.error( 7913 "'icons' tree option is deprecated since v2.14.0: use 'icon' only instead" 7914 ); 7915 } 7916 } 7917 if (opts.iconClass !== undefined) { 7918 // 2015-11-16 7919 if (opts.icon) { 7920 $.error( 7921 "'iconClass' tree option is deprecated since v2.14.0: use 'icon' only instead" 7922 ); 7923 } else { 7924 this.tree.warn( 7925 "'iconClass' tree option is deprecated since v2.14.0: use 'icon' instead" 7926 ); 7927 opts.icon = opts.iconClass; 7928 } 7929 } 7930 if (opts.tabbable !== undefined) { 7931 // 2016-04-04 7932 opts.tabindex = opts.tabbable ? "0" : "-1"; 7933 this.tree.warn( 7934 "'tabbable' tree option is deprecated since v2.17.0: use 'tabindex='" + 7935 opts.tabindex + 7936 "' instead" 7937 ); 7938 } 7939 // 7940 this.tree._callHook("treeCreate", this.tree); 7941 // Note: 'fancytreecreate' event is fired by widget base class 7942 // this.tree._triggerTreeEvent("create"); 7943 }, 7944 7945 /* Called on every $().fancytree() */ 7946 _init: function () { 7947 this.tree._callHook("treeInit", this.tree); 7948 // TODO: currently we call bind after treeInit, because treeInit 7949 // might change tree.$container. 7950 // It would be better, to move event binding into hooks altogether 7951 this._bind(); 7952 }, 7953 7954 /* Use the _setOption method to respond to changes to options. */ 7955 _setOption: function (key, value) { 7956 return this.tree._callHook( 7957 "treeSetOption", 7958 this.tree, 7959 key, 7960 value 7961 ); 7962 }, 7963 7964 /** Use the destroy method to clean up any modifications your widget has made to the DOM */ 7965 _destroy: function () { 7966 this._unbind(); 7967 this.tree._callHook("treeDestroy", this.tree); 7968 // In jQuery UI 1.8, you must invoke the destroy method from the base widget 7969 // $.Widget.prototype.destroy.call(this); 7970 // TODO: delete tree and nodes to make garbage collect easier? 7971 // TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method 7972 }, 7973 7974 // ------------------------------------------------------------------------- 7975 7976 /* Remove all event handlers for our namespace */ 7977 _unbind: function () { 7978 var ns = this.tree._ns; 7979 this.element.off(ns); 7980 this.tree.$container.off(ns); 7981 $(document).off(ns); 7982 }, 7983 /* Add mouse and kyboard handlers to the container */ 7984 _bind: function () { 7985 var self = this, 7986 opts = this.options, 7987 tree = this.tree, 7988 ns = tree._ns; 7989 // selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" ) 7990 7991 // Remove all previuous handlers for this tree 7992 this._unbind(); 7993 7994 //alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container); 7995 // tree.debug("bind events; container: ", tree.$container); 7996 tree.$container 7997 .on("focusin" + ns + " focusout" + ns, function (event) { 7998 var node = FT.getNode(event), 7999 flag = event.type === "focusin"; 8000 8001 if (!flag && node && $(event.target).is("a")) { 8002 // #764 8003 node.debug( 8004 "Ignored focusout on embedded <a> element." 8005 ); 8006 return; 8007 } 8008 // tree.treeOnFocusInOut.call(tree, event); 8009 // tree.debug("Tree container got event " + event.type, node, event, FT.getEventTarget(event)); 8010 if (flag) { 8011 if (tree._getExpiringValue("focusin")) { 8012 // #789: IE 11 may send duplicate focusin events 8013 tree.debug("Ignored double focusin."); 8014 return; 8015 } 8016 tree._setExpiringValue("focusin", true, 50); 8017 8018 if (!node) { 8019 // #789: IE 11 may send focusin before mousdown(?) 8020 node = tree._getExpiringValue("mouseDownNode"); 8021 if (node) { 8022 tree.debug( 8023 "Reconstruct mouse target for focusin from recent event." 8024 ); 8025 } 8026 } 8027 } 8028 if (node) { 8029 // For example clicking into an <input> that is part of a node 8030 tree._callHook( 8031 "nodeSetFocus", 8032 tree._makeHookContext(node, event), 8033 flag 8034 ); 8035 } else { 8036 if ( 8037 tree.tbody && 8038 $(event.target).parents( 8039 "table.fancytree-container > thead" 8040 ).length 8041 ) { 8042 // #767: ignore events in the table's header 8043 tree.debug( 8044 "Ignore focus event outside table body.", 8045 event 8046 ); 8047 } else { 8048 tree._callHook("treeSetFocus", tree, flag); 8049 } 8050 } 8051 }) 8052 .on( 8053 "selectstart" + ns, 8054 "span.fancytree-title", 8055 function (event) { 8056 // prevent mouse-drags to select text ranges 8057 // tree.debug("<span title> got event " + event.type); 8058 event.preventDefault(); 8059 } 8060 ) 8061 .on("keydown" + ns, function (event) { 8062 // TODO: also bind keyup and keypress 8063 // tree.debug("got event " + event.type + ", hasFocus:" + tree.hasFocus()); 8064 // if(opts.disabled || opts.keyboard === false || !tree.hasFocus() ){ 8065 if (opts.disabled || opts.keyboard === false) { 8066 return true; 8067 } 8068 var res, 8069 node = tree.focusNode, // node may be null 8070 ctx = tree._makeHookContext(node || tree, event), 8071 prevPhase = tree.phase; 8072 8073 try { 8074 tree.phase = "userEvent"; 8075 // If a 'fancytreekeydown' handler returns false, skip the default 8076 // handling (implemented by tree.nodeKeydown()). 8077 if (node) { 8078 res = tree._triggerNodeEvent( 8079 "keydown", 8080 node, 8081 event 8082 ); 8083 } else { 8084 res = tree._triggerTreeEvent("keydown", event); 8085 } 8086 if (res === "preventNav") { 8087 res = true; // prevent keyboard navigation, but don't prevent default handling of embedded input controls 8088 } else if (res !== false) { 8089 res = tree._callHook("nodeKeydown", ctx); 8090 } 8091 return res; 8092 } finally { 8093 tree.phase = prevPhase; 8094 } 8095 }) 8096 .on("mousedown" + ns, function (event) { 8097 var et = FT.getEventTarget(event); 8098 // self.tree.debug("event(" + event.type + "): node: ", et.node); 8099 // #712: Store the clicked node, so we can use it when we get a focusin event 8100 // ('click' event fires after focusin) 8101 // tree.debug("event(" + event.type + "): node: ", et.node); 8102 tree._lastMousedownNode = et ? et.node : null; 8103 // #789: Store the node also for a short period, so we can use it 8104 // in a *resulting* focusin event 8105 tree._setExpiringValue( 8106 "mouseDownNode", 8107 tree._lastMousedownNode 8108 ); 8109 }) 8110 .on("click" + ns + " dblclick" + ns, function (event) { 8111 if (opts.disabled) { 8112 return true; 8113 } 8114 var ctx, 8115 et = FT.getEventTarget(event), 8116 node = et.node, 8117 tree = self.tree, 8118 prevPhase = tree.phase; 8119 8120 // self.tree.debug("event(" + event.type + "): node: ", node); 8121 if (!node) { 8122 return true; // Allow bubbling of other events 8123 } 8124 ctx = tree._makeHookContext(node, event); 8125 // self.tree.debug("event(" + event.type + "): node: ", node); 8126 try { 8127 tree.phase = "userEvent"; 8128 switch (event.type) { 8129 case "click": 8130 ctx.targetType = et.type; 8131 if (node.isPagingNode()) { 8132 return ( 8133 tree._triggerNodeEvent( 8134 "clickPaging", 8135 ctx, 8136 event 8137 ) === true 8138 ); 8139 } 8140 return tree._triggerNodeEvent( 8141 "click", 8142 ctx, 8143 event 8144 ) === false 8145 ? false 8146 : tree._callHook("nodeClick", ctx); 8147 case "dblclick": 8148 ctx.targetType = et.type; 8149 return tree._triggerNodeEvent( 8150 "dblclick", 8151 ctx, 8152 event 8153 ) === false 8154 ? false 8155 : tree._callHook("nodeDblclick", ctx); 8156 } 8157 } finally { 8158 tree.phase = prevPhase; 8159 } 8160 }); 8161 }, 8162 /** Return the active node or null. 8163 * @returns {FancytreeNode} 8164 * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>). 8165 */ 8166 getActiveNode: function () { 8167 this._deprecationWarning("getActiveNode"); 8168 return this.tree.activeNode; 8169 }, 8170 /** Return the matching node or null. 8171 * @param {string} key 8172 * @returns {FancytreeNode} 8173 * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>). 8174 */ 8175 getNodeByKey: function (key) { 8176 this._deprecationWarning("getNodeByKey"); 8177 return this.tree.getNodeByKey(key); 8178 }, 8179 /** Return the invisible system root node. 8180 * @returns {FancytreeNode} 8181 * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>). 8182 */ 8183 getRootNode: function () { 8184 this._deprecationWarning("getRootNode"); 8185 return this.tree.rootNode; 8186 }, 8187 /** Return the current tree instance. 8188 * @returns {Fancytree} 8189 * @deprecated Use `$.ui.fancytree.getTree()` instead (<a href="Fancytree_Widget.html">example above</a>). 8190 */ 8191 getTree: function () { 8192 this._deprecationWarning("getTree"); 8193 return this.tree; 8194 }, 8195 } 8196 ); 8197 8198 // $.ui.fancytree was created by the widget factory. Create a local shortcut: 8199 FT = $.ui.fancytree; 8200 8201 /** 8202 * Static members in the `$.ui.fancytree` namespace. 8203 * This properties and methods can be accessed without instantiating a concrete 8204 * Fancytree instance. 8205 * 8206 * @example 8207 * // Access static members: 8208 * var node = $.ui.fancytree.getNode(element); 8209 * alert($.ui.fancytree.version); 8210 * 8211 * @mixin Fancytree_Static 8212 */ 8213 $.extend( 8214 $.ui.fancytree, 8215 /** @lends Fancytree_Static# */ 8216 { 8217 /** Version number `"MAJOR.MINOR.PATCH"` 8218 * @type {string} */ 8219 version: "2.38.3", // Set to semver by 'grunt release' 8220 /** @type {string} 8221 * @description `"production" for release builds` */ 8222 buildType: "production", // Set to 'production' by 'grunt build' 8223 /** @type {int} 8224 * @description 0: silent .. 5: verbose (default: 3 for release builds). */ 8225 debugLevel: 3, // Set to 3 by 'grunt build' 8226 // Used by $.ui.fancytree.debug() and as default for tree.options.debugLevel 8227 8228 _nextId: 1, 8229 _nextNodeKey: 1, 8230 _extensions: {}, 8231 // focusTree: null, 8232 8233 /** Expose class object as `$.ui.fancytree._FancytreeClass`. 8234 * Useful to extend `$.ui.fancytree._FancytreeClass.prototype`. 8235 * @type {Fancytree} 8236 */ 8237 _FancytreeClass: Fancytree, 8238 /** Expose class object as $.ui.fancytree._FancytreeNodeClass 8239 * Useful to extend `$.ui.fancytree._FancytreeNodeClass.prototype`. 8240 * @type {FancytreeNode} 8241 */ 8242 _FancytreeNodeClass: FancytreeNode, 8243 /* Feature checks to provide backwards compatibility */ 8244 jquerySupports: { 8245 // http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at 8246 positionMyOfs: isVersionAtLeast($.ui.version, 1, 9), 8247 }, 8248 /** Throw an error if condition fails (debug method). 8249 * @param {boolean} cond 8250 * @param {string} msg 8251 */ 8252 assert: function (cond, msg) { 8253 return _assert(cond, msg); 8254 }, 8255 /** Create a new Fancytree instance on a target element. 8256 * 8257 * @param {Element | jQueryObject | string} el Target DOM element or selector 8258 * @param {FancytreeOptions} [opts] Fancytree options 8259 * @returns {Fancytree} new tree instance 8260 * @example 8261 * var tree = $.ui.fancytree.createTree("#tree", { 8262 * source: {url: "my/webservice"} 8263 * }); // Create tree for this matching element 8264 * 8265 * @since 2.25 8266 */ 8267 createTree: function (el, opts) { 8268 var $tree = $(el).fancytree(opts); 8269 return FT.getTree($tree); 8270 }, 8271 /** Return a function that executes *fn* at most every *timeout* ms. 8272 * @param {integer} timeout 8273 * @param {function} fn 8274 * @param {boolean} [invokeAsap=false] 8275 * @param {any} [ctx] 8276 */ 8277 debounce: function (timeout, fn, invokeAsap, ctx) { 8278 var timer; 8279 if (arguments.length === 3 && typeof invokeAsap !== "boolean") { 8280 ctx = invokeAsap; 8281 invokeAsap = false; 8282 } 8283 return function () { 8284 var args = arguments; 8285 ctx = ctx || this; 8286 // eslint-disable-next-line no-unused-expressions 8287 invokeAsap && !timer && fn.apply(ctx, args); 8288 clearTimeout(timer); 8289 timer = setTimeout(function () { 8290 // eslint-disable-next-line no-unused-expressions 8291 invokeAsap || fn.apply(ctx, args); 8292 timer = null; 8293 }, timeout); 8294 }; 8295 }, 8296 /** Write message to console if debugLevel >= 4 8297 * @param {string} msg 8298 */ 8299 debug: function (msg) { 8300 if ($.ui.fancytree.debugLevel >= 4) { 8301 consoleApply("log", arguments); 8302 } 8303 }, 8304 /** Write error message to console if debugLevel >= 1. 8305 * @param {string} msg 8306 */ 8307 error: function (msg) { 8308 if ($.ui.fancytree.debugLevel >= 1) { 8309 consoleApply("error", arguments); 8310 } 8311 }, 8312 /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities. 8313 * 8314 * @param {string} s 8315 * @returns {string} 8316 */ 8317 escapeHtml: function (s) { 8318 return ("" + s).replace(REX_HTML, function (s) { 8319 return ENTITY_MAP[s]; 8320 }); 8321 }, 8322 /** Make jQuery.position() arguments backwards compatible, i.e. if 8323 * jQuery UI version <= 1.8, convert 8324 * { my: "left+3 center", at: "left bottom", of: $target } 8325 * to 8326 * { my: "left center", at: "left bottom", of: $target, offset: "3 0" } 8327 * 8328 * See http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at 8329 * and http://jsfiddle.net/mar10/6xtu9a4e/ 8330 * 8331 * @param {object} opts 8332 * @returns {object} the (potentially modified) original opts hash object 8333 */ 8334 fixPositionOptions: function (opts) { 8335 if (opts.offset || ("" + opts.my + opts.at).indexOf("%") >= 0) { 8336 $.error( 8337 "expected new position syntax (but '%' is not supported)" 8338 ); 8339 } 8340 if (!$.ui.fancytree.jquerySupports.positionMyOfs) { 8341 var // parse 'left+3 center' into ['left+3 center', 'left', '+3', 'center', undefined] 8342 myParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec( 8343 opts.my 8344 ), 8345 atParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec( 8346 opts.at 8347 ), 8348 // convert to numbers 8349 dx = 8350 (myParts[2] ? +myParts[2] : 0) + 8351 (atParts[2] ? +atParts[2] : 0), 8352 dy = 8353 (myParts[4] ? +myParts[4] : 0) + 8354 (atParts[4] ? +atParts[4] : 0); 8355 8356 opts = $.extend({}, opts, { 8357 // make a copy and overwrite 8358 my: myParts[1] + " " + myParts[3], 8359 at: atParts[1] + " " + atParts[3], 8360 }); 8361 if (dx || dy) { 8362 opts.offset = "" + dx + " " + dy; 8363 } 8364 } 8365 return opts; 8366 }, 8367 /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event. 8368 * 8369 * @param {Event} event Mouse event, e.g. click, ... 8370 * @returns {object} Return a {node: FancytreeNode, type: TYPE} object 8371 * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined 8372 */ 8373 getEventTarget: function (event) { 8374 var $target, 8375 tree, 8376 tcn = event && event.target ? event.target.className : "", 8377 res = { node: this.getNode(event.target), type: undefined }; 8378 // We use a fast version of $(res.node).hasClass() 8379 // See http://jsperf.com/test-for-classname/2 8380 if (/\bfancytree-title\b/.test(tcn)) { 8381 res.type = "title"; 8382 } else if (/\bfancytree-expander\b/.test(tcn)) { 8383 res.type = 8384 res.node.hasChildren() === false 8385 ? "prefix" 8386 : "expander"; 8387 // }else if( /\bfancytree-checkbox\b/.test(tcn) || /\bfancytree-radio\b/.test(tcn) ){ 8388 } else if (/\bfancytree-checkbox\b/.test(tcn)) { 8389 res.type = "checkbox"; 8390 } else if (/\bfancytree(-custom)?-icon\b/.test(tcn)) { 8391 res.type = "icon"; 8392 } else if (/\bfancytree-node\b/.test(tcn)) { 8393 // Somewhere near the title 8394 res.type = "title"; 8395 } else if (event && event.target) { 8396 $target = $(event.target); 8397 if ($target.is("ul[role=group]")) { 8398 // #nnn: Clicking right to a node may hit the surrounding UL 8399 tree = res.node && res.node.tree; 8400 (tree || FT).debug("Ignoring click on outer UL."); 8401 res.node = null; 8402 } else if ($target.closest(".fancytree-title").length) { 8403 // #228: clicking an embedded element inside a title 8404 res.type = "title"; 8405 } else if ($target.closest(".fancytree-checkbox").length) { 8406 // E.g. <svg> inside checkbox span 8407 res.type = "checkbox"; 8408 } else if ($target.closest(".fancytree-expander").length) { 8409 res.type = "expander"; 8410 } 8411 } 8412 return res; 8413 }, 8414 /** Return a string describing the affected node region for a mouse event. 8415 * 8416 * @param {Event} event Mouse event, e.g. click, mousemove, ... 8417 * @returns {string} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined 8418 */ 8419 getEventTargetType: function (event) { 8420 return this.getEventTarget(event).type; 8421 }, 8422 /** Return a FancytreeNode instance from element, event, or jQuery object. 8423 * 8424 * @param {Element | jQueryObject | Event} el 8425 * @returns {FancytreeNode} matching node or null 8426 */ 8427 getNode: function (el) { 8428 if (el instanceof FancytreeNode) { 8429 return el; // el already was a FancytreeNode 8430 } else if (el instanceof $) { 8431 el = el[0]; // el was a jQuery object: use the DOM element 8432 } else if (el.originalEvent !== undefined) { 8433 el = el.target; // el was an Event 8434 } 8435 while (el) { 8436 if (el.ftnode) { 8437 return el.ftnode; 8438 } 8439 el = el.parentNode; 8440 } 8441 return null; 8442 }, 8443 /** Return a Fancytree instance, from element, index, event, or jQueryObject. 8444 * 8445 * @param {Element | jQueryObject | Event | integer | string} [el] 8446 * @returns {Fancytree} matching tree or null 8447 * @example 8448 * $.ui.fancytree.getTree(); // Get first Fancytree instance on page 8449 * $.ui.fancytree.getTree(1); // Get second Fancytree instance on page 8450 * $.ui.fancytree.getTree(event); // Get tree for this mouse- or keyboard event 8451 * $.ui.fancytree.getTree("foo"); // Get tree for this `opts.treeId` 8452 * $.ui.fancytree.getTree("#tree"); // Get tree for this matching element 8453 * 8454 * @since 2.13 8455 */ 8456 getTree: function (el) { 8457 var widget, 8458 orgEl = el; 8459 8460 if (el instanceof Fancytree) { 8461 return el; // el already was a Fancytree 8462 } 8463 if (el === undefined) { 8464 el = 0; // get first tree 8465 } 8466 if (typeof el === "number") { 8467 el = $(".fancytree-container").eq(el); // el was an integer: return nth instance 8468 } else if (typeof el === "string") { 8469 // `el` may be a treeId or a selector: 8470 el = $("#ft-id-" + orgEl).eq(0); 8471 if (!el.length) { 8472 el = $(orgEl).eq(0); // el was a selector: use first match 8473 } 8474 } else if ( 8475 el instanceof Element || 8476 el instanceof HTMLDocument 8477 ) { 8478 el = $(el); 8479 } else if (el instanceof $) { 8480 el = el.eq(0); // el was a jQuery object: use the first 8481 } else if (el.originalEvent !== undefined) { 8482 el = $(el.target); // el was an Event 8483 } 8484 // el is a jQuery object wit one element here 8485 el = el.closest(":ui-fancytree"); 8486 widget = el.data("ui-fancytree") || el.data("fancytree"); // the latter is required by jQuery <= 1.8 8487 return widget ? widget.tree : null; 8488 }, 8489 /** Return an option value that has a default, but may be overridden by a 8490 * callback or a node instance attribute. 8491 * 8492 * Evaluation sequence: 8493 * 8494 * If `tree.options.<optionName>` is a callback that returns something, use that. 8495 * Else if `node.<optionName>` is defined, use that. 8496 * Else if `tree.options.<optionName>` is a value, use that. 8497 * Else use `defaultValue`. 8498 * 8499 * @param {string} optionName name of the option property (on node and tree) 8500 * @param {FancytreeNode} node passed to the callback 8501 * @param {object} nodeObject where to look for the local option property, e.g. `node` or `node.data` 8502 * @param {object} treeOption where to look for the tree option, e.g. `tree.options` or `tree.options.dnd5` 8503 * @param {any} [defaultValue] 8504 * @returns {any} 8505 * 8506 * @example 8507 * // Check for node.foo, tree,options.foo(), and tree.options.foo: 8508 * $.ui.fancytree.evalOption("foo", node, node, tree.options); 8509 * // Check for node.data.bar, tree,options.qux.bar(), and tree.options.qux.bar: 8510 * $.ui.fancytree.evalOption("bar", node, node.data, tree.options.qux); 8511 * 8512 * @since 2.22 8513 */ 8514 evalOption: function ( 8515 optionName, 8516 node, 8517 nodeObject, 8518 treeOptions, 8519 defaultValue 8520 ) { 8521 var ctx, 8522 res, 8523 tree = node.tree, 8524 treeOpt = treeOptions[optionName], 8525 nodeOpt = nodeObject[optionName]; 8526 8527 if (_isFunction(treeOpt)) { 8528 ctx = { 8529 node: node, 8530 tree: tree, 8531 widget: tree.widget, 8532 options: tree.widget.options, 8533 typeInfo: tree.types[node.type] || {}, 8534 }; 8535 res = treeOpt.call(tree, { type: optionName }, ctx); 8536 if (res == null) { 8537 res = nodeOpt; 8538 } 8539 } else { 8540 res = nodeOpt == null ? treeOpt : nodeOpt; 8541 } 8542 if (res == null) { 8543 res = defaultValue; // no option set at all: return default 8544 } 8545 return res; 8546 }, 8547 /** Set expander, checkbox, or node icon, supporting string and object format. 8548 * 8549 * @param {Element | jQueryObject} span 8550 * @param {string} baseClass 8551 * @param {string | object} icon 8552 * @since 2.27 8553 */ 8554 setSpanIcon: function (span, baseClass, icon) { 8555 var $span = $(span); 8556 8557 if (typeof icon === "string") { 8558 $span.attr("class", baseClass + " " + icon); 8559 } else { 8560 // support object syntax: { text: ligature, addClasse: classname } 8561 if (icon.text) { 8562 $span.text("" + icon.text); 8563 } else if (icon.html) { 8564 span.innerHTML = icon.html; 8565 } 8566 $span.attr( 8567 "class", 8568 baseClass + " " + (icon.addClass || "") 8569 ); 8570 } 8571 }, 8572 /** Convert a keydown or mouse event to a canonical string like 'ctrl+a', 8573 * 'ctrl+shift+f2', 'shift+leftdblclick'. 8574 * 8575 * This is especially handy for switch-statements in event handlers. 8576 * 8577 * @param {event} 8578 * @returns {string} 8579 * 8580 * @example 8581 8582 switch( $.ui.fancytree.eventToString(event) ) { 8583 case "-": 8584 tree.nodeSetExpanded(ctx, false); 8585 break; 8586 case "shift+return": 8587 tree.nodeSetActive(ctx, true); 8588 break; 8589 case "down": 8590 res = node.navigate(event.which, activate); 8591 break; 8592 default: 8593 handled = false; 8594 } 8595 if( handled ){ 8596 event.preventDefault(); 8597 } 8598 */ 8599 eventToString: function (event) { 8600 // Poor-man's hotkeys. See here for a complete implementation: 8601 // https://github.com/jeresig/jquery.hotkeys 8602 var which = event.which, 8603 et = event.type, 8604 s = []; 8605 8606 if (event.altKey) { 8607 s.push("alt"); 8608 } 8609 if (event.ctrlKey) { 8610 s.push("ctrl"); 8611 } 8612 if (event.metaKey) { 8613 s.push("meta"); 8614 } 8615 if (event.shiftKey) { 8616 s.push("shift"); 8617 } 8618 8619 if (et === "click" || et === "dblclick") { 8620 s.push(MOUSE_BUTTONS[event.button] + et); 8621 } else if (et === "wheel") { 8622 s.push(et); 8623 } else if (!IGNORE_KEYCODES[which]) { 8624 s.push( 8625 SPECIAL_KEYCODES[which] || 8626 String.fromCharCode(which).toLowerCase() 8627 ); 8628 } 8629 return s.join("+"); 8630 }, 8631 /** Write message to console if debugLevel >= 3 8632 * @param {string} msg 8633 */ 8634 info: function (msg) { 8635 if ($.ui.fancytree.debugLevel >= 3) { 8636 consoleApply("info", arguments); 8637 } 8638 }, 8639 /* @deprecated: use eventToString(event) instead. 8640 */ 8641 keyEventToString: function (event) { 8642 this.warn( 8643 "keyEventToString() is deprecated: use eventToString()" 8644 ); 8645 return this.eventToString(event); 8646 }, 8647 /** Return a wrapped handler method, that provides `this._super`. 8648 * 8649 * @example 8650 // Implement `opts.createNode` event to add the 'draggable' attribute 8651 $.ui.fancytree.overrideMethod(ctx.options, "createNode", function(event, data) { 8652 // Default processing if any 8653 this._super.apply(this, arguments); 8654 // Add 'draggable' attribute 8655 data.node.span.draggable = true; 8656 }); 8657 * 8658 * @param {object} instance 8659 * @param {string} methodName 8660 * @param {function} handler 8661 * @param {object} [context] optional context 8662 */ 8663 overrideMethod: function (instance, methodName, handler, context) { 8664 var prevSuper, 8665 _super = instance[methodName] || $.noop; 8666 8667 instance[methodName] = function () { 8668 var self = context || this; 8669 8670 try { 8671 prevSuper = self._super; 8672 self._super = _super; 8673 return handler.apply(self, arguments); 8674 } finally { 8675 self._super = prevSuper; 8676 } 8677 }; 8678 }, 8679 /** 8680 * Parse tree data from HTML <ul> markup 8681 * 8682 * @param {jQueryObject} $ul 8683 * @returns {NodeData[]} 8684 */ 8685 parseHtml: function ($ul) { 8686 var classes, 8687 className, 8688 extraClasses, 8689 i, 8690 iPos, 8691 l, 8692 tmp, 8693 tmp2, 8694 $children = $ul.find(">li"), 8695 children = []; 8696 8697 $children.each(function () { 8698 var allData, 8699 lowerCaseAttr, 8700 $li = $(this), 8701 $liSpan = $li.find(">span", this).first(), 8702 $liA = $liSpan.length ? null : $li.find(">a").first(), 8703 d = { tooltip: null, data: {} }; 8704 8705 if ($liSpan.length) { 8706 d.title = $liSpan.html(); 8707 } else if ($liA && $liA.length) { 8708 // If a <li><a> tag is specified, use it literally and extract href/target. 8709 d.title = $liA.html(); 8710 d.data.href = $liA.attr("href"); 8711 d.data.target = $liA.attr("target"); 8712 d.tooltip = $liA.attr("title"); 8713 } else { 8714 // If only a <li> tag is specified, use the trimmed string up to 8715 // the next child <ul> tag. 8716 d.title = $li.html(); 8717 iPos = d.title.search(/<ul/i); 8718 if (iPos >= 0) { 8719 d.title = d.title.substring(0, iPos); 8720 } 8721 } 8722 d.title = _trim(d.title); 8723 8724 // Make sure all fields exist 8725 for (i = 0, l = CLASS_ATTRS.length; i < l; i++) { 8726 d[CLASS_ATTRS[i]] = undefined; 8727 } 8728 // Initialize to `true`, if class is set and collect extraClasses 8729 classes = this.className.split(" "); 8730 extraClasses = []; 8731 for (i = 0, l = classes.length; i < l; i++) { 8732 className = classes[i]; 8733 if (CLASS_ATTR_MAP[className]) { 8734 d[className] = true; 8735 } else { 8736 extraClasses.push(className); 8737 } 8738 } 8739 d.extraClasses = extraClasses.join(" "); 8740 8741 // Parse node options from ID, title and class attributes 8742 tmp = $li.attr("title"); 8743 if (tmp) { 8744 d.tooltip = tmp; // overrides <a title='...'> 8745 } 8746 tmp = $li.attr("id"); 8747 if (tmp) { 8748 d.key = tmp; 8749 } 8750 // Translate hideCheckbox -> checkbox:false 8751 if ($li.attr("hideCheckbox")) { 8752 d.checkbox = false; 8753 } 8754 // Add <li data-NAME='...'> as node.data.NAME 8755 allData = _getElementDataAsDict($li); 8756 if (allData && !$.isEmptyObject(allData)) { 8757 // #507: convert data-hidecheckbox (lower case) to hideCheckbox 8758 for (lowerCaseAttr in NODE_ATTR_LOWERCASE_MAP) { 8759 if (_hasProp(allData, lowerCaseAttr)) { 8760 allData[ 8761 NODE_ATTR_LOWERCASE_MAP[lowerCaseAttr] 8762 ] = allData[lowerCaseAttr]; 8763 delete allData[lowerCaseAttr]; 8764 } 8765 } 8766 // #56: Allow to set special node.attributes from data-... 8767 for (i = 0, l = NODE_ATTRS.length; i < l; i++) { 8768 tmp = NODE_ATTRS[i]; 8769 tmp2 = allData[tmp]; 8770 if (tmp2 != null) { 8771 delete allData[tmp]; 8772 d[tmp] = tmp2; 8773 } 8774 } 8775 // All other data-... goes to node.data... 8776 $.extend(d.data, allData); 8777 } 8778 // Recursive reading of child nodes, if LI tag contains an UL tag 8779 $ul = $li.find(">ul").first(); 8780 if ($ul.length) { 8781 d.children = $.ui.fancytree.parseHtml($ul); 8782 } else { 8783 d.children = d.lazy ? undefined : null; 8784 } 8785 children.push(d); 8786 // FT.debug("parse ", d, children); 8787 }); 8788 return children; 8789 }, 8790 /** Add Fancytree extension definition to the list of globally available extensions. 8791 * 8792 * @param {object} definition 8793 */ 8794 registerExtension: function (definition) { 8795 _assert( 8796 definition.name != null, 8797 "extensions must have a `name` property." 8798 ); 8799 _assert( 8800 definition.version != null, 8801 "extensions must have a `version` property." 8802 ); 8803 $.ui.fancytree._extensions[definition.name] = definition; 8804 }, 8805 /** Replacement for the deprecated `jQuery.trim()`. 8806 * 8807 * @param {string} text 8808 */ 8809 trim: _trim, 8810 /** Inverse of escapeHtml(). 8811 * 8812 * @param {string} s 8813 * @returns {string} 8814 */ 8815 unescapeHtml: function (s) { 8816 var e = document.createElement("div"); 8817 e.innerHTML = s; 8818 return e.childNodes.length === 0 8819 ? "" 8820 : e.childNodes[0].nodeValue; 8821 }, 8822 /** Write warning message to console if debugLevel >= 2. 8823 * @param {string} msg 8824 */ 8825 warn: function (msg) { 8826 if ($.ui.fancytree.debugLevel >= 2) { 8827 consoleApply("warn", arguments); 8828 } 8829 }, 8830 } 8831 ); 8832 8833 // Value returned by `require('jquery.fancytree')` 8834 return $.ui.fancytree; 8835}); // End of closure 8836 8837 8838/*! Extension 'jquery.fancytree.childcounter.js' */// Extending Fancytree 8839// =================== 8840// 8841// See also the [live demo](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html) of this code. 8842// 8843// Every extension should have a comment header containing some information 8844// about the author, copyright and licensing. Also a pointer to the latest 8845// source code. 8846// Prefix with `/*!` so the comment is not removed by the minifier. 8847 8848/*! 8849 * jquery.fancytree.childcounter.js 8850 * 8851 * Add a child counter bubble to tree nodes. 8852 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 8853 * 8854 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 8855 * 8856 * Released under the MIT license 8857 * https://github.com/mar10/fancytree/wiki/LicenseInfo 8858 * 8859 * @version 2.38.3 8860 * @date 2023-02-01T20:52:50Z 8861 */ 8862 8863// To keep the global namespace clean, we wrap everything in a closure. 8864// The UMD wrapper pattern defines the dependencies on jQuery and the 8865// Fancytree core module, and makes sure that we can use the `require()` 8866// syntax with package loaders. 8867 8868(function (factory) { 8869 if (typeof define === "function" && define.amd) { 8870 // AMD. Register as an anonymous module. 8871 define(["jquery", "./jquery.fancytree"], factory); 8872 } else if (typeof module === "object" && module.exports) { 8873 // Node/CommonJS 8874 require("./jquery.fancytree"); 8875 module.exports = factory(require("jquery")); 8876 } else { 8877 // Browser globals 8878 factory(jQuery); 8879 } 8880})(function ($) { 8881 // Consider to use [strict mode](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/) 8882 "use strict"; 8883 8884 // The [coding guidelines](http://contribute.jquery.org/style-guide/js/) 8885 // require jshint /eslint compliance. 8886 // But for this sample, we want to allow unused variables for demonstration purpose. 8887 8888 /*eslint-disable no-unused-vars */ 8889 8890 // Adding methods 8891 // -------------- 8892 8893 // New member functions can be added to the `Fancytree` class. 8894 // This function will be available for every tree instance: 8895 // 8896 // var tree = $.ui.fancytree.getTree("#tree"); 8897 // tree.countSelected(false); 8898 8899 $.ui.fancytree._FancytreeClass.prototype.countSelected = function ( 8900 topOnly 8901 ) { 8902 var tree = this, 8903 treeOptions = tree.options; 8904 8905 return tree.getSelectedNodes(topOnly).length; 8906 }; 8907 8908 // The `FancytreeNode` class can also be easily extended. This would be called 8909 // like 8910 // node.updateCounters(); 8911 // 8912 // It is also good practice to add a docstring comment. 8913 /** 8914 * [ext-childcounter] Update counter badges for `node` and its parents. 8915 * May be called in the `loadChildren` event, to update parents of lazy loaded 8916 * nodes. 8917 * @alias FancytreeNode#updateCounters 8918 * @requires jquery.fancytree.childcounters.js 8919 */ 8920 $.ui.fancytree._FancytreeNodeClass.prototype.updateCounters = function () { 8921 var node = this, 8922 $badge = $("span.fancytree-childcounter", node.span), 8923 extOpts = node.tree.options.childcounter, 8924 count = node.countChildren(extOpts.deep); 8925 8926 node.data.childCounter = count; 8927 if ( 8928 (count || !extOpts.hideZeros) && 8929 (!node.isExpanded() || !extOpts.hideExpanded) 8930 ) { 8931 if (!$badge.length) { 8932 $badge = $("<span class='fancytree-childcounter'/>").appendTo( 8933 $( 8934 "span.fancytree-icon,span.fancytree-custom-icon", 8935 node.span 8936 ) 8937 ); 8938 } 8939 $badge.text(count); 8940 } else { 8941 $badge.remove(); 8942 } 8943 if (extOpts.deep && !node.isTopLevel() && !node.isRootNode()) { 8944 node.parent.updateCounters(); 8945 } 8946 }; 8947 8948 // Finally, we can extend the widget API and create functions that are called 8949 // like so: 8950 // 8951 // $("#tree").fancytree("widgetMethod1", "abc"); 8952 8953 $.ui.fancytree.prototype.widgetMethod1 = function (arg1) { 8954 var tree = this.tree; 8955 return arg1; 8956 }; 8957 8958 // Register a Fancytree extension 8959 // ------------------------------ 8960 // A full blown extension, extension is available for all trees and can be 8961 // enabled like so (see also the [live demo](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html)): 8962 // 8963 // <script src="../src/jquery.fancytree.js"></script> 8964 // <script src="../src/jquery.fancytree.childcounter.js"></script> 8965 // ... 8966 // 8967 // $("#tree").fancytree({ 8968 // extensions: ["childcounter"], 8969 // childcounter: { 8970 // hideExpanded: true 8971 // }, 8972 // ... 8973 // }); 8974 // 8975 8976 /* 'childcounter' extension */ 8977 $.ui.fancytree.registerExtension({ 8978 // Every extension must be registered by a unique name. 8979 name: "childcounter", 8980 // Version information should be compliant with [semver](http://semver.org) 8981 version: "2.38.3", 8982 8983 // Extension specific options and their defaults. 8984 // This options will be available as `tree.options.childcounter.hideExpanded` 8985 8986 options: { 8987 deep: true, 8988 hideZeros: true, 8989 hideExpanded: false, 8990 }, 8991 8992 // Attributes other than `options` (or functions) can be defined here, and 8993 // will be added to the tree.ext.EXTNAME namespace, in this case `tree.ext.childcounter.foo`. 8994 // They can also be accessed as `this._local.foo` from within the extension 8995 // methods. 8996 foo: 42, 8997 8998 // Local functions are prefixed with an underscore '_'. 8999 // Callable as `this._local._appendCounter()`. 9000 9001 _appendCounter: function (bar) { 9002 var tree = this; 9003 }, 9004 9005 // **Override virtual methods for this extension.** 9006 // 9007 // Fancytree implements a number of 'hook methods', prefixed by 'node...' or 'tree...'. 9008 // with a `ctx` argument (see [EventData](https://wwWendt.de/tech/fancytree/doc/jsdoc/global.html#EventData) 9009 // for details) and an extended calling context:<br> 9010 // `this` : the Fancytree instance<br> 9011 // `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME)<br> 9012 // `this._super`: the virtual function that was overridden (member of previous extension or Fancytree) 9013 // 9014 // See also the [complete list of available hook functions](https://wwWendt.de/tech/fancytree/doc/jsdoc/Fancytree_Hooks.html). 9015 9016 /* Init */ 9017 // `treeInit` is triggered when a tree is initalized. We can set up classes or 9018 // bind event handlers here... 9019 treeInit: function (ctx) { 9020 var tree = this, // same as ctx.tree, 9021 opts = ctx.options, 9022 extOpts = ctx.options.childcounter; 9023 // Optionally check for dependencies with other extensions 9024 /* this._requireExtension("glyph", false, false); */ 9025 // Call the base implementation 9026 this._superApply(arguments); 9027 // Add a class to the tree container 9028 this.$container.addClass("fancytree-ext-childcounter"); 9029 }, 9030 9031 // Destroy this tree instance (we only call the default implementation, so 9032 // this method could as well be omitted). 9033 9034 treeDestroy: function (ctx) { 9035 this._superApply(arguments); 9036 }, 9037 9038 // Overload the `renderTitle` hook, to append a counter badge 9039 nodeRenderTitle: function (ctx, title) { 9040 var node = ctx.node, 9041 extOpts = ctx.options.childcounter, 9042 count = 9043 node.data.childCounter == null 9044 ? node.countChildren(extOpts.deep) 9045 : +node.data.childCounter; 9046 // Let the base implementation render the title 9047 // We use `_super()` instead of `_superApply()` here, since it is a little bit 9048 // more performant when called often 9049 this._super(ctx, title); 9050 // Append a counter badge 9051 if ( 9052 (count || !extOpts.hideZeros) && 9053 (!node.isExpanded() || !extOpts.hideExpanded) 9054 ) { 9055 $( 9056 "span.fancytree-icon,span.fancytree-custom-icon", 9057 node.span 9058 ).append( 9059 $("<span class='fancytree-childcounter'/>").text(count) 9060 ); 9061 } 9062 }, 9063 // Overload the `setExpanded` hook, so the counters are updated 9064 nodeSetExpanded: function (ctx, flag, callOpts) { 9065 var tree = ctx.tree, 9066 node = ctx.node; 9067 // Let the base implementation expand/collapse the node, then redraw the title 9068 // after the animation has finished 9069 return this._superApply(arguments).always(function () { 9070 tree.nodeRenderTitle(ctx); 9071 }); 9072 }, 9073 9074 // End of extension definition 9075 }); 9076 // Value returned by `require('jquery.fancytree..')` 9077 return $.ui.fancytree; 9078}); // End of closure 9079 9080 9081/*! Extension 'jquery.fancytree.clones.js' *//*! 9082 * 9083 * jquery.fancytree.clones.js 9084 * Support faster lookup of nodes by key and shared ref-ids. 9085 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 9086 * 9087 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 9088 * 9089 * Released under the MIT license 9090 * https://github.com/mar10/fancytree/wiki/LicenseInfo 9091 * 9092 * @version 2.38.3 9093 * @date 2023-02-01T20:52:50Z 9094 */ 9095 9096(function (factory) { 9097 if (typeof define === "function" && define.amd) { 9098 // AMD. Register as an anonymous module. 9099 define(["jquery", "./jquery.fancytree"], factory); 9100 } else if (typeof module === "object" && module.exports) { 9101 // Node/CommonJS 9102 require("./jquery.fancytree"); 9103 module.exports = factory(require("jquery")); 9104 } else { 9105 // Browser globals 9106 factory(jQuery); 9107 } 9108})(function ($) { 9109 "use strict"; 9110 9111 /******************************************************************************* 9112 * Private functions and variables 9113 */ 9114 9115 var _assert = $.ui.fancytree.assert; 9116 9117 /* Return first occurrence of member from array. */ 9118 function _removeArrayMember(arr, elem) { 9119 // TODO: use Array.indexOf for IE >= 9 9120 var i; 9121 for (i = arr.length - 1; i >= 0; i--) { 9122 if (arr[i] === elem) { 9123 arr.splice(i, 1); 9124 return true; 9125 } 9126 } 9127 return false; 9128 } 9129 9130 /** 9131 * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011) 9132 * 9133 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a> 9134 * @see http://github.com/garycourt/murmurhash-js 9135 * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a> 9136 * @see http://sites.google.com/site/murmurhash/ 9137 * 9138 * @param {string} key ASCII only 9139 * @param {boolean} [asString=false] 9140 * @param {number} seed Positive integer only 9141 * @return {number} 32-bit positive integer hash 9142 */ 9143 function hashMurmur3(key, asString, seed) { 9144 /*eslint-disable no-bitwise */ 9145 var h1b, 9146 k1, 9147 remainder = key.length & 3, 9148 bytes = key.length - remainder, 9149 h1 = seed, 9150 c1 = 0xcc9e2d51, 9151 c2 = 0x1b873593, 9152 i = 0; 9153 9154 while (i < bytes) { 9155 k1 = 9156 (key.charCodeAt(i) & 0xff) | 9157 ((key.charCodeAt(++i) & 0xff) << 8) | 9158 ((key.charCodeAt(++i) & 0xff) << 16) | 9159 ((key.charCodeAt(++i) & 0xff) << 24); 9160 ++i; 9161 9162 k1 = 9163 ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 9164 0xffffffff; 9165 k1 = (k1 << 15) | (k1 >>> 17); 9166 k1 = 9167 ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 9168 0xffffffff; 9169 9170 h1 ^= k1; 9171 h1 = (h1 << 13) | (h1 >>> 19); 9172 h1b = 9173 ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 9174 0xffffffff; 9175 h1 = 9176 (h1b & 0xffff) + 9177 0x6b64 + 9178 ((((h1b >>> 16) + 0xe654) & 0xffff) << 16); 9179 } 9180 9181 k1 = 0; 9182 9183 switch (remainder) { 9184 case 3: 9185 k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16; 9186 // fall through 9187 case 2: 9188 k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8; 9189 // fall through 9190 case 1: 9191 k1 ^= key.charCodeAt(i) & 0xff; 9192 9193 k1 = 9194 ((k1 & 0xffff) * c1 + 9195 ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 9196 0xffffffff; 9197 k1 = (k1 << 15) | (k1 >>> 17); 9198 k1 = 9199 ((k1 & 0xffff) * c2 + 9200 ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 9201 0xffffffff; 9202 h1 ^= k1; 9203 } 9204 9205 h1 ^= key.length; 9206 9207 h1 ^= h1 >>> 16; 9208 h1 = 9209 ((h1 & 0xffff) * 0x85ebca6b + 9210 ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 9211 0xffffffff; 9212 h1 ^= h1 >>> 13; 9213 h1 = 9214 ((h1 & 0xffff) * 0xc2b2ae35 + 9215 ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 9216 0xffffffff; 9217 h1 ^= h1 >>> 16; 9218 9219 if (asString) { 9220 // Convert to 8 digit hex string 9221 return ("0000000" + (h1 >>> 0).toString(16)).substr(-8); 9222 } 9223 return h1 >>> 0; 9224 /*eslint-enable no-bitwise */ 9225 } 9226 9227 /* 9228 * Return a unique key for node by calculating the hash of the parents refKey-list. 9229 */ 9230 function calcUniqueKey(node) { 9231 var key, 9232 h1, 9233 path = $.map(node.getParentList(false, true), function (e) { 9234 return e.refKey || e.key; 9235 }); 9236 9237 path = path.join("/"); 9238 // 32-bit has a high probability of collisions, so we pump up to 64-bit 9239 // https://security.stackexchange.com/q/209882/207588 9240 9241 h1 = hashMurmur3(path, true); 9242 key = "id_" + h1 + hashMurmur3(h1 + path, true); 9243 9244 return key; 9245 } 9246 9247 /** 9248 * [ext-clones] Return a list of clone-nodes (i.e. same refKey) or null. 9249 * @param {boolean} [includeSelf=false] 9250 * @returns {FancytreeNode[] | null} 9251 * 9252 * @alias FancytreeNode#getCloneList 9253 * @requires jquery.fancytree.clones.js 9254 */ 9255 $.ui.fancytree._FancytreeNodeClass.prototype.getCloneList = function ( 9256 includeSelf 9257 ) { 9258 var key, 9259 tree = this.tree, 9260 refList = tree.refMap[this.refKey] || null, 9261 keyMap = tree.keyMap; 9262 9263 if (refList) { 9264 key = this.key; 9265 // Convert key list to node list 9266 if (includeSelf) { 9267 refList = $.map(refList, function (val) { 9268 return keyMap[val]; 9269 }); 9270 } else { 9271 refList = $.map(refList, function (val) { 9272 return val === key ? null : keyMap[val]; 9273 }); 9274 if (refList.length < 1) { 9275 refList = null; 9276 } 9277 } 9278 } 9279 return refList; 9280 }; 9281 9282 /** 9283 * [ext-clones] Return true if this node has at least another clone with same refKey. 9284 * @returns {boolean} 9285 * 9286 * @alias FancytreeNode#isClone 9287 * @requires jquery.fancytree.clones.js 9288 */ 9289 $.ui.fancytree._FancytreeNodeClass.prototype.isClone = function () { 9290 var refKey = this.refKey || null, 9291 refList = (refKey && this.tree.refMap[refKey]) || null; 9292 return !!(refList && refList.length > 1); 9293 }; 9294 9295 /** 9296 * [ext-clones] Update key and/or refKey for an existing node. 9297 * @param {string} key 9298 * @param {string} refKey 9299 * @returns {boolean} 9300 * 9301 * @alias FancytreeNode#reRegister 9302 * @requires jquery.fancytree.clones.js 9303 */ 9304 $.ui.fancytree._FancytreeNodeClass.prototype.reRegister = function ( 9305 key, 9306 refKey 9307 ) { 9308 key = key == null ? null : "" + key; 9309 refKey = refKey == null ? null : "" + refKey; 9310 // this.debug("reRegister", key, refKey); 9311 9312 var tree = this.tree, 9313 prevKey = this.key, 9314 prevRefKey = this.refKey, 9315 keyMap = tree.keyMap, 9316 refMap = tree.refMap, 9317 refList = refMap[prevRefKey] || null, 9318 // curCloneKeys = refList ? node.getCloneList(true), 9319 modified = false; 9320 9321 // Key has changed: update all references 9322 if (key != null && key !== this.key) { 9323 if (keyMap[key]) { 9324 $.error( 9325 "[ext-clones] reRegister(" + 9326 key + 9327 "): already exists: " + 9328 this 9329 ); 9330 } 9331 // Update keyMap 9332 delete keyMap[prevKey]; 9333 keyMap[key] = this; 9334 // Update refMap 9335 if (refList) { 9336 refMap[prevRefKey] = $.map(refList, function (e) { 9337 return e === prevKey ? key : e; 9338 }); 9339 } 9340 this.key = key; 9341 modified = true; 9342 } 9343 9344 // refKey has changed 9345 if (refKey != null && refKey !== this.refKey) { 9346 // Remove previous refKeys 9347 if (refList) { 9348 if (refList.length === 1) { 9349 delete refMap[prevRefKey]; 9350 } else { 9351 refMap[prevRefKey] = $.map(refList, function (e) { 9352 return e === prevKey ? null : e; 9353 }); 9354 } 9355 } 9356 // Add refKey 9357 if (refMap[refKey]) { 9358 refMap[refKey].append(key); 9359 } else { 9360 refMap[refKey] = [this.key]; 9361 } 9362 this.refKey = refKey; 9363 modified = true; 9364 } 9365 return modified; 9366 }; 9367 9368 /** 9369 * [ext-clones] Define a refKey for an existing node. 9370 * @param {string} refKey 9371 * @returns {boolean} 9372 * 9373 * @alias FancytreeNode#setRefKey 9374 * @requires jquery.fancytree.clones.js 9375 * @since 2.16 9376 */ 9377 $.ui.fancytree._FancytreeNodeClass.prototype.setRefKey = function (refKey) { 9378 return this.reRegister(null, refKey); 9379 }; 9380 9381 /** 9382 * [ext-clones] Return all nodes with a given refKey (null if not found). 9383 * @param {string} refKey 9384 * @param {FancytreeNode} [rootNode] optionally restrict results to descendants of this node 9385 * @returns {FancytreeNode[] | null} 9386 * @alias Fancytree#getNodesByRef 9387 * @requires jquery.fancytree.clones.js 9388 */ 9389 $.ui.fancytree._FancytreeClass.prototype.getNodesByRef = function ( 9390 refKey, 9391 rootNode 9392 ) { 9393 var keyMap = this.keyMap, 9394 refList = this.refMap[refKey] || null; 9395 9396 if (refList) { 9397 // Convert key list to node list 9398 if (rootNode) { 9399 refList = $.map(refList, function (val) { 9400 var node = keyMap[val]; 9401 return node.isDescendantOf(rootNode) ? node : null; 9402 }); 9403 } else { 9404 refList = $.map(refList, function (val) { 9405 return keyMap[val]; 9406 }); 9407 } 9408 if (refList.length < 1) { 9409 refList = null; 9410 } 9411 } 9412 return refList; 9413 }; 9414 9415 /** 9416 * [ext-clones] Replace a refKey with a new one. 9417 * @param {string} oldRefKey 9418 * @param {string} newRefKey 9419 * @alias Fancytree#changeRefKey 9420 * @requires jquery.fancytree.clones.js 9421 */ 9422 $.ui.fancytree._FancytreeClass.prototype.changeRefKey = function ( 9423 oldRefKey, 9424 newRefKey 9425 ) { 9426 var i, 9427 node, 9428 keyMap = this.keyMap, 9429 refList = this.refMap[oldRefKey] || null; 9430 9431 if (refList) { 9432 for (i = 0; i < refList.length; i++) { 9433 node = keyMap[refList[i]]; 9434 node.refKey = newRefKey; 9435 } 9436 delete this.refMap[oldRefKey]; 9437 this.refMap[newRefKey] = refList; 9438 } 9439 }; 9440 9441 /******************************************************************************* 9442 * Extension code 9443 */ 9444 $.ui.fancytree.registerExtension({ 9445 name: "clones", 9446 version: "2.38.3", 9447 // Default options for this extension. 9448 options: { 9449 highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers 9450 highlightClones: false, // set 'fancytree-clone' class on any node that has at least one clone 9451 }, 9452 9453 treeCreate: function (ctx) { 9454 this._superApply(arguments); 9455 ctx.tree.refMap = {}; 9456 ctx.tree.keyMap = {}; 9457 }, 9458 treeInit: function (ctx) { 9459 this.$container.addClass("fancytree-ext-clones"); 9460 _assert(ctx.options.defaultKey == null); 9461 // Generate unique / reproducible default keys 9462 ctx.options.defaultKey = function (node) { 9463 return calcUniqueKey(node); 9464 }; 9465 // The default implementation loads initial data 9466 this._superApply(arguments); 9467 }, 9468 treeClear: function (ctx) { 9469 ctx.tree.refMap = {}; 9470 ctx.tree.keyMap = {}; 9471 return this._superApply(arguments); 9472 }, 9473 treeRegisterNode: function (ctx, add, node) { 9474 var refList, 9475 len, 9476 tree = ctx.tree, 9477 keyMap = tree.keyMap, 9478 refMap = tree.refMap, 9479 key = node.key, 9480 refKey = node && node.refKey != null ? "" + node.refKey : null; 9481 9482 // ctx.tree.debug("clones.treeRegisterNode", add, node); 9483 9484 if (node.isStatusNode()) { 9485 return this._super(ctx, add, node); 9486 } 9487 9488 if (add) { 9489 if (keyMap[node.key] != null) { 9490 var other = keyMap[node.key], 9491 msg = 9492 "clones.treeRegisterNode: duplicate key '" + 9493 node.key + 9494 "': /" + 9495 node.getPath(true) + 9496 " => " + 9497 other.getPath(true); 9498 // Sometimes this exception is not visible in the console, 9499 // so we also write it: 9500 tree.error(msg); 9501 $.error(msg); 9502 } 9503 keyMap[key] = node; 9504 9505 if (refKey) { 9506 refList = refMap[refKey]; 9507 if (refList) { 9508 refList.push(key); 9509 if ( 9510 refList.length === 2 && 9511 ctx.options.clones.highlightClones 9512 ) { 9513 // Mark peer node, if it just became a clone (no need to 9514 // mark current node, since it will be rendered later anyway) 9515 keyMap[refList[0]].renderStatus(); 9516 } 9517 } else { 9518 refMap[refKey] = [key]; 9519 } 9520 // node.debug("clones.treeRegisterNode: add clone =>", refMap[refKey]); 9521 } 9522 } else { 9523 if (keyMap[key] == null) { 9524 $.error( 9525 "clones.treeRegisterNode: node.key not registered: " + 9526 node.key 9527 ); 9528 } 9529 delete keyMap[key]; 9530 if (refKey) { 9531 refList = refMap[refKey]; 9532 // node.debug("clones.treeRegisterNode: remove clone BEFORE =>", refMap[refKey]); 9533 if (refList) { 9534 len = refList.length; 9535 if (len <= 1) { 9536 _assert(len === 1); 9537 _assert(refList[0] === key); 9538 delete refMap[refKey]; 9539 } else { 9540 _removeArrayMember(refList, key); 9541 // Unmark peer node, if this was the only clone 9542 if ( 9543 len === 2 && 9544 ctx.options.clones.highlightClones 9545 ) { 9546 // node.debug("clones.treeRegisterNode: last =>", node.getCloneList()); 9547 keyMap[refList[0]].renderStatus(); 9548 } 9549 } 9550 // node.debug("clones.treeRegisterNode: remove clone =>", refMap[refKey]); 9551 } 9552 } 9553 } 9554 return this._super(ctx, add, node); 9555 }, 9556 nodeRenderStatus: function (ctx) { 9557 var $span, 9558 res, 9559 node = ctx.node; 9560 9561 res = this._super(ctx); 9562 9563 if (ctx.options.clones.highlightClones) { 9564 $span = $(node[ctx.tree.statusClassPropName]); 9565 // Only if span already exists 9566 if ($span.length && node.isClone()) { 9567 // node.debug("clones.nodeRenderStatus: ", ctx.options.clones.highlightClones); 9568 $span.addClass("fancytree-clone"); 9569 } 9570 } 9571 return res; 9572 }, 9573 nodeSetActive: function (ctx, flag, callOpts) { 9574 var res, 9575 scpn = ctx.tree.statusClassPropName, 9576 node = ctx.node; 9577 9578 res = this._superApply(arguments); 9579 9580 if (ctx.options.clones.highlightActiveClones && node.isClone()) { 9581 $.each(node.getCloneList(true), function (idx, n) { 9582 // n.debug("clones.nodeSetActive: ", flag !== false); 9583 $(n[scpn]).toggleClass( 9584 "fancytree-active-clone", 9585 flag !== false 9586 ); 9587 }); 9588 } 9589 return res; 9590 }, 9591 }); 9592 // Value returned by `require('jquery.fancytree..')` 9593 return $.ui.fancytree; 9594}); // End of closure 9595 9596 9597/*! Extension 'jquery.fancytree.dnd5.js' *//*! 9598 * jquery.fancytree.dnd5.js 9599 * 9600 * Drag-and-drop support (native HTML5). 9601 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 9602 * 9603 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 9604 * 9605 * Released under the MIT license 9606 * https://github.com/mar10/fancytree/wiki/LicenseInfo 9607 * 9608 * @version 2.38.3 9609 * @date 2023-02-01T20:52:50Z 9610 */ 9611 9612/* 9613 #TODO 9614 Compatiblity when dragging between *separate* windows: 9615 9616 Drag from Chrome Edge FF IE11 Safari 9617 To Chrome ok ok ok NO ? 9618 Edge ok ok ok NO ? 9619 FF ok ok ok NO ? 9620 IE 11 ok ok ok ok ? 9621 Safari ? ? ? ? ok 9622 9623 */ 9624 9625(function (factory) { 9626 if (typeof define === "function" && define.amd) { 9627 // AMD. Register as an anonymous module. 9628 define(["jquery", "./jquery.fancytree"], factory); 9629 } else if (typeof module === "object" && module.exports) { 9630 // Node/CommonJS 9631 require("./jquery.fancytree"); 9632 module.exports = factory(require("jquery")); 9633 } else { 9634 // Browser globals 9635 factory(jQuery); 9636 } 9637})(function ($) { 9638 "use strict"; 9639 9640 /****************************************************************************** 9641 * Private functions and variables 9642 */ 9643 var FT = $.ui.fancytree, 9644 isMac = /Mac/.test(navigator.platform), 9645 classDragSource = "fancytree-drag-source", 9646 classDragRemove = "fancytree-drag-remove", 9647 classDropAccept = "fancytree-drop-accept", 9648 classDropAfter = "fancytree-drop-after", 9649 classDropBefore = "fancytree-drop-before", 9650 classDropOver = "fancytree-drop-over", 9651 classDropReject = "fancytree-drop-reject", 9652 classDropTarget = "fancytree-drop-target", 9653 nodeMimeType = "application/x-fancytree-node", 9654 $dropMarker = null, 9655 $dragImage, 9656 $extraHelper, 9657 SOURCE_NODE = null, 9658 SOURCE_NODE_LIST = null, 9659 $sourceList = null, 9660 DRAG_ENTER_RESPONSE = null, 9661 // SESSION_DATA = null, // plain object passed to events as `data` 9662 SUGGESTED_DROP_EFFECT = null, 9663 REQUESTED_DROP_EFFECT = null, 9664 REQUESTED_EFFECT_ALLOWED = null, 9665 LAST_HIT_MODE = null, 9666 DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode 9667 9668 /* */ 9669 function _clearGlobals() { 9670 DRAG_ENTER_RESPONSE = null; 9671 DRAG_OVER_STAMP = null; 9672 REQUESTED_DROP_EFFECT = null; 9673 REQUESTED_EFFECT_ALLOWED = null; 9674 SUGGESTED_DROP_EFFECT = null; 9675 SOURCE_NODE = null; 9676 SOURCE_NODE_LIST = null; 9677 if ($sourceList) { 9678 $sourceList.removeClass(classDragSource + " " + classDragRemove); 9679 } 9680 $sourceList = null; 9681 if ($dropMarker) { 9682 $dropMarker.hide(); 9683 } 9684 // Take this badge off of me - I can't use it anymore: 9685 if ($extraHelper) { 9686 $extraHelper.remove(); 9687 $extraHelper = null; 9688 } 9689 } 9690 9691 /* Convert number to string and prepend +/-; return empty string for 0.*/ 9692 function offsetString(n) { 9693 // eslint-disable-next-line no-nested-ternary 9694 return n === 0 ? "" : n > 0 ? "+" + n : "" + n; 9695 } 9696 9697 /* Convert a dragEnter() or dragOver() response to a canonical form. 9698 * Return false or plain object 9699 * @param {string|object|boolean} r 9700 * @return {object|false} 9701 */ 9702 function normalizeDragEnterResponse(r) { 9703 var res; 9704 9705 if (!r) { 9706 return false; 9707 } 9708 if ($.isPlainObject(r)) { 9709 res = { 9710 over: !!r.over, 9711 before: !!r.before, 9712 after: !!r.after, 9713 }; 9714 } else if (Array.isArray(r)) { 9715 res = { 9716 over: $.inArray("over", r) >= 0, 9717 before: $.inArray("before", r) >= 0, 9718 after: $.inArray("after", r) >= 0, 9719 }; 9720 } else { 9721 res = { 9722 over: r === true || r === "over", 9723 before: r === true || r === "before", 9724 after: r === true || r === "after", 9725 }; 9726 } 9727 if (Object.keys(res).length === 0) { 9728 return false; 9729 } 9730 // if( Object.keys(res).length === 1 ) { 9731 // res.unique = res[0]; 9732 // } 9733 return res; 9734 } 9735 9736 /* Convert a dataTransfer.effectAllowed to a canonical form. 9737 * Return false or plain object 9738 * @param {string|boolean} r 9739 * @return {object|false} 9740 */ 9741 // function normalizeEffectAllowed(r) { 9742 // if (!r || r === "none") { 9743 // return false; 9744 // } 9745 // var all = r === "all", 9746 // res = { 9747 // copy: all || /copy/i.test(r), 9748 // link: all || /link/i.test(r), 9749 // move: all || /move/i.test(r), 9750 // }; 9751 9752 // return res; 9753 // } 9754 9755 /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */ 9756 function autoScroll(tree, event) { 9757 var spOfs, 9758 scrollTop, 9759 delta, 9760 dndOpts = tree.options.dnd5, 9761 sp = tree.$scrollParent[0], 9762 sensitivity = dndOpts.scrollSensitivity, 9763 speed = dndOpts.scrollSpeed, 9764 scrolled = 0; 9765 9766 if (sp !== document && sp.tagName !== "HTML") { 9767 spOfs = tree.$scrollParent.offset(); 9768 scrollTop = sp.scrollTop; 9769 if (spOfs.top + sp.offsetHeight - event.pageY < sensitivity) { 9770 delta = 9771 sp.scrollHeight - 9772 tree.$scrollParent.innerHeight() - 9773 scrollTop; 9774 // console.log ("sp.offsetHeight: " + sp.offsetHeight 9775 // + ", spOfs.top: " + spOfs.top 9776 // + ", scrollTop: " + scrollTop 9777 // + ", innerHeight: " + tree.$scrollParent.innerHeight() 9778 // + ", scrollHeight: " + sp.scrollHeight 9779 // + ", delta: " + delta 9780 // ); 9781 if (delta > 0) { 9782 sp.scrollTop = scrolled = scrollTop + speed; 9783 } 9784 } else if (scrollTop > 0 && event.pageY - spOfs.top < sensitivity) { 9785 sp.scrollTop = scrolled = scrollTop - speed; 9786 } 9787 } else { 9788 scrollTop = $(document).scrollTop(); 9789 if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) { 9790 scrolled = scrollTop - speed; 9791 $(document).scrollTop(scrolled); 9792 } else if ( 9793 $(window).height() - (event.pageY - scrollTop) < 9794 sensitivity 9795 ) { 9796 scrolled = scrollTop + speed; 9797 $(document).scrollTop(scrolled); 9798 } 9799 } 9800 if (scrolled) { 9801 tree.debug("autoScroll: " + scrolled + "px"); 9802 } 9803 return scrolled; 9804 } 9805 9806 /* Guess dropEffect from modifier keys. 9807 * Using rules suggested here: 9808 * https://ux.stackexchange.com/a/83769 9809 * @returns 9810 * 'copy', 'link', 'move', or 'none' 9811 */ 9812 function evalEffectModifiers(tree, event, effectDefault) { 9813 var res = effectDefault; 9814 9815 if (isMac) { 9816 if (event.metaKey && event.altKey) { 9817 // Mac: [Control] + [Option] 9818 res = "link"; 9819 } else if (event.ctrlKey) { 9820 // Chrome on Mac: [Control] 9821 res = "link"; 9822 } else if (event.metaKey) { 9823 // Mac: [Command] 9824 res = "move"; 9825 } else if (event.altKey) { 9826 // Mac: [Option] 9827 res = "copy"; 9828 } 9829 } else { 9830 if (event.ctrlKey) { 9831 // Windows: [Ctrl] 9832 res = "copy"; 9833 } else if (event.shiftKey) { 9834 // Windows: [Shift] 9835 res = "move"; 9836 } else if (event.altKey) { 9837 // Windows: [Alt] 9838 res = "link"; 9839 } 9840 } 9841 if (res !== SUGGESTED_DROP_EFFECT) { 9842 tree.info( 9843 "evalEffectModifiers: " + 9844 event.type + 9845 " - evalEffectModifiers(): " + 9846 SUGGESTED_DROP_EFFECT + 9847 " -> " + 9848 res 9849 ); 9850 } 9851 SUGGESTED_DROP_EFFECT = res; 9852 // tree.debug("evalEffectModifiers: " + res); 9853 return res; 9854 } 9855 /* 9856 * Check if the previous callback (dragEnter, dragOver, ...) has changed 9857 * the `data` object and apply those settings. 9858 * 9859 * Safari: 9860 * It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain 9861 * even if the cursor changes when [Alt] or [Ctrl] are pressed (?) 9862 * Using rules suggested here: 9863 * https://ux.stackexchange.com/a/83769 9864 * @returns 9865 * 'copy', 'link', 'move', or 'none' 9866 */ 9867 function prepareDropEffectCallback(event, data) { 9868 var tree = data.tree, 9869 dataTransfer = data.dataTransfer; 9870 9871 if (event.type === "dragstart") { 9872 data.effectAllowed = tree.options.dnd5.effectAllowed; 9873 data.dropEffect = tree.options.dnd5.dropEffectDefault; 9874 } else { 9875 data.effectAllowed = REQUESTED_EFFECT_ALLOWED; 9876 data.dropEffect = REQUESTED_DROP_EFFECT; 9877 } 9878 data.dropEffectSuggested = evalEffectModifiers( 9879 tree, 9880 event, 9881 tree.options.dnd5.dropEffectDefault 9882 ); 9883 data.isMove = data.dropEffect === "move"; 9884 data.files = dataTransfer.files || []; 9885 9886 // if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) { 9887 // tree.warn( 9888 // "prepareDropEffectCallback(" + 9889 // event.type + 9890 // "): dataTransfer.effectAllowed changed from " + 9891 // REQUESTED_EFFECT_ALLOWED + 9892 // " -> " + 9893 // dataTransfer.effectAllowed 9894 // ); 9895 // } 9896 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 9897 // tree.warn( 9898 // "prepareDropEffectCallback(" + 9899 // event.type + 9900 // "): dataTransfer.dropEffect changed from requested " + 9901 // REQUESTED_DROP_EFFECT + 9902 // " to " + 9903 // dataTransfer.dropEffect 9904 // ); 9905 // } 9906 } 9907 9908 function applyDropEffectCallback(event, data, allowDrop) { 9909 var tree = data.tree, 9910 dataTransfer = data.dataTransfer; 9911 9912 if ( 9913 event.type !== "dragstart" && 9914 REQUESTED_EFFECT_ALLOWED !== data.effectAllowed 9915 ) { 9916 tree.warn( 9917 "effectAllowed should only be changed in dragstart event: " + 9918 event.type + 9919 ": data.effectAllowed changed from " + 9920 REQUESTED_EFFECT_ALLOWED + 9921 " -> " + 9922 data.effectAllowed 9923 ); 9924 } 9925 9926 if (allowDrop === false) { 9927 tree.info("applyDropEffectCallback: allowDrop === false"); 9928 data.effectAllowed = "none"; 9929 data.dropEffect = "none"; 9930 } 9931 // if (REQUESTED_DROP_EFFECT !== data.dropEffect) { 9932 // tree.debug( 9933 // "applyDropEffectCallback(" + 9934 // event.type + 9935 // "): data.dropEffect changed from previous " + 9936 // REQUESTED_DROP_EFFECT + 9937 // " to " + 9938 // data.dropEffect 9939 // ); 9940 // } 9941 9942 data.isMove = data.dropEffect === "move"; 9943 // data.isMove = data.dropEffectSuggested === "move"; 9944 9945 // `effectAllowed` must only be defined in dragstart event, so we 9946 // store it in a global variable for reference 9947 if (event.type === "dragstart") { 9948 REQUESTED_EFFECT_ALLOWED = data.effectAllowed; 9949 REQUESTED_DROP_EFFECT = data.dropEffect; 9950 } 9951 9952 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 9953 // data.tree.info( 9954 // "applyDropEffectCallback(" + 9955 // event.type + 9956 // "): dataTransfer.dropEffect changed from " + 9957 // REQUESTED_DROP_EFFECT + 9958 // " -> " + 9959 // dataTransfer.dropEffect 9960 // ); 9961 // } 9962 dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED; 9963 dataTransfer.dropEffect = REQUESTED_DROP_EFFECT; 9964 9965 // tree.debug( 9966 // "applyDropEffectCallback(" + 9967 // event.type + 9968 // "): set " + 9969 // dataTransfer.dropEffect + 9970 // "/" + 9971 // dataTransfer.effectAllowed 9972 // ); 9973 // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) { 9974 // data.tree.warn( 9975 // "applyDropEffectCallback(" + 9976 // event.type + 9977 // "): could not set dataTransfer.dropEffect to " + 9978 // REQUESTED_DROP_EFFECT + 9979 // ": got " + 9980 // dataTransfer.dropEffect 9981 // ); 9982 // } 9983 return REQUESTED_DROP_EFFECT; 9984 } 9985 9986 /* Handle dragover event (fired every x ms) on valid drop targets. 9987 * 9988 * - Auto-scroll when cursor is in border regions 9989 * - Apply restrictioan like 'preventVoidMoves' 9990 * - Calculate hit mode 9991 * - Calculate drop effect 9992 * - Trigger dragOver() callback to let user modify hit mode and drop effect 9993 * - Adjust the drop marker accordingly 9994 * 9995 * @returns hitMode 9996 */ 9997 function handleDragOver(event, data) { 9998 // Implement auto-scrolling 9999 if (data.options.dnd5.scroll) { 10000 autoScroll(data.tree, event); 10001 } 10002 // Bail out with previous response if we get an invalid dragover 10003 if (!data.node) { 10004 data.tree.warn("Ignored dragover for non-node"); //, event, data); 10005 return LAST_HIT_MODE; 10006 } 10007 10008 var markerOffsetX, 10009 nodeOfs, 10010 pos, 10011 relPosY, 10012 hitMode = null, 10013 tree = data.tree, 10014 options = tree.options, 10015 dndOpts = options.dnd5, 10016 targetNode = data.node, 10017 sourceNode = data.otherNode, 10018 markerAt = "center", 10019 $target = $(targetNode.span), 10020 $targetTitle = $target.find("span.fancytree-title"); 10021 10022 if (DRAG_ENTER_RESPONSE === false) { 10023 tree.debug("Ignored dragover, since dragenter returned false."); 10024 return false; 10025 } else if (typeof DRAG_ENTER_RESPONSE === "string") { 10026 $.error("assert failed: dragenter returned string"); 10027 } 10028 // Calculate hitMode from relative cursor position. 10029 nodeOfs = $target.offset(); 10030 relPosY = (event.pageY - nodeOfs.top) / $target.height(); 10031 if (event.pageY === undefined) { 10032 tree.warn("event.pageY is undefined: see issue #1013."); 10033 } 10034 10035 if (DRAG_ENTER_RESPONSE.after && relPosY > 0.75) { 10036 hitMode = "after"; 10037 } else if ( 10038 !DRAG_ENTER_RESPONSE.over && 10039 DRAG_ENTER_RESPONSE.after && 10040 relPosY > 0.5 10041 ) { 10042 hitMode = "after"; 10043 } else if (DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) { 10044 hitMode = "before"; 10045 } else if ( 10046 !DRAG_ENTER_RESPONSE.over && 10047 DRAG_ENTER_RESPONSE.before && 10048 relPosY <= 0.5 10049 ) { 10050 hitMode = "before"; 10051 } else if (DRAG_ENTER_RESPONSE.over) { 10052 hitMode = "over"; 10053 } 10054 // Prevent no-ops like 'before source node' 10055 // TODO: these are no-ops when moving nodes, but not in copy mode 10056 if (dndOpts.preventVoidMoves && data.dropEffect === "move") { 10057 if (targetNode === sourceNode) { 10058 targetNode.debug("Drop over source node prevented."); 10059 hitMode = null; 10060 } else if ( 10061 hitMode === "before" && 10062 sourceNode && 10063 targetNode === sourceNode.getNextSibling() 10064 ) { 10065 targetNode.debug("Drop after source node prevented."); 10066 hitMode = null; 10067 } else if ( 10068 hitMode === "after" && 10069 sourceNode && 10070 targetNode === sourceNode.getPrevSibling() 10071 ) { 10072 targetNode.debug("Drop before source node prevented."); 10073 hitMode = null; 10074 } else if ( 10075 hitMode === "over" && 10076 sourceNode && 10077 sourceNode.parent === targetNode && 10078 sourceNode.isLastSibling() 10079 ) { 10080 targetNode.debug("Drop last child over own parent prevented."); 10081 hitMode = null; 10082 } 10083 } 10084 // Let callback modify the calculated hitMode 10085 data.hitMode = hitMode; 10086 if (hitMode && dndOpts.dragOver) { 10087 prepareDropEffectCallback(event, data); 10088 dndOpts.dragOver(targetNode, data); 10089 var allowDrop = !!hitMode; 10090 applyDropEffectCallback(event, data, allowDrop); 10091 hitMode = data.hitMode; 10092 } 10093 LAST_HIT_MODE = hitMode; 10094 // 10095 if (hitMode === "after" || hitMode === "before" || hitMode === "over") { 10096 markerOffsetX = dndOpts.dropMarkerOffsetX || 0; 10097 switch (hitMode) { 10098 case "before": 10099 markerAt = "top"; 10100 markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0; 10101 break; 10102 case "after": 10103 markerAt = "bottom"; 10104 markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0; 10105 break; 10106 } 10107 10108 pos = { 10109 my: "left" + offsetString(markerOffsetX) + " center", 10110 at: "left " + markerAt, 10111 of: $targetTitle, 10112 }; 10113 if (options.rtl) { 10114 pos.my = "right" + offsetString(-markerOffsetX) + " center"; 10115 pos.at = "right " + markerAt; 10116 // console.log("rtl", pos); 10117 } 10118 $dropMarker 10119 .toggleClass(classDropAfter, hitMode === "after") 10120 .toggleClass(classDropOver, hitMode === "over") 10121 .toggleClass(classDropBefore, hitMode === "before") 10122 .show() 10123 .position(FT.fixPositionOptions(pos)); 10124 } else { 10125 $dropMarker.hide(); 10126 // console.log("hide dropmarker") 10127 } 10128 10129 $(targetNode.span) 10130 .toggleClass( 10131 classDropTarget, 10132 hitMode === "after" || 10133 hitMode === "before" || 10134 hitMode === "over" 10135 ) 10136 .toggleClass(classDropAfter, hitMode === "after") 10137 .toggleClass(classDropBefore, hitMode === "before") 10138 .toggleClass(classDropAccept, hitMode === "over") 10139 .toggleClass(classDropReject, hitMode === false); 10140 10141 return hitMode; 10142 } 10143 10144 /* 10145 * Handle dragstart drag dragend events on the container 10146 */ 10147 function onDragEvent(event) { 10148 var json, 10149 tree = this, 10150 dndOpts = tree.options.dnd5, 10151 node = FT.getNode(event), 10152 dataTransfer = 10153 event.dataTransfer || event.originalEvent.dataTransfer, 10154 data = { 10155 tree: tree, 10156 node: node, 10157 options: tree.options, 10158 originalEvent: event.originalEvent, 10159 widget: tree.widget, 10160 dataTransfer: dataTransfer, 10161 useDefaultImage: true, 10162 dropEffect: undefined, 10163 dropEffectSuggested: undefined, 10164 effectAllowed: undefined, // set by dragstart 10165 files: undefined, // only for drop events 10166 isCancelled: undefined, // set by dragend 10167 isMove: undefined, 10168 }; 10169 10170 switch (event.type) { 10171 case "dragstart": 10172 if (!node) { 10173 tree.info("Ignored dragstart on a non-node."); 10174 return false; 10175 } 10176 // Store current source node in different formats 10177 SOURCE_NODE = node; 10178 10179 // Also optionally store selected nodes 10180 if (dndOpts.multiSource === false) { 10181 SOURCE_NODE_LIST = [node]; 10182 } else if (dndOpts.multiSource === true) { 10183 if (node.isSelected()) { 10184 SOURCE_NODE_LIST = tree.getSelectedNodes(); 10185 } else { 10186 SOURCE_NODE_LIST = [node]; 10187 } 10188 } else { 10189 SOURCE_NODE_LIST = dndOpts.multiSource(node, data); 10190 } 10191 // Cache as array of jQuery objects for faster access: 10192 $sourceList = $( 10193 $.map(SOURCE_NODE_LIST, function (n) { 10194 return n.span; 10195 }) 10196 ); 10197 // Set visual feedback 10198 $sourceList.addClass(classDragSource); 10199 10200 // Set payload 10201 // Note: 10202 // Transfer data is only accessible on dragstart and drop! 10203 // For all other events the formats and kinds in the drag 10204 // data store list of items representing dragged data can be 10205 // enumerated, but the data itself is unavailable and no new 10206 // data can be added. 10207 var nodeData = node.toDict(true, dndOpts.sourceCopyHook); 10208 nodeData.treeId = node.tree._id; 10209 json = JSON.stringify(nodeData); 10210 try { 10211 dataTransfer.setData(nodeMimeType, json); 10212 dataTransfer.setData("text/html", $(node.span).html()); 10213 dataTransfer.setData("text/plain", node.title); 10214 } catch (ex) { 10215 // IE only accepts 'text' type 10216 tree.warn( 10217 "Could not set data (IE only accepts 'text') - " + ex 10218 ); 10219 } 10220 // We always need to set the 'text' type if we want to drag 10221 // Because IE 11 only accepts this single type. 10222 // If we pass JSON here, IE can can access all node properties, 10223 // even when the source lives in another window. (D'n'd inside 10224 // the same window will always work.) 10225 // The drawback is, that in this case ALL browsers will see 10226 // the JSON representation as 'text', so dragging 10227 // to a text field will insert the JSON string instead of 10228 // the node title. 10229 if (dndOpts.setTextTypeJson) { 10230 dataTransfer.setData("text", json); 10231 } else { 10232 dataTransfer.setData("text", node.title); 10233 } 10234 10235 // Set the allowed drag modes (combinations of move, copy, and link) 10236 // (effectAllowed can only be set in the dragstart event.) 10237 // This can be overridden in the dragStart() callback 10238 prepareDropEffectCallback(event, data); 10239 10240 // Let user cancel or modify above settings 10241 // Realize potential changes by previous callback 10242 if (dndOpts.dragStart(node, data) === false) { 10243 // Cancel dragging 10244 // dataTransfer.dropEffect = "none"; 10245 _clearGlobals(); 10246 return false; 10247 } 10248 applyDropEffectCallback(event, data); 10249 10250 // Unless user set `data.useDefaultImage` to false in dragStart, 10251 // generata a default drag image now: 10252 $extraHelper = null; 10253 10254 if (data.useDefaultImage) { 10255 // Set the title as drag image (otherwise it would contain the expander) 10256 $dragImage = $(node.span).find(".fancytree-title"); 10257 10258 if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) { 10259 // Add a counter badge to node title if dragging more than one node. 10260 // We want this, because the element that is used as drag image 10261 // must be *visible* in the DOM, so we cannot create some hidden 10262 // custom markup. 10263 // See https://kryogenix.org/code/browser/custom-drag-image.html 10264 // Also, since IE 11 and Edge don't support setDragImage() alltogether, 10265 // it gives som feedback to the user. 10266 // The badge will be removed later on drag end. 10267 $extraHelper = $( 10268 "<span class='fancytree-childcounter'/>" 10269 ) 10270 .text("+" + (SOURCE_NODE_LIST.length - 1)) 10271 .appendTo($dragImage); 10272 } 10273 if (dataTransfer.setDragImage) { 10274 // IE 11 and Edge do not support this 10275 dataTransfer.setDragImage($dragImage[0], -10, -10); 10276 } 10277 } 10278 return true; 10279 10280 case "drag": 10281 // Called every few milliseconds (no matter if the 10282 // cursor is over a valid drop target) 10283 // data.tree.info("drag", SOURCE_NODE) 10284 prepareDropEffectCallback(event, data); 10285 dndOpts.dragDrag(node, data); 10286 applyDropEffectCallback(event, data); 10287 10288 $sourceList.toggleClass(classDragRemove, data.isMove); 10289 break; 10290 10291 case "dragend": 10292 // Called at the end of a d'n'd process (after drop) 10293 // Note caveat: If drop removed the dragged source element, 10294 // we may not get this event, since the target does not exist 10295 // anymore 10296 prepareDropEffectCallback(event, data); 10297 10298 _clearGlobals(); 10299 10300 data.isCancelled = !LAST_HIT_MODE; 10301 dndOpts.dragEnd(node, data, !LAST_HIT_MODE); 10302 // applyDropEffectCallback(event, data); 10303 break; 10304 } 10305 } 10306 /* 10307 * Handle dragenter dragover dragleave drop events on the container 10308 */ 10309 function onDropEvent(event) { 10310 var json, 10311 allowAutoExpand, 10312 nodeData, 10313 isSourceFtNode, 10314 r, 10315 res, 10316 tree = this, 10317 dndOpts = tree.options.dnd5, 10318 allowDrop = null, 10319 node = FT.getNode(event), 10320 dataTransfer = 10321 event.dataTransfer || event.originalEvent.dataTransfer, 10322 data = { 10323 tree: tree, 10324 node: node, 10325 options: tree.options, 10326 originalEvent: event.originalEvent, 10327 widget: tree.widget, 10328 hitMode: DRAG_ENTER_RESPONSE, 10329 dataTransfer: dataTransfer, 10330 otherNode: SOURCE_NODE || null, 10331 otherNodeList: SOURCE_NODE_LIST || null, 10332 otherNodeData: null, // set by drop event 10333 useDefaultImage: true, 10334 dropEffect: undefined, 10335 dropEffectSuggested: undefined, 10336 effectAllowed: undefined, // set by dragstart 10337 files: null, // list of File objects (may be []) 10338 isCancelled: undefined, // set by drop event 10339 isMove: undefined, 10340 }; 10341 10342 // data.isMove = dropEffect === "move"; 10343 10344 switch (event.type) { 10345 case "dragenter": 10346 // The dragenter event is fired when a dragged element or 10347 // text selection enters a valid drop target. 10348 10349 DRAG_OVER_STAMP = null; 10350 if (!node) { 10351 // Sometimes we get dragenter for the container element 10352 tree.debug( 10353 "Ignore non-node " + 10354 event.type + 10355 ": " + 10356 event.target.tagName + 10357 "." + 10358 event.target.className 10359 ); 10360 DRAG_ENTER_RESPONSE = false; 10361 break; 10362 } 10363 10364 $(node.span) 10365 .addClass(classDropOver) 10366 .removeClass(classDropAccept + " " + classDropReject); 10367 10368 // Data is only readable in the dragstart and drop event, 10369 // but we can check for the type: 10370 isSourceFtNode = 10371 $.inArray(nodeMimeType, dataTransfer.types) >= 0; 10372 10373 if (dndOpts.preventNonNodes && !isSourceFtNode) { 10374 node.debug("Reject dropping a non-node."); 10375 DRAG_ENTER_RESPONSE = false; 10376 break; 10377 } else if ( 10378 dndOpts.preventForeignNodes && 10379 (!SOURCE_NODE || SOURCE_NODE.tree !== node.tree) 10380 ) { 10381 node.debug("Reject dropping a foreign node."); 10382 DRAG_ENTER_RESPONSE = false; 10383 break; 10384 } else if ( 10385 dndOpts.preventSameParent && 10386 data.otherNode && 10387 data.otherNode.tree === node.tree && 10388 node.parent === data.otherNode.parent 10389 ) { 10390 node.debug("Reject dropping as sibling (same parent)."); 10391 DRAG_ENTER_RESPONSE = false; 10392 break; 10393 } else if ( 10394 dndOpts.preventRecursion && 10395 data.otherNode && 10396 data.otherNode.tree === node.tree && 10397 node.isDescendantOf(data.otherNode) 10398 ) { 10399 node.debug("Reject dropping below own ancestor."); 10400 DRAG_ENTER_RESPONSE = false; 10401 break; 10402 } else if (dndOpts.preventLazyParents && !node.isLoaded()) { 10403 node.warn("Drop over unloaded target node prevented."); 10404 DRAG_ENTER_RESPONSE = false; 10405 break; 10406 } 10407 $dropMarker.show(); 10408 10409 // Call dragEnter() to figure out if (and where) dropping is allowed 10410 prepareDropEffectCallback(event, data); 10411 r = dndOpts.dragEnter(node, data); 10412 10413 res = normalizeDragEnterResponse(r); 10414 // alert("res:" + JSON.stringify(res)) 10415 DRAG_ENTER_RESPONSE = res; 10416 10417 allowDrop = res && (res.over || res.before || res.after); 10418 10419 applyDropEffectCallback(event, data, allowDrop); 10420 break; 10421 10422 case "dragover": 10423 if (!node) { 10424 tree.debug( 10425 "Ignore non-node " + 10426 event.type + 10427 ": " + 10428 event.target.tagName + 10429 "." + 10430 event.target.className 10431 ); 10432 break; 10433 } 10434 // The dragover event is fired when an element or text 10435 // selection is being dragged over a valid drop target 10436 // (every few hundred milliseconds). 10437 // tree.debug( 10438 // event.type + 10439 // ": dropEffect: " + 10440 // dataTransfer.dropEffect 10441 // ); 10442 prepareDropEffectCallback(event, data); 10443 LAST_HIT_MODE = handleDragOver(event, data); 10444 10445 // The flag controls the preventDefault() below: 10446 allowDrop = !!LAST_HIT_MODE; 10447 allowAutoExpand = 10448 LAST_HIT_MODE === "over" || LAST_HIT_MODE === false; 10449 10450 if ( 10451 allowAutoExpand && 10452 !node.expanded && 10453 node.hasChildren() !== false 10454 ) { 10455 if (!DRAG_OVER_STAMP) { 10456 DRAG_OVER_STAMP = Date.now(); 10457 } else if ( 10458 dndOpts.autoExpandMS && 10459 Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS && 10460 !node.isLoading() && 10461 (!dndOpts.dragExpand || 10462 dndOpts.dragExpand(node, data) !== false) 10463 ) { 10464 node.setExpanded(); 10465 } 10466 } else { 10467 DRAG_OVER_STAMP = null; 10468 } 10469 break; 10470 10471 case "dragleave": 10472 // NOTE: dragleave is fired AFTER the dragenter event of the 10473 // FOLLOWING element. 10474 if (!node) { 10475 tree.debug( 10476 "Ignore non-node " + 10477 event.type + 10478 ": " + 10479 event.target.tagName + 10480 "." + 10481 event.target.className 10482 ); 10483 break; 10484 } 10485 if (!$(node.span).hasClass(classDropOver)) { 10486 node.debug("Ignore dragleave (multi)."); 10487 break; 10488 } 10489 $(node.span).removeClass( 10490 classDropOver + 10491 " " + 10492 classDropAccept + 10493 " " + 10494 classDropReject 10495 ); 10496 node.scheduleAction("cancel"); 10497 dndOpts.dragLeave(node, data); 10498 $dropMarker.hide(); 10499 break; 10500 10501 case "drop": 10502 // Data is only readable in the (dragstart and) drop event: 10503 10504 if ($.inArray(nodeMimeType, dataTransfer.types) >= 0) { 10505 nodeData = dataTransfer.getData(nodeMimeType); 10506 tree.info( 10507 event.type + 10508 ": getData('application/x-fancytree-node'): '" + 10509 nodeData + 10510 "'" 10511 ); 10512 } 10513 if (!nodeData) { 10514 // 1. Source is not a Fancytree node, or 10515 // 2. If the FT mime type was set, but returns '', this 10516 // is probably IE 11 (which only supports 'text') 10517 nodeData = dataTransfer.getData("text"); 10518 tree.info( 10519 event.type + ": getData('text'): '" + nodeData + "'" 10520 ); 10521 } 10522 if (nodeData) { 10523 try { 10524 // 'text' type may contain JSON if IE is involved 10525 // and setTextTypeJson option was set 10526 json = JSON.parse(nodeData); 10527 if (json.title !== undefined) { 10528 data.otherNodeData = json; 10529 } 10530 } catch (ex) { 10531 // assume 'text' type contains plain text, so `otherNodeData` 10532 // should not be set 10533 } 10534 } 10535 tree.debug( 10536 event.type + 10537 ": nodeData: '" + 10538 nodeData + 10539 "', otherNodeData: ", 10540 data.otherNodeData 10541 ); 10542 10543 $(node.span).removeClass( 10544 classDropOver + 10545 " " + 10546 classDropAccept + 10547 " " + 10548 classDropReject 10549 ); 10550 10551 // Let user implement the actual drop operation 10552 data.hitMode = LAST_HIT_MODE; 10553 prepareDropEffectCallback(event, data, !LAST_HIT_MODE); 10554 data.isCancelled = !LAST_HIT_MODE; 10555 10556 var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span, 10557 orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree; 10558 10559 dndOpts.dragDrop(node, data); 10560 // applyDropEffectCallback(event, data); 10561 10562 // Prevent browser's default drop handling, i.e. open as link, ... 10563 event.preventDefault(); 10564 10565 if (orgSourceElem && !document.body.contains(orgSourceElem)) { 10566 // The drop handler removed the original drag source from 10567 // the DOM, so the dragend event will probaly not fire. 10568 if (orgSourceTree === tree) { 10569 tree.debug( 10570 "Drop handler removed source element: generating dragEnd." 10571 ); 10572 dndOpts.dragEnd(SOURCE_NODE, data); 10573 } else { 10574 tree.warn( 10575 "Drop handler removed source element: dragend event may be lost." 10576 ); 10577 } 10578 } 10579 10580 _clearGlobals(); 10581 10582 break; 10583 } 10584 // Dnd API madness: we must PREVENT default handling to enable dropping 10585 if (allowDrop) { 10586 event.preventDefault(); 10587 return false; 10588 } 10589 } 10590 10591 /** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject. 10592 * 10593 * @returns {FancytreeNode[]} List of nodes (empty if no drag operation) 10594 * @example 10595 * $.ui.fancytree.getDragNodeList(); 10596 * 10597 * @alias Fancytree_Static#getDragNodeList 10598 * @requires jquery.fancytree.dnd5.js 10599 * @since 2.31 10600 */ 10601 $.ui.fancytree.getDragNodeList = function () { 10602 return SOURCE_NODE_LIST || []; 10603 }; 10604 10605 /** [ext-dnd5] Return the FancytreeNode that is currently being dragged. 10606 * 10607 * If multiple nodes are dragged, only the first is returned. 10608 * 10609 * @returns {FancytreeNode | null} dragged nodes or null if no drag operation 10610 * @example 10611 * $.ui.fancytree.getDragNode(); 10612 * 10613 * @alias Fancytree_Static#getDragNode 10614 * @requires jquery.fancytree.dnd5.js 10615 * @since 2.31 10616 */ 10617 $.ui.fancytree.getDragNode = function () { 10618 return SOURCE_NODE; 10619 }; 10620 10621 /****************************************************************************** 10622 * 10623 */ 10624 10625 $.ui.fancytree.registerExtension({ 10626 name: "dnd5", 10627 version: "2.38.3", 10628 // Default options for this extension. 10629 options: { 10630 autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering 10631 dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after" 10632 dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop) 10633 // #1021 `document.body` is not available yet 10634 dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root) 10635 multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed 10636 effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event) 10637 // dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string. 10638 dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver). 10639 preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees 10640 preventLazyParents: true, // Prevent dropping items on unloaded lazy Fancytree nodes 10641 preventNonNodes: false, // Prevent dropping items other than Fancytree nodes 10642 preventRecursion: true, // Prevent dropping nodes on own descendants 10643 preventSameParent: false, // Prevent dropping nodes under same direct parent 10644 preventVoidMoves: true, // Prevent dropping nodes 'before self', etc. 10645 scroll: true, // Enable auto-scrolling while dragging 10646 scrollSensitivity: 20, // Active top/bottom margin in pixel 10647 scrollSpeed: 5, // Pixel per event 10648 setTextTypeJson: false, // Allow dragging of nodes to different IE windows 10649 sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38 10650 // Events (drag support) 10651 dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag 10652 dragDrag: $.noop, // Callback(sourceNode, data) 10653 dragEnd: $.noop, // Callback(sourceNode, data) 10654 // Events (drop support) 10655 dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop 10656 dragOver: $.noop, // Callback(targetNode, data) 10657 dragExpand: $.noop, // Callback(targetNode, data), return false to prevent autoExpand 10658 dragDrop: $.noop, // Callback(targetNode, data) 10659 dragLeave: $.noop, // Callback(targetNode, data) 10660 }, 10661 10662 treeInit: function (ctx) { 10663 var $temp, 10664 tree = ctx.tree, 10665 opts = ctx.options, 10666 glyph = opts.glyph || null, 10667 dndOpts = opts.dnd5; 10668 10669 if ($.inArray("dnd", opts.extensions) >= 0) { 10670 $.error("Extensions 'dnd' and 'dnd5' are mutually exclusive."); 10671 } 10672 if (dndOpts.dragStop) { 10673 $.error( 10674 "dragStop is not used by ext-dnd5. Use dragEnd instead." 10675 ); 10676 } 10677 if (dndOpts.preventRecursiveMoves != null) { 10678 $.error( 10679 "preventRecursiveMoves was renamed to preventRecursion." 10680 ); 10681 } 10682 10683 // Implement `opts.createNode` event to add the 'draggable' attribute 10684 // #680: this must happen before calling super.treeInit() 10685 if (dndOpts.dragStart) { 10686 FT.overrideMethod( 10687 ctx.options, 10688 "createNode", 10689 function (event, data) { 10690 // Default processing if any 10691 this._super.apply(this, arguments); 10692 if (data.node.span) { 10693 data.node.span.draggable = true; 10694 } else { 10695 data.node.warn( 10696 "Cannot add `draggable`: no span tag" 10697 ); 10698 } 10699 } 10700 ); 10701 } 10702 this._superApply(arguments); 10703 10704 this.$container.addClass("fancytree-ext-dnd5"); 10705 10706 // Store the current scroll parent, which may be the tree 10707 // container, any enclosing div, or the document. 10708 // #761: scrollParent() always needs a container child 10709 $temp = $("<span>").appendTo(this.$container); 10710 this.$scrollParent = $temp.scrollParent(); 10711 $temp.remove(); 10712 10713 $dropMarker = $("#fancytree-drop-marker"); 10714 if (!$dropMarker.length) { 10715 $dropMarker = $("<div id='fancytree-drop-marker'></div>") 10716 .hide() 10717 .css({ 10718 "z-index": 1000, 10719 // Drop marker should not steal dragenter/dragover events: 10720 "pointer-events": "none", 10721 }) 10722 .prependTo(dndOpts.dropMarkerParent); 10723 if (glyph) { 10724 FT.setSpanIcon( 10725 $dropMarker[0], 10726 glyph.map._addClass, 10727 glyph.map.dropMarker 10728 ); 10729 } 10730 } 10731 $dropMarker.toggleClass("fancytree-rtl", !!opts.rtl); 10732 10733 // Enable drag support if dragStart() is specified: 10734 if (dndOpts.dragStart) { 10735 // Bind drag event handlers 10736 tree.$container.on( 10737 "dragstart drag dragend", 10738 onDragEvent.bind(tree) 10739 ); 10740 } 10741 // Enable drop support if dragEnter() is specified: 10742 if (dndOpts.dragEnter) { 10743 // Bind drop event handlers 10744 tree.$container.on( 10745 "dragenter dragover dragleave drop", 10746 onDropEvent.bind(tree) 10747 ); 10748 } 10749 }, 10750 }); 10751 // Value returned by `require('jquery.fancytree..')` 10752 return $.ui.fancytree; 10753}); // End of closure 10754 10755 10756/*! Extension 'jquery.fancytree.edit.js' *//*! 10757 * jquery.fancytree.edit.js 10758 * 10759 * Make node titles editable. 10760 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 10761 * 10762 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 10763 * 10764 * Released under the MIT license 10765 * https://github.com/mar10/fancytree/wiki/LicenseInfo 10766 * 10767 * @version 2.38.3 10768 * @date 2023-02-01T20:52:50Z 10769 */ 10770 10771(function (factory) { 10772 if (typeof define === "function" && define.amd) { 10773 // AMD. Register as an anonymous module. 10774 define(["jquery", "./jquery.fancytree"], factory); 10775 } else if (typeof module === "object" && module.exports) { 10776 // Node/CommonJS 10777 require("./jquery.fancytree"); 10778 module.exports = factory(require("jquery")); 10779 } else { 10780 // Browser globals 10781 factory(jQuery); 10782 } 10783})(function ($) { 10784 "use strict"; 10785 10786 /******************************************************************************* 10787 * Private functions and variables 10788 */ 10789 10790 var isMac = /Mac/.test(navigator.platform), 10791 escapeHtml = $.ui.fancytree.escapeHtml, 10792 trim = $.ui.fancytree.trim, 10793 unescapeHtml = $.ui.fancytree.unescapeHtml; 10794 10795 /** 10796 * [ext-edit] Start inline editing of current node title. 10797 * 10798 * @alias FancytreeNode#editStart 10799 * @requires Fancytree 10800 */ 10801 $.ui.fancytree._FancytreeNodeClass.prototype.editStart = function () { 10802 var $input, 10803 node = this, 10804 tree = this.tree, 10805 local = tree.ext.edit, 10806 instOpts = tree.options.edit, 10807 $title = $(".fancytree-title", node.span), 10808 eventData = { 10809 node: node, 10810 tree: tree, 10811 options: tree.options, 10812 isNew: $(node[tree.statusClassPropName]).hasClass( 10813 "fancytree-edit-new" 10814 ), 10815 orgTitle: node.title, 10816 input: null, 10817 dirty: false, 10818 }; 10819 10820 // beforeEdit may want to modify the title before editing 10821 if ( 10822 instOpts.beforeEdit.call( 10823 node, 10824 { type: "beforeEdit" }, 10825 eventData 10826 ) === false 10827 ) { 10828 return false; 10829 } 10830 $.ui.fancytree.assert(!local.currentNode, "recursive edit"); 10831 local.currentNode = this; 10832 local.eventData = eventData; 10833 10834 // Disable standard Fancytree mouse- and key handling 10835 tree.widget._unbind(); 10836 10837 local.lastDraggableAttrValue = node.span.draggable; 10838 if (local.lastDraggableAttrValue) { 10839 node.span.draggable = false; 10840 } 10841 10842 // #116: ext-dnd prevents the blur event, so we have to catch outer clicks 10843 $(document).on("mousedown.fancytree-edit", function (event) { 10844 if (!$(event.target).hasClass("fancytree-edit-input")) { 10845 node.editEnd(true, event); 10846 } 10847 }); 10848 10849 // Replace node with <input> 10850 $input = $("<input />", { 10851 class: "fancytree-edit-input", 10852 type: "text", 10853 value: tree.options.escapeTitles 10854 ? eventData.orgTitle 10855 : unescapeHtml(eventData.orgTitle), 10856 }); 10857 local.eventData.input = $input; 10858 if (instOpts.adjustWidthOfs != null) { 10859 $input.width($title.width() + instOpts.adjustWidthOfs); 10860 } 10861 if (instOpts.inputCss != null) { 10862 $input.css(instOpts.inputCss); 10863 } 10864 10865 $title.html($input); 10866 10867 // Focus <input> and bind keyboard handler 10868 $input 10869 .focus() 10870 .change(function (event) { 10871 $input.addClass("fancytree-edit-dirty"); 10872 }) 10873 .on("keydown", function (event) { 10874 switch (event.which) { 10875 case $.ui.keyCode.ESCAPE: 10876 node.editEnd(false, event); 10877 break; 10878 case $.ui.keyCode.ENTER: 10879 node.editEnd(true, event); 10880 return false; // so we don't start editmode on Mac 10881 } 10882 event.stopPropagation(); 10883 }) 10884 .blur(function (event) { 10885 return node.editEnd(true, event); 10886 }); 10887 10888 instOpts.edit.call(node, { type: "edit" }, eventData); 10889 }; 10890 10891 /** 10892 * [ext-edit] Stop inline editing. 10893 * @param {Boolean} [applyChanges=false] false: cancel edit, true: save (if modified) 10894 * @alias FancytreeNode#editEnd 10895 * @requires jquery.fancytree.edit.js 10896 */ 10897 $.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function ( 10898 applyChanges, 10899 _event 10900 ) { 10901 var newVal, 10902 node = this, 10903 tree = this.tree, 10904 local = tree.ext.edit, 10905 eventData = local.eventData, 10906 instOpts = tree.options.edit, 10907 $title = $(".fancytree-title", node.span), 10908 $input = $title.find("input.fancytree-edit-input"); 10909 10910 if (instOpts.trim) { 10911 $input.val(trim($input.val())); 10912 } 10913 newVal = $input.val(); 10914 10915 eventData.dirty = newVal !== node.title; 10916 eventData.originalEvent = _event; 10917 10918 // Find out, if saving is required 10919 if (applyChanges === false) { 10920 // If true/false was passed, honor this (except in rename mode, if unchanged) 10921 eventData.save = false; 10922 } else if (eventData.isNew) { 10923 // In create mode, we save everything, except for empty text 10924 eventData.save = newVal !== ""; 10925 } else { 10926 // In rename mode, we save everyting, except for empty or unchanged text 10927 eventData.save = eventData.dirty && newVal !== ""; 10928 } 10929 // Allow to break (keep editor open), modify input, or re-define data.save 10930 if ( 10931 instOpts.beforeClose.call( 10932 node, 10933 { type: "beforeClose" }, 10934 eventData 10935 ) === false 10936 ) { 10937 return false; 10938 } 10939 if ( 10940 eventData.save && 10941 instOpts.save.call(node, { type: "save" }, eventData) === false 10942 ) { 10943 return false; 10944 } 10945 $input.removeClass("fancytree-edit-dirty").off(); 10946 // Unbind outer-click handler 10947 $(document).off(".fancytree-edit"); 10948 10949 if (eventData.save) { 10950 // # 171: escape user input (not required if global escaping is on) 10951 node.setTitle( 10952 tree.options.escapeTitles ? newVal : escapeHtml(newVal) 10953 ); 10954 node.setFocus(); 10955 } else { 10956 if (eventData.isNew) { 10957 node.remove(); 10958 node = eventData.node = null; 10959 local.relatedNode.setFocus(); 10960 } else { 10961 node.renderTitle(); 10962 node.setFocus(); 10963 } 10964 } 10965 local.eventData = null; 10966 local.currentNode = null; 10967 local.relatedNode = null; 10968 // Re-enable mouse and keyboard handling 10969 tree.widget._bind(); 10970 10971 if (node && local.lastDraggableAttrValue) { 10972 node.span.draggable = true; 10973 } 10974 10975 // Set keyboard focus, even if setFocus() claims 'nothing to do' 10976 tree.$container.get(0).focus({ preventScroll: true }); 10977 eventData.input = null; 10978 instOpts.close.call(node, { type: "close" }, eventData); 10979 return true; 10980 }; 10981 10982 /** 10983 * [ext-edit] Create a new child or sibling node and start edit mode. 10984 * 10985 * @param {String} [mode='child'] 'before', 'after', or 'child' 10986 * @param {Object} [init] NodeData (or simple title string) 10987 * @alias FancytreeNode#editCreateNode 10988 * @requires jquery.fancytree.edit.js 10989 * @since 2.4 10990 */ 10991 $.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function ( 10992 mode, 10993 init 10994 ) { 10995 var newNode, 10996 tree = this.tree, 10997 self = this; 10998 10999 mode = mode || "child"; 11000 if (init == null) { 11001 init = { title: "" }; 11002 } else if (typeof init === "string") { 11003 init = { title: init }; 11004 } else { 11005 $.ui.fancytree.assert($.isPlainObject(init)); 11006 } 11007 // Make sure node is expanded (and loaded) in 'child' mode 11008 if ( 11009 mode === "child" && 11010 !this.isExpanded() && 11011 this.hasChildren() !== false 11012 ) { 11013 this.setExpanded().done(function () { 11014 self.editCreateNode(mode, init); 11015 }); 11016 return; 11017 } 11018 newNode = this.addNode(init, mode); 11019 11020 // #644: Don't filter new nodes. 11021 newNode.match = true; 11022 $(newNode[tree.statusClassPropName]) 11023 .removeClass("fancytree-hide") 11024 .addClass("fancytree-match"); 11025 11026 newNode.makeVisible(/*{noAnimation: true}*/).done(function () { 11027 $(newNode[tree.statusClassPropName]).addClass("fancytree-edit-new"); 11028 self.tree.ext.edit.relatedNode = self; 11029 newNode.editStart(); 11030 }); 11031 }; 11032 11033 /** 11034 * [ext-edit] Check if any node in this tree in edit mode. 11035 * 11036 * @returns {FancytreeNode | null} 11037 * @alias Fancytree#isEditing 11038 * @requires jquery.fancytree.edit.js 11039 */ 11040 $.ui.fancytree._FancytreeClass.prototype.isEditing = function () { 11041 return this.ext.edit ? this.ext.edit.currentNode : null; 11042 }; 11043 11044 /** 11045 * [ext-edit] Check if this node is in edit mode. 11046 * @returns {Boolean} true if node is currently beeing edited 11047 * @alias FancytreeNode#isEditing 11048 * @requires jquery.fancytree.edit.js 11049 */ 11050 $.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function () { 11051 return this.tree.ext.edit 11052 ? this.tree.ext.edit.currentNode === this 11053 : false; 11054 }; 11055 11056 /******************************************************************************* 11057 * Extension code 11058 */ 11059 $.ui.fancytree.registerExtension({ 11060 name: "edit", 11061 version: "2.38.3", 11062 // Default options for this extension. 11063 options: { 11064 adjustWidthOfs: 4, // null: don't adjust input size to content 11065 allowEmpty: false, // Prevent empty input 11066 inputCss: { minWidth: "3em" }, 11067 // triggerCancel: ["esc", "tab", "click"], 11068 triggerStart: ["f2", "mac+enter", "shift+click"], 11069 trim: true, // Trim whitespace before save 11070 // Events: 11071 beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available) 11072 beforeEdit: $.noop, // Return false to prevent edit mode 11073 close: $.noop, // Editor was removed 11074 edit: $.noop, // Editor was opened (available as data.input) 11075 // keypress: $.noop, // Not yet implemented 11076 save: $.noop, // Save data.input.val() or return false to keep editor open 11077 }, 11078 // Local attributes 11079 currentNode: null, 11080 11081 treeInit: function (ctx) { 11082 var tree = ctx.tree; 11083 11084 this._superApply(arguments); 11085 11086 this.$container 11087 .addClass("fancytree-ext-edit") 11088 .on("fancytreebeforeupdateviewport", function (event, data) { 11089 var editNode = tree.isEditing(); 11090 // When scrolling, the TR may be re-used by another node, so the 11091 // active cell marker an 11092 if (editNode) { 11093 editNode.info("Cancel edit due to scroll event."); 11094 editNode.editEnd(false, event); 11095 } 11096 }); 11097 }, 11098 nodeClick: function (ctx) { 11099 var eventStr = $.ui.fancytree.eventToString(ctx.originalEvent), 11100 triggerStart = ctx.options.edit.triggerStart; 11101 11102 if ( 11103 eventStr === "shift+click" && 11104 $.inArray("shift+click", triggerStart) >= 0 11105 ) { 11106 if (ctx.originalEvent.shiftKey) { 11107 ctx.node.editStart(); 11108 return false; 11109 } 11110 } 11111 if ( 11112 eventStr === "click" && 11113 $.inArray("clickActive", triggerStart) >= 0 11114 ) { 11115 // Only when click was inside title text (not aynwhere else in the row) 11116 if ( 11117 ctx.node.isActive() && 11118 !ctx.node.isEditing() && 11119 $(ctx.originalEvent.target).hasClass("fancytree-title") 11120 ) { 11121 ctx.node.editStart(); 11122 return false; 11123 } 11124 } 11125 return this._superApply(arguments); 11126 }, 11127 nodeDblclick: function (ctx) { 11128 if ($.inArray("dblclick", ctx.options.edit.triggerStart) >= 0) { 11129 ctx.node.editStart(); 11130 return false; 11131 } 11132 return this._superApply(arguments); 11133 }, 11134 nodeKeydown: function (ctx) { 11135 switch (ctx.originalEvent.which) { 11136 case 113: // [F2] 11137 if ($.inArray("f2", ctx.options.edit.triggerStart) >= 0) { 11138 ctx.node.editStart(); 11139 return false; 11140 } 11141 break; 11142 case $.ui.keyCode.ENTER: 11143 if ( 11144 $.inArray("mac+enter", ctx.options.edit.triggerStart) >= 11145 0 && 11146 isMac 11147 ) { 11148 ctx.node.editStart(); 11149 return false; 11150 } 11151 break; 11152 } 11153 return this._superApply(arguments); 11154 }, 11155 }); 11156 // Value returned by `require('jquery.fancytree..')` 11157 return $.ui.fancytree; 11158}); // End of closure 11159 11160 11161/*! Extension 'jquery.fancytree.filter.js' *//*! 11162 * jquery.fancytree.filter.js 11163 * 11164 * Remove or highlight tree nodes, based on a filter. 11165 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 11166 * 11167 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 11168 * 11169 * Released under the MIT license 11170 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11171 * 11172 * @version 2.38.3 11173 * @date 2023-02-01T20:52:50Z 11174 */ 11175 11176(function (factory) { 11177 if (typeof define === "function" && define.amd) { 11178 // AMD. Register as an anonymous module. 11179 define(["jquery", "./jquery.fancytree"], factory); 11180 } else if (typeof module === "object" && module.exports) { 11181 // Node/CommonJS 11182 require("./jquery.fancytree"); 11183 module.exports = factory(require("jquery")); 11184 } else { 11185 // Browser globals 11186 factory(jQuery); 11187 } 11188})(function ($) { 11189 "use strict"; 11190 11191 /******************************************************************************* 11192 * Private functions and variables 11193 */ 11194 11195 var KeyNoData = "__not_found__", 11196 escapeHtml = $.ui.fancytree.escapeHtml, 11197 exoticStartChar = "\uFFF7", 11198 exoticEndChar = "\uFFF8"; 11199 function _escapeRegex(str) { 11200 return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 11201 } 11202 11203 function extractHtmlText(s) { 11204 if (s.indexOf(">") >= 0) { 11205 return $("<div/>").html(s).text(); 11206 } 11207 return s; 11208 } 11209 11210 /** 11211 * @description Marks the matching charecters of `text` either by `mark` or 11212 * by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray` 11213 * which is an array of matching groups. 11214 * @param {string} text 11215 * @param {RegExpMatchArray} regexMatchArray 11216 */ 11217 function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) { 11218 // It is extremely infuriating that we can not use `let` or `const` or arrow functions. 11219 // Damn you IE!!! 11220 var matchingIndices = []; 11221 // get the indices of matched characters (Iterate through `RegExpMatchArray`) 11222 for ( 11223 var _matchingArrIdx = 1; 11224 _matchingArrIdx < regexMatchArray.length; 11225 _matchingArrIdx++ 11226 ) { 11227 var _mIdx = 11228 // get matching char index by cumulatively adding 11229 // the matched group length 11230 regexMatchArray[_matchingArrIdx].length + 11231 (_matchingArrIdx === 1 ? 0 : 1) + 11232 (matchingIndices[matchingIndices.length - 1] || 0); 11233 matchingIndices.push(_mIdx); 11234 } 11235 // Map each `text` char to its position and store in `textPoses`. 11236 var textPoses = text.split(""); 11237 if (escapeTitles) { 11238 // If escaping the title, then wrap the matchng char within exotic chars 11239 matchingIndices.forEach(function (v) { 11240 textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar; 11241 }); 11242 } else { 11243 // Otherwise, Wrap the matching chars within `mark`. 11244 matchingIndices.forEach(function (v) { 11245 textPoses[v] = "<mark>" + textPoses[v] + "</mark>"; 11246 }); 11247 } 11248 // Join back the modified `textPoses` to create final highlight markup. 11249 return textPoses.join(""); 11250 } 11251 $.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function ( 11252 filter, 11253 branchMode, 11254 _opts 11255 ) { 11256 var match, 11257 statusNode, 11258 re, 11259 reHighlight, 11260 reExoticStartChar, 11261 reExoticEndChar, 11262 temp, 11263 prevEnableUpdate, 11264 count = 0, 11265 treeOpts = this.options, 11266 escapeTitles = treeOpts.escapeTitles, 11267 prevAutoCollapse = treeOpts.autoCollapse, 11268 opts = $.extend({}, treeOpts.filter, _opts), 11269 hideMode = opts.mode === "hide", 11270 leavesOnly = !!opts.leavesOnly && !branchMode; 11271 11272 // Default to 'match title substring (not case sensitive)' 11273 if (typeof filter === "string") { 11274 if (filter === "") { 11275 this.warn( 11276 "Fancytree passing an empty string as a filter is handled as clearFilter()." 11277 ); 11278 this.clearFilter(); 11279 return; 11280 } 11281 if (opts.fuzzy) { 11282 // See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905 11283 // and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed 11284 // and http://www.dustindiaz.com/autocomplete-fuzzy-matching 11285 match = filter 11286 .split("") 11287 // Escaping the `filter` will not work because, 11288 // it gets further split into individual characters. So, 11289 // escape each character after splitting 11290 .map(_escapeRegex) 11291 .reduce(function (a, b) { 11292 // create capture groups for parts that comes before 11293 // the character 11294 return a + "([^" + b + "]*)" + b; 11295 }, ""); 11296 } else { 11297 match = _escapeRegex(filter); // make sure a '.' is treated literally 11298 } 11299 re = new RegExp(match, "i"); 11300 reHighlight = new RegExp(_escapeRegex(filter), "gi"); 11301 if (escapeTitles) { 11302 reExoticStartChar = new RegExp( 11303 _escapeRegex(exoticStartChar), 11304 "g" 11305 ); 11306 reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g"); 11307 } 11308 filter = function (node) { 11309 if (!node.title) { 11310 return false; 11311 } 11312 var text = escapeTitles 11313 ? node.title 11314 : extractHtmlText(node.title), 11315 // `.match` instead of `.test` to get the capture groups 11316 res = text.match(re); 11317 if (res && opts.highlight) { 11318 if (escapeTitles) { 11319 if (opts.fuzzy) { 11320 temp = _markFuzzyMatchedChars( 11321 text, 11322 res, 11323 escapeTitles 11324 ); 11325 } else { 11326 // #740: we must not apply the marks to escaped entity names, e.g. `"` 11327 // Use some exotic characters to mark matches: 11328 temp = text.replace(reHighlight, function (s) { 11329 return exoticStartChar + s + exoticEndChar; 11330 }); 11331 } 11332 // now we can escape the title... 11333 node.titleWithHighlight = escapeHtml(temp) 11334 // ... and finally insert the desired `<mark>` tags 11335 .replace(reExoticStartChar, "<mark>") 11336 .replace(reExoticEndChar, "</mark>"); 11337 } else { 11338 if (opts.fuzzy) { 11339 node.titleWithHighlight = _markFuzzyMatchedChars( 11340 text, 11341 res 11342 ); 11343 } else { 11344 node.titleWithHighlight = text.replace( 11345 reHighlight, 11346 function (s) { 11347 return "<mark>" + s + "</mark>"; 11348 } 11349 ); 11350 } 11351 } 11352 // node.debug("filter", escapeTitles, text, node.titleWithHighlight); 11353 } 11354 return !!res; 11355 }; 11356 } 11357 11358 this.enableFilter = true; 11359 this.lastFilterArgs = arguments; 11360 11361 prevEnableUpdate = this.enableUpdate(false); 11362 11363 this.$div.addClass("fancytree-ext-filter"); 11364 if (hideMode) { 11365 this.$div.addClass("fancytree-ext-filter-hide"); 11366 } else { 11367 this.$div.addClass("fancytree-ext-filter-dimm"); 11368 } 11369 this.$div.toggleClass( 11370 "fancytree-ext-filter-hide-expanders", 11371 !!opts.hideExpanders 11372 ); 11373 // Reset current filter 11374 this.rootNode.subMatchCount = 0; 11375 this.visit(function (node) { 11376 delete node.match; 11377 delete node.titleWithHighlight; 11378 node.subMatchCount = 0; 11379 }); 11380 statusNode = this.getRootNode()._findDirectChild(KeyNoData); 11381 if (statusNode) { 11382 statusNode.remove(); 11383 } 11384 11385 // Adjust node.hide, .match, and .subMatchCount properties 11386 treeOpts.autoCollapse = false; // #528 11387 11388 this.visit(function (node) { 11389 if (leavesOnly && node.children != null) { 11390 return; 11391 } 11392 var res = filter(node), 11393 matchedByBranch = false; 11394 11395 if (res === "skip") { 11396 node.visit(function (c) { 11397 c.match = false; 11398 }, true); 11399 return "skip"; 11400 } 11401 if (!res && (branchMode || res === "branch") && node.parent.match) { 11402 res = true; 11403 matchedByBranch = true; 11404 } 11405 if (res) { 11406 count++; 11407 node.match = true; 11408 node.visitParents(function (p) { 11409 if (p !== node) { 11410 p.subMatchCount += 1; 11411 } 11412 // Expand match (unless this is no real match, but only a node in a matched branch) 11413 if (opts.autoExpand && !matchedByBranch && !p.expanded) { 11414 p.setExpanded(true, { 11415 noAnimation: true, 11416 noEvents: true, 11417 scrollIntoView: false, 11418 }); 11419 p._filterAutoExpanded = true; 11420 } 11421 }, true); 11422 } 11423 }); 11424 treeOpts.autoCollapse = prevAutoCollapse; 11425 11426 if (count === 0 && opts.nodata && hideMode) { 11427 statusNode = opts.nodata; 11428 if (typeof statusNode === "function") { 11429 statusNode = statusNode(); 11430 } 11431 if (statusNode === true) { 11432 statusNode = {}; 11433 } else if (typeof statusNode === "string") { 11434 statusNode = { title: statusNode }; 11435 } 11436 statusNode = $.extend( 11437 { 11438 statusNodeType: "nodata", 11439 key: KeyNoData, 11440 title: this.options.strings.noData, 11441 }, 11442 statusNode 11443 ); 11444 11445 this.getRootNode().addNode(statusNode).match = true; 11446 } 11447 // Redraw whole tree 11448 this._callHook("treeStructureChanged", this, "applyFilter"); 11449 // this.render(); 11450 this.enableUpdate(prevEnableUpdate); 11451 return count; 11452 }; 11453 11454 /** 11455 * [ext-filter] Dimm or hide nodes. 11456 * 11457 * @param {function | string} filter 11458 * @param {boolean} [opts={autoExpand: false, leavesOnly: false}] 11459 * @returns {integer} count 11460 * @alias Fancytree#filterNodes 11461 * @requires jquery.fancytree.filter.js 11462 */ 11463 $.ui.fancytree._FancytreeClass.prototype.filterNodes = function ( 11464 filter, 11465 opts 11466 ) { 11467 if (typeof opts === "boolean") { 11468 opts = { leavesOnly: opts }; 11469 this.warn( 11470 "Fancytree.filterNodes() leavesOnly option is deprecated since 2.9.0 / 2015-04-19. Use opts.leavesOnly instead." 11471 ); 11472 } 11473 return this._applyFilterImpl(filter, false, opts); 11474 }; 11475 11476 /** 11477 * [ext-filter] Dimm or hide whole branches. 11478 * 11479 * @param {function | string} filter 11480 * @param {boolean} [opts={autoExpand: false}] 11481 * @returns {integer} count 11482 * @alias Fancytree#filterBranches 11483 * @requires jquery.fancytree.filter.js 11484 */ 11485 $.ui.fancytree._FancytreeClass.prototype.filterBranches = function ( 11486 filter, 11487 opts 11488 ) { 11489 return this._applyFilterImpl(filter, true, opts); 11490 }; 11491 11492 /** 11493 * [ext-filter] Re-apply current filter. 11494 * 11495 * @returns {integer} count 11496 * @alias Fancytree#updateFilter 11497 * @requires jquery.fancytree.filter.js 11498 * @since 2.38 11499 */ 11500 $.ui.fancytree._FancytreeClass.prototype.updateFilter = function () { 11501 if ( 11502 this.enableFilter && 11503 this.lastFilterArgs && 11504 this.options.filter.autoApply 11505 ) { 11506 this._applyFilterImpl.apply(this, this.lastFilterArgs); 11507 } else { 11508 this.warn("updateFilter(): no filter active."); 11509 } 11510 }; 11511 11512 /** 11513 * [ext-filter] Reset the filter. 11514 * 11515 * @alias Fancytree#clearFilter 11516 * @requires jquery.fancytree.filter.js 11517 */ 11518 $.ui.fancytree._FancytreeClass.prototype.clearFilter = function () { 11519 var $title, 11520 statusNode = this.getRootNode()._findDirectChild(KeyNoData), 11521 escapeTitles = this.options.escapeTitles, 11522 enhanceTitle = this.options.enhanceTitle, 11523 prevEnableUpdate = this.enableUpdate(false); 11524 11525 if (statusNode) { 11526 statusNode.remove(); 11527 } 11528 // we also counted root node's subMatchCount 11529 delete this.rootNode.match; 11530 delete this.rootNode.subMatchCount; 11531 11532 this.visit(function (node) { 11533 if (node.match && node.span) { 11534 // #491, #601 11535 $title = $(node.span).find(">span.fancytree-title"); 11536 if (escapeTitles) { 11537 $title.text(node.title); 11538 } else { 11539 $title.html(node.title); 11540 } 11541 if (enhanceTitle) { 11542 enhanceTitle( 11543 { type: "enhanceTitle" }, 11544 { node: node, $title: $title } 11545 ); 11546 } 11547 } 11548 delete node.match; 11549 delete node.subMatchCount; 11550 delete node.titleWithHighlight; 11551 if (node.$subMatchBadge) { 11552 node.$subMatchBadge.remove(); 11553 delete node.$subMatchBadge; 11554 } 11555 if (node._filterAutoExpanded && node.expanded) { 11556 node.setExpanded(false, { 11557 noAnimation: true, 11558 noEvents: true, 11559 scrollIntoView: false, 11560 }); 11561 } 11562 delete node._filterAutoExpanded; 11563 }); 11564 this.enableFilter = false; 11565 this.lastFilterArgs = null; 11566 this.$div.removeClass( 11567 "fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide" 11568 ); 11569 this._callHook("treeStructureChanged", this, "clearFilter"); 11570 // this.render(); 11571 this.enableUpdate(prevEnableUpdate); 11572 }; 11573 11574 /** 11575 * [ext-filter] Return true if a filter is currently applied. 11576 * 11577 * @returns {Boolean} 11578 * @alias Fancytree#isFilterActive 11579 * @requires jquery.fancytree.filter.js 11580 * @since 2.13 11581 */ 11582 $.ui.fancytree._FancytreeClass.prototype.isFilterActive = function () { 11583 return !!this.enableFilter; 11584 }; 11585 11586 /** 11587 * [ext-filter] Return true if this node is matched by current filter (or no filter is active). 11588 * 11589 * @returns {Boolean} 11590 * @alias FancytreeNode#isMatched 11591 * @requires jquery.fancytree.filter.js 11592 * @since 2.13 11593 */ 11594 $.ui.fancytree._FancytreeNodeClass.prototype.isMatched = function () { 11595 return !(this.tree.enableFilter && !this.match); 11596 }; 11597 11598 /******************************************************************************* 11599 * Extension code 11600 */ 11601 $.ui.fancytree.registerExtension({ 11602 name: "filter", 11603 version: "2.38.3", 11604 // Default options for this extension. 11605 options: { 11606 autoApply: true, // Re-apply last filter if lazy data is loaded 11607 autoExpand: false, // Expand all branches that contain matches while filtered 11608 counter: true, // Show a badge with number of matching child nodes near parent icons 11609 fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar' 11610 hideExpandedCounter: true, // Hide counter badge if parent is expanded 11611 hideExpanders: false, // Hide expanders if all child nodes are hidden by filter 11612 highlight: true, // Highlight matches by wrapping inside <mark> tags 11613 leavesOnly: false, // Match end nodes only 11614 nodata: true, // Display a 'no data' status node if result is empty 11615 mode: "dimm", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) 11616 }, 11617 nodeLoadChildren: function (ctx, source) { 11618 var tree = ctx.tree; 11619 11620 return this._superApply(arguments).done(function () { 11621 if ( 11622 tree.enableFilter && 11623 tree.lastFilterArgs && 11624 ctx.options.filter.autoApply 11625 ) { 11626 tree._applyFilterImpl.apply(tree, tree.lastFilterArgs); 11627 } 11628 }); 11629 }, 11630 nodeSetExpanded: function (ctx, flag, callOpts) { 11631 var node = ctx.node; 11632 11633 delete node._filterAutoExpanded; 11634 // Make sure counter badge is displayed again, when node is beeing collapsed 11635 if ( 11636 !flag && 11637 ctx.options.filter.hideExpandedCounter && 11638 node.$subMatchBadge 11639 ) { 11640 node.$subMatchBadge.show(); 11641 } 11642 return this._superApply(arguments); 11643 }, 11644 nodeRenderStatus: function (ctx) { 11645 // Set classes for current status 11646 var res, 11647 node = ctx.node, 11648 tree = ctx.tree, 11649 opts = ctx.options.filter, 11650 $title = $(node.span).find("span.fancytree-title"), 11651 $span = $(node[tree.statusClassPropName]), 11652 enhanceTitle = ctx.options.enhanceTitle, 11653 escapeTitles = ctx.options.escapeTitles; 11654 11655 res = this._super(ctx); 11656 // nothing to do, if node was not yet rendered 11657 if (!$span.length || !tree.enableFilter) { 11658 return res; 11659 } 11660 $span 11661 .toggleClass("fancytree-match", !!node.match) 11662 .toggleClass("fancytree-submatch", !!node.subMatchCount) 11663 .toggleClass( 11664 "fancytree-hide", 11665 !(node.match || node.subMatchCount) 11666 ); 11667 // Add/update counter badge 11668 if ( 11669 opts.counter && 11670 node.subMatchCount && 11671 (!node.isExpanded() || !opts.hideExpandedCounter) 11672 ) { 11673 if (!node.$subMatchBadge) { 11674 node.$subMatchBadge = $( 11675 "<span class='fancytree-childcounter'/>" 11676 ); 11677 $( 11678 "span.fancytree-icon, span.fancytree-custom-icon", 11679 node.span 11680 ).append(node.$subMatchBadge); 11681 } 11682 node.$subMatchBadge.show().text(node.subMatchCount); 11683 } else if (node.$subMatchBadge) { 11684 node.$subMatchBadge.hide(); 11685 } 11686 // node.debug("nodeRenderStatus", node.titleWithHighlight, node.title) 11687 // #601: also check for $title.length, because we don't need to render 11688 // if node.span is null (i.e. not rendered) 11689 if (node.span && (!node.isEditing || !node.isEditing.call(node))) { 11690 if (node.titleWithHighlight) { 11691 $title.html(node.titleWithHighlight); 11692 } else if (escapeTitles) { 11693 $title.text(node.title); 11694 } else { 11695 $title.html(node.title); 11696 } 11697 if (enhanceTitle) { 11698 enhanceTitle( 11699 { type: "enhanceTitle" }, 11700 { node: node, $title: $title } 11701 ); 11702 } 11703 } 11704 return res; 11705 }, 11706 }); 11707 // Value returned by `require('jquery.fancytree..')` 11708 return $.ui.fancytree; 11709}); // End of closure 11710 11711 11712/*! Extension 'jquery.fancytree.glyph.js' *//*! 11713 * jquery.fancytree.glyph.js 11714 * 11715 * Use glyph-fonts, ligature-fonts, or SVG icons instead of icon sprites. 11716 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 11717 * 11718 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 11719 * 11720 * Released under the MIT license 11721 * https://github.com/mar10/fancytree/wiki/LicenseInfo 11722 * 11723 * @version 2.38.3 11724 * @date 2023-02-01T20:52:50Z 11725 */ 11726 11727(function (factory) { 11728 if (typeof define === "function" && define.amd) { 11729 // AMD. Register as an anonymous module. 11730 define(["jquery", "./jquery.fancytree"], factory); 11731 } else if (typeof module === "object" && module.exports) { 11732 // Node/CommonJS 11733 require("./jquery.fancytree"); 11734 module.exports = factory(require("jquery")); 11735 } else { 11736 // Browser globals 11737 factory(jQuery); 11738 } 11739})(function ($) { 11740 "use strict"; 11741 11742 /****************************************************************************** 11743 * Private functions and variables 11744 */ 11745 11746 var FT = $.ui.fancytree, 11747 PRESETS = { 11748 awesome3: { 11749 // Outdated! 11750 _addClass: "", 11751 checkbox: "icon-check-empty", 11752 checkboxSelected: "icon-check", 11753 checkboxUnknown: "icon-check icon-muted", 11754 dragHelper: "icon-caret-right", 11755 dropMarker: "icon-caret-right", 11756 error: "icon-exclamation-sign", 11757 expanderClosed: "icon-caret-right", 11758 expanderLazy: "icon-angle-right", 11759 expanderOpen: "icon-caret-down", 11760 loading: "icon-refresh icon-spin", 11761 nodata: "icon-meh", 11762 noExpander: "", 11763 radio: "icon-circle-blank", 11764 radioSelected: "icon-circle", 11765 // radioUnknown: "icon-circle icon-muted", 11766 // Default node icons. 11767 // (Use tree.options.icon callback to define custom icons based on node data) 11768 doc: "icon-file-alt", 11769 docOpen: "icon-file-alt", 11770 folder: "icon-folder-close-alt", 11771 folderOpen: "icon-folder-open-alt", 11772 }, 11773 awesome4: { 11774 _addClass: "fa", 11775 checkbox: "fa-square-o", 11776 checkboxSelected: "fa-check-square-o", 11777 checkboxUnknown: "fa-square fancytree-helper-indeterminate-cb", 11778 dragHelper: "fa-arrow-right", 11779 dropMarker: "fa-long-arrow-right", 11780 error: "fa-warning", 11781 expanderClosed: "fa-caret-right", 11782 expanderLazy: "fa-angle-right", 11783 expanderOpen: "fa-caret-down", 11784 // We may prevent wobbling rotations on FF by creating a separate sub element: 11785 loading: { html: "<span class='fa fa-spinner fa-pulse' />" }, 11786 nodata: "fa-meh-o", 11787 noExpander: "", 11788 radio: "fa-circle-thin", // "fa-circle-o" 11789 radioSelected: "fa-circle", 11790 // radioUnknown: "fa-dot-circle-o", 11791 // Default node icons. 11792 // (Use tree.options.icon callback to define custom icons based on node data) 11793 doc: "fa-file-o", 11794 docOpen: "fa-file-o", 11795 folder: "fa-folder-o", 11796 folderOpen: "fa-folder-open-o", 11797 }, 11798 awesome5: { 11799 // fontawesome 5 have several different base classes 11800 // "far, fas, fal and fab" The rendered svg puts that prefix 11801 // in a different location so we have to keep them separate here 11802 _addClass: "", 11803 checkbox: "far fa-square", 11804 checkboxSelected: "far fa-check-square", 11805 // checkboxUnknown: "far fa-window-close", 11806 checkboxUnknown: 11807 "fas fa-square fancytree-helper-indeterminate-cb", 11808 radio: "far fa-circle", 11809 radioSelected: "fas fa-circle", 11810 radioUnknown: "far fa-dot-circle", 11811 dragHelper: "fas fa-arrow-right", 11812 dropMarker: "fas fa-long-arrow-alt-right", 11813 error: "fas fa-exclamation-triangle", 11814 expanderClosed: "fas fa-caret-right", 11815 expanderLazy: "fas fa-angle-right", 11816 expanderOpen: "fas fa-caret-down", 11817 loading: "fas fa-spinner fa-pulse", 11818 nodata: "far fa-meh", 11819 noExpander: "", 11820 // Default node icons. 11821 // (Use tree.options.icon callback to define custom icons based on node data) 11822 doc: "far fa-file", 11823 docOpen: "far fa-file", 11824 folder: "far fa-folder", 11825 folderOpen: "far fa-folder-open", 11826 }, 11827 bootstrap3: { 11828 _addClass: "glyphicon", 11829 checkbox: "glyphicon-unchecked", 11830 checkboxSelected: "glyphicon-check", 11831 checkboxUnknown: 11832 "glyphicon-expand fancytree-helper-indeterminate-cb", // "glyphicon-share", 11833 dragHelper: "glyphicon-play", 11834 dropMarker: "glyphicon-arrow-right", 11835 error: "glyphicon-warning-sign", 11836 expanderClosed: "glyphicon-menu-right", // glyphicon-plus-sign 11837 expanderLazy: "glyphicon-menu-right", // glyphicon-plus-sign 11838 expanderOpen: "glyphicon-menu-down", // glyphicon-minus-sign 11839 loading: "glyphicon-refresh fancytree-helper-spin", 11840 nodata: "glyphicon-info-sign", 11841 noExpander: "", 11842 radio: "glyphicon-remove-circle", // "glyphicon-unchecked", 11843 radioSelected: "glyphicon-ok-circle", // "glyphicon-check", 11844 // radioUnknown: "glyphicon-ban-circle", 11845 // Default node icons. 11846 // (Use tree.options.icon callback to define custom icons based on node data) 11847 doc: "glyphicon-file", 11848 docOpen: "glyphicon-file", 11849 folder: "glyphicon-folder-close", 11850 folderOpen: "glyphicon-folder-open", 11851 }, 11852 material: { 11853 _addClass: "material-icons", 11854 checkbox: { text: "check_box_outline_blank" }, 11855 checkboxSelected: { text: "check_box" }, 11856 checkboxUnknown: { text: "indeterminate_check_box" }, 11857 dragHelper: { text: "play_arrow" }, 11858 dropMarker: { text: "arrow-forward" }, 11859 error: { text: "warning" }, 11860 expanderClosed: { text: "chevron_right" }, 11861 expanderLazy: { text: "last_page" }, 11862 expanderOpen: { text: "expand_more" }, 11863 loading: { 11864 text: "autorenew", 11865 addClass: "fancytree-helper-spin", 11866 }, 11867 nodata: { text: "info" }, 11868 noExpander: { text: "" }, 11869 radio: { text: "radio_button_unchecked" }, 11870 radioSelected: { text: "radio_button_checked" }, 11871 // Default node icons. 11872 // (Use tree.options.icon callback to define custom icons based on node data) 11873 doc: { text: "insert_drive_file" }, 11874 docOpen: { text: "insert_drive_file" }, 11875 folder: { text: "folder" }, 11876 folderOpen: { text: "folder_open" }, 11877 }, 11878 }; 11879 11880 function setIcon(node, span, baseClass, opts, type) { 11881 var map = opts.map, 11882 icon = map[type], 11883 $span = $(span), 11884 $counter = $span.find(".fancytree-childcounter"), 11885 setClass = baseClass + " " + (map._addClass || ""); 11886 11887 // #871 Allow a callback 11888 if (typeof icon === "function") { 11889 icon = icon.call(this, node, span, type); 11890 } 11891 // node.debug( "setIcon(" + baseClass + ", " + type + "): " + "oldIcon" + " -> " + icon ); 11892 // #871: propsed this, but I am not sure how robust this is, e.g. 11893 // the prefix (fas, far) class changes are not considered? 11894 // if (span.tagName === "svg" && opts.preset === "awesome5") { 11895 // // fa5 script converts <i> to <svg> so call a specific handler. 11896 // var oldIcon = "fa-" + $span.data("icon"); 11897 // // node.debug( "setIcon(" + baseClass + ", " + type + "): " + oldIcon + " -> " + icon ); 11898 // if (typeof oldIcon === "string") { 11899 // $span.removeClass(oldIcon); 11900 // } 11901 // if (typeof icon === "string") { 11902 // $span.addClass(icon); 11903 // } 11904 // return; 11905 // } 11906 if (typeof icon === "string") { 11907 // #883: remove inner html that may be added by prev. mode 11908 span.innerHTML = ""; 11909 $span.attr("class", setClass + " " + icon).append($counter); 11910 } else if (icon) { 11911 if (icon.text) { 11912 span.textContent = "" + icon.text; 11913 } else if (icon.html) { 11914 span.innerHTML = icon.html; 11915 } else { 11916 span.innerHTML = ""; 11917 } 11918 $span 11919 .attr("class", setClass + " " + (icon.addClass || "")) 11920 .append($counter); 11921 } 11922 } 11923 11924 $.ui.fancytree.registerExtension({ 11925 name: "glyph", 11926 version: "2.38.3", 11927 // Default options for this extension. 11928 options: { 11929 preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material' 11930 map: {}, 11931 }, 11932 11933 treeInit: function (ctx) { 11934 var tree = ctx.tree, 11935 opts = ctx.options.glyph; 11936 11937 if (opts.preset) { 11938 FT.assert( 11939 !!PRESETS[opts.preset], 11940 "Invalid value for `options.glyph.preset`: " + opts.preset 11941 ); 11942 opts.map = $.extend({}, PRESETS[opts.preset], opts.map); 11943 } else { 11944 tree.warn("ext-glyph: missing `preset` option."); 11945 } 11946 this._superApply(arguments); 11947 tree.$container.addClass("fancytree-ext-glyph"); 11948 }, 11949 nodeRenderStatus: function (ctx) { 11950 var checkbox, 11951 icon, 11952 res, 11953 span, 11954 node = ctx.node, 11955 $span = $(node.span), 11956 opts = ctx.options.glyph; 11957 11958 res = this._super(ctx); 11959 11960 if (node.isRootNode()) { 11961 return res; 11962 } 11963 span = $span.children(".fancytree-expander").get(0); 11964 if (span) { 11965 // if( node.isLoading() ){ 11966 // icon = "loading"; 11967 if (node.expanded && node.hasChildren()) { 11968 icon = "expanderOpen"; 11969 } else if (node.isUndefined()) { 11970 icon = "expanderLazy"; 11971 } else if (node.hasChildren()) { 11972 icon = "expanderClosed"; 11973 } else { 11974 icon = "noExpander"; 11975 } 11976 // span.className = "fancytree-expander " + map[icon]; 11977 setIcon(node, span, "fancytree-expander", opts, icon); 11978 } 11979 11980 if (node.tr) { 11981 span = $("td", node.tr).find(".fancytree-checkbox").get(0); 11982 } else { 11983 span = $span.children(".fancytree-checkbox").get(0); 11984 } 11985 if (span) { 11986 checkbox = FT.evalOption("checkbox", node, node, opts, false); 11987 if ( 11988 (node.parent && node.parent.radiogroup) || 11989 checkbox === "radio" 11990 ) { 11991 icon = node.selected ? "radioSelected" : "radio"; 11992 setIcon( 11993 node, 11994 span, 11995 "fancytree-checkbox fancytree-radio", 11996 opts, 11997 icon 11998 ); 11999 } else { 12000 // eslint-disable-next-line no-nested-ternary 12001 icon = node.selected 12002 ? "checkboxSelected" 12003 : node.partsel 12004 ? "checkboxUnknown" 12005 : "checkbox"; 12006 // span.className = "fancytree-checkbox " + map[icon]; 12007 setIcon(node, span, "fancytree-checkbox", opts, icon); 12008 } 12009 } 12010 12011 // Standard icon (note that this does not match .fancytree-custom-icon, 12012 // that might be set by opts.icon callbacks) 12013 span = $span.children(".fancytree-icon").get(0); 12014 if (span) { 12015 if (node.statusNodeType) { 12016 icon = node.statusNodeType; // loading, error 12017 } else if (node.folder) { 12018 icon = 12019 node.expanded && node.hasChildren() 12020 ? "folderOpen" 12021 : "folder"; 12022 } else { 12023 icon = node.expanded ? "docOpen" : "doc"; 12024 } 12025 setIcon(node, span, "fancytree-icon", opts, icon); 12026 } 12027 return res; 12028 }, 12029 nodeSetStatus: function (ctx, status, message, details) { 12030 var res, 12031 span, 12032 opts = ctx.options.glyph, 12033 node = ctx.node; 12034 12035 res = this._superApply(arguments); 12036 12037 if ( 12038 status === "error" || 12039 status === "loading" || 12040 status === "nodata" 12041 ) { 12042 if (node.parent) { 12043 span = $(".fancytree-expander", node.span).get(0); 12044 if (span) { 12045 setIcon(node, span, "fancytree-expander", opts, status); 12046 } 12047 } else { 12048 // 12049 span = $( 12050 ".fancytree-statusnode-" + status, 12051 node[this.nodeContainerAttrName] 12052 ) 12053 .find(".fancytree-icon") 12054 .get(0); 12055 if (span) { 12056 setIcon(node, span, "fancytree-icon", opts, status); 12057 } 12058 } 12059 } 12060 return res; 12061 }, 12062 }); 12063 // Value returned by `require('jquery.fancytree..')` 12064 return $.ui.fancytree; 12065}); // End of closure 12066 12067 12068/*! Extension 'jquery.fancytree.gridnav.js' *//*! 12069 * jquery.fancytree.gridnav.js 12070 * 12071 * Support keyboard navigation for trees with embedded input controls. 12072 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12073 * 12074 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12075 * 12076 * Released under the MIT license 12077 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12078 * 12079 * @version 2.38.3 12080 * @date 2023-02-01T20:52:50Z 12081 */ 12082 12083(function (factory) { 12084 if (typeof define === "function" && define.amd) { 12085 // AMD. Register as an anonymous module. 12086 define([ 12087 "jquery", 12088 "./jquery.fancytree", 12089 "./jquery.fancytree.table", 12090 ], factory); 12091 } else if (typeof module === "object" && module.exports) { 12092 // Node/CommonJS 12093 require("./jquery.fancytree.table"); // core + table 12094 module.exports = factory(require("jquery")); 12095 } else { 12096 // Browser globals 12097 factory(jQuery); 12098 } 12099})(function ($) { 12100 "use strict"; 12101 12102 /******************************************************************************* 12103 * Private functions and variables 12104 */ 12105 12106 // Allow these navigation keys even when input controls are focused 12107 12108 var KC = $.ui.keyCode, 12109 // which keys are *not* handled by embedded control, but passed to tree 12110 // navigation handler: 12111 NAV_KEYS = { 12112 text: [KC.UP, KC.DOWN], 12113 checkbox: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 12114 link: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 12115 radiobutton: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT], 12116 "select-one": [KC.LEFT, KC.RIGHT], 12117 "select-multiple": [KC.LEFT, KC.RIGHT], 12118 }; 12119 12120 /* Calculate TD column index (considering colspans).*/ 12121 function getColIdx($tr, $td) { 12122 var colspan, 12123 td = $td.get(0), 12124 idx = 0; 12125 12126 $tr.children().each(function () { 12127 if (this === td) { 12128 return false; 12129 } 12130 colspan = $(this).prop("colspan"); 12131 idx += colspan ? colspan : 1; 12132 }); 12133 return idx; 12134 } 12135 12136 /* Find TD at given column index (considering colspans).*/ 12137 function findTdAtColIdx($tr, colIdx) { 12138 var colspan, 12139 res = null, 12140 idx = 0; 12141 12142 $tr.children().each(function () { 12143 if (idx >= colIdx) { 12144 res = $(this); 12145 return false; 12146 } 12147 colspan = $(this).prop("colspan"); 12148 idx += colspan ? colspan : 1; 12149 }); 12150 return res; 12151 } 12152 12153 /* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */ 12154 function findNeighbourTd($target, keyCode) { 12155 var $tr, 12156 colIdx, 12157 $td = $target.closest("td"), 12158 $tdNext = null; 12159 12160 switch (keyCode) { 12161 case KC.LEFT: 12162 $tdNext = $td.prev(); 12163 break; 12164 case KC.RIGHT: 12165 $tdNext = $td.next(); 12166 break; 12167 case KC.UP: 12168 case KC.DOWN: 12169 $tr = $td.parent(); 12170 colIdx = getColIdx($tr, $td); 12171 while (true) { 12172 $tr = keyCode === KC.UP ? $tr.prev() : $tr.next(); 12173 if (!$tr.length) { 12174 break; 12175 } 12176 // Skip hidden rows 12177 if ($tr.is(":hidden")) { 12178 continue; 12179 } 12180 // Find adjacent cell in the same column 12181 $tdNext = findTdAtColIdx($tr, colIdx); 12182 // Skip cells that don't conatain a focusable element 12183 if ($tdNext && $tdNext.find(":input,a").length) { 12184 break; 12185 } 12186 } 12187 break; 12188 } 12189 return $tdNext; 12190 } 12191 12192 /******************************************************************************* 12193 * Extension code 12194 */ 12195 $.ui.fancytree.registerExtension({ 12196 name: "gridnav", 12197 version: "2.38.3", 12198 // Default options for this extension. 12199 options: { 12200 autofocusInput: false, // Focus first embedded input if node gets activated 12201 handleCursorKeys: true, // Allow UP/DOWN in inputs to move to prev/next node 12202 }, 12203 12204 treeInit: function (ctx) { 12205 // gridnav requires the table extension to be loaded before itself 12206 this._requireExtension("table", true, true); 12207 this._superApply(arguments); 12208 12209 this.$container.addClass("fancytree-ext-gridnav"); 12210 12211 // Activate node if embedded input gets focus (due to a click) 12212 this.$container.on("focusin", function (event) { 12213 var ctx2, 12214 node = $.ui.fancytree.getNode(event.target); 12215 12216 if (node && !node.isActive()) { 12217 // Call node.setActive(), but also pass the event 12218 ctx2 = ctx.tree._makeHookContext(node, event); 12219 ctx.tree._callHook("nodeSetActive", ctx2, true); 12220 } 12221 }); 12222 }, 12223 nodeSetActive: function (ctx, flag, callOpts) { 12224 var $outer, 12225 opts = ctx.options.gridnav, 12226 node = ctx.node, 12227 event = ctx.originalEvent || {}, 12228 triggeredByInput = $(event.target).is(":input"); 12229 12230 flag = flag !== false; 12231 12232 this._superApply(arguments); 12233 12234 if (flag) { 12235 if (ctx.options.titlesTabbable) { 12236 if (!triggeredByInput) { 12237 $(node.span).find("span.fancytree-title").focus(); 12238 node.setFocus(); 12239 } 12240 // If one node is tabbable, the container no longer needs to be 12241 ctx.tree.$container.attr("tabindex", "-1"); 12242 // ctx.tree.$container.removeAttr("tabindex"); 12243 } else if (opts.autofocusInput && !triggeredByInput) { 12244 // Set focus to input sub input (if node was clicked, but not 12245 // when TAB was pressed ) 12246 $outer = $(node.tr || node.span); 12247 $outer.find(":input:enabled").first().focus(); 12248 } 12249 } 12250 }, 12251 nodeKeydown: function (ctx) { 12252 var inputType, 12253 handleKeys, 12254 $td, 12255 opts = ctx.options.gridnav, 12256 event = ctx.originalEvent, 12257 $target = $(event.target); 12258 12259 if ($target.is(":input:enabled")) { 12260 inputType = $target.prop("type"); 12261 } else if ($target.is("a")) { 12262 inputType = "link"; 12263 } 12264 // ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType); 12265 12266 if (inputType && opts.handleCursorKeys) { 12267 handleKeys = NAV_KEYS[inputType]; 12268 if (handleKeys && $.inArray(event.which, handleKeys) >= 0) { 12269 $td = findNeighbourTd($target, event.which); 12270 if ($td && $td.length) { 12271 // ctx.node.debug("ignore keydown in input", event.which, handleKeys); 12272 $td.find(":input:enabled,a").focus(); 12273 // Prevent Fancytree default navigation 12274 return false; 12275 } 12276 } 12277 return true; 12278 } 12279 // ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType); 12280 return this._superApply(arguments); 12281 }, 12282 }); 12283 // Value returned by `require('jquery.fancytree..')` 12284 return $.ui.fancytree; 12285}); // End of closure 12286 12287 12288/*! Extension 'jquery.fancytree.multi.js' *//*! 12289 * jquery.fancytree.multi.js 12290 * 12291 * Allow multiple selection of nodes by mouse or keyboard. 12292 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12293 * 12294 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12295 * 12296 * Released under the MIT license 12297 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12298 * 12299 * @version 2.38.3 12300 * @date 2023-02-01T20:52:50Z 12301 */ 12302 12303(function (factory) { 12304 if (typeof define === "function" && define.amd) { 12305 // AMD. Register as an anonymous module. 12306 define(["jquery", "./jquery.fancytree"], factory); 12307 } else if (typeof module === "object" && module.exports) { 12308 // Node/CommonJS 12309 require("./jquery.fancytree"); 12310 module.exports = factory(require("jquery")); 12311 } else { 12312 // Browser globals 12313 factory(jQuery); 12314 } 12315})(function ($) { 12316 "use strict"; 12317 12318 /******************************************************************************* 12319 * Private functions and variables 12320 */ 12321 12322 // var isMac = /Mac/.test(navigator.platform); 12323 12324 /******************************************************************************* 12325 * Extension code 12326 */ 12327 $.ui.fancytree.registerExtension({ 12328 name: "multi", 12329 version: "2.38.3", 12330 // Default options for this extension. 12331 options: { 12332 allowNoSelect: false, // 12333 mode: "sameParent", // 12334 // Events: 12335 // beforeSelect: $.noop // Return false to prevent cancel/save (data.input is available) 12336 }, 12337 12338 treeInit: function (ctx) { 12339 this._superApply(arguments); 12340 this.$container.addClass("fancytree-ext-multi"); 12341 if (ctx.options.selectMode === 1) { 12342 $.error( 12343 "Fancytree ext-multi: selectMode: 1 (single) is not compatible." 12344 ); 12345 } 12346 }, 12347 nodeClick: function (ctx) { 12348 var //pluginOpts = ctx.options.multi, 12349 tree = ctx.tree, 12350 node = ctx.node, 12351 activeNode = tree.getActiveNode() || tree.getFirstChild(), 12352 isCbClick = ctx.targetType === "checkbox", 12353 isExpanderClick = ctx.targetType === "expander", 12354 eventStr = $.ui.fancytree.eventToString(ctx.originalEvent); 12355 12356 switch (eventStr) { 12357 case "click": 12358 if (isExpanderClick) { 12359 break; 12360 } // Default handler will expand/collapse 12361 if (!isCbClick) { 12362 tree.selectAll(false); 12363 // Select clicked node (radio-button mode) 12364 node.setSelected(); 12365 } 12366 // Default handler will toggle checkbox clicks and activate 12367 break; 12368 case "shift+click": 12369 // node.debug("click") 12370 tree.visitRows( 12371 function (n) { 12372 // n.debug("click2", n===node, node) 12373 n.setSelected(); 12374 if (n === node) { 12375 return false; 12376 } 12377 }, 12378 { 12379 start: activeNode, 12380 reverse: activeNode.isBelowOf(node), 12381 } 12382 ); 12383 break; 12384 case "ctrl+click": 12385 case "meta+click": // Mac: [Command] 12386 node.toggleSelected(); 12387 return; 12388 } 12389 return this._superApply(arguments); 12390 }, 12391 nodeKeydown: function (ctx) { 12392 var tree = ctx.tree, 12393 node = ctx.node, 12394 event = ctx.originalEvent, 12395 eventStr = $.ui.fancytree.eventToString(event); 12396 12397 switch (eventStr) { 12398 case "up": 12399 case "down": 12400 tree.selectAll(false); 12401 node.navigate(event.which, true); 12402 tree.getActiveNode().setSelected(); 12403 break; 12404 case "shift+up": 12405 case "shift+down": 12406 node.navigate(event.which, true); 12407 tree.getActiveNode().setSelected(); 12408 break; 12409 } 12410 return this._superApply(arguments); 12411 }, 12412 }); 12413 // Value returned by `require('jquery.fancytree..')` 12414 return $.ui.fancytree; 12415}); // End of closure 12416 12417 12418/*! Extension 'jquery.fancytree.persist.js' *//*! 12419 * jquery.fancytree.persist.js 12420 * 12421 * Persist tree status in cookiesRemove or highlight tree nodes, based on a filter. 12422 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12423 * 12424 * @depends: js-cookie or jquery-cookie 12425 * 12426 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12427 * 12428 * Released under the MIT license 12429 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12430 * 12431 * @version 2.38.3 12432 * @date 2023-02-01T20:52:50Z 12433 */ 12434 12435(function (factory) { 12436 if (typeof define === "function" && define.amd) { 12437 // AMD. Register as an anonymous module. 12438 define(["jquery", "./jquery.fancytree"], factory); 12439 } else if (typeof module === "object" && module.exports) { 12440 // Node/CommonJS 12441 require("./jquery.fancytree"); 12442 module.exports = factory(require("jquery")); 12443 } else { 12444 // Browser globals 12445 factory(jQuery); 12446 } 12447})(function ($) { 12448 "use strict"; 12449 /* global Cookies:false */ 12450 12451 /******************************************************************************* 12452 * Private functions and variables 12453 */ 12454 var cookieStore = null, 12455 localStorageStore = null, 12456 sessionStorageStore = null, 12457 _assert = $.ui.fancytree.assert, 12458 ACTIVE = "active", 12459 EXPANDED = "expanded", 12460 FOCUS = "focus", 12461 SELECTED = "selected"; 12462 12463 // Accessing window.xxxStorage may raise security exceptions (see #1022) 12464 try { 12465 _assert(window.localStorage && window.localStorage.getItem); 12466 localStorageStore = { 12467 get: function (key) { 12468 return window.localStorage.getItem(key); 12469 }, 12470 set: function (key, value) { 12471 window.localStorage.setItem(key, value); 12472 }, 12473 remove: function (key) { 12474 window.localStorage.removeItem(key); 12475 }, 12476 }; 12477 } catch (e) { 12478 $.ui.fancytree.warn("Could not access window.localStorage", e); 12479 } 12480 12481 try { 12482 _assert(window.sessionStorage && window.sessionStorage.getItem); 12483 sessionStorageStore = { 12484 get: function (key) { 12485 return window.sessionStorage.getItem(key); 12486 }, 12487 set: function (key, value) { 12488 window.sessionStorage.setItem(key, value); 12489 }, 12490 remove: function (key) { 12491 window.sessionStorage.removeItem(key); 12492 }, 12493 }; 12494 } catch (e) { 12495 $.ui.fancytree.warn("Could not access window.sessionStorage", e); 12496 } 12497 12498 if (typeof Cookies === "function") { 12499 // Assume https://github.com/js-cookie/js-cookie 12500 cookieStore = { 12501 get: Cookies.get, 12502 set: function (key, value) { 12503 Cookies.set(key, value, this.options.persist.cookie); 12504 }, 12505 remove: Cookies.remove, 12506 }; 12507 } else if ($ && typeof $.cookie === "function") { 12508 // Fall back to https://github.com/carhartl/jquery-cookie 12509 cookieStore = { 12510 get: $.cookie, 12511 set: function (key, value) { 12512 $.cookie(key, value, this.options.persist.cookie); 12513 }, 12514 remove: $.removeCookie, 12515 }; 12516 } 12517 12518 /* Recursively load lazy nodes 12519 * @param {string} mode 'load', 'expand', false 12520 */ 12521 function _loadLazyNodes(tree, local, keyList, mode, dfd) { 12522 var i, 12523 key, 12524 l, 12525 node, 12526 foundOne = false, 12527 expandOpts = tree.options.persist.expandOpts, 12528 deferredList = [], 12529 missingKeyList = []; 12530 12531 keyList = keyList || []; 12532 dfd = dfd || $.Deferred(); 12533 12534 for (i = 0, l = keyList.length; i < l; i++) { 12535 key = keyList[i]; 12536 node = tree.getNodeByKey(key); 12537 if (node) { 12538 if (mode && node.isUndefined()) { 12539 foundOne = true; 12540 tree.debug( 12541 "_loadLazyNodes: " + node + " is lazy: loading..." 12542 ); 12543 if (mode === "expand") { 12544 deferredList.push(node.setExpanded(true, expandOpts)); 12545 } else { 12546 deferredList.push(node.load()); 12547 } 12548 } else { 12549 tree.debug("_loadLazyNodes: " + node + " already loaded."); 12550 node.setExpanded(true, expandOpts); 12551 } 12552 } else { 12553 missingKeyList.push(key); 12554 tree.debug("_loadLazyNodes: " + node + " was not yet found."); 12555 } 12556 } 12557 12558 $.when.apply($, deferredList).always(function () { 12559 // All lazy-expands have finished 12560 if (foundOne && missingKeyList.length > 0) { 12561 // If we read new nodes from server, try to resolve yet-missing keys 12562 _loadLazyNodes(tree, local, missingKeyList, mode, dfd); 12563 } else { 12564 if (missingKeyList.length) { 12565 tree.warn( 12566 "_loadLazyNodes: could not load those keys: ", 12567 missingKeyList 12568 ); 12569 for (i = 0, l = missingKeyList.length; i < l; i++) { 12570 key = keyList[i]; 12571 local._appendKey(EXPANDED, keyList[i], false); 12572 } 12573 } 12574 dfd.resolve(); 12575 } 12576 }); 12577 return dfd; 12578 } 12579 12580 /** 12581 * [ext-persist] Remove persistence data of the given type(s). 12582 * Called like 12583 * $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected"); 12584 * 12585 * @alias Fancytree#clearPersistData 12586 * @requires jquery.fancytree.persist.js 12587 */ 12588 $.ui.fancytree._FancytreeClass.prototype.clearPersistData = function ( 12589 types 12590 ) { 12591 var local = this.ext.persist, 12592 prefix = local.cookiePrefix; 12593 12594 types = types || "active expanded focus selected"; 12595 if (types.indexOf(ACTIVE) >= 0) { 12596 local._data(prefix + ACTIVE, null); 12597 } 12598 if (types.indexOf(EXPANDED) >= 0) { 12599 local._data(prefix + EXPANDED, null); 12600 } 12601 if (types.indexOf(FOCUS) >= 0) { 12602 local._data(prefix + FOCUS, null); 12603 } 12604 if (types.indexOf(SELECTED) >= 0) { 12605 local._data(prefix + SELECTED, null); 12606 } 12607 }; 12608 12609 $.ui.fancytree._FancytreeClass.prototype.clearCookies = function (types) { 12610 this.warn( 12611 "'tree.clearCookies()' is deprecated since v2.27.0: use 'clearPersistData()' instead." 12612 ); 12613 return this.clearPersistData(types); 12614 }; 12615 12616 /** 12617 * [ext-persist] Return persistence information from cookies 12618 * 12619 * Called like 12620 * $.ui.fancytree.getTree("#tree").getPersistData(); 12621 * 12622 * @alias Fancytree#getPersistData 12623 * @requires jquery.fancytree.persist.js 12624 */ 12625 $.ui.fancytree._FancytreeClass.prototype.getPersistData = function () { 12626 var local = this.ext.persist, 12627 prefix = local.cookiePrefix, 12628 delim = local.cookieDelimiter, 12629 res = {}; 12630 12631 res[ACTIVE] = local._data(prefix + ACTIVE); 12632 res[EXPANDED] = (local._data(prefix + EXPANDED) || "").split(delim); 12633 res[SELECTED] = (local._data(prefix + SELECTED) || "").split(delim); 12634 res[FOCUS] = local._data(prefix + FOCUS); 12635 return res; 12636 }; 12637 12638 /****************************************************************************** 12639 * Extension code 12640 */ 12641 $.ui.fancytree.registerExtension({ 12642 name: "persist", 12643 version: "2.38.3", 12644 // Default options for this extension. 12645 options: { 12646 cookieDelimiter: "~", 12647 cookiePrefix: undefined, // 'fancytree-<treeId>-' by default 12648 cookie: { 12649 raw: false, 12650 expires: "", 12651 path: "", 12652 domain: "", 12653 secure: false, 12654 }, 12655 expandLazy: false, // true: recursively expand and load lazy nodes 12656 expandOpts: undefined, // optional `opts` argument passed to setExpanded() 12657 fireActivate: true, // false: suppress `activate` event after active node was restored 12658 overrideSource: true, // true: cookie takes precedence over `source` data attributes. 12659 store: "auto", // 'cookie': force cookie, 'local': force localStore, 'session': force sessionStore 12660 types: "active expanded focus selected", 12661 }, 12662 12663 /* Generic read/write string data to cookie, sessionStorage or localStorage. */ 12664 _data: function (key, value) { 12665 var store = this._local.store; 12666 12667 if (value === undefined) { 12668 return store.get.call(this, key); 12669 } else if (value === null) { 12670 store.remove.call(this, key); 12671 } else { 12672 store.set.call(this, key, value); 12673 } 12674 }, 12675 12676 /* Append `key` to a cookie. */ 12677 _appendKey: function (type, key, flag) { 12678 key = "" + key; // #90 12679 var local = this._local, 12680 instOpts = this.options.persist, 12681 delim = instOpts.cookieDelimiter, 12682 cookieName = local.cookiePrefix + type, 12683 data = local._data(cookieName), 12684 keyList = data ? data.split(delim) : [], 12685 idx = $.inArray(key, keyList); 12686 // Remove, even if we add a key, so the key is always the last entry 12687 if (idx >= 0) { 12688 keyList.splice(idx, 1); 12689 } 12690 // Append key to cookie 12691 if (flag) { 12692 keyList.push(key); 12693 } 12694 local._data(cookieName, keyList.join(delim)); 12695 }, 12696 12697 treeInit: function (ctx) { 12698 var tree = ctx.tree, 12699 opts = ctx.options, 12700 local = this._local, 12701 instOpts = this.options.persist; 12702 12703 // // For 'auto' or 'cookie' mode, the cookie plugin must be available 12704 // _assert((instOpts.store !== "auto" && instOpts.store !== "cookie") || cookieStore, 12705 // "Missing required plugin for 'persist' extension: js.cookie.js or jquery.cookie.js"); 12706 12707 local.cookiePrefix = 12708 instOpts.cookiePrefix || "fancytree-" + tree._id + "-"; 12709 local.storeActive = instOpts.types.indexOf(ACTIVE) >= 0; 12710 local.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0; 12711 local.storeSelected = instOpts.types.indexOf(SELECTED) >= 0; 12712 local.storeFocus = instOpts.types.indexOf(FOCUS) >= 0; 12713 local.store = null; 12714 12715 if (instOpts.store === "auto") { 12716 instOpts.store = localStorageStore ? "local" : "cookie"; 12717 } 12718 if ($.isPlainObject(instOpts.store)) { 12719 local.store = instOpts.store; 12720 } else if (instOpts.store === "cookie") { 12721 local.store = cookieStore; 12722 } else if (instOpts.store === "local") { 12723 local.store = 12724 instOpts.store === "local" 12725 ? localStorageStore 12726 : sessionStorageStore; 12727 } else if (instOpts.store === "session") { 12728 local.store = 12729 instOpts.store === "local" 12730 ? localStorageStore 12731 : sessionStorageStore; 12732 } 12733 _assert(local.store, "Need a valid store."); 12734 12735 // Bind init-handler to apply cookie state 12736 tree.$div.on("fancytreeinit", function (event) { 12737 if ( 12738 tree._triggerTreeEvent("beforeRestore", null, {}) === false 12739 ) { 12740 return; 12741 } 12742 12743 var cookie, 12744 dfd, 12745 i, 12746 keyList, 12747 node, 12748 prevFocus = local._data(local.cookiePrefix + FOCUS), // record this before node.setActive() overrides it; 12749 noEvents = instOpts.fireActivate === false; 12750 12751 // tree.debug("document.cookie:", document.cookie); 12752 12753 cookie = local._data(local.cookiePrefix + EXPANDED); 12754 keyList = cookie && cookie.split(instOpts.cookieDelimiter); 12755 12756 if (local.storeExpanded) { 12757 // Recursively load nested lazy nodes if expandLazy is 'expand' or 'load' 12758 // Also remove expand-cookies for unmatched nodes 12759 dfd = _loadLazyNodes( 12760 tree, 12761 local, 12762 keyList, 12763 instOpts.expandLazy ? "expand" : false, 12764 null 12765 ); 12766 } else { 12767 // nothing to do 12768 dfd = new $.Deferred().resolve(); 12769 } 12770 12771 dfd.done(function () { 12772 if (local.storeSelected) { 12773 cookie = local._data(local.cookiePrefix + SELECTED); 12774 if (cookie) { 12775 keyList = cookie.split(instOpts.cookieDelimiter); 12776 for (i = 0; i < keyList.length; i++) { 12777 node = tree.getNodeByKey(keyList[i]); 12778 if (node) { 12779 if ( 12780 node.selected === undefined || 12781 (instOpts.overrideSource && 12782 node.selected === false) 12783 ) { 12784 // node.setSelected(); 12785 node.selected = true; 12786 node.renderStatus(); 12787 } 12788 } else { 12789 // node is no longer member of the tree: remove from cookie also 12790 local._appendKey( 12791 SELECTED, 12792 keyList[i], 12793 false 12794 ); 12795 } 12796 } 12797 } 12798 // In selectMode 3 we have to fix the child nodes, since we 12799 // only stored the selected *top* nodes 12800 if (tree.options.selectMode === 3) { 12801 tree.visit(function (n) { 12802 if (n.selected) { 12803 n.fixSelection3AfterClick(); 12804 return "skip"; 12805 } 12806 }); 12807 } 12808 } 12809 if (local.storeActive) { 12810 cookie = local._data(local.cookiePrefix + ACTIVE); 12811 if ( 12812 cookie && 12813 (opts.persist.overrideSource || !tree.activeNode) 12814 ) { 12815 node = tree.getNodeByKey(cookie); 12816 if (node) { 12817 node.debug("persist: set active", cookie); 12818 // We only want to set the focus if the container 12819 // had the keyboard focus before 12820 node.setActive(true, { 12821 noFocus: true, 12822 noEvents: noEvents, 12823 }); 12824 } 12825 } 12826 } 12827 if (local.storeFocus && prevFocus) { 12828 node = tree.getNodeByKey(prevFocus); 12829 if (node) { 12830 // node.debug("persist: set focus", cookie); 12831 if (tree.options.titlesTabbable) { 12832 $(node.span).find(".fancytree-title").focus(); 12833 } else { 12834 $(tree.$container).focus(); 12835 } 12836 // node.setFocus(); 12837 } 12838 } 12839 tree._triggerTreeEvent("restore", null, {}); 12840 }); 12841 }); 12842 // Init the tree 12843 return this._superApply(arguments); 12844 }, 12845 nodeSetActive: function (ctx, flag, callOpts) { 12846 var res, 12847 local = this._local; 12848 12849 flag = flag !== false; 12850 res = this._superApply(arguments); 12851 12852 if (local.storeActive) { 12853 local._data( 12854 local.cookiePrefix + ACTIVE, 12855 this.activeNode ? this.activeNode.key : null 12856 ); 12857 } 12858 return res; 12859 }, 12860 nodeSetExpanded: function (ctx, flag, callOpts) { 12861 var res, 12862 node = ctx.node, 12863 local = this._local; 12864 12865 flag = flag !== false; 12866 res = this._superApply(arguments); 12867 12868 if (local.storeExpanded) { 12869 local._appendKey(EXPANDED, node.key, flag); 12870 } 12871 return res; 12872 }, 12873 nodeSetFocus: function (ctx, flag) { 12874 var res, 12875 local = this._local; 12876 12877 flag = flag !== false; 12878 res = this._superApply(arguments); 12879 12880 if (local.storeFocus) { 12881 local._data( 12882 local.cookiePrefix + FOCUS, 12883 this.focusNode ? this.focusNode.key : null 12884 ); 12885 } 12886 return res; 12887 }, 12888 nodeSetSelected: function (ctx, flag, callOpts) { 12889 var res, 12890 selNodes, 12891 tree = ctx.tree, 12892 node = ctx.node, 12893 local = this._local; 12894 12895 flag = flag !== false; 12896 res = this._superApply(arguments); 12897 12898 if (local.storeSelected) { 12899 if (tree.options.selectMode === 3) { 12900 // In selectMode 3 we only store the the selected *top* nodes. 12901 // De-selecting a node may also de-select some parents, so we 12902 // calculate the current status again 12903 selNodes = $.map(tree.getSelectedNodes(true), function (n) { 12904 return n.key; 12905 }); 12906 selNodes = selNodes.join( 12907 ctx.options.persist.cookieDelimiter 12908 ); 12909 local._data(local.cookiePrefix + SELECTED, selNodes); 12910 } else { 12911 // beforeSelect can prevent the change - flag doesn't reflect the node.selected state 12912 local._appendKey(SELECTED, node.key, node.selected); 12913 } 12914 } 12915 return res; 12916 }, 12917 }); 12918 // Value returned by `require('jquery.fancytree..')` 12919 return $.ui.fancytree; 12920}); // End of closure 12921 12922 12923/*! Extension 'jquery.fancytree.table.js' *//*! 12924 * jquery.fancytree.table.js 12925 * 12926 * Render tree as table (aka 'tree grid', 'table tree'). 12927 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 12928 * 12929 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 12930 * 12931 * Released under the MIT license 12932 * https://github.com/mar10/fancytree/wiki/LicenseInfo 12933 * 12934 * @version 2.38.3 12935 * @date 2023-02-01T20:52:50Z 12936 */ 12937 12938(function (factory) { 12939 if (typeof define === "function" && define.amd) { 12940 // AMD. Register as an anonymous module. 12941 define(["jquery", "./jquery.fancytree"], factory); 12942 } else if (typeof module === "object" && module.exports) { 12943 // Node/CommonJS 12944 require("./jquery.fancytree"); 12945 module.exports = factory(require("jquery")); 12946 } else { 12947 // Browser globals 12948 factory(jQuery); 12949 } 12950})(function ($) { 12951 "use strict"; 12952 12953 /****************************************************************************** 12954 * Private functions and variables 12955 */ 12956 var _assert = $.ui.fancytree.assert; 12957 12958 function insertFirstChild(referenceNode, newNode) { 12959 referenceNode.insertBefore(newNode, referenceNode.firstChild); 12960 } 12961 12962 function insertSiblingAfter(referenceNode, newNode) { 12963 referenceNode.parentNode.insertBefore( 12964 newNode, 12965 referenceNode.nextSibling 12966 ); 12967 } 12968 12969 /* Show/hide all rows that are structural descendants of `parent`. */ 12970 function setChildRowVisibility(parent, flag) { 12971 parent.visit(function (node) { 12972 var tr = node.tr; 12973 // currentFlag = node.hide ? false : flag; // fix for ext-filter 12974 if (tr) { 12975 tr.style.display = node.hide || !flag ? "none" : ""; 12976 } 12977 if (!node.expanded) { 12978 return "skip"; 12979 } 12980 }); 12981 } 12982 12983 /* Find node that is rendered in previous row. */ 12984 function findPrevRowNode(node) { 12985 var i, 12986 last, 12987 prev, 12988 parent = node.parent, 12989 siblings = parent ? parent.children : null; 12990 12991 if (siblings && siblings.length > 1 && siblings[0] !== node) { 12992 // use the lowest descendant of the preceeding sibling 12993 i = $.inArray(node, siblings); 12994 prev = siblings[i - 1]; 12995 _assert(prev.tr); 12996 // descend to lowest child (with a <tr> tag) 12997 while (prev.children && prev.children.length) { 12998 last = prev.children[prev.children.length - 1]; 12999 if (!last.tr) { 13000 break; 13001 } 13002 prev = last; 13003 } 13004 } else { 13005 // if there is no preceding sibling, use the direct parent 13006 prev = parent; 13007 } 13008 return prev; 13009 } 13010 13011 $.ui.fancytree.registerExtension({ 13012 name: "table", 13013 version: "2.38.3", 13014 // Default options for this extension. 13015 options: { 13016 checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) 13017 indentation: 16, // indent every node level by 16px 13018 mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR 13019 nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0) 13020 }, 13021 // Overide virtual methods for this extension. 13022 // `this` : is this extension object 13023 // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) 13024 treeInit: function (ctx) { 13025 var i, 13026 n, 13027 $row, 13028 $tbody, 13029 tree = ctx.tree, 13030 opts = ctx.options, 13031 tableOpts = opts.table, 13032 $table = tree.widget.element; 13033 13034 if (tableOpts.customStatus != null) { 13035 if (opts.renderStatusColumns == null) { 13036 tree.warn( 13037 "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead." 13038 ); 13039 opts.renderStatusColumns = tableOpts.customStatus; 13040 } else { 13041 $.error( 13042 "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead." 13043 ); 13044 } 13045 } 13046 if (opts.renderStatusColumns) { 13047 if (opts.renderStatusColumns === true) { 13048 opts.renderStatusColumns = opts.renderColumns; 13049 // } else if( opts.renderStatusColumns === "wide" ) { 13050 // opts.renderStatusColumns = _renderStatusNodeWide; 13051 } 13052 } 13053 13054 $table.addClass("fancytree-container fancytree-ext-table"); 13055 $tbody = $table.find(">tbody"); 13056 if (!$tbody.length) { 13057 // TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s: 13058 if ($table.find(">tr").length) { 13059 $.error( 13060 "Expected table > tbody > tr. If you see this please open an issue." 13061 ); 13062 } 13063 $tbody = $("<tbody>").appendTo($table); 13064 } 13065 13066 tree.tbody = $tbody[0]; 13067 13068 // Prepare row templates: 13069 // Determine column count from table header if any 13070 tree.columnCount = $("thead >tr", $table) 13071 .last() 13072 .find(">th", $table).length; 13073 // Read TR templates from tbody if any 13074 $row = $tbody.children("tr").first(); 13075 if ($row.length) { 13076 n = $row.children("td").length; 13077 if (tree.columnCount && n !== tree.columnCount) { 13078 tree.warn( 13079 "Column count mismatch between thead (" + 13080 tree.columnCount + 13081 ") and tbody (" + 13082 n + 13083 "): using tbody." 13084 ); 13085 tree.columnCount = n; 13086 } 13087 $row = $row.clone(); 13088 } else { 13089 // Only thead is defined: create default row markup 13090 _assert( 13091 tree.columnCount >= 1, 13092 "Need either <thead> or <tbody> with <td> elements to determine column count." 13093 ); 13094 $row = $("<tr />"); 13095 for (i = 0; i < tree.columnCount; i++) { 13096 $row.append("<td />"); 13097 } 13098 } 13099 $row.find(">td") 13100 .eq(tableOpts.nodeColumnIdx) 13101 .html("<span class='fancytree-node' />"); 13102 if (opts.aria) { 13103 $row.attr("role", "row"); 13104 $row.find("td").attr("role", "gridcell"); 13105 } 13106 tree.rowFragment = document.createDocumentFragment(); 13107 tree.rowFragment.appendChild($row.get(0)); 13108 13109 // // If tbody contains a second row, use this as status node template 13110 // $row = $tbody.children("tr").eq(1); 13111 // if( $row.length === 0 ) { 13112 // tree.statusRowFragment = tree.rowFragment; 13113 // } else { 13114 // $row = $row.clone(); 13115 // tree.statusRowFragment = document.createDocumentFragment(); 13116 // tree.statusRowFragment.appendChild($row.get(0)); 13117 // } 13118 // 13119 $tbody.empty(); 13120 13121 // Make sure that status classes are set on the node's <tr> elements 13122 tree.statusClassPropName = "tr"; 13123 tree.ariaPropName = "tr"; 13124 this.nodeContainerAttrName = "tr"; 13125 13126 // #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table 13127 tree.$container = $table; 13128 13129 this._superApply(arguments); 13130 13131 // standard Fancytree created a root UL 13132 $(tree.rootNode.ul).remove(); 13133 tree.rootNode.ul = null; 13134 13135 // Add container to the TAB chain 13136 // #577: Allow to set tabindex to "0", "-1" and "" 13137 this.$container.attr("tabindex", opts.tabindex); 13138 // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1"); 13139 if (opts.aria) { 13140 tree.$container 13141 .attr("role", "treegrid") 13142 .attr("aria-readonly", true); 13143 } 13144 }, 13145 nodeRemoveChildMarkup: function (ctx) { 13146 var node = ctx.node; 13147 // node.debug("nodeRemoveChildMarkup()"); 13148 node.visit(function (n) { 13149 if (n.tr) { 13150 $(n.tr).remove(); 13151 n.tr = null; 13152 } 13153 }); 13154 }, 13155 nodeRemoveMarkup: function (ctx) { 13156 var node = ctx.node; 13157 // node.debug("nodeRemoveMarkup()"); 13158 if (node.tr) { 13159 $(node.tr).remove(); 13160 node.tr = null; 13161 } 13162 this.nodeRemoveChildMarkup(ctx); 13163 }, 13164 /* Override standard render. */ 13165 nodeRender: function (ctx, force, deep, collapsed, _recursive) { 13166 var children, 13167 firstTr, 13168 i, 13169 l, 13170 newRow, 13171 prevNode, 13172 prevTr, 13173 subCtx, 13174 tree = ctx.tree, 13175 node = ctx.node, 13176 opts = ctx.options, 13177 isRootNode = !node.parent; 13178 13179 if (tree._enableUpdate === false) { 13180 // $.ui.fancytree.debug("*** nodeRender _enableUpdate: false"); 13181 return; 13182 } 13183 if (!_recursive) { 13184 ctx.hasCollapsedParents = node.parent && !node.parent.expanded; 13185 } 13186 // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); 13187 if (!isRootNode) { 13188 if (node.tr && force) { 13189 this.nodeRemoveMarkup(ctx); 13190 } 13191 if (node.tr) { 13192 if (force) { 13193 // Set icon, link, and title (normally this is only required on initial render) 13194 this.nodeRenderTitle(ctx); // triggers renderColumns() 13195 } else { 13196 // Update element classes according to node state 13197 this.nodeRenderStatus(ctx); 13198 } 13199 } else { 13200 if (ctx.hasCollapsedParents && !deep) { 13201 // #166: we assume that the parent will be (recursively) rendered 13202 // later anyway. 13203 // node.debug("nodeRender ignored due to unrendered parent"); 13204 return; 13205 } 13206 // Create new <tr> after previous row 13207 // if( node.isStatusNode() ) { 13208 // newRow = tree.statusRowFragment.firstChild.cloneNode(true); 13209 // } else { 13210 newRow = tree.rowFragment.firstChild.cloneNode(true); 13211 // } 13212 prevNode = findPrevRowNode(node); 13213 // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key); 13214 _assert(prevNode); 13215 if (collapsed === true && _recursive) { 13216 // hide all child rows, so we can use an animation to show it later 13217 newRow.style.display = "none"; 13218 } else if (deep && ctx.hasCollapsedParents) { 13219 // also hide this row if deep === true but any parent is collapsed 13220 newRow.style.display = "none"; 13221 // newRow.style.color = "red"; 13222 } 13223 if (prevNode.tr) { 13224 insertSiblingAfter(prevNode.tr, newRow); 13225 } else { 13226 _assert( 13227 !prevNode.parent, 13228 "prev. row must have a tr, or be system root" 13229 ); 13230 // tree.tbody.appendChild(newRow); 13231 insertFirstChild(tree.tbody, newRow); // #675 13232 } 13233 node.tr = newRow; 13234 if (node.key && opts.generateIds) { 13235 node.tr.id = opts.idPrefix + node.key; 13236 } 13237 node.tr.ftnode = node; 13238 // if(opts.aria){ 13239 // $(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key); 13240 // } 13241 node.span = $("span.fancytree-node", node.tr).get(0); 13242 // Set icon, link, and title (normally this is only required on initial render) 13243 this.nodeRenderTitle(ctx); 13244 // Allow tweaking, binding, after node was created for the first time 13245 // tree._triggerNodeEvent("createNode", ctx); 13246 if (opts.createNode) { 13247 opts.createNode.call(tree, { type: "createNode" }, ctx); 13248 } 13249 } 13250 } 13251 // Allow tweaking after node state was rendered 13252 // tree._triggerNodeEvent("renderNode", ctx); 13253 if (opts.renderNode) { 13254 opts.renderNode.call(tree, { type: "renderNode" }, ctx); 13255 } 13256 // Visit child nodes 13257 // Add child markup 13258 children = node.children; 13259 if (children && (isRootNode || deep || node.expanded)) { 13260 for (i = 0, l = children.length; i < l; i++) { 13261 subCtx = $.extend({}, ctx, { node: children[i] }); 13262 subCtx.hasCollapsedParents = 13263 subCtx.hasCollapsedParents || !node.expanded; 13264 this.nodeRender(subCtx, force, deep, collapsed, true); 13265 } 13266 } 13267 // Make sure, that <tr> order matches node.children order. 13268 if (children && !_recursive) { 13269 // we only have to do it once, for the root branch 13270 prevTr = node.tr || null; 13271 firstTr = tree.tbody.firstChild; 13272 // Iterate over all descendants 13273 node.visit(function (n) { 13274 if (n.tr) { 13275 if ( 13276 !n.parent.expanded && 13277 n.tr.style.display !== "none" 13278 ) { 13279 // fix after a node was dropped over a collapsed 13280 n.tr.style.display = "none"; 13281 setChildRowVisibility(n, false); 13282 } 13283 if (n.tr.previousSibling !== prevTr) { 13284 node.debug("_fixOrder: mismatch at node: " + n); 13285 var nextTr = prevTr ? prevTr.nextSibling : firstTr; 13286 tree.tbody.insertBefore(n.tr, nextTr); 13287 } 13288 prevTr = n.tr; 13289 } 13290 }); 13291 } 13292 // Update element classes according to node state 13293 // if(!isRootNode){ 13294 // this.nodeRenderStatus(ctx); 13295 // } 13296 }, 13297 nodeRenderTitle: function (ctx, title) { 13298 var $cb, 13299 res, 13300 tree = ctx.tree, 13301 node = ctx.node, 13302 opts = ctx.options, 13303 isStatusNode = node.isStatusNode(); 13304 13305 res = this._super(ctx, title); 13306 13307 if (node.isRootNode()) { 13308 return res; 13309 } 13310 // Move checkbox to custom column 13311 if ( 13312 opts.checkbox && 13313 !isStatusNode && 13314 opts.table.checkboxColumnIdx != null 13315 ) { 13316 $cb = $("span.fancytree-checkbox", node.span); //.detach(); 13317 $(node.tr) 13318 .find("td") 13319 .eq(+opts.table.checkboxColumnIdx) 13320 .html($cb); 13321 } 13322 // Update element classes according to node state 13323 this.nodeRenderStatus(ctx); 13324 13325 if (isStatusNode) { 13326 if (opts.renderStatusColumns) { 13327 // Let user code write column content 13328 opts.renderStatusColumns.call( 13329 tree, 13330 { type: "renderStatusColumns" }, 13331 ctx 13332 ); 13333 } else if (opts.table.mergeStatusColumns && node.isTopLevel()) { 13334 $(node.tr) 13335 .find(">td") 13336 .eq(0) 13337 .prop("colspan", tree.columnCount) 13338 .text(node.title) 13339 .addClass("fancytree-status-merged") 13340 .nextAll() 13341 .remove(); 13342 } // else: default rendering for status node: leave other cells empty 13343 } else if (opts.renderColumns) { 13344 opts.renderColumns.call(tree, { type: "renderColumns" }, ctx); 13345 } 13346 return res; 13347 }, 13348 nodeRenderStatus: function (ctx) { 13349 var indent, 13350 node = ctx.node, 13351 opts = ctx.options; 13352 13353 this._super(ctx); 13354 13355 $(node.tr).removeClass("fancytree-node"); 13356 // indent 13357 indent = (node.getLevel() - 1) * opts.table.indentation; 13358 if (opts.rtl) { 13359 $(node.span).css({ paddingRight: indent + "px" }); 13360 } else { 13361 $(node.span).css({ paddingLeft: indent + "px" }); 13362 } 13363 }, 13364 /* Expand node, return Deferred.promise. */ 13365 nodeSetExpanded: function (ctx, flag, callOpts) { 13366 // flag defaults to true 13367 flag = flag !== false; 13368 13369 if ((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) { 13370 // Expanded state isn't changed - just call base implementation 13371 return this._superApply(arguments); 13372 } 13373 13374 var dfd = new $.Deferred(), 13375 subOpts = $.extend({}, callOpts, { 13376 noEvents: true, 13377 noAnimation: true, 13378 }); 13379 13380 callOpts = callOpts || {}; 13381 13382 function _afterExpand(ok, args) { 13383 // ctx.tree.info("ok:" + ok, args); 13384 if (ok) { 13385 // #1108 minExpandLevel: 2 together with table extension does not work 13386 // don't call when 'ok' is false: 13387 setChildRowVisibility(ctx.node, flag); 13388 if ( 13389 flag && 13390 ctx.options.autoScroll && 13391 !callOpts.noAnimation && 13392 ctx.node.hasChildren() 13393 ) { 13394 // Scroll down to last child, but keep current node visible 13395 ctx.node 13396 .getLastChild() 13397 .scrollIntoView(true, { topNode: ctx.node }) 13398 .always(function () { 13399 if (!callOpts.noEvents) { 13400 ctx.tree._triggerNodeEvent( 13401 flag ? "expand" : "collapse", 13402 ctx 13403 ); 13404 } 13405 dfd.resolveWith(ctx.node); 13406 }); 13407 } else { 13408 if (!callOpts.noEvents) { 13409 ctx.tree._triggerNodeEvent( 13410 flag ? "expand" : "collapse", 13411 ctx 13412 ); 13413 } 13414 dfd.resolveWith(ctx.node); 13415 } 13416 } else { 13417 if (!callOpts.noEvents) { 13418 ctx.tree._triggerNodeEvent( 13419 flag ? "expand" : "collapse", 13420 ctx 13421 ); 13422 } 13423 dfd.rejectWith(ctx.node); 13424 } 13425 } 13426 // Call base-expand with disabled events and animation 13427 this._super(ctx, flag, subOpts) 13428 .done(function () { 13429 _afterExpand(true, arguments); 13430 }) 13431 .fail(function () { 13432 _afterExpand(false, arguments); 13433 }); 13434 return dfd.promise(); 13435 }, 13436 nodeSetStatus: function (ctx, status, message, details) { 13437 if (status === "ok") { 13438 var node = ctx.node, 13439 firstChild = node.children ? node.children[0] : null; 13440 if (firstChild && firstChild.isStatusNode()) { 13441 $(firstChild.tr).remove(); 13442 } 13443 } 13444 return this._superApply(arguments); 13445 }, 13446 treeClear: function (ctx) { 13447 this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); 13448 return this._superApply(arguments); 13449 }, 13450 treeDestroy: function (ctx) { 13451 this.$container.find("tbody").empty(); 13452 if (this.$source) { 13453 this.$source.removeClass("fancytree-helper-hidden"); 13454 } 13455 return this._superApply(arguments); 13456 }, 13457 /*, 13458 treeSetFocus: function(ctx, flag) { 13459// alert("treeSetFocus" + ctx.tree.$container); 13460 ctx.tree.$container.focus(); 13461 $.ui.fancytree.focusTree = ctx.tree; 13462 }*/ 13463 }); 13464 // Value returned by `require('jquery.fancytree..')` 13465 return $.ui.fancytree; 13466}); // End of closure 13467 13468 13469/*! Extension 'jquery.fancytree.themeroller.js' *//*! 13470 * jquery.fancytree.themeroller.js 13471 * 13472 * Enable jQuery UI ThemeRoller styles. 13473 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 13474 * 13475 * @see http://jqueryui.com/themeroller/ 13476 * 13477 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 13478 * 13479 * Released under the MIT license 13480 * https://github.com/mar10/fancytree/wiki/LicenseInfo 13481 * 13482 * @version 2.38.3 13483 * @date 2023-02-01T20:52:50Z 13484 */ 13485 13486(function (factory) { 13487 if (typeof define === "function" && define.amd) { 13488 // AMD. Register as an anonymous module. 13489 define(["jquery", "./jquery.fancytree"], factory); 13490 } else if (typeof module === "object" && module.exports) { 13491 // Node/CommonJS 13492 require("./jquery.fancytree"); 13493 module.exports = factory(require("jquery")); 13494 } else { 13495 // Browser globals 13496 factory(jQuery); 13497 } 13498})(function ($) { 13499 "use strict"; 13500 13501 /******************************************************************************* 13502 * Extension code 13503 */ 13504 $.ui.fancytree.registerExtension({ 13505 name: "themeroller", 13506 version: "2.38.3", 13507 // Default options for this extension. 13508 options: { 13509 activeClass: "ui-state-active", // Class added to active node 13510 // activeClass: "ui-state-highlight", 13511 addClass: "ui-corner-all", // Class added to all nodes 13512 focusClass: "ui-state-focus", // Class added to focused node 13513 hoverClass: "ui-state-hover", // Class added to hovered node 13514 selectedClass: "ui-state-highlight", // Class added to selected nodes 13515 // selectedClass: "ui-state-active" 13516 }, 13517 13518 treeInit: function (ctx) { 13519 var $el = ctx.widget.element, 13520 opts = ctx.options.themeroller; 13521 13522 this._superApply(arguments); 13523 13524 if ($el[0].nodeName === "TABLE") { 13525 $el.addClass("ui-widget ui-corner-all"); 13526 $el.find(">thead tr").addClass("ui-widget-header"); 13527 $el.find(">tbody").addClass("ui-widget-conent"); 13528 } else { 13529 $el.addClass("ui-widget ui-widget-content ui-corner-all"); 13530 } 13531 13532 $el.on( 13533 "mouseenter mouseleave", 13534 ".fancytree-node", 13535 function (event) { 13536 var node = $.ui.fancytree.getNode(event.target), 13537 flag = event.type === "mouseenter"; 13538 13539 $(node.tr ? node.tr : node.span).toggleClass( 13540 opts.hoverClass + " " + opts.addClass, 13541 flag 13542 ); 13543 } 13544 ); 13545 }, 13546 treeDestroy: function (ctx) { 13547 this._superApply(arguments); 13548 ctx.widget.element.removeClass( 13549 "ui-widget ui-widget-content ui-corner-all" 13550 ); 13551 }, 13552 nodeRenderStatus: function (ctx) { 13553 var classes = {}, 13554 node = ctx.node, 13555 $el = $(node.tr ? node.tr : node.span), 13556 opts = ctx.options.themeroller; 13557 13558 this._super(ctx); 13559 /* 13560 .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. 13561 .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. 13562 .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. 13563 13564 .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. 13565 .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. 13566 .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. 13567 .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. 13568*/ 13569 // Set ui-state-* class (handle the case that the same class is assigned 13570 // to different states) 13571 classes[opts.activeClass] = false; 13572 classes[opts.focusClass] = false; 13573 classes[opts.selectedClass] = false; 13574 if (node.isActive()) { 13575 classes[opts.activeClass] = true; 13576 } 13577 if (node.hasFocus()) { 13578 classes[opts.focusClass] = true; 13579 } 13580 // activeClass takes precedence before selectedClass: 13581 if (node.isSelected() && !node.isActive()) { 13582 classes[opts.selectedClass] = true; 13583 } 13584 $el.toggleClass(opts.activeClass, classes[opts.activeClass]); 13585 $el.toggleClass(opts.focusClass, classes[opts.focusClass]); 13586 $el.toggleClass(opts.selectedClass, classes[opts.selectedClass]); 13587 // Additional classes (e.g. 'ui-corner-all') 13588 $el.addClass(opts.addClass); 13589 }, 13590 }); 13591 // Value returned by `require('jquery.fancytree..')` 13592 return $.ui.fancytree; 13593}); // End of closure 13594 13595 13596/*! Extension 'jquery.fancytree.wide.js' *//*! 13597 * jquery.fancytree.wide.js 13598 * Support for 100% wide selection bars. 13599 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) 13600 * 13601 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de) 13602 * 13603 * Released under the MIT license 13604 * https://github.com/mar10/fancytree/wiki/LicenseInfo 13605 * 13606 * @version 2.38.3 13607 * @date 2023-02-01T20:52:50Z 13608 */ 13609 13610(function (factory) { 13611 if (typeof define === "function" && define.amd) { 13612 // AMD. Register as an anonymous module. 13613 define(["jquery", "./jquery.fancytree"], factory); 13614 } else if (typeof module === "object" && module.exports) { 13615 // Node/CommonJS 13616 require("./jquery.fancytree"); 13617 module.exports = factory(require("jquery")); 13618 } else { 13619 // Browser globals 13620 factory(jQuery); 13621 } 13622})(function ($) { 13623 "use strict"; 13624 13625 var reNumUnit = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/; // split "1.5em" to ["1.5", "em"] 13626 13627 /******************************************************************************* 13628 * Private functions and variables 13629 */ 13630 // var _assert = $.ui.fancytree.assert; 13631 13632 /* Calculate inner width without scrollbar */ 13633 // function realInnerWidth($el) { 13634 // // http://blog.jquery.com/2012/08/16/jquery-1-8-box-sizing-width-csswidth-and-outerwidth/ 13635 // // inst.contWidth = parseFloat(this.$container.css("width"), 10); 13636 // // 'Client width without scrollbar' - 'padding' 13637 // return $el[0].clientWidth - ($el.innerWidth() - parseFloat($el.css("width"), 10)); 13638 // } 13639 13640 /* Create a global embedded CSS style for the tree. */ 13641 function defineHeadStyleElement(id, cssText) { 13642 id = "fancytree-style-" + id; 13643 var $headStyle = $("#" + id); 13644 13645 if (!cssText) { 13646 $headStyle.remove(); 13647 return null; 13648 } 13649 if (!$headStyle.length) { 13650 $headStyle = $("<style />") 13651 .attr("id", id) 13652 .addClass("fancytree-style") 13653 .prop("type", "text/css") 13654 .appendTo("head"); 13655 } 13656 try { 13657 $headStyle.html(cssText); 13658 } catch (e) { 13659 // fix for IE 6-8 13660 $headStyle[0].styleSheet.cssText = cssText; 13661 } 13662 return $headStyle; 13663 } 13664 13665 /* Calculate the CSS rules that indent title spans. */ 13666 function renderLevelCss( 13667 containerId, 13668 depth, 13669 levelOfs, 13670 lineOfs, 13671 labelOfs, 13672 measureUnit 13673 ) { 13674 var i, 13675 prefix = "#" + containerId + " span.fancytree-level-", 13676 rules = []; 13677 13678 for (i = 0; i < depth; i++) { 13679 rules.push( 13680 prefix + 13681 (i + 1) + 13682 " span.fancytree-title { padding-left: " + 13683 (i * levelOfs + lineOfs) + 13684 measureUnit + 13685 "; }" 13686 ); 13687 } 13688 // Some UI animations wrap the UL inside a DIV and set position:relative on both. 13689 // This breaks the left:0 and padding-left:nn settings of the title 13690 rules.push( 13691 "#" + 13692 containerId + 13693 " div.ui-effects-wrapper ul li span.fancytree-title, " + 13694 "#" + 13695 containerId + 13696 " li.fancytree-animating span.fancytree-title " + // #716 13697 "{ padding-left: " + 13698 labelOfs + 13699 measureUnit + 13700 "; position: static; width: auto; }" 13701 ); 13702 return rules.join("\n"); 13703 } 13704 13705 // /** 13706 // * [ext-wide] Recalculate the width of the selection bar after the tree container 13707 // * was resized.<br> 13708 // * May be called explicitly on container resize, since there is no resize event 13709 // * for DIV tags. 13710 // * 13711 // * @alias Fancytree#wideUpdate 13712 // * @requires jquery.fancytree.wide.js 13713 // */ 13714 // $.ui.fancytree._FancytreeClass.prototype.wideUpdate = function(){ 13715 // var inst = this.ext.wide, 13716 // prevCw = inst.contWidth, 13717 // prevLo = inst.lineOfs; 13718 13719 // inst.contWidth = realInnerWidth(this.$container); 13720 // // Each title is precceeded by 2 or 3 icons (16px + 3 margin) 13721 // // + 1px title border and 3px title padding 13722 // // TODO: use code from treeInit() below 13723 // inst.lineOfs = (this.options.checkbox ? 3 : 2) * 19; 13724 // if( prevCw !== inst.contWidth || prevLo !== inst.lineOfs ) { 13725 // this.debug("wideUpdate: " + inst.contWidth); 13726 // this.visit(function(node){ 13727 // node.tree._callHook("nodeRenderTitle", node); 13728 // }); 13729 // } 13730 // }; 13731 13732 /******************************************************************************* 13733 * Extension code 13734 */ 13735 $.ui.fancytree.registerExtension({ 13736 name: "wide", 13737 version: "2.38.3", 13738 // Default options for this extension. 13739 options: { 13740 iconWidth: null, // Adjust this if @fancy-icon-width != "16px" 13741 iconSpacing: null, // Adjust this if @fancy-icon-spacing != "3px" 13742 labelSpacing: null, // Adjust this if padding between icon and label != "3px" 13743 levelOfs: null, // Adjust this if ul padding != "16px" 13744 }, 13745 13746 treeCreate: function (ctx) { 13747 this._superApply(arguments); 13748 this.$container.addClass("fancytree-ext-wide"); 13749 13750 var containerId, 13751 cssText, 13752 iconSpacingUnit, 13753 labelSpacingUnit, 13754 iconWidthUnit, 13755 levelOfsUnit, 13756 instOpts = ctx.options.wide, 13757 // css sniffing 13758 $dummyLI = $( 13759 "<li id='fancytreeTemp'><span class='fancytree-node'><span class='fancytree-icon' /><span class='fancytree-title' /></span><ul />" 13760 ).appendTo(ctx.tree.$container), 13761 $dummyIcon = $dummyLI.find(".fancytree-icon"), 13762 $dummyUL = $dummyLI.find("ul"), 13763 // $dummyTitle = $dummyLI.find(".fancytree-title"), 13764 iconSpacing = 13765 instOpts.iconSpacing || $dummyIcon.css("margin-left"), 13766 iconWidth = instOpts.iconWidth || $dummyIcon.css("width"), 13767 labelSpacing = instOpts.labelSpacing || "3px", 13768 levelOfs = instOpts.levelOfs || $dummyUL.css("padding-left"); 13769 13770 $dummyLI.remove(); 13771 13772 iconSpacingUnit = iconSpacing.match(reNumUnit)[2]; 13773 iconSpacing = parseFloat(iconSpacing, 10); 13774 labelSpacingUnit = labelSpacing.match(reNumUnit)[2]; 13775 labelSpacing = parseFloat(labelSpacing, 10); 13776 iconWidthUnit = iconWidth.match(reNumUnit)[2]; 13777 iconWidth = parseFloat(iconWidth, 10); 13778 levelOfsUnit = levelOfs.match(reNumUnit)[2]; 13779 if ( 13780 iconSpacingUnit !== iconWidthUnit || 13781 levelOfsUnit !== iconWidthUnit || 13782 labelSpacingUnit !== iconWidthUnit 13783 ) { 13784 $.error( 13785 "iconWidth, iconSpacing, and levelOfs must have the same css measure unit" 13786 ); 13787 } 13788 this._local.measureUnit = iconWidthUnit; 13789 this._local.levelOfs = parseFloat(levelOfs); 13790 this._local.lineOfs = 13791 (1 + 13792 (ctx.options.checkbox ? 1 : 0) + 13793 (ctx.options.icon === false ? 0 : 1)) * 13794 (iconWidth + iconSpacing) + 13795 iconSpacing; 13796 this._local.labelOfs = labelSpacing; 13797 this._local.maxDepth = 10; 13798 13799 // Get/Set a unique Id on the container (if not already exists) 13800 containerId = this.$container.uniqueId().attr("id"); 13801 // Generated css rules for some levels (extended on demand) 13802 cssText = renderLevelCss( 13803 containerId, 13804 this._local.maxDepth, 13805 this._local.levelOfs, 13806 this._local.lineOfs, 13807 this._local.labelOfs, 13808 this._local.measureUnit 13809 ); 13810 defineHeadStyleElement(containerId, cssText); 13811 }, 13812 treeDestroy: function (ctx) { 13813 // Remove generated css rules 13814 defineHeadStyleElement(this.$container.attr("id"), null); 13815 return this._superApply(arguments); 13816 }, 13817 nodeRenderStatus: function (ctx) { 13818 var containerId, 13819 cssText, 13820 res, 13821 node = ctx.node, 13822 level = node.getLevel(); 13823 13824 res = this._super(ctx); 13825 // Generate some more level-n rules if required 13826 if (level > this._local.maxDepth) { 13827 containerId = this.$container.attr("id"); 13828 this._local.maxDepth *= 2; 13829 node.debug( 13830 "Define global ext-wide css up to level " + 13831 this._local.maxDepth 13832 ); 13833 cssText = renderLevelCss( 13834 containerId, 13835 this._local.maxDepth, 13836 this._local.levelOfs, 13837 this._local.lineOfs, 13838 this._local.labelSpacing, 13839 this._local.measureUnit 13840 ); 13841 defineHeadStyleElement(containerId, cssText); 13842 } 13843 // Add level-n class to apply indentation padding. 13844 // (Setting element style would not work, since it cannot easily be 13845 // overriden while animations run) 13846 $(node.span).addClass("fancytree-level-" + level); 13847 return res; 13848 }, 13849 }); 13850 // Value returned by `require('jquery.fancytree..')` 13851 return $.ui.fancytree; 13852}); // End of closure 13853 13854// Value returned by `require('jquery.fancytree')` 13855return $.ui.fancytree; 13856})); // End of closure 13857