You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

13857 lines
397 KiB

  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. NOTE: Original jQuery UI wrapper was replaced with a simple IIFE.
  11. See README-Fancytree.md
  12. */
  13. (function( $ ) {
  14. $.ui = $.ui || {};
  15. var version = $.ui.version = "1.13.2";
  16. /*!
  17. * jQuery UI Widget 1.13.2
  18. * http://jqueryui.com
  19. *
  20. * Copyright jQuery Foundation and other contributors
  21. * Released under the MIT license.
  22. * http://jquery.org/license
  23. */
  24. //>>label: Widget
  25. //>>group: Core
  26. //>>description: Provides a factory for creating stateful widgets with a common API.
  27. //>>docs: http://api.jqueryui.com/jQuery.widget/
  28. //>>demos: http://jqueryui.com/widget/
  29. var widgetUuid = 0;
  30. var widgetHasOwnProperty = Array.prototype.hasOwnProperty;
  31. var widgetSlice = Array.prototype.slice;
  32. $.cleanData = ( function( orig ) {
  33. return function( elems ) {
  34. var events, elem, i;
  35. for ( i = 0; ( elem = elems[ i ] ) != null; i++ ) {
  36. // Only trigger remove when necessary to save time
  37. events = $._data( elem, "events" );
  38. if ( events && events.remove ) {
  39. $( elem ).triggerHandler( "remove" );
  40. }
  41. }
  42. orig( elems );
  43. };
  44. } )( $.cleanData );
  45. $.widget = function( name, base, prototype ) {
  46. var existingConstructor, constructor, basePrototype;
  47. // ProxiedPrototype allows the provided prototype to remain unmodified
  48. // so that it can be used as a mixin for multiple widgets (#8876)
  49. var proxiedPrototype = {};
  50. var namespace = name.split( "." )[ 0 ];
  51. name = name.split( "." )[ 1 ];
  52. var fullName = namespace + "-" + name;
  53. if ( !prototype ) {
  54. prototype = base;
  55. base = $.Widget;
  56. }
  57. if ( Array.isArray( prototype ) ) {
  58. prototype = $.extend.apply( null, [ {} ].concat( prototype ) );
  59. }
  60. // Create selector for plugin
  61. $.expr.pseudos[ fullName.toLowerCase() ] = function( elem ) {
  62. return !!$.data( elem, fullName );
  63. };
  64. $[ namespace ] = $[ namespace ] || {};
  65. existingConstructor = $[ namespace ][ name ];
  66. constructor = $[ namespace ][ name ] = function( options, element ) {
  67. // Allow instantiation without "new" keyword
  68. if ( !this || !this._createWidget ) {
  69. return new constructor( options, element );
  70. }
  71. // Allow instantiation without initializing for simple inheritance
  72. // must use "new" keyword (the code above always passes args)
  73. if ( arguments.length ) {
  74. this._createWidget( options, element );
  75. }
  76. };
  77. // Extend with the existing constructor to carry over any static properties
  78. $.extend( constructor, existingConstructor, {
  79. version: prototype.version,
  80. // Copy the object used to create the prototype in case we need to
  81. // redefine the widget later
  82. _proto: $.extend( {}, prototype ),
  83. // Track widgets that inherit from this widget in case this widget is
  84. // redefined after a widget inherits from it
  85. _childConstructors: []
  86. } );
  87. basePrototype = new base();
  88. // We need to make the options hash a property directly on the new instance
  89. // otherwise we'll modify the options hash on the prototype that we're
  90. // inheriting from
  91. basePrototype.options = $.widget.extend( {}, basePrototype.options );
  92. $.each( prototype, function( prop, value ) {
  93. if ( typeof value !== "function" ) {
  94. proxiedPrototype[ prop ] = value;
  95. return;
  96. }
  97. proxiedPrototype[ prop ] = ( function() {
  98. function _super() {
  99. return base.prototype[ prop ].apply( this, arguments );
  100. }
  101. function _superApply( args ) {
  102. return base.prototype[ prop ].apply( this, args );
  103. }
  104. return function() {
  105. var __super = this._super;
  106. var __superApply = this._superApply;
  107. var returnValue;
  108. this._super = _super;
  109. this._superApply = _superApply;
  110. returnValue = value.apply( this, arguments );
  111. this._super = __super;
  112. this._superApply = __superApply;
  113. return returnValue;
  114. };
  115. } )();
  116. } );
  117. constructor.prototype = $.widget.extend( basePrototype, {
  118. // TODO: remove support for widgetEventPrefix
  119. // always use the name + a colon as the prefix, e.g., draggable:start
  120. // don't prefix for widgets that aren't DOM-based
  121. widgetEventPrefix: existingConstructor ? ( basePrototype.widgetEventPrefix || name ) : name
  122. }, proxiedPrototype, {
  123. constructor: constructor,
  124. namespace: namespace,
  125. widgetName: name,
  126. widgetFullName: fullName
  127. } );
  128. // If this widget is being redefined then we need to find all widgets that
  129. // are inheriting from it and redefine all of them so that they inherit from
  130. // the new version of this widget. We're essentially trying to replace one
  131. // level in the prototype chain.
  132. if ( existingConstructor ) {
  133. $.each( existingConstructor._childConstructors, function( i, child ) {
  134. var childPrototype = child.prototype;
  135. // Redefine the child widget using the same prototype that was
  136. // originally used, but inherit from the new version of the base
  137. $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor,
  138. child._proto );
  139. } );
  140. // Remove the list of existing child constructors from the old constructor
  141. // so the old child constructors can be garbage collected
  142. delete existingConstructor._childConstructors;
  143. } else {
  144. base._childConstructors.push( constructor );
  145. }
  146. $.widget.bridge( name, constructor );
  147. return constructor;
  148. };
  149. $.widget.extend = function( target ) {
  150. var input = widgetSlice.call( arguments, 1 );
  151. var inputIndex = 0;
  152. var inputLength = input.length;
  153. var key;
  154. var value;
  155. for ( ; inputIndex < inputLength; inputIndex++ ) {
  156. for ( key in input[ inputIndex ] ) {
  157. value = input[ inputIndex ][ key ];
  158. if ( widgetHasOwnProperty.call( input[ inputIndex ], key ) && value !== undefined ) {
  159. // Clone objects
  160. if ( $.isPlainObject( value ) ) {
  161. target[ key ] = $.isPlainObject( target[ key ] ) ?
  162. $.widget.extend( {}, target[ key ], value ) :
  163. // Don't extend strings, arrays, etc. with objects
  164. $.widget.extend( {}, value );
  165. // Copy everything else by reference
  166. } else {
  167. target[ key ] = value;
  168. }
  169. }
  170. }
  171. }
  172. return target;
  173. };
  174. $.widget.bridge = function( name, object ) {
  175. var fullName = object.prototype.widgetFullName || name;
  176. $.fn[ name ] = function( options ) {
  177. var isMethodCall = typeof options === "string";
  178. var args = widgetSlice.call( arguments, 1 );
  179. var returnValue = this;
  180. if ( isMethodCall ) {
  181. // If this is an empty collection, we need to have the instance method
  182. // return undefined instead of the jQuery instance
  183. if ( !this.length && options === "instance" ) {
  184. returnValue = undefined;
  185. } else {
  186. this.each( function() {
  187. var methodValue;
  188. var instance = $.data( this, fullName );
  189. if ( options === "instance" ) {
  190. returnValue = instance;
  191. return false;
  192. }
  193. if ( !instance ) {
  194. return $.error( "cannot call methods on " + name +
  195. " prior to initialization; " +
  196. "attempted to call method '" + options + "'" );
  197. }
  198. if ( typeof instance[ options ] !== "function" ||
  199. options.charAt( 0 ) === "_" ) {
  200. return $.error( "no such method '" + options + "' for " + name +
  201. " widget instance" );
  202. }
  203. methodValue = instance[ options ].apply( instance, args );
  204. if ( methodValue !== instance && methodValue !== undefined ) {
  205. returnValue = methodValue && methodValue.jquery ?
  206. returnValue.pushStack( methodValue.get() ) :
  207. methodValue;
  208. return false;
  209. }
  210. } );
  211. }
  212. } else {
  213. // Allow multiple hashes to be passed on init
  214. if ( args.length ) {
  215. options = $.widget.extend.apply( null, [ options ].concat( args ) );
  216. }
  217. this.each( function() {
  218. var instance = $.data( this, fullName );
  219. if ( instance ) {
  220. instance.option( options || {} );
  221. if ( instance._init ) {
  222. instance._init();
  223. }
  224. } else {
  225. $.data( this, fullName, new object( options, this ) );
  226. }
  227. } );
  228. }
  229. return returnValue;
  230. };
  231. };
  232. $.Widget = function( /* options, element */ ) {};
  233. $.Widget._childConstructors = [];
  234. $.Widget.prototype = {
  235. widgetName: "widget",
  236. widgetEventPrefix: "",
  237. defaultElement: "<div>",
  238. options: {
  239. classes: {},
  240. disabled: false,
  241. // Callbacks
  242. create: null
  243. },
  244. _createWidget: function( options, element ) {
  245. element = $( element || this.defaultElement || this )[ 0 ];
  246. this.element = $( element );
  247. this.uuid = widgetUuid++;
  248. this.eventNamespace = "." + this.widgetName + this.uuid;
  249. this.bindings = $();
  250. this.hoverable = $();
  251. this.focusable = $();
  252. this.classesElementLookup = {};
  253. if ( element !== this ) {
  254. $.data( element, this.widgetFullName, this );
  255. this._on( true, this.element, {
  256. remove: function( event ) {
  257. if ( event.target === element ) {
  258. this.destroy();
  259. }
  260. }
  261. } );
  262. this.document = $( element.style ?
  263. // Element within the document
  264. element.ownerDocument :
  265. // Element is window or document
  266. element.document || element );
  267. this.window = $( this.document[ 0 ].defaultView || this.document[ 0 ].parentWindow );
  268. }
  269. this.options = $.widget.extend( {},
  270. this.options,
  271. this._getCreateOptions(),
  272. options );
  273. this._create();
  274. if ( this.options.disabled ) {
  275. this._setOptionDisabled( this.options.disabled );
  276. }
  277. this._trigger( "create", null, this._getCreateEventData() );
  278. this._init();
  279. },
  280. _getCreateOptions: function() {
  281. return {};
  282. },
  283. _getCreateEventData: $.noop,
  284. _create: $.noop,
  285. _init: $.noop,
  286. destroy: function() {
  287. var that = this;
  288. this._destroy();
  289. $.each( this.classesElementLookup, function( key, value ) {
  290. that._removeClass( value, key );
  291. } );
  292. // We can probably remove the unbind calls in 2.0
  293. // all event bindings should go through this._on()
  294. this.element
  295. .off( this.eventNamespace )
  296. .removeData( this.widgetFullName );
  297. this.widget()
  298. .off( this.eventNamespace )
  299. .removeAttr( "aria-disabled" );
  300. // Clean up events and states
  301. this.bindings.off( this.eventNamespace );
  302. },
  303. _destroy: $.noop,
  304. widget: function() {
  305. return this.element;
  306. },
  307. option: function( key, value ) {
  308. var options = key;
  309. var parts;
  310. var curOption;
  311. var i;
  312. if ( arguments.length === 0 ) {
  313. // Don't return a reference to the internal hash
  314. return $.widget.extend( {}, this.options );
  315. }
  316. if ( typeof key === "string" ) {
  317. // Handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } }
  318. options = {};
  319. parts = key.split( "." );
  320. key = parts.shift();
  321. if ( parts.length ) {
  322. curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] );
  323. for ( i = 0; i < parts.length - 1; i++ ) {
  324. curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {};
  325. curOption = curOption[ parts[ i ] ];
  326. }
  327. key = parts.pop();
  328. if ( arguments.length === 1 ) {
  329. return curOption[ key ] === undefined ? null : curOption[ key ];
  330. }
  331. curOption[ key ] = value;
  332. } else {
  333. if ( arguments.length === 1 ) {
  334. return this.options[ key ] === undefined ? null : this.options[ key ];
  335. }
  336. options[ key ] = value;
  337. }
  338. }
  339. this._setOptions( options );
  340. return this;
  341. },
  342. _setOptions: function( options ) {
  343. var key;
  344. for ( key in options ) {
  345. this._setOption( key, options[ key ] );
  346. }
  347. return this;
  348. },
  349. _setOption: function( key, value ) {
  350. if ( key === "classes" ) {
  351. this._setOptionClasses( value );
  352. }
  353. this.options[ key ] = value;
  354. if ( key === "disabled" ) {
  355. this._setOptionDisabled( value );
  356. }
  357. return this;
  358. },
  359. _setOptionClasses: function( value ) {
  360. var classKey, elements, currentElements;
  361. for ( classKey in value ) {
  362. currentElements = this.classesElementLookup[ classKey ];
  363. if ( value[ classKey ] === this.options.classes[ classKey ] ||
  364. !currentElements ||
  365. !currentElements.length ) {
  366. continue;
  367. }
  368. // We are doing this to create a new jQuery object because the _removeClass() call
  369. // on the next line is going to destroy the reference to the current elements being
  370. // tracked. We need to save a copy of this collection so that we can add the new classes
  371. // below.
  372. elements = $( currentElements.get() );
  373. this._removeClass( currentElements, classKey );
  374. // We don't use _addClass() here, because that uses this.options.classes
  375. // for generating the string of classes. We want to use the value passed in from
  376. // _setOption(), this is the new value of the classes option which was passed to
  377. // _setOption(). We pass this value directly to _classes().
  378. elements.addClass( this._classes( {
  379. element: elements,
  380. keys: classKey,
  381. classes: value,
  382. add: true
  383. } ) );
  384. }
  385. },
  386. _setOptionDisabled: function( value ) {
  387. this._toggleClass( this.widget(), this.widgetFullName + "-disabled", null, !!value );
  388. // If the widget is becoming disabled, then nothing is interactive
  389. if ( value ) {
  390. this._removeClass( this.hoverable, null, "ui-state-hover" );
  391. this._removeClass( this.focusable, null, "ui-state-focus" );
  392. }
  393. },
  394. enable: function() {
  395. return this._setOptions( { disabled: false } );
  396. },
  397. disable: function() {
  398. return this._setOptions( { disabled: true } );
  399. },
  400. _classes: function( options ) {
  401. var full = [];
  402. var that = this;
  403. options = $.extend( {
  404. element: this.element,
  405. classes: this.options.classes || {}
  406. }, options );
  407. function bindRemoveEvent() {
  408. var nodesToBind = [];
  409. options.element.each( function( _, element ) {
  410. var isTracked = $.map( that.classesElementLookup, function( elements ) {
  411. return elements;
  412. } )
  413. .some( function( elements ) {
  414. return elements.is( element );
  415. } );
  416. if ( !isTracked ) {
  417. nodesToBind.push( element );
  418. }
  419. } );
  420. that._on( $( nodesToBind ), {
  421. remove: "_untrackClassesElement"
  422. } );
  423. }
  424. function processClassString( classes, checkOption ) {
  425. var current, i;
  426. for ( i = 0; i < classes.length; i++ ) {
  427. current = that.classesElementLookup[ classes[ i ] ] || $();
  428. if ( options.add ) {
  429. bindRemoveEvent();
  430. current = $( $.uniqueSort( current.get().concat( options.element.get() ) ) );
  431. } else {
  432. current = $( current.not( options.element ).get() );
  433. }
  434. that.classesElementLookup[ classes[ i ] ] = current;
  435. full.push( classes[ i ] );
  436. if ( checkOption && options.classes[ classes[ i ] ] ) {
  437. full.push( options.classes[ classes[ i ] ] );
  438. }
  439. }
  440. }
  441. if ( options.keys ) {
  442. processClassString( options.keys.match( /\S+/g ) || [], true );
  443. }
  444. if ( options.extra ) {
  445. processClassString( options.extra.match( /\S+/g ) || [] );
  446. }
  447. return full.join( " " );
  448. },
  449. _untrackClassesElement: function( event ) {
  450. var that = this;
  451. $.each( that.classesElementLookup, function( key, value ) {
  452. if ( $.inArray( event.target, value ) !== -1 ) {
  453. that.classesElementLookup[ key ] = $( value.not( event.target ).get() );
  454. }
  455. } );
  456. this._off( $( event.target ) );
  457. },
  458. _removeClass: function( element, keys, extra ) {
  459. return this._toggleClass( element, keys, extra, false );
  460. },
  461. _addClass: function( element, keys, extra ) {
  462. return this._toggleClass( element, keys, extra, true );
  463. },
  464. _toggleClass: function( element, keys, extra, add ) {
  465. add = ( typeof add === "boolean" ) ? add : extra;
  466. var shift = ( typeof element === "string" || element === null ),
  467. options = {
  468. extra: shift ? keys : extra,
  469. keys: shift ? element : keys,
  470. element: shift ? this.element : element,
  471. add: add
  472. };
  473. options.element.toggleClass( this._classes( options ), add );
  474. return this;
  475. },
  476. _on: function( suppressDisabledCheck, element, handlers ) {
  477. var delegateElement;
  478. var instance = this;
  479. // No suppressDisabledCheck flag, shuffle arguments
  480. if ( typeof suppressDisabledCheck !== "boolean" ) {
  481. handlers = element;
  482. element = suppressDisabledCheck;
  483. suppressDisabledCheck = false;
  484. }
  485. // No element argument, shuffle and use this.element
  486. if ( !handlers ) {
  487. handlers = element;
  488. element = this.element;
  489. delegateElement = this.widget();
  490. } else {
  491. element = delegateElement = $( element );
  492. this.bindings = this.bindings.add( element );
  493. }
  494. $.each( handlers, function( event, handler ) {
  495. function handlerProxy() {
  496. // Allow widgets to customize the disabled handling
  497. // - disabled as an array instead of boolean
  498. // - disabled class as method for disabling individual parts
  499. if ( !suppressDisabledCheck &&
  500. ( instance.options.disabled === true ||
  501. $( this ).hasClass( "ui-state-disabled" ) ) ) {
  502. return;
  503. }
  504. return ( typeof handler === "string" ? instance[ handler ] : handler )
  505. .apply( instance, arguments );
  506. }
  507. // Copy the guid so direct unbinding works
  508. if ( typeof handler !== "string" ) {
  509. handlerProxy.guid = handler.guid =
  510. handler.guid || handlerProxy.guid || $.guid++;
  511. }
  512. var match = event.match( /^([\w:-]*)\s*(.*)$/ );
  513. var eventName = match[ 1 ] + instance.eventNamespace;
  514. var selector = match[ 2 ];
  515. if ( selector ) {
  516. delegateElement.on( eventName, selector, handlerProxy );
  517. } else {
  518. element.on( eventName, handlerProxy );
  519. }
  520. } );
  521. },
  522. _off: function( element, eventName ) {
  523. eventName = ( eventName || "" ).split( " " ).join( this.eventNamespace + " " ) +
  524. this.eventNamespace;
  525. element.off( eventName );
  526. // Clear the stack to avoid memory leaks (#10056)
  527. this.bindings = $( this.bindings.not( element ).get() );
  528. this.focusable = $( this.focusable.not( element ).get() );
  529. this.hoverable = $( this.hoverable.not( element ).get() );
  530. },
  531. _delay: function( handler, delay ) {
  532. function handlerProxy() {
  533. return ( typeof handler === "string" ? instance[ handler ] : handler )
  534. .apply( instance, arguments );
  535. }
  536. var instance = this;
  537. return setTimeout( handlerProxy, delay || 0 );
  538. },
  539. _hoverable: function( element ) {
  540. this.hoverable = this.hoverable.add( element );
  541. this._on( element, {
  542. mouseenter: function( event ) {
  543. this._addClass( $( event.currentTarget ), null, "ui-state-hover" );
  544. },
  545. mouseleave: function( event ) {
  546. this._removeClass( $( event.currentTarget ), null, "ui-state-hover" );
  547. }
  548. } );
  549. },
  550. _focusable: function( element ) {
  551. this.focusable = this.focusable.add( element );
  552. this._on( element, {
  553. focusin: function( event ) {
  554. this._addClass( $( event.currentTarget ), null, "ui-state-focus" );
  555. },
  556. focusout: function( event ) {
  557. this._removeClass( $( event.currentTarget ), null, "ui-state-focus" );
  558. }
  559. } );
  560. },
  561. _trigger: function( type, event, data ) {
  562. var prop, orig;
  563. var callback = this.options[ type ];
  564. data = data || {};
  565. event = $.Event( event );
  566. event.type = ( type === this.widgetEventPrefix ?
  567. type :
  568. this.widgetEventPrefix + type ).toLowerCase();
  569. // The original event may come from any element
  570. // so we need to reset the target on the new event
  571. event.target = this.element[ 0 ];
  572. // Copy original event properties over to the new event
  573. orig = event.originalEvent;
  574. if ( orig ) {
  575. for ( prop in orig ) {
  576. if ( !( prop in event ) ) {
  577. event[ prop ] = orig[ prop ];
  578. }
  579. }
  580. }
  581. this.element.trigger( event, data );
  582. return !( typeof callback === "function" &&
  583. callback.apply( this.element[ 0 ], [ event ].concat( data ) ) === false ||
  584. event.isDefaultPrevented() );
  585. }
  586. };
  587. $.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) {
  588. $.Widget.prototype[ "_" + method ] = function( element, options, callback ) {
  589. if ( typeof options === "string" ) {
  590. options = { effect: options };
  591. }
  592. var hasOptions;
  593. var effectName = !options ?
  594. method :
  595. options === true || typeof options === "number" ?
  596. defaultEffect :
  597. options.effect || defaultEffect;
  598. options = options || {};
  599. if ( typeof options === "number" ) {
  600. options = { duration: options };
  601. } else if ( options === true ) {
  602. options = {};
  603. }
  604. hasOptions = !$.isEmptyObject( options );
  605. options.complete = callback;
  606. if ( options.delay ) {
  607. element.delay( options.delay );
  608. }
  609. if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) {
  610. element[ method ]( options );
  611. } else if ( effectName !== method && element[ effectName ] ) {
  612. element[ effectName ]( options.duration, options.easing, callback );
  613. } else {
  614. element.queue( function( next ) {
  615. $( this )[ method ]();
  616. if ( callback ) {
  617. callback.call( element[ 0 ] );
  618. }
  619. next();
  620. } );
  621. }
  622. };
  623. } );
  624. var widget = $.widget;
  625. /*!
  626. * jQuery UI Position 1.13.2
  627. * http://jqueryui.com
  628. *
  629. * Copyright jQuery Foundation and other contributors
  630. * Released under the MIT license.
  631. * http://jquery.org/license
  632. *
  633. * http://api.jqueryui.com/position/
  634. */
  635. //>>label: Position
  636. //>>group: Core
  637. //>>description: Positions elements relative to other elements.
  638. //>>docs: http://api.jqueryui.com/position/
  639. //>>demos: http://jqueryui.com/position/
  640. ( function() {
  641. var cachedScrollbarWidth,
  642. max = Math.max,
  643. abs = Math.abs,
  644. rhorizontal = /left|center|right/,
  645. rvertical = /top|center|bottom/,
  646. roffset = /[\+\-]\d+(\.[\d]+)?%?/,
  647. rposition = /^\w+/,
  648. rpercent = /%$/,
  649. _position = $.fn.position;
  650. function getOffsets( offsets, width, height ) {
  651. return [
  652. parseFloat( offsets[ 0 ] ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ),
  653. parseFloat( offsets[ 1 ] ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 )
  654. ];
  655. }
  656. function parseCss( element, property ) {
  657. return parseInt( $.css( element, property ), 10 ) || 0;
  658. }
  659. function isWindow( obj ) {
  660. return obj != null && obj === obj.window;
  661. }
  662. function getDimensions( elem ) {
  663. var raw = elem[ 0 ];
  664. if ( raw.nodeType === 9 ) {
  665. return {
  666. width: elem.width(),
  667. height: elem.height(),
  668. offset: { top: 0, left: 0 }
  669. };
  670. }
  671. if ( isWindow( raw ) ) {
  672. return {
  673. width: elem.width(),
  674. height: elem.height(),
  675. offset: { top: elem.scrollTop(), left: elem.scrollLeft() }
  676. };
  677. }
  678. if ( raw.preventDefault ) {
  679. return {
  680. width: 0,
  681. height: 0,
  682. offset: { top: raw.pageY, left: raw.pageX }
  683. };
  684. }
  685. return {
  686. width: elem.outerWidth(),
  687. height: elem.outerHeight(),
  688. offset: elem.offset()
  689. };
  690. }
  691. $.position = {
  692. scrollbarWidth: function() {
  693. if ( cachedScrollbarWidth !== undefined ) {
  694. return cachedScrollbarWidth;
  695. }
  696. var w1, w2,
  697. div = $( "<div style=" +
  698. "'display:block;position:absolute;width:200px;height:200px;overflow:hidden;'>" +
  699. "<div style='height:300px;width:auto;'></div></div>" ),
  700. innerDiv = div.children()[ 0 ];
  701. $( "body" ).append( div );
  702. w1 = innerDiv.offsetWidth;
  703. div.css( "overflow", "scroll" );
  704. w2 = innerDiv.offsetWidth;
  705. if ( w1 === w2 ) {
  706. w2 = div[ 0 ].clientWidth;
  707. }
  708. div.remove();
  709. return ( cachedScrollbarWidth = w1 - w2 );
  710. },
  711. getScrollInfo: function( within ) {
  712. var overflowX = within.isWindow || within.isDocument ? "" :
  713. within.element.css( "overflow-x" ),
  714. overflowY = within.isWindow || within.isDocument ? "" :
  715. within.element.css( "overflow-y" ),
  716. hasOverflowX = overflowX === "scroll" ||
  717. ( overflowX === "auto" && within.width < within.element[ 0 ].scrollWidth ),
  718. hasOverflowY = overflowY === "scroll" ||
  719. ( overflowY === "auto" && within.height < within.element[ 0 ].scrollHeight );
  720. return {
  721. width: hasOverflowY ? $.position.scrollbarWidth() : 0,
  722. height: hasOverflowX ? $.position.scrollbarWidth() : 0
  723. };
  724. },
  725. getWithinInfo: function( element ) {
  726. var withinElement = $( element || window ),
  727. isElemWindow = isWindow( withinElement[ 0 ] ),
  728. isDocument = !!withinElement[ 0 ] && withinElement[ 0 ].nodeType === 9,
  729. hasOffset = !isElemWindow && !isDocument;
  730. return {
  731. element: withinElement,
  732. isWindow: isElemWindow,
  733. isDocument: isDocument,
  734. offset: hasOffset ? $( element ).offset() : { left: 0, top: 0 },
  735. scrollLeft: withinElement.scrollLeft(),
  736. scrollTop: withinElement.scrollTop(),
  737. width: withinElement.outerWidth(),
  738. height: withinElement.outerHeight()
  739. };
  740. }
  741. };
  742. $.fn.position = function( options ) {
  743. if ( !options || !options.of ) {
  744. return _position.apply( this, arguments );
  745. }
  746. // Make a copy, we don't want to modify arguments
  747. options = $.extend( {}, options );
  748. var atOffset, targetWidth, targetHeight, targetOffset, basePosition, dimensions,
  749. // Make sure string options are treated as CSS selectors
  750. target = typeof options.of === "string" ?
  751. $( document ).find( options.of ) :
  752. $( options.of ),
  753. within = $.position.getWithinInfo( options.within ),
  754. scrollInfo = $.position.getScrollInfo( within ),
  755. collision = ( options.collision || "flip" ).split( " " ),
  756. offsets = {};
  757. dimensions = getDimensions( target );
  758. if ( target[ 0 ].preventDefault ) {
  759. // Force left top to allow flipping
  760. options.at = "left top";
  761. }
  762. targetWidth = dimensions.width;
  763. targetHeight = dimensions.height;
  764. targetOffset = dimensions.offset;
  765. // Clone to reuse original targetOffset later
  766. basePosition = $.extend( {}, targetOffset );
  767. // Force my and at to have valid horizontal and vertical positions
  768. // if a value is missing or invalid, it will be converted to center
  769. $.each( [ "my", "at" ], function() {
  770. var pos = ( options[ this ] || "" ).split( " " ),
  771. horizontalOffset,
  772. verticalOffset;
  773. if ( pos.length === 1 ) {
  774. pos = rhorizontal.test( pos[ 0 ] ) ?
  775. pos.concat( [ "center" ] ) :
  776. rvertical.test( pos[ 0 ] ) ?
  777. [ "center" ].concat( pos ) :
  778. [ "center", "center" ];
  779. }
  780. pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center";
  781. pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center";
  782. // Calculate offsets
  783. horizontalOffset = roffset.exec( pos[ 0 ] );
  784. verticalOffset = roffset.exec( pos[ 1 ] );
  785. offsets[ this ] = [
  786. horizontalOffset ? horizontalOffset[ 0 ] : 0,
  787. verticalOffset ? verticalOffset[ 0 ] : 0
  788. ];
  789. // Reduce to just the positions without the offsets
  790. options[ this ] = [
  791. rposition.exec( pos[ 0 ] )[ 0 ],
  792. rposition.exec( pos[ 1 ] )[ 0 ]
  793. ];
  794. } );
  795. // Normalize collision option
  796. if ( collision.length === 1 ) {
  797. collision[ 1 ] = collision[ 0 ];
  798. }
  799. if ( options.at[ 0 ] === "right" ) {
  800. basePosition.left += targetWidth;
  801. } else if ( options.at[ 0 ] === "center" ) {
  802. basePosition.left += targetWidth / 2;
  803. }
  804. if ( options.at[ 1 ] === "bottom" ) {
  805. basePosition.top += targetHeight;
  806. } else if ( options.at[ 1 ] === "center" ) {
  807. basePosition.top += targetHeight / 2;
  808. }
  809. atOffset = getOffsets( offsets.at, targetWidth, targetHeight );
  810. basePosition.left += atOffset[ 0 ];
  811. basePosition.top += atOffset[ 1 ];
  812. return this.each( function() {
  813. var collisionPosition, using,
  814. elem = $( this ),
  815. elemWidth = elem.outerWidth(),
  816. elemHeight = elem.outerHeight(),
  817. marginLeft = parseCss( this, "marginLeft" ),
  818. marginTop = parseCss( this, "marginTop" ),
  819. collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) +
  820. scrollInfo.width,
  821. collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) +
  822. scrollInfo.height,
  823. position = $.extend( {}, basePosition ),
  824. myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() );
  825. if ( options.my[ 0 ] === "right" ) {
  826. position.left -= elemWidth;
  827. } else if ( options.my[ 0 ] === "center" ) {
  828. position.left -= elemWidth / 2;
  829. }
  830. if ( options.my[ 1 ] === "bottom" ) {
  831. position.top -= elemHeight;
  832. } else if ( options.my[ 1 ] === "center" ) {
  833. position.top -= elemHeight / 2;
  834. }
  835. position.left += myOffset[ 0 ];
  836. position.top += myOffset[ 1 ];
  837. collisionPosition = {
  838. marginLeft: marginLeft,
  839. marginTop: marginTop
  840. };
  841. $.each( [ "left", "top" ], function( i, dir ) {
  842. if ( $.ui.position[ collision[ i ] ] ) {
  843. $.ui.position[ collision[ i ] ][ dir ]( position, {
  844. targetWidth: targetWidth,
  845. targetHeight: targetHeight,
  846. elemWidth: elemWidth,
  847. elemHeight: elemHeight,
  848. collisionPosition: collisionPosition,
  849. collisionWidth: collisionWidth,
  850. collisionHeight: collisionHeight,
  851. offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ],
  852. my: options.my,
  853. at: options.at,
  854. within: within,
  855. elem: elem
  856. } );
  857. }
  858. } );
  859. if ( options.using ) {
  860. // Adds feedback as second argument to using callback, if present
  861. using = function( props ) {
  862. var left = targetOffset.left - position.left,
  863. right = left + targetWidth - elemWidth,
  864. top = targetOffset.top - position.top,
  865. bottom = top + targetHeight - elemHeight,
  866. feedback = {
  867. target: {
  868. element: target,
  869. left: targetOffset.left,
  870. top: targetOffset.top,
  871. width: targetWidth,
  872. height: targetHeight
  873. },
  874. element: {
  875. element: elem,
  876. left: position.left,
  877. top: position.top,
  878. width: elemWidth,
  879. height: elemHeight
  880. },
  881. horizontal: right < 0 ? "left" : left > 0 ? "right" : "center",
  882. vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle"
  883. };
  884. if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) {
  885. feedback.horizontal = "center";
  886. }
  887. if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) {
  888. feedback.vertical = "middle";
  889. }
  890. if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) {
  891. feedback.important = "horizontal";
  892. } else {
  893. feedback.important = "vertical";
  894. }
  895. options.using.call( this, props, feedback );
  896. };
  897. }
  898. elem.offset( $.extend( position, { using: using } ) );
  899. } );
  900. };
  901. $.ui.position = {
  902. fit: {
  903. left: function( position, data ) {
  904. var within = data.within,
  905. withinOffset = within.isWindow ? within.scrollLeft : within.offset.left,
  906. outerWidth = within.width,
  907. collisionPosLeft = position.left - data.collisionPosition.marginLeft,
  908. overLeft = withinOffset - collisionPosLeft,
  909. overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset,
  910. newOverRight;
  911. // Element is wider than within
  912. if ( data.collisionWidth > outerWidth ) {
  913. // Element is initially over the left side of within
  914. if ( overLeft > 0 && overRight <= 0 ) {
  915. newOverRight = position.left + overLeft + data.collisionWidth - outerWidth -
  916. withinOffset;
  917. position.left += overLeft - newOverRight;
  918. // Element is initially over right side of within
  919. } else if ( overRight > 0 && overLeft <= 0 ) {
  920. position.left = withinOffset;
  921. // Element is initially over both left and right sides of within
  922. } else {
  923. if ( overLeft > overRight ) {
  924. position.left = withinOffset + outerWidth - data.collisionWidth;
  925. } else {
  926. position.left = withinOffset;
  927. }
  928. }
  929. // Too far left -> align with left edge
  930. } else if ( overLeft > 0 ) {
  931. position.left += overLeft;
  932. // Too far right -> align with right edge
  933. } else if ( overRight > 0 ) {
  934. position.left -= overRight;
  935. // Adjust based on position and margin
  936. } else {
  937. position.left = max( position.left - collisionPosLeft, position.left );
  938. }
  939. },
  940. top: function( position, data ) {
  941. var within = data.within,
  942. withinOffset = within.isWindow ? within.scrollTop : within.offset.top,
  943. outerHeight = data.within.height,
  944. collisionPosTop = position.top - data.collisionPosition.marginTop,
  945. overTop = withinOffset - collisionPosTop,
  946. overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset,
  947. newOverBottom;
  948. // Element is taller than within
  949. if ( data.collisionHeight > outerHeight ) {
  950. // Element is initially over the top of within
  951. if ( overTop > 0 && overBottom <= 0 ) {
  952. newOverBottom = position.top + overTop + data.collisionHeight - outerHeight -
  953. withinOffset;
  954. position.top += overTop - newOverBottom;
  955. // Element is initially over bottom of within
  956. } else if ( overBottom > 0 && overTop <= 0 ) {
  957. position.top = withinOffset;
  958. // Element is initially over both top and bottom of within
  959. } else {
  960. if ( overTop > overBottom ) {
  961. position.top = withinOffset + outerHeight - data.collisionHeight;
  962. } else {
  963. position.top = withinOffset;
  964. }
  965. }
  966. // Too far up -> align with top
  967. } else if ( overTop > 0 ) {
  968. position.top += overTop;
  969. // Too far down -> align with bottom edge
  970. } else if ( overBottom > 0 ) {
  971. position.top -= overBottom;
  972. // Adjust based on position and margin
  973. } else {
  974. position.top = max( position.top - collisionPosTop, position.top );
  975. }
  976. }
  977. },
  978. flip: {
  979. left: function( position, data ) {
  980. var within = data.within,
  981. withinOffset = within.offset.left + within.scrollLeft,
  982. outerWidth = within.width,
  983. offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left,
  984. collisionPosLeft = position.left - data.collisionPosition.marginLeft,
  985. overLeft = collisionPosLeft - offsetLeft,
  986. overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft,
  987. myOffset = data.my[ 0 ] === "left" ?
  988. -data.elemWidth :
  989. data.my[ 0 ] === "right" ?
  990. data.elemWidth :
  991. 0,
  992. atOffset = data.at[ 0 ] === "left" ?
  993. data.targetWidth :
  994. data.at[ 0 ] === "right" ?
  995. -data.targetWidth :
  996. 0,
  997. offset = -2 * data.offset[ 0 ],
  998. newOverRight,
  999. newOverLeft;
  1000. if ( overLeft < 0 ) {
  1001. newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth -
  1002. outerWidth - withinOffset;
  1003. if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) {
  1004. position.left += myOffset + atOffset + offset;
  1005. }
  1006. } else if ( overRight > 0 ) {
  1007. newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset +
  1008. atOffset + offset - offsetLeft;
  1009. if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) {
  1010. position.left += myOffset + atOffset + offset;
  1011. }
  1012. }
  1013. },
  1014. top: function( position, data ) {
  1015. var within = data.within,
  1016. withinOffset = within.offset.top + within.scrollTop,
  1017. outerHeight = within.height,
  1018. offsetTop = within.isWindow ? within.scrollTop : within.offset.top,
  1019. collisionPosTop = position.top - data.collisionPosition.marginTop,
  1020. overTop = collisionPosTop - offsetTop,
  1021. overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop,
  1022. top = data.my[ 1 ] === "top",
  1023. myOffset = top ?
  1024. -data.elemHeight :
  1025. data.my[ 1 ] === "bottom" ?
  1026. data.elemHeight :
  1027. 0,
  1028. atOffset = data.at[ 1 ] === "top" ?
  1029. data.targetHeight :
  1030. data.at[ 1 ] === "bottom" ?
  1031. -data.targetHeight :
  1032. 0,
  1033. offset = -2 * data.offset[ 1 ],
  1034. newOverTop,
  1035. newOverBottom;
  1036. if ( overTop < 0 ) {
  1037. newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight -
  1038. outerHeight - withinOffset;
  1039. if ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) {
  1040. position.top += myOffset + atOffset + offset;
  1041. }
  1042. } else if ( overBottom > 0 ) {
  1043. newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset +
  1044. offset - offsetTop;
  1045. if ( newOverTop > 0 || abs( newOverTop ) < overBottom ) {
  1046. position.top += myOffset + atOffset + offset;
  1047. }
  1048. }
  1049. }
  1050. },
  1051. flipfit: {
  1052. left: function() {
  1053. $.ui.position.flip.left.apply( this, arguments );
  1054. $.ui.position.fit.left.apply( this, arguments );
  1055. },
  1056. top: function() {
  1057. $.ui.position.flip.top.apply( this, arguments );
  1058. $.ui.position.fit.top.apply( this, arguments );
  1059. }
  1060. }
  1061. };
  1062. } )();
  1063. var position = $.ui.position;
  1064. /*!
  1065. * jQuery UI Support for jQuery core 1.8.x and newer 1.13.2
  1066. * http://jqueryui.com
  1067. *
  1068. * Copyright jQuery Foundation and other contributors
  1069. * Released under the MIT license.
  1070. * http://jquery.org/license
  1071. *
  1072. */
  1073. //>>label: jQuery 1.8+ Support
  1074. //>>group: Core
  1075. //>>description: Support version 1.8.x and newer of jQuery core
  1076. // Support: jQuery 1.9.x or older
  1077. // $.expr[ ":" ] is deprecated.
  1078. if ( !$.expr.pseudos ) {
  1079. $.expr.pseudos = $.expr[ ":" ];
  1080. }
  1081. // Support: jQuery 1.11.x or older
  1082. // $.unique has been renamed to $.uniqueSort
  1083. if ( !$.uniqueSort ) {
  1084. $.uniqueSort = $.unique;
  1085. }
  1086. // Support: jQuery 2.2.x or older.
  1087. // This method has been defined in jQuery 3.0.0.
  1088. // Code from https://github.com/jquery/jquery/blob/e539bac79e666bba95bba86d690b4e609dca2286/src/selector/escapeSelector.js
  1089. if ( !$.escapeSelector ) {
  1090. // CSS string/identifier serialization
  1091. // https://drafts.csswg.org/cssom/#common-serializing-idioms
  1092. var rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;
  1093. var fcssescape = function( ch, asCodePoint ) {
  1094. if ( asCodePoint ) {
  1095. // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
  1096. if ( ch === "\0" ) {
  1097. return "\uFFFD";
  1098. }
  1099. // Control characters and (dependent upon position) numbers get escaped as code points
  1100. return ch.slice( 0, -1 ) + "\\" + ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
  1101. }
  1102. // Other potentially-special ASCII characters get backslash-escaped
  1103. return "\\" + ch;
  1104. };
  1105. $.escapeSelector = function( sel ) {
  1106. return ( sel + "" ).replace( rcssescape, fcssescape );
  1107. };
  1108. }
  1109. // Support: jQuery 3.4.x or older
  1110. // These methods have been defined in jQuery 3.5.0.
  1111. if ( !$.fn.even || !$.fn.odd ) {
  1112. $.fn.extend( {
  1113. even: function() {
  1114. return this.filter( function( i ) {
  1115. return i % 2 === 0;
  1116. } );
  1117. },
  1118. odd: function() {
  1119. return this.filter( function( i ) {
  1120. return i % 2 === 1;
  1121. } );
  1122. }
  1123. } );
  1124. }
  1125. ;
  1126. /*!
  1127. * jQuery UI Keycode 1.13.2
  1128. * http://jqueryui.com
  1129. *
  1130. * Copyright jQuery Foundation and other contributors
  1131. * Released under the MIT license.
  1132. * http://jquery.org/license
  1133. */
  1134. //>>label: Keycode
  1135. //>>group: Core
  1136. //>>description: Provide keycodes as keynames
  1137. //>>docs: http://api.jqueryui.com/jQuery.ui.keyCode/
  1138. var keycode = $.ui.keyCode = {
  1139. BACKSPACE: 8,
  1140. COMMA: 188,
  1141. DELETE: 46,
  1142. DOWN: 40,
  1143. END: 35,
  1144. ENTER: 13,
  1145. ESCAPE: 27,
  1146. HOME: 36,
  1147. LEFT: 37,
  1148. PAGE_DOWN: 34,
  1149. PAGE_UP: 33,
  1150. PERIOD: 190,
  1151. RIGHT: 39,
  1152. SPACE: 32,
  1153. TAB: 9,
  1154. UP: 38
  1155. };
  1156. /*!
  1157. * jQuery UI Scroll Parent 1.13.2
  1158. * http://jqueryui.com
  1159. *
  1160. * Copyright jQuery Foundation and other contributors
  1161. * Released under the MIT license.
  1162. * http://jquery.org/license
  1163. */
  1164. //>>label: scrollParent
  1165. //>>group: Core
  1166. //>>description: Get the closest ancestor element that is scrollable.
  1167. //>>docs: http://api.jqueryui.com/scrollParent/
  1168. var scrollParent = $.fn.scrollParent = function( includeHidden ) {
  1169. var position = this.css( "position" ),
  1170. excludeStaticParent = position === "absolute",
  1171. overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/,
  1172. scrollParent = this.parents().filter( function() {
  1173. var parent = $( this );
  1174. if ( excludeStaticParent && parent.css( "position" ) === "static" ) {
  1175. return false;
  1176. }
  1177. return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) +
  1178. parent.css( "overflow-x" ) );
  1179. } ).eq( 0 );
  1180. return position === "fixed" || !scrollParent.length ?
  1181. $( this[ 0 ].ownerDocument || document ) :
  1182. scrollParent;
  1183. };
  1184. /*!
  1185. * jQuery UI Unique ID 1.13.2
  1186. * http://jqueryui.com
  1187. *
  1188. * Copyright jQuery Foundation and other contributors
  1189. * Released under the MIT license.
  1190. * http://jquery.org/license
  1191. */
  1192. //>>label: uniqueId
  1193. //>>group: Core
  1194. //>>description: Functions to generate and remove uniqueId's
  1195. //>>docs: http://api.jqueryui.com/uniqueId/
  1196. var uniqueId = $.fn.extend( {
  1197. uniqueId: ( function() {
  1198. var uuid = 0;
  1199. return function() {
  1200. return this.each( function() {
  1201. if ( !this.id ) {
  1202. this.id = "ui-id-" + ( ++uuid );
  1203. }
  1204. } );
  1205. };
  1206. } )(),
  1207. removeUniqueId: function() {
  1208. return this.each( function() {
  1209. if ( /^ui-id-\d+$/.test( this.id ) ) {
  1210. $( this ).removeAttr( "id" );
  1211. }
  1212. } );
  1213. }
  1214. } );
  1215. // NOTE: Original jQuery UI wrapper was replaced. See README-Fancytree.md
  1216. // }));
  1217. })(jQuery);
  1218. (function( factory ) {
  1219. if ( typeof define === "function" && define.amd ) {
  1220. // AMD. Register as an anonymous module.
  1221. define( [ "jquery" ], factory );
  1222. } else if ( typeof module === "object" && module.exports ) {
  1223. // Node/CommonJS
  1224. module.exports = factory(require("jquery"));
  1225. } else {
  1226. // Browser globals
  1227. factory( jQuery );
  1228. }
  1229. }(function( $ ) {
  1230. /*! Fancytree Core *//*!
  1231. * jquery.fancytree.js
  1232. * Tree view control with support for lazy loading and much more.
  1233. * https://github.com/mar10/fancytree/
  1234. *
  1235. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  1236. * Released under the MIT license
  1237. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  1238. *
  1239. * @version 2.38.3
  1240. * @date 2023-02-01T20:52:50Z
  1241. */
  1242. /** Core Fancytree module.
  1243. */
  1244. // UMD wrapper for the Fancytree core module
  1245. (function (factory) {
  1246. if (typeof define === "function" && define.amd) {
  1247. // AMD. Register as an anonymous module.
  1248. define(["jquery", "./jquery.fancytree.ui-deps"], factory);
  1249. } else if (typeof module === "object" && module.exports) {
  1250. // Node/CommonJS
  1251. require("./jquery.fancytree.ui-deps");
  1252. module.exports = factory(require("jquery"));
  1253. } else {
  1254. // Browser globals
  1255. factory(jQuery);
  1256. }
  1257. })(function ($) {
  1258. "use strict";
  1259. // prevent duplicate loading
  1260. if ($.ui && $.ui.fancytree) {
  1261. $.ui.fancytree.warn("Fancytree: ignored duplicate include");
  1262. return;
  1263. }
  1264. /******************************************************************************
  1265. * Private functions and variables
  1266. */
  1267. var i,
  1268. attr,
  1269. FT = null, // initialized below
  1270. TEST_IMG = new RegExp(/\.|\//), // strings are considered image urls if they contain '.' or '/'
  1271. REX_HTML = /[&<>"'/]/g, // Escape those characters
  1272. REX_TOOLTIP = /[<>"'/]/g, // Don't escape `&` in tooltips
  1273. RECURSIVE_REQUEST_ERROR = "$recursive_request",
  1274. INVALID_REQUEST_TARGET_ERROR = "$request_target_invalid",
  1275. ENTITY_MAP = {
  1276. "&": "&amp;",
  1277. "<": "&lt;",
  1278. ">": "&gt;",
  1279. '"': "&quot;",
  1280. "'": "&#39;",
  1281. "/": "&#x2F;",
  1282. },
  1283. IGNORE_KEYCODES = { 16: true, 17: true, 18: true },
  1284. SPECIAL_KEYCODES = {
  1285. 8: "backspace",
  1286. 9: "tab",
  1287. 10: "return",
  1288. 13: "return",
  1289. // 16: null, 17: null, 18: null, // ignore shift, ctrl, alt
  1290. 19: "pause",
  1291. 20: "capslock",
  1292. 27: "esc",
  1293. 32: "space",
  1294. 33: "pageup",
  1295. 34: "pagedown",
  1296. 35: "end",
  1297. 36: "home",
  1298. 37: "left",
  1299. 38: "up",
  1300. 39: "right",
  1301. 40: "down",
  1302. 45: "insert",
  1303. 46: "del",
  1304. 59: ";",
  1305. 61: "=",
  1306. // 91: null, 93: null, // ignore left and right meta
  1307. 96: "0",
  1308. 97: "1",
  1309. 98: "2",
  1310. 99: "3",
  1311. 100: "4",
  1312. 101: "5",
  1313. 102: "6",
  1314. 103: "7",
  1315. 104: "8",
  1316. 105: "9",
  1317. 106: "*",
  1318. 107: "+",
  1319. 109: "-",
  1320. 110: ".",
  1321. 111: "/",
  1322. 112: "f1",
  1323. 113: "f2",
  1324. 114: "f3",
  1325. 115: "f4",
  1326. 116: "f5",
  1327. 117: "f6",
  1328. 118: "f7",
  1329. 119: "f8",
  1330. 120: "f9",
  1331. 121: "f10",
  1332. 122: "f11",
  1333. 123: "f12",
  1334. 144: "numlock",
  1335. 145: "scroll",
  1336. 173: "-",
  1337. 186: ";",
  1338. 187: "=",
  1339. 188: ",",
  1340. 189: "-",
  1341. 190: ".",
  1342. 191: "/",
  1343. 192: "`",
  1344. 219: "[",
  1345. 220: "\\",
  1346. 221: "]",
  1347. 222: "'",
  1348. },
  1349. MODIFIERS = {
  1350. 16: "shift",
  1351. 17: "ctrl",
  1352. 18: "alt",
  1353. 91: "meta",
  1354. 93: "meta",
  1355. },
  1356. MOUSE_BUTTONS = { 0: "", 1: "left", 2: "middle", 3: "right" },
  1357. // Boolean attributes that can be set with equivalent class names in the LI tags
  1358. // Note: v2.23: checkbox and hideCheckbox are *not* in this list
  1359. CLASS_ATTRS =
  1360. "active expanded focus folder lazy radiogroup selected unselectable unselectableIgnore".split(
  1361. " "
  1362. ),
  1363. CLASS_ATTR_MAP = {},
  1364. // Top-level Fancytree attributes, that can be set by dict
  1365. TREE_ATTRS = "columns types".split(" "),
  1366. // TREE_ATTR_MAP = {},
  1367. // Top-level FancytreeNode attributes, that can be set by dict
  1368. NODE_ATTRS =
  1369. "checkbox expanded extraClasses folder icon iconTooltip key lazy partsel radiogroup refKey selected statusNodeType title tooltip type unselectable unselectableIgnore unselectableStatus".split(
  1370. " "
  1371. ),
  1372. NODE_ATTR_MAP = {},
  1373. // Mapping of lowercase -> real name (because HTML5 data-... attribute only supports lowercase)
  1374. NODE_ATTR_LOWERCASE_MAP = {},
  1375. // Attribute names that should NOT be added to node.data
  1376. NONE_NODE_DATA_MAP = {
  1377. active: true,
  1378. children: true,
  1379. data: true,
  1380. focus: true,
  1381. };
  1382. for (i = 0; i < CLASS_ATTRS.length; i++) {
  1383. CLASS_ATTR_MAP[CLASS_ATTRS[i]] = true;
  1384. }
  1385. for (i = 0; i < NODE_ATTRS.length; i++) {
  1386. attr = NODE_ATTRS[i];
  1387. NODE_ATTR_MAP[attr] = true;
  1388. if (attr !== attr.toLowerCase()) {
  1389. NODE_ATTR_LOWERCASE_MAP[attr.toLowerCase()] = attr;
  1390. }
  1391. }
  1392. // for(i=0; i<TREE_ATTRS.length; i++) {
  1393. // TREE_ATTR_MAP[TREE_ATTRS[i]] = true;
  1394. // }
  1395. function _assert(cond, msg) {
  1396. // TODO: see qunit.js extractStacktrace()
  1397. if (!cond) {
  1398. msg = msg ? ": " + msg : "";
  1399. msg = "Fancytree assertion failed" + msg;
  1400. // consoleApply("assert", [!!cond, msg]);
  1401. // #1041: Raised exceptions may not be visible in the browser
  1402. // console if inside promise chains, so we also print directly:
  1403. $.ui.fancytree.error(msg);
  1404. // Throw exception:
  1405. $.error(msg);
  1406. }
  1407. }
  1408. function _hasProp(object, property) {
  1409. return Object.prototype.hasOwnProperty.call(object, property);
  1410. }
  1411. /* Replacement for the deprecated `jQuery.isFunction()`. */
  1412. function _isFunction(obj) {
  1413. return typeof obj === "function";
  1414. }
  1415. /* Replacement for the deprecated `jQuery.trim()`. */
  1416. function _trim(text) {
  1417. return text == null ? "" : text.trim();
  1418. }
  1419. /* Replacement for the deprecated `jQuery.isArray()`. */
  1420. var _isArray = Array.isArray;
  1421. _assert($.ui, "Fancytree requires jQuery UI (http://jqueryui.com)");
  1422. function consoleApply(method, args) {
  1423. var i,
  1424. s,
  1425. fn = window.console ? window.console[method] : null;
  1426. if (fn) {
  1427. try {
  1428. fn.apply(window.console, args);
  1429. } catch (e) {
  1430. // IE 8?
  1431. s = "";
  1432. for (i = 0; i < args.length; i++) {
  1433. s += args[i];
  1434. }
  1435. fn(s);
  1436. }
  1437. }
  1438. }
  1439. /* support: IE8 Polyfil for Date.now() */
  1440. if (!Date.now) {
  1441. Date.now = function now() {
  1442. return new Date().getTime();
  1443. };
  1444. }
  1445. /*Return true if x is a FancytreeNode.*/
  1446. function _isNode(x) {
  1447. return !!(x.tree && x.statusNodeType !== undefined);
  1448. }
  1449. /** Return true if dotted version string is equal or higher than requested version.
  1450. *
  1451. * See http://jsfiddle.net/mar10/FjSAN/
  1452. */
  1453. function isVersionAtLeast(dottedVersion, major, minor, patch) {
  1454. var i,
  1455. v,
  1456. t,
  1457. verParts = $.map(_trim(dottedVersion).split("."), function (e) {
  1458. return parseInt(e, 10);
  1459. }),
  1460. testParts = $.map(
  1461. Array.prototype.slice.call(arguments, 1),
  1462. function (e) {
  1463. return parseInt(e, 10);
  1464. }
  1465. );
  1466. for (i = 0; i < testParts.length; i++) {
  1467. v = verParts[i] || 0;
  1468. t = testParts[i] || 0;
  1469. if (v !== t) {
  1470. return v > t;
  1471. }
  1472. }
  1473. return true;
  1474. }
  1475. /**
  1476. * Deep-merge a list of objects (but replace array-type options).
  1477. *
  1478. * jQuery's $.extend(true, ...) method does a deep merge, that also merges Arrays.
  1479. * This variant is used to merge extension defaults with user options, and should
  1480. * merge objects, but override arrays (for example the `triggerStart: [...]` option
  1481. * of ext-edit). Also `null` values are copied over and not skipped.
  1482. *
  1483. * See issue #876
  1484. *
  1485. * Example:
  1486. * _simpleDeepMerge({}, o1, o2);
  1487. */
  1488. function _simpleDeepMerge() {
  1489. var options,
  1490. name,
  1491. src,
  1492. copy,
  1493. clone,
  1494. target = arguments[0] || {},
  1495. i = 1,
  1496. length = arguments.length;
  1497. // Handle case when target is a string or something (possible in deep copy)
  1498. if (typeof target !== "object" && !_isFunction(target)) {
  1499. target = {};
  1500. }
  1501. if (i === length) {
  1502. throw Error("need at least two args");
  1503. }
  1504. for (; i < length; i++) {
  1505. // Only deal with non-null/undefined values
  1506. if ((options = arguments[i]) != null) {
  1507. // Extend the base object
  1508. for (name in options) {
  1509. if (_hasProp(options, name)) {
  1510. src = target[name];
  1511. copy = options[name];
  1512. // Prevent never-ending loop
  1513. if (target === copy) {
  1514. continue;
  1515. }
  1516. // Recurse if we're merging plain objects
  1517. // (NOTE: unlike $.extend, we don't merge arrays, but replace them)
  1518. if (copy && $.isPlainObject(copy)) {
  1519. clone = src && $.isPlainObject(src) ? src : {};
  1520. // Never move original objects, clone them
  1521. target[name] = _simpleDeepMerge(clone, copy);
  1522. // Don't bring in undefined values
  1523. } else if (copy !== undefined) {
  1524. target[name] = copy;
  1525. }
  1526. }
  1527. }
  1528. }
  1529. }
  1530. // Return the modified object
  1531. return target;
  1532. }
  1533. /** Return a wrapper that calls sub.methodName() and exposes
  1534. * this : tree
  1535. * this._local : tree.ext.EXTNAME
  1536. * this._super : base.methodName.call()
  1537. * this._superApply : base.methodName.apply()
  1538. */
  1539. function _makeVirtualFunction(methodName, tree, base, extension, extName) {
  1540. // $.ui.fancytree.debug("_makeVirtualFunction", methodName, tree, base, extension, extName);
  1541. // if(rexTestSuper && !rexTestSuper.test(func)){
  1542. // // extension.methodName() doesn't call _super(), so no wrapper required
  1543. // return func;
  1544. // }
  1545. // Use an immediate function as closure
  1546. var proxy = (function () {
  1547. var prevFunc = tree[methodName], // org. tree method or prev. proxy
  1548. baseFunc = extension[methodName], //
  1549. _local = tree.ext[extName],
  1550. _super = function () {
  1551. return prevFunc.apply(tree, arguments);
  1552. },
  1553. _superApply = function (args) {
  1554. return prevFunc.apply(tree, args);
  1555. };
  1556. // Return the wrapper function
  1557. return function () {
  1558. var prevLocal = tree._local,
  1559. prevSuper = tree._super,
  1560. prevSuperApply = tree._superApply;
  1561. try {
  1562. tree._local = _local;
  1563. tree._super = _super;
  1564. tree._superApply = _superApply;
  1565. return baseFunc.apply(tree, arguments);
  1566. } finally {
  1567. tree._local = prevLocal;
  1568. tree._super = prevSuper;
  1569. tree._superApply = prevSuperApply;
  1570. }
  1571. };
  1572. })(); // end of Immediate Function
  1573. return proxy;
  1574. }
  1575. /**
  1576. * Subclass `base` by creating proxy functions
  1577. */
  1578. function _subclassObject(tree, base, extension, extName) {
  1579. // $.ui.fancytree.debug("_subclassObject", tree, base, extension, extName);
  1580. for (var attrName in extension) {
  1581. if (typeof extension[attrName] === "function") {
  1582. if (typeof tree[attrName] === "function") {
  1583. // override existing method
  1584. tree[attrName] = _makeVirtualFunction(
  1585. attrName,
  1586. tree,
  1587. base,
  1588. extension,
  1589. extName
  1590. );
  1591. } else if (attrName.charAt(0) === "_") {
  1592. // Create private methods in tree.ext.EXTENSION namespace
  1593. tree.ext[extName][attrName] = _makeVirtualFunction(
  1594. attrName,
  1595. tree,
  1596. base,
  1597. extension,
  1598. extName
  1599. );
  1600. } else {
  1601. $.error(
  1602. "Could not override tree." +
  1603. attrName +
  1604. ". Use prefix '_' to create tree." +
  1605. extName +
  1606. "._" +
  1607. attrName
  1608. );
  1609. }
  1610. } else {
  1611. // Create member variables in tree.ext.EXTENSION namespace
  1612. if (attrName !== "options") {
  1613. tree.ext[extName][attrName] = extension[attrName];
  1614. }
  1615. }
  1616. }
  1617. }
  1618. function _getResolvedPromise(context, argArray) {
  1619. if (context === undefined) {
  1620. return $.Deferred(function () {
  1621. this.resolve();
  1622. }).promise();
  1623. }
  1624. return $.Deferred(function () {
  1625. this.resolveWith(context, argArray);
  1626. }).promise();
  1627. }
  1628. function _getRejectedPromise(context, argArray) {
  1629. if (context === undefined) {
  1630. return $.Deferred(function () {
  1631. this.reject();
  1632. }).promise();
  1633. }
  1634. return $.Deferred(function () {
  1635. this.rejectWith(context, argArray);
  1636. }).promise();
  1637. }
  1638. function _makeResolveFunc(deferred, context) {
  1639. return function () {
  1640. deferred.resolveWith(context);
  1641. };
  1642. }
  1643. function _getElementDataAsDict($el) {
  1644. // Evaluate 'data-NAME' attributes with special treatment for 'data-json'.
  1645. var d = $.extend({}, $el.data()),
  1646. json = d.json;
  1647. delete d.fancytree; // added to container by widget factory (old jQuery UI)
  1648. delete d.uiFancytree; // added to container by widget factory
  1649. if (json) {
  1650. delete d.json;
  1651. // <li data-json='...'> is already returned as object (http://api.jquery.com/data/#data-html5)
  1652. d = $.extend(d, json);
  1653. }
  1654. return d;
  1655. }
  1656. function _escapeTooltip(s) {
  1657. return ("" + s).replace(REX_TOOLTIP, function (s) {
  1658. return ENTITY_MAP[s];
  1659. });
  1660. }
  1661. // TODO: use currying
  1662. function _makeNodeTitleMatcher(s) {
  1663. s = s.toLowerCase();
  1664. return function (node) {
  1665. return node.title.toLowerCase().indexOf(s) >= 0;
  1666. };
  1667. }
  1668. function _makeNodeTitleStartMatcher(s) {
  1669. var reMatch = new RegExp("^" + s, "i");
  1670. return function (node) {
  1671. return reMatch.test(node.title);
  1672. };
  1673. }
  1674. /******************************************************************************
  1675. * FancytreeNode
  1676. */
  1677. /**
  1678. * Creates a new FancytreeNode
  1679. *
  1680. * @class FancytreeNode
  1681. * @classdesc A FancytreeNode represents the hierarchical data model and operations.
  1682. *
  1683. * @param {FancytreeNode} parent
  1684. * @param {NodeData} obj
  1685. *
  1686. * @property {Fancytree} tree The tree instance
  1687. * @property {FancytreeNode} parent The parent node
  1688. * @property {string} key Node id (must be unique inside the tree)
  1689. * @property {string} title Display name (may contain HTML)
  1690. * @property {object} data Contains all extra data that was passed on node creation
  1691. * @property {FancytreeNode[] | null | undefined} children Array of child nodes.<br>
  1692. * For lazy nodes, null or undefined means 'not yet loaded'. Use an empty array
  1693. * to define a node that has no children.
  1694. * @property {boolean} expanded Use isExpanded(), setExpanded() to access this property.
  1695. * @property {string} extraClasses Additional CSS classes, added to the node's `<span>`.<br>
  1696. * Note: use `node.add/remove/toggleClass()` to modify.
  1697. * @property {boolean} folder Folder nodes have different default icons and click behavior.<br>
  1698. * Note: Also non-folders may have children.
  1699. * @property {string} statusNodeType null for standard nodes. Otherwise type of special system node: 'error', 'loading', 'nodata', or 'paging'.
  1700. * @property {boolean} lazy True if this node is loaded on demand, i.e. on first expansion.
  1701. * @property {boolean} selected Use isSelected(), setSelected() to access this property.
  1702. * @property {string} tooltip Alternative description used as hover popup
  1703. * @property {string} iconTooltip Description used as hover popup for icon. @since 2.27
  1704. * @property {string} type Node type, used with tree.types map. @since 2.27
  1705. */
  1706. function FancytreeNode(parent, obj) {
  1707. var i, l, name, cl;
  1708. this.parent = parent;
  1709. this.tree = parent.tree;
  1710. this.ul = null;
  1711. this.li = null; // <li id='key' ftnode=this> tag
  1712. this.statusNodeType = null; // if this is a temp. node to display the status of its parent
  1713. this._isLoading = false; // if this node itself is loading
  1714. this._error = null; // {message: '...'} if a load error occurred
  1715. this.data = {};
  1716. // TODO: merge this code with node.toDict()
  1717. // copy attributes from obj object
  1718. for (i = 0, l = NODE_ATTRS.length; i < l; i++) {
  1719. name = NODE_ATTRS[i];
  1720. this[name] = obj[name];
  1721. }
  1722. // unselectableIgnore and unselectableStatus imply unselectable
  1723. if (
  1724. this.unselectableIgnore != null ||
  1725. this.unselectableStatus != null
  1726. ) {
  1727. this.unselectable = true;
  1728. }
  1729. if (obj.hideCheckbox) {
  1730. $.error(
  1731. "'hideCheckbox' node option was removed in v2.23.0: use 'checkbox: false'"
  1732. );
  1733. }
  1734. // node.data += obj.data
  1735. if (obj.data) {
  1736. $.extend(this.data, obj.data);
  1737. }
  1738. // Copy all other attributes to this.data.NAME
  1739. for (name in obj) {
  1740. if (
  1741. !NODE_ATTR_MAP[name] &&
  1742. (this.tree.options.copyFunctionsToData ||
  1743. !_isFunction(obj[name])) &&
  1744. !NONE_NODE_DATA_MAP[name]
  1745. ) {
  1746. // node.data.NAME = obj.NAME
  1747. this.data[name] = obj[name];
  1748. }
  1749. }
  1750. // Fix missing key
  1751. if (this.key == null) {
  1752. // test for null OR undefined
  1753. if (this.tree.options.defaultKey) {
  1754. this.key = "" + this.tree.options.defaultKey(this);
  1755. _assert(this.key, "defaultKey() must return a unique key");
  1756. } else {
  1757. this.key = "_" + FT._nextNodeKey++;
  1758. }
  1759. } else {
  1760. this.key = "" + this.key; // Convert to string (#217)
  1761. }
  1762. // Fix tree.activeNode
  1763. // TODO: not elegant: we use obj.active as marker to set tree.activeNode
  1764. // when loading from a dictionary.
  1765. if (obj.active) {
  1766. _assert(
  1767. this.tree.activeNode === null,
  1768. "only one active node allowed"
  1769. );
  1770. this.tree.activeNode = this;
  1771. }
  1772. if (obj.selected) {
  1773. // #186
  1774. this.tree.lastSelectedNode = this;
  1775. }
  1776. // TODO: handle obj.focus = true
  1777. // Create child nodes
  1778. cl = obj.children;
  1779. if (cl) {
  1780. if (cl.length) {
  1781. this._setChildren(cl);
  1782. } else {
  1783. // if an empty array was passed for a lazy node, keep it, in order to mark it 'loaded'
  1784. this.children = this.lazy ? [] : null;
  1785. }
  1786. } else {
  1787. this.children = null;
  1788. }
  1789. // Add to key/ref map (except for root node)
  1790. // if( parent ) {
  1791. this.tree._callHook("treeRegisterNode", this.tree, true, this);
  1792. // }
  1793. }
  1794. FancytreeNode.prototype = /** @lends FancytreeNode# */ {
  1795. /* Return the direct child FancytreeNode with a given key, index. */
  1796. _findDirectChild: function (ptr) {
  1797. var i,
  1798. l,
  1799. cl = this.children;
  1800. if (cl) {
  1801. if (typeof ptr === "string") {
  1802. for (i = 0, l = cl.length; i < l; i++) {
  1803. if (cl[i].key === ptr) {
  1804. return cl[i];
  1805. }
  1806. }
  1807. } else if (typeof ptr === "number") {
  1808. return this.children[ptr];
  1809. } else if (ptr.parent === this) {
  1810. return ptr;
  1811. }
  1812. }
  1813. return null;
  1814. },
  1815. // TODO: activate()
  1816. // TODO: activateSilently()
  1817. /* Internal helper called in recursive addChildren sequence.*/
  1818. _setChildren: function (children) {
  1819. _assert(
  1820. children && (!this.children || this.children.length === 0),
  1821. "only init supported"
  1822. );
  1823. this.children = [];
  1824. for (var i = 0, l = children.length; i < l; i++) {
  1825. this.children.push(new FancytreeNode(this, children[i]));
  1826. }
  1827. this.tree._callHook(
  1828. "treeStructureChanged",
  1829. this.tree,
  1830. "setChildren"
  1831. );
  1832. },
  1833. /**
  1834. * Append (or insert) a list of child nodes.
  1835. *
  1836. * @param {NodeData[]} children array of child node definitions (also single child accepted)
  1837. * @param {FancytreeNode | string | Integer} [insertBefore] child node (or key or index of such).
  1838. * If omitted, the new children are appended.
  1839. * @returns {FancytreeNode} first child added
  1840. *
  1841. * @see FancytreeNode#applyPatch
  1842. */
  1843. addChildren: function (children, insertBefore) {
  1844. var i,
  1845. l,
  1846. pos,
  1847. origFirstChild = this.getFirstChild(),
  1848. origLastChild = this.getLastChild(),
  1849. firstNode = null,
  1850. nodeList = [];
  1851. if ($.isPlainObject(children)) {
  1852. children = [children];
  1853. }
  1854. if (!this.children) {
  1855. this.children = [];
  1856. }
  1857. for (i = 0, l = children.length; i < l; i++) {
  1858. nodeList.push(new FancytreeNode(this, children[i]));
  1859. }
  1860. firstNode = nodeList[0];
  1861. if (insertBefore == null) {
  1862. this.children = this.children.concat(nodeList);
  1863. } else {
  1864. // Returns null if insertBefore is not a direct child:
  1865. insertBefore = this._findDirectChild(insertBefore);
  1866. pos = $.inArray(insertBefore, this.children);
  1867. _assert(pos >= 0, "insertBefore must be an existing child");
  1868. // insert nodeList after children[pos]
  1869. this.children.splice.apply(
  1870. this.children,
  1871. [pos, 0].concat(nodeList)
  1872. );
  1873. }
  1874. if (origFirstChild && !insertBefore) {
  1875. // #708: Fast path -- don't render every child of root, just the new ones!
  1876. // #723, #729: but only if it's appended to an existing child list
  1877. for (i = 0, l = nodeList.length; i < l; i++) {
  1878. nodeList[i].render(); // New nodes were never rendered before
  1879. }
  1880. // Adjust classes where status may have changed
  1881. // Has a first child
  1882. if (origFirstChild !== this.getFirstChild()) {
  1883. // Different first child -- recompute classes
  1884. origFirstChild.renderStatus();
  1885. }
  1886. if (origLastChild !== this.getLastChild()) {
  1887. // Different last child -- recompute classes
  1888. origLastChild.renderStatus();
  1889. }
  1890. } else if (!this.parent || this.parent.ul || this.tr) {
  1891. // render if the parent was rendered (or this is a root node)
  1892. this.render();
  1893. }
  1894. if (this.tree.options.selectMode === 3) {
  1895. this.fixSelection3FromEndNodes();
  1896. }
  1897. this.triggerModifyChild(
  1898. "add",
  1899. nodeList.length === 1 ? nodeList[0] : null
  1900. );
  1901. return firstNode;
  1902. },
  1903. /**
  1904. * Add class to node's span tag and to .extraClasses.
  1905. *
  1906. * @param {string} className class name
  1907. *
  1908. * @since 2.17
  1909. */
  1910. addClass: function (className) {
  1911. return this.toggleClass(className, true);
  1912. },
  1913. /**
  1914. * Append or prepend a node, or append a child node.
  1915. *
  1916. * This a convenience function that calls addChildren()
  1917. *
  1918. * @param {NodeData} node node definition
  1919. * @param {string} [mode=child] 'before', 'after', 'firstChild', or 'child' ('over' is a synonym for 'child')
  1920. * @returns {FancytreeNode} new node
  1921. */
  1922. addNode: function (node, mode) {
  1923. if (mode === undefined || mode === "over") {
  1924. mode = "child";
  1925. }
  1926. switch (mode) {
  1927. case "after":
  1928. return this.getParent().addChildren(
  1929. node,
  1930. this.getNextSibling()
  1931. );
  1932. case "before":
  1933. return this.getParent().addChildren(node, this);
  1934. case "firstChild":
  1935. // Insert before the first child if any
  1936. var insertBefore = this.children ? this.children[0] : null;
  1937. return this.addChildren(node, insertBefore);
  1938. case "child":
  1939. case "over":
  1940. return this.addChildren(node);
  1941. }
  1942. _assert(false, "Invalid mode: " + mode);
  1943. },
  1944. /**Add child status nodes that indicate 'More...', etc.
  1945. *
  1946. * This also maintains the node's `partload` property.
  1947. * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes.
  1948. * @param {string} [mode='child'] 'child'|firstChild'
  1949. * @since 2.15
  1950. */
  1951. addPagingNode: function (node, mode) {
  1952. var i, n;
  1953. mode = mode || "child";
  1954. if (node === false) {
  1955. for (i = this.children.length - 1; i >= 0; i--) {
  1956. n = this.children[i];
  1957. if (n.statusNodeType === "paging") {
  1958. this.removeChild(n);
  1959. }
  1960. }
  1961. this.partload = false;
  1962. return;
  1963. }
  1964. node = $.extend(
  1965. {
  1966. title: this.tree.options.strings.moreData,
  1967. statusNodeType: "paging",
  1968. icon: false,
  1969. },
  1970. node
  1971. );
  1972. this.partload = true;
  1973. return this.addNode(node, mode);
  1974. },
  1975. /**
  1976. * Append new node after this.
  1977. *
  1978. * This a convenience function that calls addNode(node, 'after')
  1979. *
  1980. * @param {NodeData} node node definition
  1981. * @returns {FancytreeNode} new node
  1982. */
  1983. appendSibling: function (node) {
  1984. return this.addNode(node, "after");
  1985. },
  1986. /**
  1987. * (experimental) Apply a modification (or navigation) operation.
  1988. *
  1989. * @param {string} cmd
  1990. * @param {object} [opts]
  1991. * @see Fancytree#applyCommand
  1992. * @since 2.32
  1993. */
  1994. applyCommand: function (cmd, opts) {
  1995. return this.tree.applyCommand(cmd, this, opts);
  1996. },
  1997. /**
  1998. * Modify existing child nodes.
  1999. *
  2000. * @param {NodePatch} patch
  2001. * @returns {$.Promise}
  2002. * @see FancytreeNode#addChildren
  2003. */
  2004. applyPatch: function (patch) {
  2005. // patch [key, null] means 'remove'
  2006. if (patch === null) {
  2007. this.remove();
  2008. return _getResolvedPromise(this);
  2009. }
  2010. // TODO: make sure that root node is not collapsed or modified
  2011. // copy (most) attributes to node.ATTR or node.data.ATTR
  2012. var name,
  2013. promise,
  2014. v,
  2015. IGNORE_MAP = { children: true, expanded: true, parent: true }; // TODO: should be global
  2016. for (name in patch) {
  2017. if (_hasProp(patch, name)) {
  2018. v = patch[name];
  2019. if (!IGNORE_MAP[name] && !_isFunction(v)) {
  2020. if (NODE_ATTR_MAP[name]) {
  2021. this[name] = v;
  2022. } else {
  2023. this.data[name] = v;
  2024. }
  2025. }
  2026. }
  2027. }
  2028. // Remove and/or create children
  2029. if (_hasProp(patch, "children")) {
  2030. this.removeChildren();
  2031. if (patch.children) {
  2032. // only if not null and not empty list
  2033. // TODO: addChildren instead?
  2034. this._setChildren(patch.children);
  2035. }
  2036. // TODO: how can we APPEND or INSERT child nodes?
  2037. }
  2038. if (this.isVisible()) {
  2039. this.renderTitle();
  2040. this.renderStatus();
  2041. }
  2042. // Expand collapse (final step, since this may be async)
  2043. if (_hasProp(patch, "expanded")) {
  2044. promise = this.setExpanded(patch.expanded);
  2045. } else {
  2046. promise = _getResolvedPromise(this);
  2047. }
  2048. return promise;
  2049. },
  2050. /** Collapse all sibling nodes.
  2051. * @returns {$.Promise}
  2052. */
  2053. collapseSiblings: function () {
  2054. return this.tree._callHook("nodeCollapseSiblings", this);
  2055. },
  2056. /** Copy this node as sibling or child of `node`.
  2057. *
  2058. * @param {FancytreeNode} node source node
  2059. * @param {string} [mode=child] 'before' | 'after' | 'child'
  2060. * @param {Function} [map] callback function(NodeData, FancytreeNode) that could modify the new node
  2061. * @returns {FancytreeNode} new
  2062. */
  2063. copyTo: function (node, mode, map) {
  2064. return node.addNode(this.toDict(true, map), mode);
  2065. },
  2066. /** Count direct and indirect children.
  2067. *
  2068. * @param {boolean} [deep=true] pass 'false' to only count direct children
  2069. * @returns {int} number of child nodes
  2070. */
  2071. countChildren: function (deep) {
  2072. var cl = this.children,
  2073. i,
  2074. l,
  2075. n;
  2076. if (!cl) {
  2077. return 0;
  2078. }
  2079. n = cl.length;
  2080. if (deep !== false) {
  2081. for (i = 0, l = n; i < l; i++) {
  2082. n += cl[i].countChildren();
  2083. }
  2084. }
  2085. return n;
  2086. },
  2087. // TODO: deactivate()
  2088. /** Write to browser console if debugLevel >= 4 (prepending node info)
  2089. *
  2090. * @param {*} msg string or object or array of such
  2091. */
  2092. debug: function (msg) {
  2093. if (this.tree.options.debugLevel >= 4) {
  2094. Array.prototype.unshift.call(arguments, this.toString());
  2095. consoleApply("log", arguments);
  2096. }
  2097. },
  2098. /** Deprecated.
  2099. * @deprecated since 2014-02-16. Use resetLazy() instead.
  2100. */
  2101. discard: function () {
  2102. this.warn(
  2103. "FancytreeNode.discard() is deprecated since 2014-02-16. Use .resetLazy() instead."
  2104. );
  2105. return this.resetLazy();
  2106. },
  2107. /** Remove DOM elements for all descendents. May be called on .collapse event
  2108. * to keep the DOM small.
  2109. * @param {boolean} [includeSelf=false]
  2110. */
  2111. discardMarkup: function (includeSelf) {
  2112. var fn = includeSelf ? "nodeRemoveMarkup" : "nodeRemoveChildMarkup";
  2113. this.tree._callHook(fn, this);
  2114. },
  2115. /** Write error to browser console if debugLevel >= 1 (prepending tree info)
  2116. *
  2117. * @param {*} msg string or object or array of such
  2118. */
  2119. error: function (msg) {
  2120. if (this.tree.options.debugLevel >= 1) {
  2121. Array.prototype.unshift.call(arguments, this.toString());
  2122. consoleApply("error", arguments);
  2123. }
  2124. },
  2125. /**Find all nodes that match condition (excluding self).
  2126. *
  2127. * @param {string | function(node)} match title string to search for, or a
  2128. * callback function that returns `true` if a node is matched.
  2129. * @returns {FancytreeNode[]} array of nodes (may be empty)
  2130. */
  2131. findAll: function (match) {
  2132. match = _isFunction(match) ? match : _makeNodeTitleMatcher(match);
  2133. var res = [];
  2134. this.visit(function (n) {
  2135. if (match(n)) {
  2136. res.push(n);
  2137. }
  2138. });
  2139. return res;
  2140. },
  2141. /**Find first node that matches condition (excluding self).
  2142. *
  2143. * @param {string | function(node)} match title string to search for, or a
  2144. * callback function that returns `true` if a node is matched.
  2145. * @returns {FancytreeNode} matching node or null
  2146. * @see FancytreeNode#findAll
  2147. */
  2148. findFirst: function (match) {
  2149. match = _isFunction(match) ? match : _makeNodeTitleMatcher(match);
  2150. var res = null;
  2151. this.visit(function (n) {
  2152. if (match(n)) {
  2153. res = n;
  2154. return false;
  2155. }
  2156. });
  2157. return res;
  2158. },
  2159. /** Find a node relative to self.
  2160. *
  2161. * @param {number|string} where The keyCode that would normally trigger this move,
  2162. * or a keyword ('down', 'first', 'last', 'left', 'parent', 'right', 'up').
  2163. * @returns {FancytreeNode}
  2164. * @since v2.31
  2165. */
  2166. findRelatedNode: function (where, includeHidden) {
  2167. return this.tree.findRelatedNode(this, where, includeHidden);
  2168. },
  2169. /* Apply selection state (internal use only) */
  2170. _changeSelectStatusAttrs: function (state) {
  2171. var changed = false,
  2172. opts = this.tree.options,
  2173. unselectable = FT.evalOption(
  2174. "unselectable",
  2175. this,
  2176. this,
  2177. opts,
  2178. false
  2179. ),
  2180. unselectableStatus = FT.evalOption(
  2181. "unselectableStatus",
  2182. this,
  2183. this,
  2184. opts,
  2185. undefined
  2186. );
  2187. if (unselectable && unselectableStatus != null) {
  2188. state = unselectableStatus;
  2189. }
  2190. switch (state) {
  2191. case false:
  2192. changed = this.selected || this.partsel;
  2193. this.selected = false;
  2194. this.partsel = false;
  2195. break;
  2196. case true:
  2197. changed = !this.selected || !this.partsel;
  2198. this.selected = true;
  2199. this.partsel = true;
  2200. break;
  2201. case undefined:
  2202. changed = this.selected || !this.partsel;
  2203. this.selected = false;
  2204. this.partsel = true;
  2205. break;
  2206. default:
  2207. _assert(false, "invalid state: " + state);
  2208. }
  2209. // this.debug("fixSelection3AfterLoad() _changeSelectStatusAttrs()", state, changed);
  2210. if (changed) {
  2211. this.renderStatus();
  2212. }
  2213. return changed;
  2214. },
  2215. /**
  2216. * Fix selection status, after this node was (de)selected in multi-hier mode.
  2217. * This includes (de)selecting all children.
  2218. */
  2219. fixSelection3AfterClick: function (callOpts) {
  2220. var flag = this.isSelected();
  2221. // this.debug("fixSelection3AfterClick()");
  2222. this.visit(function (node) {
  2223. node._changeSelectStatusAttrs(flag);
  2224. if (node.radiogroup) {
  2225. // #931: don't (de)select this branch
  2226. return "skip";
  2227. }
  2228. });
  2229. this.fixSelection3FromEndNodes(callOpts);
  2230. },
  2231. /**
  2232. * Fix selection status for multi-hier mode.
  2233. * Only end-nodes are considered to update the descendants branch and parents.
  2234. * Should be called after this node has loaded new children or after
  2235. * children have been modified using the API.
  2236. */
  2237. fixSelection3FromEndNodes: function (callOpts) {
  2238. var opts = this.tree.options;
  2239. // this.debug("fixSelection3FromEndNodes()");
  2240. _assert(opts.selectMode === 3, "expected selectMode 3");
  2241. // Visit all end nodes and adjust their parent's `selected` and `partsel`
  2242. // attributes. Return selection state true, false, or undefined.
  2243. function _walk(node) {
  2244. var i,
  2245. l,
  2246. child,
  2247. s,
  2248. state,
  2249. allSelected,
  2250. someSelected,
  2251. unselIgnore,
  2252. unselState,
  2253. children = node.children;
  2254. if (children && children.length) {
  2255. // check all children recursively
  2256. allSelected = true;
  2257. someSelected = false;
  2258. for (i = 0, l = children.length; i < l; i++) {
  2259. child = children[i];
  2260. // the selection state of a node is not relevant; we need the end-nodes
  2261. s = _walk(child);
  2262. // if( !child.unselectableIgnore ) {
  2263. unselIgnore = FT.evalOption(
  2264. "unselectableIgnore",
  2265. child,
  2266. child,
  2267. opts,
  2268. false
  2269. );
  2270. if (!unselIgnore) {
  2271. if (s !== false) {
  2272. someSelected = true;
  2273. }
  2274. if (s !== true) {
  2275. allSelected = false;
  2276. }
  2277. }
  2278. }
  2279. // eslint-disable-next-line no-nested-ternary
  2280. state = allSelected
  2281. ? true
  2282. : someSelected
  2283. ? undefined
  2284. : false;
  2285. } else {
  2286. // This is an end-node: simply report the status
  2287. unselState = FT.evalOption(
  2288. "unselectableStatus",
  2289. node,
  2290. node,
  2291. opts,
  2292. undefined
  2293. );
  2294. state = unselState == null ? !!node.selected : !!unselState;
  2295. }
  2296. // #939: Keep a `partsel` flag that was explicitly set on a lazy node
  2297. if (
  2298. node.partsel &&
  2299. !node.selected &&
  2300. node.lazy &&
  2301. node.children == null
  2302. ) {
  2303. state = undefined;
  2304. }
  2305. node._changeSelectStatusAttrs(state);
  2306. return state;
  2307. }
  2308. _walk(this);
  2309. // Update parent's state
  2310. this.visitParents(function (node) {
  2311. var i,
  2312. l,
  2313. child,
  2314. state,
  2315. unselIgnore,
  2316. unselState,
  2317. children = node.children,
  2318. allSelected = true,
  2319. someSelected = false;
  2320. for (i = 0, l = children.length; i < l; i++) {
  2321. child = children[i];
  2322. unselIgnore = FT.evalOption(
  2323. "unselectableIgnore",
  2324. child,
  2325. child,
  2326. opts,
  2327. false
  2328. );
  2329. if (!unselIgnore) {
  2330. unselState = FT.evalOption(
  2331. "unselectableStatus",
  2332. child,
  2333. child,
  2334. opts,
  2335. undefined
  2336. );
  2337. state =
  2338. unselState == null
  2339. ? !!child.selected
  2340. : !!unselState;
  2341. // When fixing the parents, we trust the sibling status (i.e.
  2342. // we don't recurse)
  2343. if (state || child.partsel) {
  2344. someSelected = true;
  2345. }
  2346. if (!state) {
  2347. allSelected = false;
  2348. }
  2349. }
  2350. }
  2351. // eslint-disable-next-line no-nested-ternary
  2352. state = allSelected ? true : someSelected ? undefined : false;
  2353. node._changeSelectStatusAttrs(state);
  2354. });
  2355. },
  2356. // TODO: focus()
  2357. /**
  2358. * Update node data. If dict contains 'children', then also replace
  2359. * the hole sub tree.
  2360. * @param {NodeData} dict
  2361. *
  2362. * @see FancytreeNode#addChildren
  2363. * @see FancytreeNode#applyPatch
  2364. */
  2365. fromDict: function (dict) {
  2366. // copy all other attributes to this.data.xxx
  2367. for (var name in dict) {
  2368. if (NODE_ATTR_MAP[name]) {
  2369. // node.NAME = dict.NAME
  2370. this[name] = dict[name];
  2371. } else if (name === "data") {
  2372. // node.data += dict.data
  2373. $.extend(this.data, dict.data);
  2374. } else if (
  2375. !_isFunction(dict[name]) &&
  2376. !NONE_NODE_DATA_MAP[name]
  2377. ) {
  2378. // node.data.NAME = dict.NAME
  2379. this.data[name] = dict[name];
  2380. }
  2381. }
  2382. if (dict.children) {
  2383. // recursively set children and render
  2384. this.removeChildren();
  2385. this.addChildren(dict.children);
  2386. }
  2387. this.renderTitle();
  2388. /*
  2389. var children = dict.children;
  2390. if(children === undefined){
  2391. this.data = $.extend(this.data, dict);
  2392. this.render();
  2393. return;
  2394. }
  2395. dict = $.extend({}, dict);
  2396. dict.children = undefined;
  2397. this.data = $.extend(this.data, dict);
  2398. this.removeChildren();
  2399. this.addChild(children);
  2400. */
  2401. },
  2402. /** Return the list of child nodes (undefined for unexpanded lazy nodes).
  2403. * @returns {FancytreeNode[] | undefined}
  2404. */
  2405. getChildren: function () {
  2406. if (this.hasChildren() === undefined) {
  2407. // TODO: only required for lazy nodes?
  2408. return undefined; // Lazy node: unloaded, currently loading, or load error
  2409. }
  2410. return this.children;
  2411. },
  2412. /** Return the first child node or null.
  2413. * @returns {FancytreeNode | null}
  2414. */
  2415. getFirstChild: function () {
  2416. return this.children ? this.children[0] : null;
  2417. },
  2418. /** Return the 0-based child index.
  2419. * @returns {int}
  2420. */
  2421. getIndex: function () {
  2422. // return this.parent.children.indexOf(this);
  2423. return $.inArray(this, this.parent.children); // indexOf doesn't work in IE7
  2424. },
  2425. /** Return the hierarchical child index (1-based, e.g. '3.2.4').
  2426. * @param {string} [separator="."]
  2427. * @param {int} [digits=1]
  2428. * @returns {string}
  2429. */
  2430. getIndexHier: function (separator, digits) {
  2431. separator = separator || ".";
  2432. var s,
  2433. res = [];
  2434. $.each(this.getParentList(false, true), function (i, o) {
  2435. s = "" + (o.getIndex() + 1);
  2436. if (digits) {
  2437. // prepend leading zeroes
  2438. s = ("0000000" + s).substr(-digits);
  2439. }
  2440. res.push(s);
  2441. });
  2442. return res.join(separator);
  2443. },
  2444. /** Return the parent keys separated by options.keyPathSeparator, e.g. "/id_1/id_17/id_32".
  2445. *
  2446. * (Unlike `node.getPath()`, this method prepends a "/" and inverts the first argument.)
  2447. *
  2448. * @see FancytreeNode#getPath
  2449. * @param {boolean} [excludeSelf=false]
  2450. * @returns {string}
  2451. */
  2452. getKeyPath: function (excludeSelf) {
  2453. var sep = this.tree.options.keyPathSeparator;
  2454. return sep + this.getPath(!excludeSelf, "key", sep);
  2455. },
  2456. /** Return the last child of this node or null.
  2457. * @returns {FancytreeNode | null}
  2458. */
  2459. getLastChild: function () {
  2460. return this.children
  2461. ? this.children[this.children.length - 1]
  2462. : null;
  2463. },
  2464. /** Return node depth. 0: System root node, 1: visible top-level node, 2: first sub-level, ... .
  2465. * @returns {int}
  2466. */
  2467. getLevel: function () {
  2468. var level = 0,
  2469. dtn = this.parent;
  2470. while (dtn) {
  2471. level++;
  2472. dtn = dtn.parent;
  2473. }
  2474. return level;
  2475. },
  2476. /** Return the successor node (under the same parent) or null.
  2477. * @returns {FancytreeNode | null}
  2478. */
  2479. getNextSibling: function () {
  2480. // TODO: use indexOf, if available: (not in IE6)
  2481. if (this.parent) {
  2482. var i,
  2483. l,
  2484. ac = this.parent.children;
  2485. for (i = 0, l = ac.length - 1; i < l; i++) {
  2486. // up to length-2, so next(last) = null
  2487. if (ac[i] === this) {
  2488. return ac[i + 1];
  2489. }
  2490. }
  2491. }
  2492. return null;
  2493. },
  2494. /** Return the parent node (null for the system root node).
  2495. * @returns {FancytreeNode | null}
  2496. */
  2497. getParent: function () {
  2498. // TODO: return null for top-level nodes?
  2499. return this.parent;
  2500. },
  2501. /** Return an array of all parent nodes (top-down).
  2502. * @param {boolean} [includeRoot=false] Include the invisible system root node.
  2503. * @param {boolean} [includeSelf=false] Include the node itself.
  2504. * @returns {FancytreeNode[]}
  2505. */
  2506. getParentList: function (includeRoot, includeSelf) {
  2507. var l = [],
  2508. dtn = includeSelf ? this : this.parent;
  2509. while (dtn) {
  2510. if (includeRoot || dtn.parent) {
  2511. l.unshift(dtn);
  2512. }
  2513. dtn = dtn.parent;
  2514. }
  2515. return l;
  2516. },
  2517. /** Return a string representing the hierachical node path, e.g. "a/b/c".
  2518. * @param {boolean} [includeSelf=true]
  2519. * @param {string | function} [part="title"] node property name or callback
  2520. * @param {string} [separator="/"]
  2521. * @returns {string}
  2522. * @since v2.31
  2523. */
  2524. getPath: function (includeSelf, part, separator) {
  2525. includeSelf = includeSelf !== false;
  2526. part = part || "title";
  2527. separator = separator || "/";
  2528. var val,
  2529. path = [],
  2530. isFunc = _isFunction(part);
  2531. this.visitParents(function (n) {
  2532. if (n.parent) {
  2533. val = isFunc ? part(n) : n[part];
  2534. path.unshift(val);
  2535. }
  2536. }, includeSelf);
  2537. return path.join(separator);
  2538. },
  2539. /** Return the predecessor node (under the same parent) or null.
  2540. * @returns {FancytreeNode | null}
  2541. */
  2542. getPrevSibling: function () {
  2543. if (this.parent) {
  2544. var i,
  2545. l,
  2546. ac = this.parent.children;
  2547. for (i = 1, l = ac.length; i < l; i++) {
  2548. // start with 1, so prev(first) = null
  2549. if (ac[i] === this) {
  2550. return ac[i - 1];
  2551. }
  2552. }
  2553. }
  2554. return null;
  2555. },
  2556. /**
  2557. * Return an array of selected descendant nodes.
  2558. * @param {boolean} [stopOnParents=false] only return the topmost selected
  2559. * node (useful with selectMode 3)
  2560. * @returns {FancytreeNode[]}
  2561. */
  2562. getSelectedNodes: function (stopOnParents) {
  2563. var nodeList = [];
  2564. this.visit(function (node) {
  2565. if (node.selected) {
  2566. nodeList.push(node);
  2567. if (stopOnParents === true) {
  2568. return "skip"; // stop processing this branch
  2569. }
  2570. }
  2571. });
  2572. return nodeList;
  2573. },
  2574. /** Return true if node has children. Return undefined if not sure, i.e. the node is lazy and not yet loaded).
  2575. * @returns {boolean | undefined}
  2576. */
  2577. hasChildren: function () {
  2578. if (this.lazy) {
  2579. if (this.children == null) {
  2580. // null or undefined: Not yet loaded
  2581. return undefined;
  2582. } else if (this.children.length === 0) {
  2583. // Loaded, but response was empty
  2584. return false;
  2585. } else if (
  2586. this.children.length === 1 &&
  2587. this.children[0].isStatusNode()
  2588. ) {
  2589. // Currently loading or load error
  2590. return undefined;
  2591. }
  2592. return true;
  2593. }
  2594. return !!(this.children && this.children.length);
  2595. },
  2596. /**
  2597. * Return true if node has `className` defined in .extraClasses.
  2598. *
  2599. * @param {string} className class name (separate multiple classes by space)
  2600. * @returns {boolean}
  2601. *
  2602. * @since 2.32
  2603. */
  2604. hasClass: function (className) {
  2605. return (
  2606. (" " + (this.extraClasses || "") + " ").indexOf(
  2607. " " + className + " "
  2608. ) >= 0
  2609. );
  2610. },
  2611. /** Return true if node has keyboard focus.
  2612. * @returns {boolean}
  2613. */
  2614. hasFocus: function () {
  2615. return this.tree.hasFocus() && this.tree.focusNode === this;
  2616. },
  2617. /** Write to browser console if debugLevel >= 3 (prepending node info)
  2618. *
  2619. * @param {*} msg string or object or array of such
  2620. */
  2621. info: function (msg) {
  2622. if (this.tree.options.debugLevel >= 3) {
  2623. Array.prototype.unshift.call(arguments, this.toString());
  2624. consoleApply("info", arguments);
  2625. }
  2626. },
  2627. /** Return true if node is active (see also FancytreeNode#isSelected).
  2628. * @returns {boolean}
  2629. */
  2630. isActive: function () {
  2631. return this.tree.activeNode === this;
  2632. },
  2633. /** Return true if node is vertically below `otherNode`, i.e. rendered in a subsequent row.
  2634. * @param {FancytreeNode} otherNode
  2635. * @returns {boolean}
  2636. * @since 2.28
  2637. */
  2638. isBelowOf: function (otherNode) {
  2639. return this.getIndexHier(".", 5) > otherNode.getIndexHier(".", 5);
  2640. },
  2641. /** Return true if node is a direct child of otherNode.
  2642. * @param {FancytreeNode} otherNode
  2643. * @returns {boolean}
  2644. */
  2645. isChildOf: function (otherNode) {
  2646. return this.parent && this.parent === otherNode;
  2647. },
  2648. /** Return true, if node is a direct or indirect sub node of otherNode.
  2649. * @param {FancytreeNode} otherNode
  2650. * @returns {boolean}
  2651. */
  2652. isDescendantOf: function (otherNode) {
  2653. if (!otherNode || otherNode.tree !== this.tree) {
  2654. return false;
  2655. }
  2656. var p = this.parent;
  2657. while (p) {
  2658. if (p === otherNode) {
  2659. return true;
  2660. }
  2661. if (p === p.parent) {
  2662. $.error("Recursive parent link: " + p);
  2663. }
  2664. p = p.parent;
  2665. }
  2666. return false;
  2667. },
  2668. /** Return true if node is expanded.
  2669. * @returns {boolean}
  2670. */
  2671. isExpanded: function () {
  2672. return !!this.expanded;
  2673. },
  2674. /** Return true if node is the first node of its parent's children.
  2675. * @returns {boolean}
  2676. */
  2677. isFirstSibling: function () {
  2678. var p = this.parent;
  2679. return !p || p.children[0] === this;
  2680. },
  2681. /** Return true if node is a folder, i.e. has the node.folder attribute set.
  2682. * @returns {boolean}
  2683. */
  2684. isFolder: function () {
  2685. return !!this.folder;
  2686. },
  2687. /** Return true if node is the last node of its parent's children.
  2688. * @returns {boolean}
  2689. */
  2690. isLastSibling: function () {
  2691. var p = this.parent;
  2692. return !p || p.children[p.children.length - 1] === this;
  2693. },
  2694. /** Return true if node is lazy (even if data was already loaded)
  2695. * @returns {boolean}
  2696. */
  2697. isLazy: function () {
  2698. return !!this.lazy;
  2699. },
  2700. /** Return true if node is lazy and loaded. For non-lazy nodes always return true.
  2701. * @returns {boolean}
  2702. */
  2703. isLoaded: function () {
  2704. return !this.lazy || this.hasChildren() !== undefined; // Also checks if the only child is a status node
  2705. },
  2706. /** Return true if children are currently beeing loaded, i.e. a Ajax request is pending.
  2707. * @returns {boolean}
  2708. */
  2709. isLoading: function () {
  2710. return !!this._isLoading;
  2711. },
  2712. /*
  2713. * @deprecated since v2.4.0: Use isRootNode() instead
  2714. */
  2715. isRoot: function () {
  2716. return this.isRootNode();
  2717. },
  2718. /** Return true if node is partially selected (tri-state).
  2719. * @returns {boolean}
  2720. * @since 2.23
  2721. */
  2722. isPartsel: function () {
  2723. return !this.selected && !!this.partsel;
  2724. },
  2725. /** (experimental) Return true if this is partially loaded.
  2726. * @returns {boolean}
  2727. * @since 2.15
  2728. */
  2729. isPartload: function () {
  2730. return !!this.partload;
  2731. },
  2732. /** Return true if this is the (invisible) system root node.
  2733. * @returns {boolean}
  2734. * @since 2.4
  2735. */
  2736. isRootNode: function () {
  2737. return this.tree.rootNode === this;
  2738. },
  2739. /** Return true if node is selected, i.e. has a checkmark set (see also FancytreeNode#isActive).
  2740. * @returns {boolean}
  2741. */
  2742. isSelected: function () {
  2743. return !!this.selected;
  2744. },
  2745. /** Return true if this node is a temporarily generated system node like
  2746. * 'loading', 'paging', or 'error' (node.statusNodeType contains the type).
  2747. * @returns {boolean}
  2748. */
  2749. isStatusNode: function () {
  2750. return !!this.statusNodeType;
  2751. },
  2752. /** Return true if this node is a status node of type 'paging'.
  2753. * @returns {boolean}
  2754. * @since 2.15
  2755. */
  2756. isPagingNode: function () {
  2757. return this.statusNodeType === "paging";
  2758. },
  2759. /** Return true if this a top level node, i.e. a direct child of the (invisible) system root node.
  2760. * @returns {boolean}
  2761. * @since 2.4
  2762. */
  2763. isTopLevel: function () {
  2764. return this.tree.rootNode === this.parent;
  2765. },
  2766. /** Return true if node is lazy and not yet loaded. For non-lazy nodes always return false.
  2767. * @returns {boolean}
  2768. */
  2769. isUndefined: function () {
  2770. return this.hasChildren() === undefined; // also checks if the only child is a status node
  2771. },
  2772. /** Return true if all parent nodes are expanded. Note: this does not check
  2773. * whether the node is scrolled into the visible part of the screen.
  2774. * @returns {boolean}
  2775. */
  2776. isVisible: function () {
  2777. var i,
  2778. l,
  2779. n,
  2780. hasFilter = this.tree.enableFilter,
  2781. parents = this.getParentList(false, false);
  2782. // TODO: check $(n.span).is(":visible")
  2783. // i.e. return false for nodes (but not parents) that are hidden
  2784. // by a filter
  2785. if (hasFilter && !this.match && !this.subMatchCount) {
  2786. // this.debug( "isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")" );
  2787. return false;
  2788. }
  2789. for (i = 0, l = parents.length; i < l; i++) {
  2790. n = parents[i];
  2791. if (!n.expanded) {
  2792. // this.debug("isVisible: HIDDEN (parent collapsed)");
  2793. return false;
  2794. }
  2795. // if (hasFilter && !n.match && !n.subMatchCount) {
  2796. // this.debug("isVisible: HIDDEN (" + hasFilter + ", " + this.match + ", " + this.match + ")");
  2797. // return false;
  2798. // }
  2799. }
  2800. // this.debug("isVisible: VISIBLE");
  2801. return true;
  2802. },
  2803. /** Deprecated.
  2804. * @deprecated since 2014-02-16: use load() instead.
  2805. */
  2806. lazyLoad: function (discard) {
  2807. $.error(
  2808. "FancytreeNode.lazyLoad() is deprecated since 2014-02-16. Use .load() instead."
  2809. );
  2810. },
  2811. /**
  2812. * Load all children of a lazy node if neccessary. The <i>expanded</i> state is maintained.
  2813. * @param {boolean} [forceReload=false] Pass true to discard any existing nodes before. Otherwise this method does nothing if the node was already loaded.
  2814. * @returns {$.Promise}
  2815. */
  2816. load: function (forceReload) {
  2817. var res,
  2818. source,
  2819. self = this,
  2820. wasExpanded = this.isExpanded();
  2821. _assert(this.isLazy(), "load() requires a lazy node");
  2822. // _assert( forceReload || this.isUndefined(), "Pass forceReload=true to re-load a lazy node" );
  2823. if (!forceReload && !this.isUndefined()) {
  2824. return _getResolvedPromise(this);
  2825. }
  2826. if (this.isLoaded()) {
  2827. this.resetLazy(); // also collapses
  2828. }
  2829. // This method is also called by setExpanded() and loadKeyPath(), so we
  2830. // have to avoid recursion.
  2831. source = this.tree._triggerNodeEvent("lazyLoad", this);
  2832. if (source === false) {
  2833. // #69
  2834. return _getResolvedPromise(this);
  2835. }
  2836. _assert(
  2837. typeof source !== "boolean",
  2838. "lazyLoad event must return source in data.result"
  2839. );
  2840. res = this.tree._callHook("nodeLoadChildren", this, source);
  2841. if (wasExpanded) {
  2842. this.expanded = true;
  2843. res.always(function () {
  2844. self.render();
  2845. });
  2846. } else {
  2847. res.always(function () {
  2848. self.renderStatus(); // fix expander icon to 'loaded'
  2849. });
  2850. }
  2851. return res;
  2852. },
  2853. /** Expand all parents and optionally scroll into visible area as neccessary.
  2854. * Promise is resolved, when lazy loading and animations are done.
  2855. * @param {object} [opts] passed to `setExpanded()`.
  2856. * Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true}
  2857. * @returns {$.Promise}
  2858. */
  2859. makeVisible: function (opts) {
  2860. var i,
  2861. self = this,
  2862. deferreds = [],
  2863. dfd = new $.Deferred(),
  2864. parents = this.getParentList(false, false),
  2865. len = parents.length,
  2866. effects = !(opts && opts.noAnimation === true),
  2867. scroll = !(opts && opts.scrollIntoView === false);
  2868. // Expand bottom-up, so only the top node is animated
  2869. for (i = len - 1; i >= 0; i--) {
  2870. // self.debug("pushexpand" + parents[i]);
  2871. deferreds.push(parents[i].setExpanded(true, opts));
  2872. }
  2873. $.when.apply($, deferreds).done(function () {
  2874. // All expands have finished
  2875. // self.debug("expand DONE", scroll);
  2876. if (scroll) {
  2877. self.scrollIntoView(effects).done(function () {
  2878. // self.debug("scroll DONE");
  2879. dfd.resolve();
  2880. });
  2881. } else {
  2882. dfd.resolve();
  2883. }
  2884. });
  2885. return dfd.promise();
  2886. },
  2887. /** Move this node to targetNode.
  2888. * @param {FancytreeNode} targetNode
  2889. * @param {string} mode <pre>
  2890. * 'child': append this node as last child of targetNode.
  2891. * This is the default. To be compatble with the D'n'd
  2892. * hitMode, we also accept 'over'.
  2893. * 'firstChild': add this node as first child of targetNode.
  2894. * 'before': add this node as sibling before targetNode.
  2895. * 'after': add this node as sibling after targetNode.</pre>
  2896. * @param {function} [map] optional callback(FancytreeNode) to allow modifcations
  2897. */
  2898. moveTo: function (targetNode, mode, map) {
  2899. if (mode === undefined || mode === "over") {
  2900. mode = "child";
  2901. } else if (mode === "firstChild") {
  2902. if (targetNode.children && targetNode.children.length) {
  2903. mode = "before";
  2904. targetNode = targetNode.children[0];
  2905. } else {
  2906. mode = "child";
  2907. }
  2908. }
  2909. var pos,
  2910. tree = this.tree,
  2911. prevParent = this.parent,
  2912. targetParent =
  2913. mode === "child" ? targetNode : targetNode.parent;
  2914. if (this === targetNode) {
  2915. return;
  2916. } else if (!this.parent) {
  2917. $.error("Cannot move system root");
  2918. } else if (targetParent.isDescendantOf(this)) {
  2919. $.error("Cannot move a node to its own descendant");
  2920. }
  2921. if (targetParent !== prevParent) {
  2922. prevParent.triggerModifyChild("remove", this);
  2923. }
  2924. // Unlink this node from current parent
  2925. if (this.parent.children.length === 1) {
  2926. if (this.parent === targetParent) {
  2927. return; // #258
  2928. }
  2929. this.parent.children = this.parent.lazy ? [] : null;
  2930. this.parent.expanded = false;
  2931. } else {
  2932. pos = $.inArray(this, this.parent.children);
  2933. _assert(pos >= 0, "invalid source parent");
  2934. this.parent.children.splice(pos, 1);
  2935. }
  2936. // Remove from source DOM parent
  2937. // if(this.parent.ul){
  2938. // this.parent.ul.removeChild(this.li);
  2939. // }
  2940. // Insert this node to target parent's child list
  2941. this.parent = targetParent;
  2942. if (targetParent.hasChildren()) {
  2943. switch (mode) {
  2944. case "child":
  2945. // Append to existing target children
  2946. targetParent.children.push(this);
  2947. break;
  2948. case "before":
  2949. // Insert this node before target node
  2950. pos = $.inArray(targetNode, targetParent.children);
  2951. _assert(pos >= 0, "invalid target parent");
  2952. targetParent.children.splice(pos, 0, this);
  2953. break;
  2954. case "after":
  2955. // Insert this node after target node
  2956. pos = $.inArray(targetNode, targetParent.children);
  2957. _assert(pos >= 0, "invalid target parent");
  2958. targetParent.children.splice(pos + 1, 0, this);
  2959. break;
  2960. default:
  2961. $.error("Invalid mode " + mode);
  2962. }
  2963. } else {
  2964. targetParent.children = [this];
  2965. }
  2966. // Parent has no <ul> tag yet:
  2967. // if( !targetParent.ul ) {
  2968. // // This is the parent's first child: create UL tag
  2969. // // (Hidden, because it will be
  2970. // targetParent.ul = document.createElement("ul");
  2971. // targetParent.ul.style.display = "none";
  2972. // targetParent.li.appendChild(targetParent.ul);
  2973. // }
  2974. // // Issue 319: Add to target DOM parent (only if node was already rendered(expanded))
  2975. // if(this.li){
  2976. // targetParent.ul.appendChild(this.li);
  2977. // }
  2978. // Let caller modify the nodes
  2979. if (map) {
  2980. targetNode.visit(map, true);
  2981. }
  2982. if (targetParent === prevParent) {
  2983. targetParent.triggerModifyChild("move", this);
  2984. } else {
  2985. // prevParent.triggerModifyChild("remove", this);
  2986. targetParent.triggerModifyChild("add", this);
  2987. }
  2988. // Handle cross-tree moves
  2989. if (tree !== targetNode.tree) {
  2990. // Fix node.tree for all source nodes
  2991. // _assert(false, "Cross-tree move is not yet implemented.");
  2992. this.warn("Cross-tree moveTo is experimental!");
  2993. this.visit(function (n) {
  2994. // TODO: fix selection state and activation, ...
  2995. n.tree = targetNode.tree;
  2996. }, true);
  2997. }
  2998. // A collaposed node won't re-render children, so we have to remove it manually
  2999. // if( !targetParent.expanded ){
  3000. // prevParent.ul.removeChild(this.li);
  3001. // }
  3002. tree._callHook("treeStructureChanged", tree, "moveTo");
  3003. // Update HTML markup
  3004. if (!prevParent.isDescendantOf(targetParent)) {
  3005. prevParent.render();
  3006. }
  3007. if (
  3008. !targetParent.isDescendantOf(prevParent) &&
  3009. targetParent !== prevParent
  3010. ) {
  3011. targetParent.render();
  3012. }
  3013. // TODO: fix selection state
  3014. // TODO: fix active state
  3015. /*
  3016. var tree = this.tree;
  3017. var opts = tree.options;
  3018. var pers = tree.persistence;
  3019. // Always expand, if it's below minExpandLevel
  3020. // tree.logDebug ("%s._addChildNode(%o), l=%o", this, ftnode, ftnode.getLevel());
  3021. if ( opts.minExpandLevel >= ftnode.getLevel() ) {
  3022. // tree.logDebug ("Force expand for %o", ftnode);
  3023. this.bExpanded = true;
  3024. }
  3025. // In multi-hier mode, update the parents selection state
  3026. // DT issue #82: only if not initializing, because the children may not exist yet
  3027. // if( !ftnode.data.isStatusNode() && opts.selectMode==3 && !isInitializing )
  3028. // ftnode._fixSelectionState();
  3029. // In multi-hier mode, update the parents selection state
  3030. if( ftnode.bSelected && opts.selectMode==3 ) {
  3031. var p = this;
  3032. while( p ) {
  3033. if( !p.hasSubSel )
  3034. p._setSubSel(true);
  3035. p = p.parent;
  3036. }
  3037. }
  3038. // render this node and the new child
  3039. if ( tree.bEnableUpdate )
  3040. this.render();
  3041. return ftnode;
  3042. */
  3043. },
  3044. /** Set focus relative to this node and optionally activate.
  3045. *
  3046. * 'left' collapses the node if it is expanded, or move to the parent
  3047. * otherwise.
  3048. * 'right' expands the node if it is collapsed, or move to the first
  3049. * child otherwise.
  3050. *
  3051. * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
  3052. * (Alternatively the keyCode that would normally trigger this move,
  3053. * e.g. `$.ui.keyCode.LEFT` = 'left'.
  3054. * @param {boolean} [activate=true]
  3055. * @returns {$.Promise}
  3056. */
  3057. navigate: function (where, activate) {
  3058. var node,
  3059. KC = $.ui.keyCode;
  3060. // Handle optional expand/collapse action for LEFT/RIGHT
  3061. switch (where) {
  3062. case "left":
  3063. case KC.LEFT:
  3064. if (this.expanded) {
  3065. return this.setExpanded(false);
  3066. }
  3067. break;
  3068. case "right":
  3069. case KC.RIGHT:
  3070. if (!this.expanded && (this.children || this.lazy)) {
  3071. return this.setExpanded();
  3072. }
  3073. break;
  3074. }
  3075. // Otherwise activate or focus the related node
  3076. node = this.findRelatedNode(where);
  3077. if (node) {
  3078. // setFocus/setActive will scroll later (if autoScroll is specified)
  3079. try {
  3080. node.makeVisible({ scrollIntoView: false });
  3081. } catch (e) {} // #272
  3082. if (activate === false) {
  3083. node.setFocus();
  3084. return _getResolvedPromise();
  3085. }
  3086. return node.setActive();
  3087. }
  3088. this.warn("Could not find related node '" + where + "'.");
  3089. return _getResolvedPromise();
  3090. },
  3091. /**
  3092. * Remove this node (not allowed for system root).
  3093. */
  3094. remove: function () {
  3095. return this.parent.removeChild(this);
  3096. },
  3097. /**
  3098. * Remove childNode from list of direct children.
  3099. * @param {FancytreeNode} childNode
  3100. */
  3101. removeChild: function (childNode) {
  3102. return this.tree._callHook("nodeRemoveChild", this, childNode);
  3103. },
  3104. /**
  3105. * Remove all child nodes and descendents. This converts the node into a leaf.<br>
  3106. * If this was a lazy node, it is still considered 'loaded'; call node.resetLazy()
  3107. * in order to trigger lazyLoad on next expand.
  3108. */
  3109. removeChildren: function () {
  3110. return this.tree._callHook("nodeRemoveChildren", this);
  3111. },
  3112. /**
  3113. * Remove class from node's span tag and .extraClasses.
  3114. *
  3115. * @param {string} className class name
  3116. *
  3117. * @since 2.17
  3118. */
  3119. removeClass: function (className) {
  3120. return this.toggleClass(className, false);
  3121. },
  3122. /**
  3123. * This method renders and updates all HTML markup that is required
  3124. * to display this node in its current state.<br>
  3125. * Note:
  3126. * <ul>
  3127. * <li>It should only be neccessary to call this method after the node object
  3128. * was modified by direct access to its properties, because the common
  3129. * API methods (node.setTitle(), moveTo(), addChildren(), remove(), ...)
  3130. * already handle this.
  3131. * <li> {@link FancytreeNode#renderTitle} and {@link FancytreeNode#renderStatus}
  3132. * are implied. If changes are more local, calling only renderTitle() or
  3133. * renderStatus() may be sufficient and faster.
  3134. * </ul>
  3135. *
  3136. * @param {boolean} [force=false] re-render, even if html markup was already created
  3137. * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed
  3138. */
  3139. render: function (force, deep) {
  3140. return this.tree._callHook("nodeRender", this, force, deep);
  3141. },
  3142. /** Create HTML markup for the node's outer `<span>` (expander, checkbox, icon, and title).
  3143. * Implies {@link FancytreeNode#renderStatus}.
  3144. * @see Fancytree_Hooks#nodeRenderTitle
  3145. */
  3146. renderTitle: function () {
  3147. return this.tree._callHook("nodeRenderTitle", this);
  3148. },
  3149. /** Update element's CSS classes according to node state.
  3150. * @see Fancytree_Hooks#nodeRenderStatus
  3151. */
  3152. renderStatus: function () {
  3153. return this.tree._callHook("nodeRenderStatus", this);
  3154. },
  3155. /**
  3156. * (experimental) Replace this node with `source`.
  3157. * (Currently only available for paging nodes.)
  3158. * @param {NodeData[]} source List of child node definitions
  3159. * @since 2.15
  3160. */
  3161. replaceWith: function (source) {
  3162. var res,
  3163. parent = this.parent,
  3164. pos = $.inArray(this, parent.children),
  3165. self = this;
  3166. _assert(
  3167. this.isPagingNode(),
  3168. "replaceWith() currently requires a paging status node"
  3169. );
  3170. res = this.tree._callHook("nodeLoadChildren", this, source);
  3171. res.done(function (data) {
  3172. // New nodes are currently children of `this`.
  3173. var children = self.children;
  3174. // Prepend newly loaded child nodes to `this`
  3175. // Move new children after self
  3176. for (i = 0; i < children.length; i++) {
  3177. children[i].parent = parent;
  3178. }
  3179. parent.children.splice.apply(
  3180. parent.children,
  3181. [pos + 1, 0].concat(children)
  3182. );
  3183. // Remove self
  3184. self.children = null;
  3185. self.remove();
  3186. // Redraw new nodes
  3187. parent.render();
  3188. // TODO: set node.partload = false if this was tha last paging node?
  3189. // parent.addPagingNode(false);
  3190. }).fail(function () {
  3191. self.setExpanded();
  3192. });
  3193. return res;
  3194. // $.error("Not implemented: replaceWith()");
  3195. },
  3196. /**
  3197. * Remove all children, collapse, and set the lazy-flag, so that the lazyLoad
  3198. * event is triggered on next expand.
  3199. */
  3200. resetLazy: function () {
  3201. this.removeChildren();
  3202. this.expanded = false;
  3203. this.lazy = true;
  3204. this.children = undefined;
  3205. this.renderStatus();
  3206. },
  3207. /** Schedule activity for delayed execution (cancel any pending request).
  3208. * scheduleAction('cancel') will only cancel a pending request (if any).
  3209. * @param {string} mode
  3210. * @param {number} ms
  3211. */
  3212. scheduleAction: function (mode, ms) {
  3213. if (this.tree.timer) {
  3214. clearTimeout(this.tree.timer);
  3215. this.tree.debug("clearTimeout(%o)", this.tree.timer);
  3216. }
  3217. this.tree.timer = null;
  3218. var self = this; // required for closures
  3219. switch (mode) {
  3220. case "cancel":
  3221. // Simply made sure that timer was cleared
  3222. break;
  3223. case "expand":
  3224. this.tree.timer = setTimeout(function () {
  3225. self.tree.debug("setTimeout: trigger expand");
  3226. self.setExpanded(true);
  3227. }, ms);
  3228. break;
  3229. case "activate":
  3230. this.tree.timer = setTimeout(function () {
  3231. self.tree.debug("setTimeout: trigger activate");
  3232. self.setActive(true);
  3233. }, ms);
  3234. break;
  3235. default:
  3236. $.error("Invalid mode " + mode);
  3237. }
  3238. // this.tree.debug("setTimeout(%s, %s): %s", mode, ms, this.tree.timer);
  3239. },
  3240. /**
  3241. *
  3242. * @param {boolean | PlainObject} [effects=false] animation options.
  3243. * @param {object} [options=null] {topNode: null, effects: ..., parent: ...} this node will remain visible in
  3244. * any case, even if `this` is outside the scroll pane.
  3245. * @returns {$.Promise}
  3246. */
  3247. scrollIntoView: function (effects, options) {
  3248. if (options !== undefined && _isNode(options)) {
  3249. throw Error(
  3250. "scrollIntoView() with 'topNode' option is deprecated since 2014-05-08. Use 'options.topNode' instead."
  3251. );
  3252. }
  3253. // The scroll parent is typically the plain tree's <UL> container.
  3254. // For ext-table, we choose the nearest parent that has `position: relative`
  3255. // and `overflow` set.
  3256. // (This default can be overridden by the local or global `scrollParent` option.)
  3257. var opts = $.extend(
  3258. {
  3259. effects:
  3260. effects === true
  3261. ? { duration: 200, queue: false }
  3262. : effects,
  3263. scrollOfs: this.tree.options.scrollOfs,
  3264. scrollParent: this.tree.options.scrollParent,
  3265. topNode: null,
  3266. },
  3267. options
  3268. ),
  3269. $scrollParent = opts.scrollParent,
  3270. $container = this.tree.$container,
  3271. overflowY = $container.css("overflow-y");
  3272. if (!$scrollParent) {
  3273. if (this.tree.tbody) {
  3274. $scrollParent = $container.scrollParent();
  3275. } else if (overflowY === "scroll" || overflowY === "auto") {
  3276. $scrollParent = $container;
  3277. } else {
  3278. // #922 plain tree in a non-fixed-sized UL scrolls inside its parent
  3279. $scrollParent = $container.scrollParent();
  3280. }
  3281. } else if (!$scrollParent.jquery) {
  3282. // Make sure we have a jQuery object
  3283. $scrollParent = $($scrollParent);
  3284. }
  3285. if (
  3286. $scrollParent[0] === document ||
  3287. $scrollParent[0] === document.body
  3288. ) {
  3289. // `document` may be returned by $().scrollParent(), if nothing is found,
  3290. // but would not work: (see #894)
  3291. this.debug(
  3292. "scrollIntoView(): normalizing scrollParent to 'window':",
  3293. $scrollParent[0]
  3294. );
  3295. $scrollParent = $(window);
  3296. }
  3297. // eslint-disable-next-line one-var
  3298. var topNodeY,
  3299. nodeY,
  3300. horzScrollbarHeight,
  3301. containerOffsetTop,
  3302. dfd = new $.Deferred(),
  3303. self = this,
  3304. nodeHeight = $(this.span).height(),
  3305. topOfs = opts.scrollOfs.top || 0,
  3306. bottomOfs = opts.scrollOfs.bottom || 0,
  3307. containerHeight = $scrollParent.height(),
  3308. scrollTop = $scrollParent.scrollTop(),
  3309. $animateTarget = $scrollParent,
  3310. isParentWindow = $scrollParent[0] === window,
  3311. topNode = opts.topNode || null,
  3312. newScrollTop = null;
  3313. // this.debug("scrollIntoView(), scrollTop=" + scrollTop, opts.scrollOfs);
  3314. // _assert($(this.span).is(":visible"), "scrollIntoView node is invisible"); // otherwise we cannot calc offsets
  3315. if (this.isRootNode() || !this.isVisible()) {
  3316. // We cannot calc offsets for hidden elements
  3317. this.info("scrollIntoView(): node is invisible.");
  3318. return _getResolvedPromise();
  3319. }
  3320. if (isParentWindow) {
  3321. nodeY = $(this.span).offset().top;
  3322. topNodeY =
  3323. topNode && topNode.span ? $(topNode.span).offset().top : 0;
  3324. $animateTarget = $("html,body");
  3325. } else {
  3326. _assert(
  3327. $scrollParent[0] !== document &&
  3328. $scrollParent[0] !== document.body,
  3329. "scrollParent should be a simple element or `window`, not document or body."
  3330. );
  3331. containerOffsetTop = $scrollParent.offset().top;
  3332. nodeY =
  3333. $(this.span).offset().top - containerOffsetTop + scrollTop; // relative to scroll parent
  3334. topNodeY = topNode
  3335. ? $(topNode.span).offset().top -
  3336. containerOffsetTop +
  3337. scrollTop
  3338. : 0;
  3339. horzScrollbarHeight = Math.max(
  3340. 0,
  3341. $scrollParent.innerHeight() - $scrollParent[0].clientHeight
  3342. );
  3343. containerHeight -= horzScrollbarHeight;
  3344. }
  3345. // this.debug(" scrollIntoView(), nodeY=" + nodeY + ", containerHeight=" + containerHeight);
  3346. if (nodeY < scrollTop + topOfs) {
  3347. // Node is above visible container area
  3348. newScrollTop = nodeY - topOfs;
  3349. // this.debug(" scrollIntoView(), UPPER newScrollTop=" + newScrollTop);
  3350. } else if (
  3351. nodeY + nodeHeight >
  3352. scrollTop + containerHeight - bottomOfs
  3353. ) {
  3354. newScrollTop = nodeY + nodeHeight - containerHeight + bottomOfs;
  3355. // this.debug(" scrollIntoView(), LOWER newScrollTop=" + newScrollTop);
  3356. // If a topNode was passed, make sure that it is never scrolled
  3357. // outside the upper border
  3358. if (topNode) {
  3359. _assert(
  3360. topNode.isRootNode() || topNode.isVisible(),
  3361. "topNode must be visible"
  3362. );
  3363. if (topNodeY < newScrollTop) {
  3364. newScrollTop = topNodeY - topOfs;
  3365. // this.debug(" scrollIntoView(), TOP newScrollTop=" + newScrollTop);
  3366. }
  3367. }
  3368. }
  3369. if (newScrollTop === null) {
  3370. dfd.resolveWith(this);
  3371. } else {
  3372. // this.debug(" scrollIntoView(), SET newScrollTop=" + newScrollTop);
  3373. if (opts.effects) {
  3374. opts.effects.complete = function () {
  3375. dfd.resolveWith(self);
  3376. };
  3377. $animateTarget.stop(true).animate(
  3378. {
  3379. scrollTop: newScrollTop,
  3380. },
  3381. opts.effects
  3382. );
  3383. } else {
  3384. $animateTarget[0].scrollTop = newScrollTop;
  3385. dfd.resolveWith(this);
  3386. }
  3387. }
  3388. return dfd.promise();
  3389. },
  3390. /**Activate this node.
  3391. *
  3392. * The `cell` option requires the ext-table and ext-ariagrid extensions.
  3393. *
  3394. * @param {boolean} [flag=true] pass false to deactivate
  3395. * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false, cell: null}
  3396. * @returns {$.Promise}
  3397. */
  3398. setActive: function (flag, opts) {
  3399. return this.tree._callHook("nodeSetActive", this, flag, opts);
  3400. },
  3401. /**Expand or collapse this node. Promise is resolved, when lazy loading and animations are done.
  3402. * @param {boolean} [flag=true] pass false to collapse
  3403. * @param {object} [opts] additional options. Defaults to {noAnimation: false, noEvents: false}
  3404. * @returns {$.Promise}
  3405. */
  3406. setExpanded: function (flag, opts) {
  3407. return this.tree._callHook("nodeSetExpanded", this, flag, opts);
  3408. },
  3409. /**Set keyboard focus to this node.
  3410. * @param {boolean} [flag=true] pass false to blur
  3411. * @see Fancytree#setFocus
  3412. */
  3413. setFocus: function (flag) {
  3414. return this.tree._callHook("nodeSetFocus", this, flag);
  3415. },
  3416. /**Select this node, i.e. check the checkbox.
  3417. * @param {boolean} [flag=true] pass false to deselect
  3418. * @param {object} [opts] additional options. Defaults to {noEvents: false, p
  3419. * propagateDown: null, propagateUp: null, callback: null }
  3420. */
  3421. setSelected: function (flag, opts) {
  3422. return this.tree._callHook("nodeSetSelected", this, flag, opts);
  3423. },
  3424. /**Mark a lazy node as 'error', 'loading', 'nodata', or 'ok'.
  3425. * @param {string} status 'error'|'loading'|'nodata'|'ok'
  3426. * @param {string} [message]
  3427. * @param {string} [details]
  3428. */
  3429. setStatus: function (status, message, details) {
  3430. return this.tree._callHook(
  3431. "nodeSetStatus",
  3432. this,
  3433. status,
  3434. message,
  3435. details
  3436. );
  3437. },
  3438. /**Rename this node.
  3439. * @param {string} title
  3440. */
  3441. setTitle: function (title) {
  3442. this.title = title;
  3443. this.renderTitle();
  3444. this.triggerModify("rename");
  3445. },
  3446. /**Sort child list by title.
  3447. * @param {function} [cmp] custom compare function(a, b) that returns -1, 0, or 1 (defaults to sort by title).
  3448. * @param {boolean} [deep=false] pass true to sort all descendant nodes
  3449. */
  3450. sortChildren: function (cmp, deep) {
  3451. var i,
  3452. l,
  3453. cl = this.children;
  3454. if (!cl) {
  3455. return;
  3456. }
  3457. cmp =
  3458. cmp ||
  3459. function (a, b) {
  3460. var x = a.title.toLowerCase(),
  3461. y = b.title.toLowerCase();
  3462. // eslint-disable-next-line no-nested-ternary
  3463. return x === y ? 0 : x > y ? 1 : -1;
  3464. };
  3465. cl.sort(cmp);
  3466. if (deep) {
  3467. for (i = 0, l = cl.length; i < l; i++) {
  3468. if (cl[i].children) {
  3469. cl[i].sortChildren(cmp, "$norender$");
  3470. }
  3471. }
  3472. }
  3473. if (deep !== "$norender$") {
  3474. this.render();
  3475. }
  3476. this.triggerModifyChild("sort");
  3477. },
  3478. /** Convert node (or whole branch) into a plain object.
  3479. *
  3480. * The result is compatible with node.addChildren().
  3481. *
  3482. * @param {boolean} [recursive=false] include child nodes
  3483. * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications.
  3484. * Return `false` to ignore this node or `"skip"` to include this node without its children.
  3485. * @returns {NodeData}
  3486. */
  3487. toDict: function (recursive, callback) {
  3488. var i,
  3489. l,
  3490. node,
  3491. res,
  3492. dict = {},
  3493. self = this;
  3494. $.each(NODE_ATTRS, function (i, a) {
  3495. if (self[a] || self[a] === false) {
  3496. dict[a] = self[a];
  3497. }
  3498. });
  3499. if (!$.isEmptyObject(this.data)) {
  3500. dict.data = $.extend({}, this.data);
  3501. if ($.isEmptyObject(dict.data)) {
  3502. delete dict.data;
  3503. }
  3504. }
  3505. if (callback) {
  3506. res = callback(dict, self);
  3507. if (res === false) {
  3508. return false; // Don't include this node nor its children
  3509. }
  3510. if (res === "skip") {
  3511. recursive = false; // Include this node, but not the children
  3512. }
  3513. }
  3514. if (recursive) {
  3515. if (_isArray(this.children)) {
  3516. dict.children = [];
  3517. for (i = 0, l = this.children.length; i < l; i++) {
  3518. node = this.children[i];
  3519. if (!node.isStatusNode()) {
  3520. res = node.toDict(true, callback);
  3521. if (res !== false) {
  3522. dict.children.push(res);
  3523. }
  3524. }
  3525. }
  3526. }
  3527. }
  3528. return dict;
  3529. },
  3530. /**
  3531. * Set, clear, or toggle class of node's span tag and .extraClasses.
  3532. *
  3533. * @param {string} className class name (separate multiple classes by space)
  3534. * @param {boolean} [flag] true/false to add/remove class. If omitted, class is toggled.
  3535. * @returns {boolean} true if a class was added
  3536. *
  3537. * @since 2.17
  3538. */
  3539. toggleClass: function (value, flag) {
  3540. var className,
  3541. hasClass,
  3542. rnotwhite = /\S+/g,
  3543. classNames = value.match(rnotwhite) || [],
  3544. i = 0,
  3545. wasAdded = false,
  3546. statusElem = this[this.tree.statusClassPropName],
  3547. curClasses = " " + (this.extraClasses || "") + " ";
  3548. // this.info("toggleClass('" + value + "', " + flag + ")", curClasses);
  3549. // Modify DOM element directly if it already exists
  3550. if (statusElem) {
  3551. $(statusElem).toggleClass(value, flag);
  3552. }
  3553. // Modify node.extraClasses to make this change persistent
  3554. // Toggle if flag was not passed
  3555. while ((className = classNames[i++])) {
  3556. hasClass = curClasses.indexOf(" " + className + " ") >= 0;
  3557. flag = flag === undefined ? !hasClass : !!flag;
  3558. if (flag) {
  3559. if (!hasClass) {
  3560. curClasses += className + " ";
  3561. wasAdded = true;
  3562. }
  3563. } else {
  3564. while (curClasses.indexOf(" " + className + " ") > -1) {
  3565. curClasses = curClasses.replace(
  3566. " " + className + " ",
  3567. " "
  3568. );
  3569. }
  3570. }
  3571. }
  3572. this.extraClasses = _trim(curClasses);
  3573. // this.info("-> toggleClass('" + value + "', " + flag + "): '" + this.extraClasses + "'");
  3574. return wasAdded;
  3575. },
  3576. /** Flip expanded status. */
  3577. toggleExpanded: function () {
  3578. return this.tree._callHook("nodeToggleExpanded", this);
  3579. },
  3580. /** Flip selection status. */
  3581. toggleSelected: function () {
  3582. return this.tree._callHook("nodeToggleSelected", this);
  3583. },
  3584. toString: function () {
  3585. return "FancytreeNode@" + this.key + "[title='" + this.title + "']";
  3586. // return "<FancytreeNode(#" + this.key + ", '" + this.title + "')>";
  3587. },
  3588. /**
  3589. * Trigger `modifyChild` event on a parent to signal that a child was modified.
  3590. * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
  3591. * @param {FancytreeNode} [childNode]
  3592. * @param {object} [extra]
  3593. */
  3594. triggerModifyChild: function (operation, childNode, extra) {
  3595. var data,
  3596. modifyChild = this.tree.options.modifyChild;
  3597. if (modifyChild) {
  3598. if (childNode && childNode.parent !== this) {
  3599. $.error(
  3600. "childNode " + childNode + " is not a child of " + this
  3601. );
  3602. }
  3603. data = {
  3604. node: this,
  3605. tree: this.tree,
  3606. operation: operation,
  3607. childNode: childNode || null,
  3608. };
  3609. if (extra) {
  3610. $.extend(data, extra);
  3611. }
  3612. modifyChild({ type: "modifyChild" }, data);
  3613. }
  3614. },
  3615. /**
  3616. * Trigger `modifyChild` event on node.parent(!).
  3617. * @param {string} operation Type of change: 'add', 'remove', 'rename', 'move', 'data', ...
  3618. * @param {object} [extra]
  3619. */
  3620. triggerModify: function (operation, extra) {
  3621. this.parent.triggerModifyChild(operation, this, extra);
  3622. },
  3623. /** Call fn(node) for all child nodes in hierarchical order (depth-first).<br>
  3624. * Stop iteration, if fn() returns false. Skip current branch, if fn() returns "skip".<br>
  3625. * Return false if iteration was stopped.
  3626. *
  3627. * @param {function} fn the callback function.
  3628. * Return false to stop iteration, return "skip" to skip this node and
  3629. * its children only.
  3630. * @param {boolean} [includeSelf=false]
  3631. * @returns {boolean}
  3632. */
  3633. visit: function (fn, includeSelf) {
  3634. var i,
  3635. l,
  3636. res = true,
  3637. children = this.children;
  3638. if (includeSelf === true) {
  3639. res = fn(this);
  3640. if (res === false || res === "skip") {
  3641. return res;
  3642. }
  3643. }
  3644. if (children) {
  3645. for (i = 0, l = children.length; i < l; i++) {
  3646. res = children[i].visit(fn, true);
  3647. if (res === false) {
  3648. break;
  3649. }
  3650. }
  3651. }
  3652. return res;
  3653. },
  3654. /** Call fn(node) for all child nodes and recursively load lazy children.<br>
  3655. * <b>Note:</b> If you need this method, you probably should consider to review
  3656. * your architecture! Recursivley loading nodes is a perfect way for lazy
  3657. * programmers to flood the server with requests ;-)
  3658. *
  3659. * @param {function} [fn] optional callback function.
  3660. * Return false to stop iteration, return "skip" to skip this node and
  3661. * its children only.
  3662. * @param {boolean} [includeSelf=false]
  3663. * @returns {$.Promise}
  3664. * @since 2.4
  3665. */
  3666. visitAndLoad: function (fn, includeSelf, _recursion) {
  3667. var dfd,
  3668. res,
  3669. loaders,
  3670. node = this;
  3671. // node.debug("visitAndLoad");
  3672. if (fn && includeSelf === true) {
  3673. res = fn(node);
  3674. if (res === false || res === "skip") {
  3675. return _recursion ? res : _getResolvedPromise();
  3676. }
  3677. }
  3678. if (!node.children && !node.lazy) {
  3679. return _getResolvedPromise();
  3680. }
  3681. dfd = new $.Deferred();
  3682. loaders = [];
  3683. // node.debug("load()...");
  3684. node.load().done(function () {
  3685. // node.debug("load()... done.");
  3686. for (var i = 0, l = node.children.length; i < l; i++) {
  3687. res = node.children[i].visitAndLoad(fn, true, true);
  3688. if (res === false) {
  3689. dfd.reject();
  3690. break;
  3691. } else if (res !== "skip") {
  3692. loaders.push(res); // Add promise to the list
  3693. }
  3694. }
  3695. $.when.apply(this, loaders).then(function () {
  3696. dfd.resolve();
  3697. });
  3698. });
  3699. return dfd.promise();
  3700. },
  3701. /** Call fn(node) for all parent nodes, bottom-up, including invisible system root.<br>
  3702. * Stop iteration, if fn() returns false.<br>
  3703. * Return false if iteration was stopped.
  3704. *
  3705. * @param {function} fn the callback function.
  3706. * Return false to stop iteration, return "skip" to skip this node and children only.
  3707. * @param {boolean} [includeSelf=false]
  3708. * @returns {boolean}
  3709. */
  3710. visitParents: function (fn, includeSelf) {
  3711. // Visit parent nodes (bottom up)
  3712. if (includeSelf && fn(this) === false) {
  3713. return false;
  3714. }
  3715. var p = this.parent;
  3716. while (p) {
  3717. if (fn(p) === false) {
  3718. return false;
  3719. }
  3720. p = p.parent;
  3721. }
  3722. return true;
  3723. },
  3724. /** Call fn(node) for all sibling nodes.<br>
  3725. * Stop iteration, if fn() returns false.<br>
  3726. * Return false if iteration was stopped.
  3727. *
  3728. * @param {function} fn the callback function.
  3729. * Return false to stop iteration.
  3730. * @param {boolean} [includeSelf=false]
  3731. * @returns {boolean}
  3732. */
  3733. visitSiblings: function (fn, includeSelf) {
  3734. var i,
  3735. l,
  3736. n,
  3737. ac = this.parent.children;
  3738. for (i = 0, l = ac.length; i < l; i++) {
  3739. n = ac[i];
  3740. if (includeSelf || n !== this) {
  3741. if (fn(n) === false) {
  3742. return false;
  3743. }
  3744. }
  3745. }
  3746. return true;
  3747. },
  3748. /** Write warning to browser console if debugLevel >= 2 (prepending node info)
  3749. *
  3750. * @param {*} msg string or object or array of such
  3751. */
  3752. warn: function (msg) {
  3753. if (this.tree.options.debugLevel >= 2) {
  3754. Array.prototype.unshift.call(arguments, this.toString());
  3755. consoleApply("warn", arguments);
  3756. }
  3757. },
  3758. };
  3759. /******************************************************************************
  3760. * Fancytree
  3761. */
  3762. /**
  3763. * Construct a new tree object.
  3764. *
  3765. * @class Fancytree
  3766. * @classdesc The controller behind a fancytree.
  3767. * This class also contains 'hook methods': see {@link Fancytree_Hooks}.
  3768. *
  3769. * @param {Widget} widget
  3770. *
  3771. * @property {string} _id Automatically generated unique tree instance ID, e.g. "1".
  3772. * @property {string} _ns Automatically generated unique tree namespace, e.g. ".fancytree-1".
  3773. * @property {FancytreeNode} activeNode Currently active node or null.
  3774. * @property {string} ariaPropName Property name of FancytreeNode that contains the element which will receive the aria attributes.
  3775. * Typically "li", but "tr" for table extension.
  3776. * @property {jQueryObject} $container Outer `<ul>` element (or `<table>` element for ext-table).
  3777. * @property {jQueryObject} $div A jQuery object containing the element used to instantiate the tree widget (`widget.element`)
  3778. * @property {object|array} columns Recommended place to store shared column meta data. @since 2.27
  3779. * @property {object} data Metadata, i.e. properties that may be passed to `source` in addition to a children array.
  3780. * @property {object} ext Hash of all active plugin instances.
  3781. * @property {FancytreeNode} focusNode Currently focused node or null.
  3782. * @property {FancytreeNode} lastSelectedNode Used to implement selectMode 1 (single select)
  3783. * @property {string} nodeContainerAttrName Property name of FancytreeNode that contains the outer element of single nodes.
  3784. * Typically "li", but "tr" for table extension.
  3785. * @property {FancytreeOptions} options Current options, i.e. default options + options passed to constructor.
  3786. * @property {FancytreeNode} rootNode Invisible system root node.
  3787. * @property {string} statusClassPropName Property name of FancytreeNode that contains the element which will receive the status classes.
  3788. * Typically "span", but "tr" for table extension.
  3789. * @property {object} types Map for shared type specific meta data, used with node.type attribute. @since 2.27
  3790. * @property {object} viewport See ext-vieport. @since v2.31
  3791. * @property {object} widget Base widget instance.
  3792. */
  3793. function Fancytree(widget) {
  3794. this.widget = widget;
  3795. this.$div = widget.element;
  3796. this.options = widget.options;
  3797. if (this.options) {
  3798. if (this.options.lazyload !== undefined) {
  3799. $.error(
  3800. "The 'lazyload' event is deprecated since 2014-02-25. Use 'lazyLoad' (with uppercase L) instead."
  3801. );
  3802. }
  3803. if (this.options.loaderror !== undefined) {
  3804. $.error(
  3805. "The 'loaderror' event was renamed since 2014-07-03. Use 'loadError' (with uppercase E) instead."
  3806. );
  3807. }
  3808. if (this.options.fx !== undefined) {
  3809. $.error(
  3810. "The 'fx' option was replaced by 'toggleEffect' since 2014-11-30."
  3811. );
  3812. }
  3813. if (this.options.removeNode !== undefined) {
  3814. $.error(
  3815. "The 'removeNode' event was replaced by 'modifyChild' since 2.20 (2016-09-10)."
  3816. );
  3817. }
  3818. }
  3819. this.ext = {}; // Active extension instances
  3820. this.types = {};
  3821. this.columns = {};
  3822. // allow to init tree.data.foo from <div data-foo=''>
  3823. this.data = _getElementDataAsDict(this.$div);
  3824. // TODO: use widget.uuid instead?
  3825. this._id = "" + (this.options.treeId || $.ui.fancytree._nextId++);
  3826. // TODO: use widget.eventNamespace instead?
  3827. this._ns = ".fancytree-" + this._id; // append for namespaced events
  3828. this.activeNode = null;
  3829. this.focusNode = null;
  3830. this._hasFocus = null;
  3831. this._tempCache = {};
  3832. this._lastMousedownNode = null;
  3833. this._enableUpdate = true;
  3834. this.lastSelectedNode = null;
  3835. this.systemFocusElement = null;
  3836. this.lastQuicksearchTerm = "";
  3837. this.lastQuicksearchTime = 0;
  3838. this.viewport = null; // ext-grid
  3839. this.statusClassPropName = "span";
  3840. this.ariaPropName = "li";
  3841. this.nodeContainerAttrName = "li";
  3842. // Remove previous markup if any
  3843. this.$div.find(">ul.fancytree-container").remove();
  3844. // Create a node without parent.
  3845. var fakeParent = { tree: this },
  3846. $ul;
  3847. this.rootNode = new FancytreeNode(fakeParent, {
  3848. title: "root",
  3849. key: "root_" + this._id,
  3850. children: null,
  3851. expanded: true,
  3852. });
  3853. this.rootNode.parent = null;
  3854. // Create root markup
  3855. $ul = $("<ul>", {
  3856. id: "ft-id-" + this._id,
  3857. class: "ui-fancytree fancytree-container fancytree-plain",
  3858. }).appendTo(this.$div);
  3859. this.$container = $ul;
  3860. this.rootNode.ul = $ul[0];
  3861. if (this.options.debugLevel == null) {
  3862. this.options.debugLevel = FT.debugLevel;
  3863. }
  3864. // // Add container to the TAB chain
  3865. // // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant
  3866. // // #577: Allow to set tabindex to "0", "-1" and ""
  3867. // this.$container.attr("tabindex", this.options.tabindex);
  3868. // if( this.options.rtl ) {
  3869. // this.$container.attr("DIR", "RTL").addClass("fancytree-rtl");
  3870. // // }else{
  3871. // // this.$container.attr("DIR", null).removeClass("fancytree-rtl");
  3872. // }
  3873. // if(this.options.aria){
  3874. // this.$container.attr("role", "tree");
  3875. // if( this.options.selectMode !== 1 ) {
  3876. // this.$container.attr("aria-multiselectable", true);
  3877. // }
  3878. // }
  3879. }
  3880. Fancytree.prototype = /** @lends Fancytree# */ {
  3881. /* Return a context object that can be re-used for _callHook().
  3882. * @param {Fancytree | FancytreeNode | EventData} obj
  3883. * @param {Event} originalEvent
  3884. * @param {Object} extra
  3885. * @returns {EventData}
  3886. */
  3887. _makeHookContext: function (obj, originalEvent, extra) {
  3888. var ctx, tree;
  3889. if (obj.node !== undefined) {
  3890. // obj is already a context object
  3891. if (originalEvent && obj.originalEvent !== originalEvent) {
  3892. $.error("invalid args");
  3893. }
  3894. ctx = obj;
  3895. } else if (obj.tree) {
  3896. // obj is a FancytreeNode
  3897. tree = obj.tree;
  3898. ctx = {
  3899. node: obj,
  3900. tree: tree,
  3901. widget: tree.widget,
  3902. options: tree.widget.options,
  3903. originalEvent: originalEvent,
  3904. typeInfo: tree.types[obj.type] || {},
  3905. };
  3906. } else if (obj.widget) {
  3907. // obj is a Fancytree
  3908. ctx = {
  3909. node: null,
  3910. tree: obj,
  3911. widget: obj.widget,
  3912. options: obj.widget.options,
  3913. originalEvent: originalEvent,
  3914. };
  3915. } else {
  3916. $.error("invalid args");
  3917. }
  3918. if (extra) {
  3919. $.extend(ctx, extra);
  3920. }
  3921. return ctx;
  3922. },
  3923. /* Trigger a hook function: funcName(ctx, [...]).
  3924. *
  3925. * @param {string} funcName
  3926. * @param {Fancytree|FancytreeNode|EventData} contextObject
  3927. * @param {any} [_extraArgs] optional additional arguments
  3928. * @returns {any}
  3929. */
  3930. _callHook: function (funcName, contextObject, _extraArgs) {
  3931. var ctx = this._makeHookContext(contextObject),
  3932. fn = this[funcName],
  3933. args = Array.prototype.slice.call(arguments, 2);
  3934. if (!_isFunction(fn)) {
  3935. $.error("_callHook('" + funcName + "') is not a function");
  3936. }
  3937. args.unshift(ctx);
  3938. // this.debug("_hook", funcName, ctx.node && ctx.node.toString() || ctx.tree.toString(), args);
  3939. return fn.apply(this, args);
  3940. },
  3941. _setExpiringValue: function (key, value, ms) {
  3942. this._tempCache[key] = {
  3943. value: value,
  3944. expire: Date.now() + (+ms || 50),
  3945. };
  3946. },
  3947. _getExpiringValue: function (key) {
  3948. var entry = this._tempCache[key];
  3949. if (entry && entry.expire > Date.now()) {
  3950. return entry.value;
  3951. }
  3952. delete this._tempCache[key];
  3953. return null;
  3954. },
  3955. /* Check if this tree has extension `name` enabled.
  3956. *
  3957. * @param {string} name name of the required extension
  3958. */
  3959. _usesExtension: function (name) {
  3960. return $.inArray(name, this.options.extensions) >= 0;
  3961. },
  3962. /* Check if current extensions dependencies are met and throw an error if not.
  3963. *
  3964. * This method may be called inside the `treeInit` hook for custom extensions.
  3965. *
  3966. * @param {string} name name of the required extension
  3967. * @param {boolean} [required=true] pass `false` if the extension is optional, but we want to check for order if it is present
  3968. * @param {boolean} [before] `true` if `name` must be included before this, `false` otherwise (use `null` if order doesn't matter)
  3969. * @param {string} [message] optional error message (defaults to a descriptve error message)
  3970. */
  3971. _requireExtension: function (name, required, before, message) {
  3972. if (before != null) {
  3973. before = !!before;
  3974. }
  3975. var thisName = this._local.name,
  3976. extList = this.options.extensions,
  3977. isBefore =
  3978. $.inArray(name, extList) < $.inArray(thisName, extList),
  3979. isMissing = required && this.ext[name] == null,
  3980. badOrder = !isMissing && before != null && before !== isBefore;
  3981. _assert(
  3982. thisName && thisName !== name,
  3983. "invalid or same name '" + thisName + "' (require yourself?)"
  3984. );
  3985. if (isMissing || badOrder) {
  3986. if (!message) {
  3987. if (isMissing || required) {
  3988. message =
  3989. "'" +
  3990. thisName +
  3991. "' extension requires '" +
  3992. name +
  3993. "'";
  3994. if (badOrder) {
  3995. message +=
  3996. " to be registered " +
  3997. (before ? "before" : "after") +
  3998. " itself";
  3999. }
  4000. } else {
  4001. message =
  4002. "If used together, `" +
  4003. name +
  4004. "` must be registered " +
  4005. (before ? "before" : "after") +
  4006. " `" +
  4007. thisName +
  4008. "`";
  4009. }
  4010. }
  4011. $.error(message);
  4012. return false;
  4013. }
  4014. return true;
  4015. },
  4016. /** Activate node with a given key and fire focus and activate events.
  4017. *
  4018. * A previously activated node will be deactivated.
  4019. * If activeVisible option is set, all parents will be expanded as necessary.
  4020. * Pass key = false, to deactivate the current node only.
  4021. * @param {string} key
  4022. * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false}
  4023. * @returns {FancytreeNode} activated node (null, if not found)
  4024. */
  4025. activateKey: function (key, opts) {
  4026. var node = this.getNodeByKey(key);
  4027. if (node) {
  4028. node.setActive(true, opts);
  4029. } else if (this.activeNode) {
  4030. this.activeNode.setActive(false, opts);
  4031. }
  4032. return node;
  4033. },
  4034. /** (experimental) Add child status nodes that indicate 'More...', ....
  4035. * @param {boolean|object} node optional node definition. Pass `false` to remove all paging nodes.
  4036. * @param {string} [mode='append'] 'child'|firstChild'
  4037. * @since 2.15
  4038. */
  4039. addPagingNode: function (node, mode) {
  4040. return this.rootNode.addPagingNode(node, mode);
  4041. },
  4042. /**
  4043. * (experimental) Apply a modification (or navigation) operation.
  4044. *
  4045. * Valid commands:
  4046. * - 'moveUp', 'moveDown'
  4047. * - 'indent', 'outdent'
  4048. * - 'remove'
  4049. * - 'edit', 'addChild', 'addSibling': (reqires ext-edit extension)
  4050. * - 'cut', 'copy', 'paste': (use an internal singleton 'clipboard')
  4051. * - 'down', 'first', 'last', 'left', 'parent', 'right', 'up': navigate
  4052. *
  4053. * @param {string} cmd
  4054. * @param {FancytreeNode} [node=active_node]
  4055. * @param {object} [opts] Currently unused
  4056. *
  4057. * @since 2.32
  4058. */
  4059. applyCommand: function (cmd, node, opts_) {
  4060. var // clipboard,
  4061. refNode;
  4062. // opts = $.extend(
  4063. // { setActive: true, clipboard: CLIPBOARD },
  4064. // opts_
  4065. // );
  4066. node = node || this.getActiveNode();
  4067. // clipboard = opts.clipboard;
  4068. switch (cmd) {
  4069. // Sorting and indentation:
  4070. case "moveUp":
  4071. refNode = node.getPrevSibling();
  4072. if (refNode) {
  4073. node.moveTo(refNode, "before");
  4074. node.setActive();
  4075. }
  4076. break;
  4077. case "moveDown":
  4078. refNode = node.getNextSibling();
  4079. if (refNode) {
  4080. node.moveTo(refNode, "after");
  4081. node.setActive();
  4082. }
  4083. break;
  4084. case "indent":
  4085. refNode = node.getPrevSibling();
  4086. if (refNode) {
  4087. node.moveTo(refNode, "child");
  4088. refNode.setExpanded();
  4089. node.setActive();
  4090. }
  4091. break;
  4092. case "outdent":
  4093. if (!node.isTopLevel()) {
  4094. node.moveTo(node.getParent(), "after");
  4095. node.setActive();
  4096. }
  4097. break;
  4098. // Remove:
  4099. case "remove":
  4100. refNode = node.getPrevSibling() || node.getParent();
  4101. node.remove();
  4102. if (refNode) {
  4103. refNode.setActive();
  4104. }
  4105. break;
  4106. // Add, edit (requires ext-edit):
  4107. case "addChild":
  4108. node.editCreateNode("child", "");
  4109. break;
  4110. case "addSibling":
  4111. node.editCreateNode("after", "");
  4112. break;
  4113. case "rename":
  4114. node.editStart();
  4115. break;
  4116. // Simple clipboard simulation:
  4117. // case "cut":
  4118. // clipboard = { mode: cmd, data: node };
  4119. // break;
  4120. // case "copy":
  4121. // clipboard = {
  4122. // mode: cmd,
  4123. // data: node.toDict(function(d, n) {
  4124. // delete d.key;
  4125. // }),
  4126. // };
  4127. // break;
  4128. // case "clear":
  4129. // clipboard = null;
  4130. // break;
  4131. // case "paste":
  4132. // if (clipboard.mode === "cut") {
  4133. // // refNode = node.getPrevSibling();
  4134. // clipboard.data.moveTo(node, "child");
  4135. // clipboard.data.setActive();
  4136. // } else if (clipboard.mode === "copy") {
  4137. // node.addChildren(clipboard.data).setActive();
  4138. // }
  4139. // break;
  4140. // Navigation commands:
  4141. case "down":
  4142. case "first":
  4143. case "last":
  4144. case "left":
  4145. case "parent":
  4146. case "right":
  4147. case "up":
  4148. return node.navigate(cmd);
  4149. default:
  4150. $.error("Unhandled command: '" + cmd + "'");
  4151. }
  4152. },
  4153. /** (experimental) Modify existing data model.
  4154. *
  4155. * @param {Array} patchList array of [key, NodePatch] arrays
  4156. * @returns {$.Promise} resolved, when all patches have been applied
  4157. * @see TreePatch
  4158. */
  4159. applyPatch: function (patchList) {
  4160. var dfd,
  4161. i,
  4162. p2,
  4163. key,
  4164. patch,
  4165. node,
  4166. patchCount = patchList.length,
  4167. deferredList = [];
  4168. for (i = 0; i < patchCount; i++) {
  4169. p2 = patchList[i];
  4170. _assert(
  4171. p2.length === 2,
  4172. "patchList must be an array of length-2-arrays"
  4173. );
  4174. key = p2[0];
  4175. patch = p2[1];
  4176. node = key === null ? this.rootNode : this.getNodeByKey(key);
  4177. if (node) {
  4178. dfd = new $.Deferred();
  4179. deferredList.push(dfd);
  4180. node.applyPatch(patch).always(_makeResolveFunc(dfd, node));
  4181. } else {
  4182. this.warn("could not find node with key '" + key + "'");
  4183. }
  4184. }
  4185. // Return a promise that is resolved, when ALL patches were applied
  4186. return $.when.apply($, deferredList).promise();
  4187. },
  4188. /* TODO: implement in dnd extension
  4189. cancelDrag: function() {
  4190. var dd = $.ui.ddmanager.current;
  4191. if(dd){
  4192. dd.cancel();
  4193. }
  4194. },
  4195. */
  4196. /** Remove all nodes.
  4197. * @since 2.14
  4198. */
  4199. clear: function (source) {
  4200. this._callHook("treeClear", this);
  4201. },
  4202. /** Return the number of nodes.
  4203. * @returns {integer}
  4204. */
  4205. count: function () {
  4206. return this.rootNode.countChildren();
  4207. },
  4208. /** Write to browser console if debugLevel >= 4 (prepending tree name)
  4209. *
  4210. * @param {*} msg string or object or array of such
  4211. */
  4212. debug: function (msg) {
  4213. if (this.options.debugLevel >= 4) {
  4214. Array.prototype.unshift.call(arguments, this.toString());
  4215. consoleApply("log", arguments);
  4216. }
  4217. },
  4218. /** Destroy this widget, restore previous markup and cleanup resources.
  4219. *
  4220. * @since 2.34
  4221. */
  4222. destroy: function () {
  4223. this.widget.destroy();
  4224. },
  4225. /** Enable (or disable) the tree control.
  4226. *
  4227. * @param {boolean} [flag=true] pass false to disable
  4228. * @since 2.30
  4229. */
  4230. enable: function (flag) {
  4231. if (flag === false) {
  4232. this.widget.disable();
  4233. } else {
  4234. this.widget.enable();
  4235. }
  4236. },
  4237. /** Temporarily suppress rendering to improve performance on bulk-updates.
  4238. *
  4239. * @param {boolean} flag
  4240. * @returns {boolean} previous status
  4241. * @since 2.19
  4242. */
  4243. enableUpdate: function (flag) {
  4244. flag = flag !== false;
  4245. if (!!this._enableUpdate === !!flag) {
  4246. return flag;
  4247. }
  4248. this._enableUpdate = flag;
  4249. if (flag) {
  4250. this.debug("enableUpdate(true): redraw "); //, this._dirtyRoots);
  4251. this._callHook("treeStructureChanged", this, "enableUpdate");
  4252. this.render();
  4253. } else {
  4254. // this._dirtyRoots = null;
  4255. this.debug("enableUpdate(false)...");
  4256. }
  4257. return !flag; // return previous value
  4258. },
  4259. /** Write error to browser console if debugLevel >= 1 (prepending tree info)
  4260. *
  4261. * @param {*} msg string or object or array of such
  4262. */
  4263. error: function (msg) {
  4264. if (this.options.debugLevel >= 1) {
  4265. Array.prototype.unshift.call(arguments, this.toString());
  4266. consoleApply("error", arguments);
  4267. }
  4268. },
  4269. /** Expand (or collapse) all parent nodes.
  4270. *
  4271. * This convenience method uses `tree.visit()` and `tree.setExpanded()`
  4272. * internally.
  4273. *
  4274. * @param {boolean} [flag=true] pass false to collapse
  4275. * @param {object} [opts] passed to setExpanded()
  4276. * @since 2.30
  4277. */
  4278. expandAll: function (flag, opts) {
  4279. var prev = this.enableUpdate(false);
  4280. flag = flag !== false;
  4281. this.visit(function (node) {
  4282. if (
  4283. node.hasChildren() !== false &&
  4284. node.isExpanded() !== flag
  4285. ) {
  4286. node.setExpanded(flag, opts);
  4287. }
  4288. });
  4289. this.enableUpdate(prev);
  4290. },
  4291. /**Find all nodes that matches condition.
  4292. *
  4293. * @param {string | function(node)} match title string to search for, or a
  4294. * callback function that returns `true` if a node is matched.
  4295. * @returns {FancytreeNode[]} array of nodes (may be empty)
  4296. * @see FancytreeNode#findAll
  4297. * @since 2.12
  4298. */
  4299. findAll: function (match) {
  4300. return this.rootNode.findAll(match);
  4301. },
  4302. /**Find first node that matches condition.
  4303. *
  4304. * @param {string | function(node)} match title string to search for, or a
  4305. * callback function that returns `true` if a node is matched.
  4306. * @returns {FancytreeNode} matching node or null
  4307. * @see FancytreeNode#findFirst
  4308. * @since 2.12
  4309. */
  4310. findFirst: function (match) {
  4311. return this.rootNode.findFirst(match);
  4312. },
  4313. /** Find the next visible node that starts with `match`, starting at `startNode`
  4314. * and wrap-around at the end.
  4315. *
  4316. * @param {string|function} match
  4317. * @param {FancytreeNode} [startNode] defaults to first node
  4318. * @returns {FancytreeNode} matching node or null
  4319. */
  4320. findNextNode: function (match, startNode) {
  4321. //, visibleOnly) {
  4322. var res = null,
  4323. firstNode = this.getFirstChild();
  4324. match =
  4325. typeof match === "string"
  4326. ? _makeNodeTitleStartMatcher(match)
  4327. : match;
  4328. startNode = startNode || firstNode;
  4329. function _checkNode(n) {
  4330. // console.log("_check " + n)
  4331. if (match(n)) {
  4332. res = n;
  4333. }
  4334. if (res || n === startNode) {
  4335. return false;
  4336. }
  4337. }
  4338. this.visitRows(_checkNode, {
  4339. start: startNode,
  4340. includeSelf: false,
  4341. });
  4342. // Wrap around search
  4343. if (!res && startNode !== firstNode) {
  4344. this.visitRows(_checkNode, {
  4345. start: firstNode,
  4346. includeSelf: true,
  4347. });
  4348. }
  4349. return res;
  4350. },
  4351. /** Find a node relative to another node.
  4352. *
  4353. * @param {FancytreeNode} node
  4354. * @param {string|number} where 'down', 'first', 'last', 'left', 'parent', 'right', or 'up'.
  4355. * (Alternatively the keyCode that would normally trigger this move,
  4356. * e.g. `$.ui.keyCode.LEFT` = 'left'.
  4357. * @param {boolean} [includeHidden=false] Not yet implemented
  4358. * @returns {FancytreeNode|null}
  4359. * @since v2.31
  4360. */
  4361. findRelatedNode: function (node, where, includeHidden) {
  4362. var res = null,
  4363. KC = $.ui.keyCode;
  4364. switch (where) {
  4365. case "parent":
  4366. case KC.BACKSPACE:
  4367. if (node.parent && node.parent.parent) {
  4368. res = node.parent;
  4369. }
  4370. break;
  4371. case "first":
  4372. case KC.HOME:
  4373. // First visible node
  4374. this.visit(function (n) {
  4375. if (n.isVisible()) {
  4376. res = n;
  4377. return false;
  4378. }
  4379. });
  4380. break;
  4381. case "last":
  4382. case KC.END:
  4383. this.visit(function (n) {
  4384. // last visible node
  4385. if (n.isVisible()) {
  4386. res = n;
  4387. }
  4388. });
  4389. break;
  4390. case "left":
  4391. case KC.LEFT:
  4392. if (node.expanded) {
  4393. node.setExpanded(false);
  4394. } else if (node.parent && node.parent.parent) {
  4395. res = node.parent;
  4396. }
  4397. break;
  4398. case "right":
  4399. case KC.RIGHT:
  4400. if (!node.expanded && (node.children || node.lazy)) {
  4401. node.setExpanded();
  4402. res = node;
  4403. } else if (node.children && node.children.length) {
  4404. res = node.children[0];
  4405. }
  4406. break;
  4407. case "up":
  4408. case KC.UP:
  4409. this.visitRows(
  4410. function (n) {
  4411. res = n;
  4412. return false;
  4413. },
  4414. { start: node, reverse: true, includeSelf: false }
  4415. );
  4416. break;
  4417. case "down":
  4418. case KC.DOWN:
  4419. this.visitRows(
  4420. function (n) {
  4421. res = n;
  4422. return false;
  4423. },
  4424. { start: node, includeSelf: false }
  4425. );
  4426. break;
  4427. default:
  4428. this.tree.warn("Unknown relation '" + where + "'.");
  4429. }
  4430. return res;
  4431. },
  4432. // TODO: fromDict
  4433. /**
  4434. * Generate INPUT elements that can be submitted with html forms.
  4435. *
  4436. * In selectMode 3 only the topmost selected nodes are considered, unless
  4437. * `opts.stopOnParents: false` is passed.
  4438. *
  4439. * @example
  4440. * // Generate input elements for active and selected nodes
  4441. * tree.generateFormElements();
  4442. * // Generate input elements selected nodes, using a custom `name` attribute
  4443. * tree.generateFormElements("cust_sel", false);
  4444. * // Generate input elements using a custom filter
  4445. * tree.generateFormElements(true, true, { filter: function(node) {
  4446. * return node.isSelected() && node.data.yes;
  4447. * }});
  4448. *
  4449. * @param {boolean | string} [selected=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID[]')
  4450. * @param {boolean | string} [active=true] Pass false to disable, pass a string to override the field name (default: 'ft_ID_active')
  4451. * @param {object} [opts] default { filter: null, stopOnParents: true }
  4452. */
  4453. generateFormElements: function (selected, active, opts) {
  4454. opts = opts || {};
  4455. var nodeList,
  4456. selectedName =
  4457. typeof selected === "string"
  4458. ? selected
  4459. : "ft_" + this._id + "[]",
  4460. activeName =
  4461. typeof active === "string"
  4462. ? active
  4463. : "ft_" + this._id + "_active",
  4464. id = "fancytree_result_" + this._id,
  4465. $result = $("#" + id),
  4466. stopOnParents =
  4467. this.options.selectMode === 3 &&
  4468. opts.stopOnParents !== false;
  4469. if ($result.length) {
  4470. $result.empty();
  4471. } else {
  4472. $result = $("<div>", {
  4473. id: id,
  4474. })
  4475. .hide()
  4476. .insertAfter(this.$container);
  4477. }
  4478. if (active !== false && this.activeNode) {
  4479. $result.append(
  4480. $("<input>", {
  4481. type: "radio",
  4482. name: activeName,
  4483. value: this.activeNode.key,
  4484. checked: true,
  4485. })
  4486. );
  4487. }
  4488. function _appender(node) {
  4489. $result.append(
  4490. $("<input>", {
  4491. type: "checkbox",
  4492. name: selectedName,
  4493. value: node.key,
  4494. checked: true,
  4495. })
  4496. );
  4497. }
  4498. if (opts.filter) {
  4499. this.visit(function (node) {
  4500. var res = opts.filter(node);
  4501. if (res === "skip") {
  4502. return res;
  4503. }
  4504. if (res !== false) {
  4505. _appender(node);
  4506. }
  4507. });
  4508. } else if (selected !== false) {
  4509. nodeList = this.getSelectedNodes(stopOnParents);
  4510. $.each(nodeList, function (idx, node) {
  4511. _appender(node);
  4512. });
  4513. }
  4514. },
  4515. /**
  4516. * Return the currently active node or null.
  4517. * @returns {FancytreeNode}
  4518. */
  4519. getActiveNode: function () {
  4520. return this.activeNode;
  4521. },
  4522. /** Return the first top level node if any (not the invisible root node).
  4523. * @returns {FancytreeNode | null}
  4524. */
  4525. getFirstChild: function () {
  4526. return this.rootNode.getFirstChild();
  4527. },
  4528. /**
  4529. * Return node that has keyboard focus or null.
  4530. * @returns {FancytreeNode}
  4531. */
  4532. getFocusNode: function () {
  4533. return this.focusNode;
  4534. },
  4535. /**
  4536. * Return current option value.
  4537. * (Note: this is the preferred variant of `$().fancytree("option", "KEY")`)
  4538. *
  4539. * @param {string} name option name (may contain '.')
  4540. * @returns {any}
  4541. */
  4542. getOption: function (optionName) {
  4543. return this.widget.option(optionName);
  4544. },
  4545. /**
  4546. * Return node with a given key or null if not found.
  4547. *
  4548. * @param {string} key
  4549. * @param {FancytreeNode} [searchRoot] only search below this node
  4550. * @returns {FancytreeNode | null}
  4551. */
  4552. getNodeByKey: function (key, searchRoot) {
  4553. // Search the DOM by element ID (assuming this is faster than traversing all nodes).
  4554. var el, match;
  4555. // TODO: use tree.keyMap if available
  4556. // TODO: check opts.generateIds === true
  4557. if (!searchRoot) {
  4558. el = document.getElementById(this.options.idPrefix + key);
  4559. if (el) {
  4560. return el.ftnode ? el.ftnode : null;
  4561. }
  4562. }
  4563. // Not found in the DOM, but still may be in an unrendered part of tree
  4564. searchRoot = searchRoot || this.rootNode;
  4565. match = null;
  4566. key = "" + key; // Convert to string (#1005)
  4567. searchRoot.visit(function (node) {
  4568. if (node.key === key) {
  4569. match = node;
  4570. return false; // Stop iteration
  4571. }
  4572. }, true);
  4573. return match;
  4574. },
  4575. /** Return the invisible system root node.
  4576. * @returns {FancytreeNode}
  4577. */
  4578. getRootNode: function () {
  4579. return this.rootNode;
  4580. },
  4581. /**
  4582. * Return an array of selected nodes.
  4583. *
  4584. * Note: you cannot send this result via Ajax directly. Instead the
  4585. * node object need to be converted to plain objects, for example
  4586. * by using `$.map()` and `node.toDict()`.
  4587. * @param {boolean} [stopOnParents=false] only return the topmost selected
  4588. * node (useful with selectMode 3)
  4589. * @returns {FancytreeNode[]}
  4590. */
  4591. getSelectedNodes: function (stopOnParents) {
  4592. return this.rootNode.getSelectedNodes(stopOnParents);
  4593. },
  4594. /** Return true if the tree control has keyboard focus
  4595. * @returns {boolean}
  4596. */
  4597. hasFocus: function () {
  4598. // var ae = document.activeElement,
  4599. // hasFocus = !!(
  4600. // ae && $(ae).closest(".fancytree-container").length
  4601. // );
  4602. // if (hasFocus !== !!this._hasFocus) {
  4603. // this.warn(
  4604. // "hasFocus(): fix inconsistent container state, now: " +
  4605. // hasFocus
  4606. // );
  4607. // this._hasFocus = hasFocus;
  4608. // this.$container.toggleClass("fancytree-treefocus", hasFocus);
  4609. // }
  4610. // return hasFocus;
  4611. return !!this._hasFocus;
  4612. },
  4613. /** Write to browser console if debugLevel >= 3 (prepending tree name)
  4614. * @param {*} msg string or object or array of such
  4615. */
  4616. info: function (msg) {
  4617. if (this.options.debugLevel >= 3) {
  4618. Array.prototype.unshift.call(arguments, this.toString());
  4619. consoleApply("info", arguments);
  4620. }
  4621. },
  4622. /** Return true if any node is currently beeing loaded, i.e. a Ajax request is pending.
  4623. * @returns {boolean}
  4624. * @since 2.32
  4625. */
  4626. isLoading: function () {
  4627. var res = false;
  4628. this.rootNode.visit(function (n) {
  4629. // also visit rootNode
  4630. if (n._isLoading || n._requestId) {
  4631. res = true;
  4632. return false;
  4633. }
  4634. }, true);
  4635. return res;
  4636. },
  4637. /*
  4638. TODO: isInitializing: function() {
  4639. return ( this.phase=="init" || this.phase=="postInit" );
  4640. },
  4641. TODO: isReloading: function() {
  4642. return ( this.phase=="init" || this.phase=="postInit" ) && this.options.persist && this.persistence.cookiesFound;
  4643. },
  4644. TODO: isUserEvent: function() {
  4645. return ( this.phase=="userEvent" );
  4646. },
  4647. */
  4648. /**
  4649. * Make sure that a node with a given ID is loaded, by traversing - and
  4650. * loading - its parents. This method is meant for lazy hierarchies.
  4651. * A callback is executed for every node as we go.
  4652. * @example
  4653. * // Resolve using node.key:
  4654. * tree.loadKeyPath("/_3/_23/_26/_27", function(node, status){
  4655. * if(status === "loaded") {
  4656. * console.log("loaded intermediate node " + node);
  4657. * }else if(status === "ok") {
  4658. * node.activate();
  4659. * }
  4660. * });
  4661. * // Use deferred promise:
  4662. * tree.loadKeyPath("/_3/_23/_26/_27").progress(function(data){
  4663. * if(data.status === "loaded") {
  4664. * console.log("loaded intermediate node " + data.node);
  4665. * }else if(data.status === "ok") {
  4666. * node.activate();
  4667. * }
  4668. * }).done(function(){
  4669. * ...
  4670. * });
  4671. * // Custom path segment resolver:
  4672. * tree.loadKeyPath("/321/431/21/2", {
  4673. * matchKey: function(node, key){
  4674. * return node.data.refKey === key;
  4675. * },
  4676. * callback: function(node, status){
  4677. * if(status === "loaded") {
  4678. * console.log("loaded intermediate node " + node);
  4679. * }else if(status === "ok") {
  4680. * node.activate();
  4681. * }
  4682. * }
  4683. * });
  4684. * @param {string | string[]} keyPathList one or more key paths (e.g. '/3/2_1/7')
  4685. * @param {function | object} optsOrCallback callback(node, status) is called for every visited node ('loading', 'loaded', 'ok', 'error').
  4686. * Pass an object to define custom key matchers for the path segments: {callback: function, matchKey: function}.
  4687. * @returns {$.Promise}
  4688. */
  4689. loadKeyPath: function (keyPathList, optsOrCallback) {
  4690. var callback,
  4691. i,
  4692. path,
  4693. self = this,
  4694. dfd = new $.Deferred(),
  4695. parent = this.getRootNode(),
  4696. sep = this.options.keyPathSeparator,
  4697. pathSegList = [],
  4698. opts = $.extend({}, optsOrCallback);
  4699. // Prepare options
  4700. if (typeof optsOrCallback === "function") {
  4701. callback = optsOrCallback;
  4702. } else if (optsOrCallback && optsOrCallback.callback) {
  4703. callback = optsOrCallback.callback;
  4704. }
  4705. opts.callback = function (ctx, node, status) {
  4706. if (callback) {
  4707. callback.call(ctx, node, status);
  4708. }
  4709. dfd.notifyWith(ctx, [{ node: node, status: status }]);
  4710. };
  4711. if (opts.matchKey == null) {
  4712. opts.matchKey = function (node, key) {
  4713. return node.key === key;
  4714. };
  4715. }
  4716. // Convert array of path strings to array of segment arrays
  4717. if (!_isArray(keyPathList)) {
  4718. keyPathList = [keyPathList];
  4719. }
  4720. for (i = 0; i < keyPathList.length; i++) {
  4721. path = keyPathList[i];
  4722. // strip leading slash
  4723. if (path.charAt(0) === sep) {
  4724. path = path.substr(1);
  4725. }
  4726. // segListMap[path] = { parent: parent, segList: path.split(sep) };
  4727. pathSegList.push(path.split(sep));
  4728. // targetList.push({ parent: parent, segList: path.split(sep)/* , path: path*/});
  4729. }
  4730. // The timeout forces async behavior always (even if nodes are all loaded)
  4731. // This way a potential progress() event will fire.
  4732. setTimeout(function () {
  4733. self._loadKeyPathImpl(dfd, opts, parent, pathSegList).done(
  4734. function () {
  4735. dfd.resolve();
  4736. }
  4737. );
  4738. }, 0);
  4739. return dfd.promise();
  4740. },
  4741. /*
  4742. * Resolve a list of paths, relative to one parent node.
  4743. */
  4744. _loadKeyPathImpl: function (dfd, opts, parent, pathSegList) {
  4745. var deferredList,
  4746. i,
  4747. key,
  4748. node,
  4749. nodeKey,
  4750. remain,
  4751. remainMap,
  4752. tmpParent,
  4753. segList,
  4754. subDfd,
  4755. self = this;
  4756. function __findChild(parent, key) {
  4757. // console.log("__findChild", key, parent);
  4758. var i,
  4759. l,
  4760. cl = parent.children;
  4761. if (cl) {
  4762. for (i = 0, l = cl.length; i < l; i++) {
  4763. if (opts.matchKey(cl[i], key)) {
  4764. return cl[i];
  4765. }
  4766. }
  4767. }
  4768. return null;
  4769. }
  4770. // console.log("_loadKeyPathImpl, parent=", parent, ", pathSegList=", pathSegList);
  4771. // Pass 1:
  4772. // Handle all path segments for nodes that are already loaded.
  4773. // Collect distinct top-most lazy nodes in a map.
  4774. // Note that we can use node.key to de-dupe entries, even if a custom matcher would
  4775. // look for other node attributes.
  4776. // map[node.key] => {node: node, pathList: [list of remaining rest-paths]}
  4777. remainMap = {};
  4778. for (i = 0; i < pathSegList.length; i++) {
  4779. segList = pathSegList[i];
  4780. // target = targetList[i];
  4781. // Traverse and pop path segments (i.e. keys), until we hit a lazy, unloaded node
  4782. tmpParent = parent;
  4783. while (segList.length) {
  4784. key = segList.shift();
  4785. node = __findChild(tmpParent, key);
  4786. if (!node) {
  4787. this.warn(
  4788. "loadKeyPath: key not found: " +
  4789. key +
  4790. " (parent: " +
  4791. tmpParent +
  4792. ")"
  4793. );
  4794. opts.callback(this, key, "error");
  4795. break;
  4796. } else if (segList.length === 0) {
  4797. opts.callback(this, node, "ok");
  4798. break;
  4799. } else if (!node.lazy || node.hasChildren() !== undefined) {
  4800. opts.callback(this, node, "loaded");
  4801. tmpParent = node;
  4802. } else {
  4803. opts.callback(this, node, "loaded");
  4804. key = node.key; //target.segList.join(sep);
  4805. if (remainMap[key]) {
  4806. remainMap[key].pathSegList.push(segList);
  4807. } else {
  4808. remainMap[key] = {
  4809. parent: node,
  4810. pathSegList: [segList],
  4811. };
  4812. }
  4813. break;
  4814. }
  4815. }
  4816. }
  4817. // console.log("_loadKeyPathImpl AFTER pass 1, remainMap=", remainMap);
  4818. // Now load all lazy nodes and continue iteration for remaining paths
  4819. deferredList = [];
  4820. // Avoid jshint warning 'Don't make functions within a loop.':
  4821. function __lazyload(dfd, parent, pathSegList) {
  4822. // console.log("__lazyload", parent, "pathSegList=", pathSegList);
  4823. opts.callback(self, parent, "loading");
  4824. parent
  4825. .load()
  4826. .done(function () {
  4827. self._loadKeyPathImpl
  4828. .call(self, dfd, opts, parent, pathSegList)
  4829. .always(_makeResolveFunc(dfd, self));
  4830. })
  4831. .fail(function (errMsg) {
  4832. self.warn("loadKeyPath: error loading lazy " + parent);
  4833. opts.callback(self, node, "error");
  4834. dfd.rejectWith(self);
  4835. });
  4836. }
  4837. // remainMap contains parent nodes, each with a list of relative sub-paths.
  4838. // We start loading all of them now, and pass the the list to each loader.
  4839. for (nodeKey in remainMap) {
  4840. if (_hasProp(remainMap, nodeKey)) {
  4841. remain = remainMap[nodeKey];
  4842. // console.log("for(): remain=", remain, "remainMap=", remainMap);
  4843. // key = remain.segList.shift();
  4844. // node = __findChild(remain.parent, key);
  4845. // if (node == null) { // #576
  4846. // // Issue #576, refactored for v2.27:
  4847. // // The root cause was, that sometimes the wrong parent was used here
  4848. // // to find the next segment.
  4849. // // Falling back to getNodeByKey() was a hack that no longer works if a custom
  4850. // // matcher is used, because we cannot assume that a single segment-key is unique
  4851. // // throughout the tree.
  4852. // self.error("loadKeyPath: error loading child by key '" + key + "' (parent: " + target.parent + ")", target);
  4853. // // node = self.getNodeByKey(key);
  4854. // continue;
  4855. // }
  4856. subDfd = new $.Deferred();
  4857. deferredList.push(subDfd);
  4858. __lazyload(subDfd, remain.parent, remain.pathSegList);
  4859. }
  4860. }
  4861. // Return a promise that is resolved, when ALL paths were loaded
  4862. return $.when.apply($, deferredList).promise();
  4863. },
  4864. /** Re-fire beforeActivate, activate, and (optional) focus events.
  4865. * Calling this method in the `init` event, will activate the node that
  4866. * was marked 'active' in the source data, and optionally set the keyboard
  4867. * focus.
  4868. * @param [setFocus=false]
  4869. */
  4870. reactivate: function (setFocus) {
  4871. var res,
  4872. node = this.activeNode;
  4873. if (!node) {
  4874. return _getResolvedPromise();
  4875. }
  4876. this.activeNode = null; // Force re-activating
  4877. res = node.setActive(true, { noFocus: true });
  4878. if (setFocus) {
  4879. node.setFocus();
  4880. }
  4881. return res;
  4882. },
  4883. /** Reload tree from source and return a promise.
  4884. * @param [source] optional new source (defaults to initial source data)
  4885. * @returns {$.Promise}
  4886. */
  4887. reload: function (source) {
  4888. this._callHook("treeClear", this);
  4889. return this._callHook("treeLoad", this, source);
  4890. },
  4891. /**Render tree (i.e. create DOM elements for all top-level nodes).
  4892. * @param {boolean} [force=false] create DOM elemnts, even if parent is collapsed
  4893. * @param {boolean} [deep=false]
  4894. */
  4895. render: function (force, deep) {
  4896. return this.rootNode.render(force, deep);
  4897. },
  4898. /**(De)select all nodes.
  4899. * @param {boolean} [flag=true]
  4900. * @since 2.28
  4901. */
  4902. selectAll: function (flag) {
  4903. this.visit(function (node) {
  4904. node.setSelected(flag);
  4905. });
  4906. },
  4907. // TODO: selectKey: function(key, select)
  4908. // TODO: serializeArray: function(stopOnParents)
  4909. /**
  4910. * @param {boolean} [flag=true]
  4911. */
  4912. setFocus: function (flag) {
  4913. return this._callHook("treeSetFocus", this, flag);
  4914. },
  4915. /**
  4916. * Set current option value.
  4917. * (Note: this is the preferred variant of `$().fancytree("option", "KEY", VALUE)`)
  4918. * @param {string} name option name (may contain '.')
  4919. * @param {any} new value
  4920. */
  4921. setOption: function (optionName, value) {
  4922. return this.widget.option(optionName, value);
  4923. },
  4924. /**
  4925. * Call console.time() when in debug mode (verbose >= 4).
  4926. *
  4927. * @param {string} label
  4928. */
  4929. debugTime: function (label) {
  4930. if (this.options.debugLevel >= 4) {
  4931. window.console.time(this + " - " + label);
  4932. }
  4933. },
  4934. /**
  4935. * Call console.timeEnd() when in debug mode (verbose >= 4).
  4936. *
  4937. * @param {string} label
  4938. */
  4939. debugTimeEnd: function (label) {
  4940. if (this.options.debugLevel >= 4) {
  4941. window.console.timeEnd(this + " - " + label);
  4942. }
  4943. },
  4944. /**
  4945. * Return all nodes as nested list of {@link NodeData}.
  4946. *
  4947. * @param {boolean} [includeRoot=false] Returns the hidden system root node (and its children)
  4948. * @param {function} [callback] callback(dict, node) is called for every node, in order to allow modifications.
  4949. * Return `false` to ignore this node or "skip" to include this node without its children.
  4950. * @returns {Array | object}
  4951. * @see FancytreeNode#toDict
  4952. */
  4953. toDict: function (includeRoot, callback) {
  4954. var res = this.rootNode.toDict(true, callback);
  4955. return includeRoot ? res : res.children;
  4956. },
  4957. /* Implicitly called for string conversions.
  4958. * @returns {string}
  4959. */
  4960. toString: function () {
  4961. return "Fancytree@" + this._id;
  4962. // return "<Fancytree(#" + this._id + ")>";
  4963. },
  4964. /* _trigger a widget event with additional node ctx.
  4965. * @see EventData
  4966. */
  4967. _triggerNodeEvent: function (type, node, originalEvent, extra) {
  4968. // this.debug("_trigger(" + type + "): '" + ctx.node.title + "'", ctx);
  4969. var ctx = this._makeHookContext(node, originalEvent, extra),
  4970. res = this.widget._trigger(type, originalEvent, ctx);
  4971. if (res !== false && ctx.result !== undefined) {
  4972. return ctx.result;
  4973. }
  4974. return res;
  4975. },
  4976. /* _trigger a widget event with additional tree data. */
  4977. _triggerTreeEvent: function (type, originalEvent, extra) {
  4978. // this.debug("_trigger(" + type + ")", ctx);
  4979. var ctx = this._makeHookContext(this, originalEvent, extra),
  4980. res = this.widget._trigger(type, originalEvent, ctx);
  4981. if (res !== false && ctx.result !== undefined) {
  4982. return ctx.result;
  4983. }
  4984. return res;
  4985. },
  4986. /** Call fn(node) for all nodes in hierarchical order (depth-first).
  4987. *
  4988. * @param {function} fn the callback function.
  4989. * Return false to stop iteration, return "skip" to skip this node and children only.
  4990. * @returns {boolean} false, if the iterator was stopped.
  4991. */
  4992. visit: function (fn) {
  4993. return this.rootNode.visit(fn, false);
  4994. },
  4995. /** Call fn(node) for all nodes in vertical order, top down (or bottom up).<br>
  4996. * Stop iteration, if fn() returns false.<br>
  4997. * Return false if iteration was stopped.
  4998. *
  4999. * @param {function} fn the callback function.
  5000. * Return false to stop iteration, return "skip" to skip this node and children only.
  5001. * @param {object} [options]
  5002. * Defaults:
  5003. * {start: First top node, reverse: false, includeSelf: true, includeHidden: false}
  5004. * @returns {boolean} false if iteration was cancelled
  5005. * @since 2.28
  5006. */
  5007. visitRows: function (fn, opts) {
  5008. if (!this.rootNode.hasChildren()) {
  5009. return false;
  5010. }
  5011. if (opts && opts.reverse) {
  5012. delete opts.reverse;
  5013. return this._visitRowsUp(fn, opts);
  5014. }
  5015. opts = opts || {};
  5016. var i,
  5017. nextIdx,
  5018. parent,
  5019. res,
  5020. siblings,
  5021. siblingOfs = 0,
  5022. skipFirstNode = opts.includeSelf === false,
  5023. includeHidden = !!opts.includeHidden,
  5024. checkFilter = !includeHidden && this.enableFilter,
  5025. node = opts.start || this.rootNode.children[0];
  5026. parent = node.parent;
  5027. while (parent) {
  5028. // visit siblings
  5029. siblings = parent.children;
  5030. nextIdx = siblings.indexOf(node) + siblingOfs;
  5031. _assert(
  5032. nextIdx >= 0,
  5033. "Could not find " +
  5034. node +
  5035. " in parent's children: " +
  5036. parent
  5037. );
  5038. for (i = nextIdx; i < siblings.length; i++) {
  5039. node = siblings[i];
  5040. if (checkFilter && !node.match && !node.subMatchCount) {
  5041. continue;
  5042. }
  5043. if (!skipFirstNode && fn(node) === false) {
  5044. return false;
  5045. }
  5046. skipFirstNode = false;
  5047. // Dive into node's child nodes
  5048. if (
  5049. node.children &&
  5050. node.children.length &&
  5051. (includeHidden || node.expanded)
  5052. ) {
  5053. // Disable warning: Functions declared within loops referencing an outer
  5054. // scoped variable may lead to confusing semantics:
  5055. /*jshint -W083 */
  5056. res = node.visit(function (n) {
  5057. if (checkFilter && !n.match && !n.subMatchCount) {
  5058. return "skip";
  5059. }
  5060. if (fn(n) === false) {
  5061. return false;
  5062. }
  5063. if (!includeHidden && n.children && !n.expanded) {
  5064. return "skip";
  5065. }
  5066. }, false);
  5067. /*jshint +W083 */
  5068. if (res === false) {
  5069. return false;
  5070. }
  5071. }
  5072. }
  5073. // Visit parent nodes (bottom up)
  5074. node = parent;
  5075. parent = parent.parent;
  5076. siblingOfs = 1; //
  5077. }
  5078. return true;
  5079. },
  5080. /* Call fn(node) for all nodes in vertical order, bottom up.
  5081. */
  5082. _visitRowsUp: function (fn, opts) {
  5083. var children,
  5084. idx,
  5085. parent,
  5086. includeHidden = !!opts.includeHidden,
  5087. node = opts.start || this.rootNode.children[0];
  5088. while (true) {
  5089. parent = node.parent;
  5090. children = parent.children;
  5091. if (children[0] === node) {
  5092. // If this is already the first sibling, goto parent
  5093. node = parent;
  5094. if (!node.parent) {
  5095. break; // first node of the tree
  5096. }
  5097. children = parent.children;
  5098. } else {
  5099. // Otherwise, goto prev. sibling
  5100. idx = children.indexOf(node);
  5101. node = children[idx - 1];
  5102. // If the prev. sibling has children, follow down to last descendant
  5103. while (
  5104. // See: https://github.com/eslint/eslint/issues/11302
  5105. // eslint-disable-next-line no-unmodified-loop-condition
  5106. (includeHidden || node.expanded) &&
  5107. node.children &&
  5108. node.children.length
  5109. ) {
  5110. children = node.children;
  5111. parent = node;
  5112. node = children[children.length - 1];
  5113. }
  5114. }
  5115. // Skip invisible
  5116. if (!includeHidden && !node.isVisible()) {
  5117. continue;
  5118. }
  5119. if (fn(node) === false) {
  5120. return false;
  5121. }
  5122. }
  5123. },
  5124. /** Write warning to browser console if debugLevel >= 2 (prepending tree info)
  5125. *
  5126. * @param {*} msg string or object or array of such
  5127. */
  5128. warn: function (msg) {
  5129. if (this.options.debugLevel >= 2) {
  5130. Array.prototype.unshift.call(arguments, this.toString());
  5131. consoleApply("warn", arguments);
  5132. }
  5133. },
  5134. };
  5135. /**
  5136. * These additional methods of the {@link Fancytree} class are 'hook functions'
  5137. * that can be used and overloaded by extensions.
  5138. *
  5139. * @see [writing extensions](https://github.com/mar10/fancytree/wiki/TutorialExtensions)
  5140. * @mixin Fancytree_Hooks
  5141. */
  5142. $.extend(
  5143. Fancytree.prototype,
  5144. /** @lends Fancytree_Hooks# */
  5145. {
  5146. /** Default handling for mouse click events.
  5147. *
  5148. * @param {EventData} ctx
  5149. */
  5150. nodeClick: function (ctx) {
  5151. var activate,
  5152. expand,
  5153. // event = ctx.originalEvent,
  5154. targetType = ctx.targetType,
  5155. node = ctx.node;
  5156. // this.debug("ftnode.onClick(" + event.type + "): ftnode:" + this + ", button:" + event.button + ", which: " + event.which, ctx);
  5157. // TODO: use switch
  5158. // TODO: make sure clicks on embedded <input> doesn't steal focus (see table sample)
  5159. if (targetType === "expander") {
  5160. if (node.isLoading()) {
  5161. // #495: we probably got a click event while a lazy load is pending.
  5162. // The 'expanded' state is not yet set, so 'toggle' would expand
  5163. // and trigger lazyLoad again.
  5164. // It would be better to allow to collapse/expand the status node
  5165. // while loading (instead of ignoring), but that would require some
  5166. // more work.
  5167. node.debug("Got 2nd click while loading: ignored");
  5168. return;
  5169. }
  5170. // Clicking the expander icon always expands/collapses
  5171. this._callHook("nodeToggleExpanded", ctx);
  5172. } else if (targetType === "checkbox") {
  5173. // Clicking the checkbox always (de)selects
  5174. this._callHook("nodeToggleSelected", ctx);
  5175. if (ctx.options.focusOnSelect) {
  5176. // #358
  5177. this._callHook("nodeSetFocus", ctx, true);
  5178. }
  5179. } else {
  5180. // Honor `clickFolderMode` for
  5181. expand = false;
  5182. activate = true;
  5183. if (node.folder) {
  5184. switch (ctx.options.clickFolderMode) {
  5185. case 2: // expand only
  5186. expand = true;
  5187. activate = false;
  5188. break;
  5189. case 3: // expand and activate
  5190. activate = true;
  5191. expand = true; //!node.isExpanded();
  5192. break;
  5193. // else 1 or 4: just activate
  5194. }
  5195. }
  5196. if (activate) {
  5197. this.nodeSetFocus(ctx);
  5198. this._callHook("nodeSetActive", ctx, true);
  5199. }
  5200. if (expand) {
  5201. if (!activate) {
  5202. // this._callHook("nodeSetFocus", ctx);
  5203. }
  5204. // this._callHook("nodeSetExpanded", ctx, true);
  5205. this._callHook("nodeToggleExpanded", ctx);
  5206. }
  5207. }
  5208. // Make sure that clicks stop, otherwise <a href='#'> jumps to the top
  5209. // if(event.target.localName === "a" && event.target.className === "fancytree-title"){
  5210. // event.preventDefault();
  5211. // }
  5212. // TODO: return promise?
  5213. },
  5214. /** Collapse all other children of same parent.
  5215. *
  5216. * @param {EventData} ctx
  5217. * @param {object} callOpts
  5218. */
  5219. nodeCollapseSiblings: function (ctx, callOpts) {
  5220. // TODO: return promise?
  5221. var ac,
  5222. i,
  5223. l,
  5224. node = ctx.node;
  5225. if (node.parent) {
  5226. ac = node.parent.children;
  5227. for (i = 0, l = ac.length; i < l; i++) {
  5228. if (ac[i] !== node && ac[i].expanded) {
  5229. this._callHook(
  5230. "nodeSetExpanded",
  5231. ac[i],
  5232. false,
  5233. callOpts
  5234. );
  5235. }
  5236. }
  5237. }
  5238. },
  5239. /** Default handling for mouse douleclick events.
  5240. * @param {EventData} ctx
  5241. */
  5242. nodeDblclick: function (ctx) {
  5243. // TODO: return promise?
  5244. if (
  5245. ctx.targetType === "title" &&
  5246. ctx.options.clickFolderMode === 4
  5247. ) {
  5248. // this.nodeSetFocus(ctx);
  5249. // this._callHook("nodeSetActive", ctx, true);
  5250. this._callHook("nodeToggleExpanded", ctx);
  5251. }
  5252. // TODO: prevent text selection on dblclicks
  5253. if (ctx.targetType === "title") {
  5254. ctx.originalEvent.preventDefault();
  5255. }
  5256. },
  5257. /** Default handling for mouse keydown events.
  5258. *
  5259. * NOTE: this may be called with node == null if tree (but no node) has focus.
  5260. * @param {EventData} ctx
  5261. */
  5262. nodeKeydown: function (ctx) {
  5263. // TODO: return promise?
  5264. var matchNode,
  5265. stamp,
  5266. _res,
  5267. focusNode,
  5268. event = ctx.originalEvent,
  5269. node = ctx.node,
  5270. tree = ctx.tree,
  5271. opts = ctx.options,
  5272. which = event.which,
  5273. // #909: Use event.key, to get unicode characters.
  5274. // We can't use `/\w/.test(key)`, because that would
  5275. // only detect plain ascii alpha-numerics. But we still need
  5276. // to ignore modifier-only, whitespace, cursor-keys, etc.
  5277. key = event.key || String.fromCharCode(which),
  5278. specialModifiers = !!(
  5279. event.altKey ||
  5280. event.ctrlKey ||
  5281. event.metaKey
  5282. ),
  5283. isAlnum =
  5284. !MODIFIERS[which] &&
  5285. !SPECIAL_KEYCODES[which] &&
  5286. !specialModifiers,
  5287. $target = $(event.target),
  5288. handled = true,
  5289. activate = !(event.ctrlKey || !opts.autoActivate);
  5290. // (node || FT).debug("ftnode.nodeKeydown(" + event.type + "): ftnode:" + this + ", charCode:" + event.charCode + ", keyCode: " + event.keyCode + ", which: " + event.which);
  5291. // FT.debug( "eventToString(): " + FT.eventToString(event) + ", key='" + key + "', isAlnum: " + isAlnum );
  5292. // Set focus to active (or first node) if no other node has the focus yet
  5293. if (!node) {
  5294. focusNode = this.getActiveNode() || this.getFirstChild();
  5295. if (focusNode) {
  5296. focusNode.setFocus();
  5297. node = ctx.node = this.focusNode;
  5298. node.debug("Keydown force focus on active node");
  5299. }
  5300. }
  5301. if (
  5302. opts.quicksearch &&
  5303. isAlnum &&
  5304. !$target.is(":input:enabled")
  5305. ) {
  5306. // Allow to search for longer streaks if typed in quickly
  5307. stamp = Date.now();
  5308. if (stamp - tree.lastQuicksearchTime > 500) {
  5309. tree.lastQuicksearchTerm = "";
  5310. }
  5311. tree.lastQuicksearchTime = stamp;
  5312. tree.lastQuicksearchTerm += key;
  5313. // tree.debug("quicksearch find", tree.lastQuicksearchTerm);
  5314. matchNode = tree.findNextNode(
  5315. tree.lastQuicksearchTerm,
  5316. tree.getActiveNode()
  5317. );
  5318. if (matchNode) {
  5319. matchNode.setActive();
  5320. }
  5321. event.preventDefault();
  5322. return;
  5323. }
  5324. switch (FT.eventToString(event)) {
  5325. case "+":
  5326. case "=": // 187: '+' @ Chrome, Safari
  5327. tree.nodeSetExpanded(ctx, true);
  5328. break;
  5329. case "-":
  5330. tree.nodeSetExpanded(ctx, false);
  5331. break;
  5332. case "space":
  5333. if (node.isPagingNode()) {
  5334. tree._triggerNodeEvent("clickPaging", ctx, event);
  5335. } else if (
  5336. FT.evalOption("checkbox", node, node, opts, false)
  5337. ) {
  5338. // #768
  5339. tree.nodeToggleSelected(ctx);
  5340. } else {
  5341. tree.nodeSetActive(ctx, true);
  5342. }
  5343. break;
  5344. case "return":
  5345. tree.nodeSetActive(ctx, true);
  5346. break;
  5347. case "home":
  5348. case "end":
  5349. case "backspace":
  5350. case "left":
  5351. case "right":
  5352. case "up":
  5353. case "down":
  5354. _res = node.navigate(event.which, activate);
  5355. break;
  5356. default:
  5357. handled = false;
  5358. }
  5359. if (handled) {
  5360. event.preventDefault();
  5361. }
  5362. },
  5363. // /** Default handling for mouse keypress events. */
  5364. // nodeKeypress: function(ctx) {
  5365. // var event = ctx.originalEvent;
  5366. // },
  5367. // /** Trigger lazyLoad event (async). */
  5368. // nodeLazyLoad: function(ctx) {
  5369. // var node = ctx.node;
  5370. // if(this._triggerNodeEvent())
  5371. // },
  5372. /** Load child nodes (async).
  5373. *
  5374. * @param {EventData} ctx
  5375. * @param {object[]|object|string|$.Promise|function} source
  5376. * @returns {$.Promise} The deferred will be resolved as soon as the (ajax)
  5377. * data was rendered.
  5378. */
  5379. nodeLoadChildren: function (ctx, source) {
  5380. var ajax,
  5381. delay,
  5382. ajaxDfd = null,
  5383. resultDfd,
  5384. isAsync = true,
  5385. tree = ctx.tree,
  5386. node = ctx.node,
  5387. nodePrevParent = node.parent,
  5388. tag = "nodeLoadChildren",
  5389. requestId = Date.now();
  5390. // `source` is a callback: use the returned result instead:
  5391. if (_isFunction(source)) {
  5392. source = source.call(tree, { type: "source" }, ctx);
  5393. _assert(
  5394. !_isFunction(source),
  5395. "source callback must not return another function"
  5396. );
  5397. }
  5398. // `source` is already a promise:
  5399. if (_isFunction(source.then)) {
  5400. // _assert(_isFunction(source.always), "Expected jQuery?");
  5401. ajaxDfd = source;
  5402. } else if (source.url) {
  5403. // `source` is an Ajax options object
  5404. ajax = $.extend({}, ctx.options.ajax, source);
  5405. if (ajax.debugDelay) {
  5406. // Simulate a slow server
  5407. delay = ajax.debugDelay;
  5408. delete ajax.debugDelay; // remove debug option
  5409. if (_isArray(delay)) {
  5410. // random delay range [min..max]
  5411. delay =
  5412. delay[0] +
  5413. Math.random() * (delay[1] - delay[0]);
  5414. }
  5415. node.warn(
  5416. "nodeLoadChildren waiting debugDelay " +
  5417. Math.round(delay) +
  5418. " ms ..."
  5419. );
  5420. ajaxDfd = $.Deferred(function (ajaxDfd) {
  5421. setTimeout(function () {
  5422. $.ajax(ajax)
  5423. .done(function () {
  5424. ajaxDfd.resolveWith(this, arguments);
  5425. })
  5426. .fail(function () {
  5427. ajaxDfd.rejectWith(this, arguments);
  5428. });
  5429. }, delay);
  5430. });
  5431. } else {
  5432. ajaxDfd = $.ajax(ajax);
  5433. }
  5434. } else if ($.isPlainObject(source) || _isArray(source)) {
  5435. // `source` is already a constant dict or list, but we convert
  5436. // to a thenable for unified processing.
  5437. // 2020-01-03: refactored.
  5438. // `ajaxDfd = $.when(source)` would do the trick, but the returned
  5439. // promise will resolve async, which broke some tests and
  5440. // would probably also break current implementations out there.
  5441. // So we mock-up a thenable that resolves synchronously:
  5442. ajaxDfd = {
  5443. then: function (resolve, reject) {
  5444. resolve(source, null, null);
  5445. },
  5446. };
  5447. isAsync = false;
  5448. } else {
  5449. $.error("Invalid source type: " + source);
  5450. }
  5451. // Check for overlapping requests
  5452. if (node._requestId) {
  5453. node.warn(
  5454. "Recursive load request #" +
  5455. requestId +
  5456. " while #" +
  5457. node._requestId +
  5458. " is pending."
  5459. );
  5460. node._requestId = requestId;
  5461. // node.debug("Send load request #" + requestId);
  5462. }
  5463. if (isAsync) {
  5464. tree.debugTime(tag);
  5465. tree.nodeSetStatus(ctx, "loading");
  5466. }
  5467. // The async Ajax request has now started...
  5468. // Defer the deferred:
  5469. // we want to be able to reject invalid responses, even if
  5470. // the raw HTTP Ajax XHR resolved as Ok.
  5471. // We use the ajaxDfd.then() syntax here, which is compatible with
  5472. // jQuery and ECMA6.
  5473. // However resultDfd is a jQuery deferred, which is currently the
  5474. // expected result type of nodeLoadChildren()
  5475. resultDfd = new $.Deferred();
  5476. ajaxDfd.then(
  5477. function (data, textStatus, jqXHR) {
  5478. // ajaxDfd was resolved, but we reject or resolve resultDfd
  5479. // depending on the response data
  5480. var errorObj, res;
  5481. if (
  5482. (source.dataType === "json" ||
  5483. source.dataType === "jsonp") &&
  5484. typeof data === "string"
  5485. ) {
  5486. $.error(
  5487. "Ajax request returned a string (did you get the JSON dataType wrong?)."
  5488. );
  5489. }
  5490. if (node._requestId && node._requestId > requestId) {
  5491. // The expected request time stamp is later than `requestId`
  5492. // (which was kept as as closure variable to this handler function)
  5493. // node.warn("Ignored load response for obsolete request #" + requestId + " (expected #" + node._requestId + ")");
  5494. resultDfd.rejectWith(this, [
  5495. RECURSIVE_REQUEST_ERROR,
  5496. ]);
  5497. return;
  5498. // } else {
  5499. // node.debug("Response returned for load request #" + requestId);
  5500. }
  5501. if (node.parent === null && nodePrevParent !== null) {
  5502. resultDfd.rejectWith(this, [
  5503. INVALID_REQUEST_TARGET_ERROR,
  5504. ]);
  5505. return;
  5506. }
  5507. // Allow to adjust the received response data in the `postProcess` event.
  5508. if (ctx.options.postProcess) {
  5509. // The handler may either
  5510. // - modify `ctx.response` in-place (and leave `ctx.result` undefined)
  5511. // => res = undefined
  5512. // - return a replacement in `ctx.result`
  5513. // => res = <new data>
  5514. // If res contains an `error` property, an error status is displayed
  5515. try {
  5516. res = tree._triggerNodeEvent(
  5517. "postProcess",
  5518. ctx,
  5519. ctx.originalEvent,
  5520. {
  5521. response: data,
  5522. error: null,
  5523. dataType: source.dataType,
  5524. }
  5525. );
  5526. if (res.error) {
  5527. tree.warn(
  5528. "postProcess returned error:",
  5529. res
  5530. );
  5531. }
  5532. } catch (e) {
  5533. res = {
  5534. error: e,
  5535. message: "" + e,
  5536. details: "postProcess failed",
  5537. };
  5538. }
  5539. if (res.error) {
  5540. // Either postProcess failed with an exception, or the returned
  5541. // result object has an 'error' property attached:
  5542. errorObj = $.isPlainObject(res.error)
  5543. ? res.error
  5544. : { message: res.error };
  5545. errorObj = tree._makeHookContext(
  5546. node,
  5547. null,
  5548. errorObj
  5549. );
  5550. resultDfd.rejectWith(this, [errorObj]);
  5551. return;
  5552. }
  5553. if (
  5554. _isArray(res) ||
  5555. ($.isPlainObject(res) && _isArray(res.children))
  5556. ) {
  5557. // Use `ctx.result` if valid
  5558. // (otherwise use existing data, which may have been modified in-place)
  5559. data = res;
  5560. }
  5561. } else if (
  5562. data &&
  5563. _hasProp(data, "d") &&
  5564. ctx.options.enableAspx
  5565. ) {
  5566. // Process ASPX WebMethod JSON object inside "d" property
  5567. // (only if no postProcess event was defined)
  5568. if (ctx.options.enableAspx === 42) {
  5569. tree.warn(
  5570. "The default for enableAspx will change to `false` in the fututure. " +
  5571. "Pass `enableAspx: true` or implement postProcess to silence this warning."
  5572. );
  5573. }
  5574. data =
  5575. typeof data.d === "string"
  5576. ? $.parseJSON(data.d)
  5577. : data.d;
  5578. }
  5579. resultDfd.resolveWith(this, [data]);
  5580. },
  5581. function (jqXHR, textStatus, errorThrown) {
  5582. // ajaxDfd was rejected, so we reject resultDfd as well
  5583. var errorObj = tree._makeHookContext(node, null, {
  5584. error: jqXHR,
  5585. args: Array.prototype.slice.call(arguments),
  5586. message: errorThrown,
  5587. details: jqXHR.status + ": " + errorThrown,
  5588. });
  5589. resultDfd.rejectWith(this, [errorObj]);
  5590. }
  5591. );
  5592. // The async Ajax request has now started.
  5593. // resultDfd will be resolved/rejected after the response arrived,
  5594. // was postProcessed, and checked.
  5595. // Now we implement the UI update and add the data to the tree.
  5596. // We also return this promise to the caller.
  5597. resultDfd
  5598. .done(function (data) {
  5599. tree.nodeSetStatus(ctx, "ok");
  5600. var children, metaData, noDataRes;
  5601. if ($.isPlainObject(data)) {
  5602. // We got {foo: 'abc', children: [...]}
  5603. // Copy extra properties to tree.data.foo
  5604. _assert(
  5605. node.isRootNode(),
  5606. "source may only be an object for root nodes (expecting an array of child objects otherwise)"
  5607. );
  5608. _assert(
  5609. _isArray(data.children),
  5610. "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')"
  5611. );
  5612. metaData = data;
  5613. children = data.children;
  5614. delete metaData.children;
  5615. // Copy some attributes to tree.data
  5616. $.each(TREE_ATTRS, function (i, attr) {
  5617. if (metaData[attr] !== undefined) {
  5618. tree[attr] = metaData[attr];
  5619. delete metaData[attr];
  5620. }
  5621. });
  5622. // Copy all other attributes to tree.data.NAME
  5623. $.extend(tree.data, metaData);
  5624. } else {
  5625. children = data;
  5626. }
  5627. _assert(
  5628. _isArray(children),
  5629. "expected array of children"
  5630. );
  5631. node._setChildren(children);
  5632. if (tree.options.nodata && children.length === 0) {
  5633. if (_isFunction(tree.options.nodata)) {
  5634. noDataRes = tree.options.nodata.call(
  5635. tree,
  5636. { type: "nodata" },
  5637. ctx
  5638. );
  5639. } else if (
  5640. tree.options.nodata === true &&
  5641. node.isRootNode()
  5642. ) {
  5643. noDataRes = tree.options.strings.noData;
  5644. } else if (
  5645. typeof tree.options.nodata === "string" &&
  5646. node.isRootNode()
  5647. ) {
  5648. noDataRes = tree.options.nodata;
  5649. }
  5650. if (noDataRes) {
  5651. node.setStatus("nodata", noDataRes);
  5652. }
  5653. }
  5654. // trigger fancytreeloadchildren
  5655. tree._triggerNodeEvent("loadChildren", node);
  5656. })
  5657. .fail(function (error) {
  5658. var ctxErr;
  5659. if (error === RECURSIVE_REQUEST_ERROR) {
  5660. node.warn(
  5661. "Ignored response for obsolete load request #" +
  5662. requestId +
  5663. " (expected #" +
  5664. node._requestId +
  5665. ")"
  5666. );
  5667. return;
  5668. } else if (error === INVALID_REQUEST_TARGET_ERROR) {
  5669. node.warn(
  5670. "Lazy parent node was removed while loading: discarding response."
  5671. );
  5672. return;
  5673. } else if (error.node && error.error && error.message) {
  5674. // error is already a context object
  5675. ctxErr = error;
  5676. } else {
  5677. ctxErr = tree._makeHookContext(node, null, {
  5678. error: error, // it can be jqXHR or any custom error
  5679. args: Array.prototype.slice.call(arguments),
  5680. message: error
  5681. ? error.message || error.toString()
  5682. : "",
  5683. });
  5684. if (ctxErr.message === "[object Object]") {
  5685. ctxErr.message = "";
  5686. }
  5687. }
  5688. node.warn(
  5689. "Load children failed (" + ctxErr.message + ")",
  5690. ctxErr
  5691. );
  5692. if (
  5693. tree._triggerNodeEvent(
  5694. "loadError",
  5695. ctxErr,
  5696. null
  5697. ) !== false
  5698. ) {
  5699. tree.nodeSetStatus(
  5700. ctx,
  5701. "error",
  5702. ctxErr.message,
  5703. ctxErr.details
  5704. );
  5705. }
  5706. })
  5707. .always(function () {
  5708. node._requestId = null;
  5709. if (isAsync) {
  5710. tree.debugTimeEnd(tag);
  5711. }
  5712. });
  5713. return resultDfd.promise();
  5714. },
  5715. /** [Not Implemented] */
  5716. nodeLoadKeyPath: function (ctx, keyPathList) {
  5717. // TODO: implement and improve
  5718. // http://code.google.com/p/dynatree/issues/detail?id=222
  5719. },
  5720. /**
  5721. * Remove a single direct child of ctx.node.
  5722. * @param {EventData} ctx
  5723. * @param {FancytreeNode} childNode dircect child of ctx.node
  5724. */
  5725. nodeRemoveChild: function (ctx, childNode) {
  5726. var idx,
  5727. node = ctx.node,
  5728. // opts = ctx.options,
  5729. subCtx = $.extend({}, ctx, { node: childNode }),
  5730. children = node.children;
  5731. // FT.debug("nodeRemoveChild()", node.toString(), childNode.toString());
  5732. if (children.length === 1) {
  5733. _assert(childNode === children[0], "invalid single child");
  5734. return this.nodeRemoveChildren(ctx);
  5735. }
  5736. if (
  5737. this.activeNode &&
  5738. (childNode === this.activeNode ||
  5739. this.activeNode.isDescendantOf(childNode))
  5740. ) {
  5741. this.activeNode.setActive(false); // TODO: don't fire events
  5742. }
  5743. if (
  5744. this.focusNode &&
  5745. (childNode === this.focusNode ||
  5746. this.focusNode.isDescendantOf(childNode))
  5747. ) {
  5748. this.focusNode = null;
  5749. }
  5750. // TODO: persist must take care to clear select and expand cookies
  5751. this.nodeRemoveMarkup(subCtx);
  5752. this.nodeRemoveChildren(subCtx);
  5753. idx = $.inArray(childNode, children);
  5754. _assert(idx >= 0, "invalid child");
  5755. // Notify listeners
  5756. node.triggerModifyChild("remove", childNode);
  5757. // Unlink to support GC
  5758. childNode.visit(function (n) {
  5759. n.parent = null;
  5760. }, true);
  5761. this._callHook("treeRegisterNode", this, false, childNode);
  5762. // remove from child list
  5763. children.splice(idx, 1);
  5764. },
  5765. /**Remove HTML markup for all descendents of ctx.node.
  5766. * @param {EventData} ctx
  5767. */
  5768. nodeRemoveChildMarkup: function (ctx) {
  5769. var node = ctx.node;
  5770. // FT.debug("nodeRemoveChildMarkup()", node.toString());
  5771. // TODO: Unlink attr.ftnode to support GC
  5772. if (node.ul) {
  5773. if (node.isRootNode()) {
  5774. $(node.ul).empty();
  5775. } else {
  5776. $(node.ul).remove();
  5777. node.ul = null;
  5778. }
  5779. node.visit(function (n) {
  5780. n.li = n.ul = null;
  5781. });
  5782. }
  5783. },
  5784. /**Remove all descendants of ctx.node.
  5785. * @param {EventData} ctx
  5786. */
  5787. nodeRemoveChildren: function (ctx) {
  5788. var //subCtx,
  5789. tree = ctx.tree,
  5790. node = ctx.node,
  5791. children = node.children;
  5792. // opts = ctx.options;
  5793. // FT.debug("nodeRemoveChildren()", node.toString());
  5794. if (!children) {
  5795. return;
  5796. }
  5797. if (this.activeNode && this.activeNode.isDescendantOf(node)) {
  5798. this.activeNode.setActive(false); // TODO: don't fire events
  5799. }
  5800. if (this.focusNode && this.focusNode.isDescendantOf(node)) {
  5801. this.focusNode = null;
  5802. }
  5803. // TODO: persist must take care to clear select and expand cookies
  5804. this.nodeRemoveChildMarkup(ctx);
  5805. // Unlink children to support GC
  5806. // TODO: also delete this.children (not possible using visit())
  5807. // subCtx = $.extend({}, ctx);
  5808. node.triggerModifyChild("remove", null);
  5809. node.visit(function (n) {
  5810. n.parent = null;
  5811. tree._callHook("treeRegisterNode", tree, false, n);
  5812. });
  5813. if (node.lazy) {
  5814. // 'undefined' would be interpreted as 'not yet loaded' for lazy nodes
  5815. node.children = [];
  5816. } else {
  5817. node.children = null;
  5818. }
  5819. if (!node.isRootNode()) {
  5820. node.expanded = false; // #449, #459
  5821. }
  5822. this.nodeRenderStatus(ctx);
  5823. },
  5824. /**Remove HTML markup for ctx.node and all its descendents.
  5825. * @param {EventData} ctx
  5826. */
  5827. nodeRemoveMarkup: function (ctx) {
  5828. var node = ctx.node;
  5829. // FT.debug("nodeRemoveMarkup()", node.toString());
  5830. // TODO: Unlink attr.ftnode to support GC
  5831. if (node.li) {
  5832. $(node.li).remove();
  5833. node.li = null;
  5834. }
  5835. this.nodeRemoveChildMarkup(ctx);
  5836. },
  5837. /**
  5838. * Create `<li><span>..</span> .. </li>` tags for this node.
  5839. *
  5840. * This method takes care that all HTML markup is created that is required
  5841. * to display this node in its current state.
  5842. *
  5843. * Call this method to create new nodes, or after the strucuture
  5844. * was changed (e.g. after moving this node or adding/removing children)
  5845. * nodeRenderTitle() and nodeRenderStatus() are implied.
  5846. *
  5847. * ```html
  5848. * <li id='KEY' ftnode=NODE>
  5849. * <span class='fancytree-node fancytree-expanded fancytree-has-children fancytree-lastsib fancytree-exp-el fancytree-ico-e'>
  5850. * <span class="fancytree-expander"></span>
  5851. * <span class="fancytree-checkbox"></span> // only present in checkbox mode
  5852. * <span class="fancytree-icon"></span>
  5853. * <a href="#" class="fancytree-title"> Node 1 </a>
  5854. * </span>
  5855. * <ul> // only present if node has children
  5856. * <li id='KEY' ftnode=NODE> child1 ... </li>
  5857. * <li id='KEY' ftnode=NODE> child2 ... </li>
  5858. * </ul>
  5859. * </li>
  5860. * ```
  5861. *
  5862. * @param {EventData} ctx
  5863. * @param {boolean} [force=false] re-render, even if html markup was already created
  5864. * @param {boolean} [deep=false] also render all descendants, even if parent is collapsed
  5865. * @param {boolean} [collapsed=false] force root node to be collapsed, so we can apply animated expand later
  5866. */
  5867. nodeRender: function (ctx, force, deep, collapsed, _recursive) {
  5868. /* This method must take care of all cases where the current data mode
  5869. * (i.e. node hierarchy) does not match the current markup.
  5870. *
  5871. * - node was not yet rendered:
  5872. * create markup
  5873. * - node was rendered: exit fast
  5874. * - children have been added
  5875. * - children have been removed
  5876. */
  5877. var childLI,
  5878. childNode1,
  5879. childNode2,
  5880. i,
  5881. l,
  5882. next,
  5883. subCtx,
  5884. node = ctx.node,
  5885. tree = ctx.tree,
  5886. opts = ctx.options,
  5887. aria = opts.aria,
  5888. firstTime = false,
  5889. parent = node.parent,
  5890. isRootNode = !parent,
  5891. children = node.children,
  5892. successorLi = null;
  5893. // FT.debug("nodeRender(" + !!force + ", " + !!deep + ")", node.toString());
  5894. if (tree._enableUpdate === false) {
  5895. // tree.debug("no render", tree._enableUpdate);
  5896. return;
  5897. }
  5898. if (!isRootNode && !parent.ul) {
  5899. // Calling node.collapse on a deep, unrendered node
  5900. return;
  5901. }
  5902. _assert(isRootNode || parent.ul, "parent UL must exist");
  5903. // Render the node
  5904. if (!isRootNode) {
  5905. // Discard markup on force-mode, or if it is not linked to parent <ul>
  5906. if (
  5907. node.li &&
  5908. (force || node.li.parentNode !== node.parent.ul)
  5909. ) {
  5910. if (node.li.parentNode === node.parent.ul) {
  5911. // #486: store following node, so we can insert the new markup there later
  5912. successorLi = node.li.nextSibling;
  5913. } else {
  5914. // May happen, when a top-level node was dropped over another
  5915. this.debug(
  5916. "Unlinking " +
  5917. node +
  5918. " (must be child of " +
  5919. node.parent +
  5920. ")"
  5921. );
  5922. }
  5923. // this.debug("nodeRemoveMarkup...");
  5924. this.nodeRemoveMarkup(ctx);
  5925. }
  5926. // Create <li><span /> </li>
  5927. // node.debug("render...");
  5928. if (node.li) {
  5929. // this.nodeRenderTitle(ctx);
  5930. this.nodeRenderStatus(ctx);
  5931. } else {
  5932. // node.debug("render... really");
  5933. firstTime = true;
  5934. node.li = document.createElement("li");
  5935. node.li.ftnode = node;
  5936. if (node.key && opts.generateIds) {
  5937. node.li.id = opts.idPrefix + node.key;
  5938. }
  5939. node.span = document.createElement("span");
  5940. node.span.className = "fancytree-node";
  5941. if (aria && !node.tr) {
  5942. $(node.li).attr("role", "treeitem");
  5943. }
  5944. node.li.appendChild(node.span);
  5945. // Create inner HTML for the <span> (expander, checkbox, icon, and title)
  5946. this.nodeRenderTitle(ctx);
  5947. // Allow tweaking and binding, after node was created for the first time
  5948. if (opts.createNode) {
  5949. opts.createNode.call(
  5950. tree,
  5951. { type: "createNode" },
  5952. ctx
  5953. );
  5954. }
  5955. }
  5956. // Allow tweaking after node state was rendered
  5957. if (opts.renderNode) {
  5958. opts.renderNode.call(tree, { type: "renderNode" }, ctx);
  5959. }
  5960. }
  5961. // Visit child nodes
  5962. if (children) {
  5963. if (isRootNode || node.expanded || deep === true) {
  5964. // Create a UL to hold the children
  5965. if (!node.ul) {
  5966. node.ul = document.createElement("ul");
  5967. if (
  5968. (collapsed === true && !_recursive) ||
  5969. !node.expanded
  5970. ) {
  5971. // hide top UL, so we can use an animation to show it later
  5972. node.ul.style.display = "none";
  5973. }
  5974. if (aria) {
  5975. $(node.ul).attr("role", "group");
  5976. }
  5977. if (node.li) {
  5978. // issue #67
  5979. node.li.appendChild(node.ul);
  5980. } else {
  5981. node.tree.$div.append(node.ul);
  5982. }
  5983. }
  5984. // Add child markup
  5985. for (i = 0, l = children.length; i < l; i++) {
  5986. subCtx = $.extend({}, ctx, { node: children[i] });
  5987. this.nodeRender(subCtx, force, deep, false, true);
  5988. }
  5989. // Remove <li> if nodes have moved to another parent
  5990. childLI = node.ul.firstChild;
  5991. while (childLI) {
  5992. childNode2 = childLI.ftnode;
  5993. if (childNode2 && childNode2.parent !== node) {
  5994. node.debug(
  5995. "_fixParent: remove missing " + childNode2,
  5996. childLI
  5997. );
  5998. next = childLI.nextSibling;
  5999. childLI.parentNode.removeChild(childLI);
  6000. childLI = next;
  6001. } else {
  6002. childLI = childLI.nextSibling;
  6003. }
  6004. }
  6005. // Make sure, that <li> order matches node.children order.
  6006. childLI = node.ul.firstChild;
  6007. for (i = 0, l = children.length - 1; i < l; i++) {
  6008. childNode1 = children[i];
  6009. childNode2 = childLI.ftnode;
  6010. if (childNode1 === childNode2) {
  6011. childLI = childLI.nextSibling;
  6012. } else {
  6013. // node.debug("_fixOrder: mismatch at index " + i + ": " + childNode1 + " != " + childNode2);
  6014. node.ul.insertBefore(
  6015. childNode1.li,
  6016. childNode2.li
  6017. );
  6018. }
  6019. }
  6020. }
  6021. } else {
  6022. // No children: remove markup if any
  6023. if (node.ul) {
  6024. // alert("remove child markup for " + node);
  6025. this.warn("remove child markup for " + node);
  6026. this.nodeRemoveChildMarkup(ctx);
  6027. }
  6028. }
  6029. if (!isRootNode) {
  6030. // Update element classes according to node state
  6031. // this.nodeRenderStatus(ctx);
  6032. // Finally add the whole structure to the DOM, so the browser can render
  6033. if (firstTime) {
  6034. // #486: successorLi is set, if we re-rendered (i.e. discarded)
  6035. // existing markup, which we want to insert at the same position.
  6036. // (null is equivalent to append)
  6037. // parent.ul.appendChild(node.li);
  6038. parent.ul.insertBefore(node.li, successorLi);
  6039. }
  6040. }
  6041. },
  6042. /** Create HTML inside the node's outer `<span>` (i.e. expander, checkbox,
  6043. * icon, and title).
  6044. *
  6045. * nodeRenderStatus() is implied.
  6046. * @param {EventData} ctx
  6047. * @param {string} [title] optinal new title
  6048. */
  6049. nodeRenderTitle: function (ctx, title) {
  6050. // set node connector images, links and text
  6051. var checkbox,
  6052. className,
  6053. icon,
  6054. nodeTitle,
  6055. role,
  6056. tabindex,
  6057. tooltip,
  6058. iconTooltip,
  6059. node = ctx.node,
  6060. tree = ctx.tree,
  6061. opts = ctx.options,
  6062. aria = opts.aria,
  6063. level = node.getLevel(),
  6064. ares = [];
  6065. if (title !== undefined) {
  6066. node.title = title;
  6067. }
  6068. if (!node.span || tree._enableUpdate === false) {
  6069. // Silently bail out if node was not rendered yet, assuming
  6070. // node.render() will be called as the node becomes visible
  6071. return;
  6072. }
  6073. // Connector (expanded, expandable or simple)
  6074. role =
  6075. aria && node.hasChildren() !== false
  6076. ? " role='button'"
  6077. : "";
  6078. if (level < opts.minExpandLevel) {
  6079. if (!node.lazy) {
  6080. node.expanded = true;
  6081. }
  6082. if (level > 1) {
  6083. ares.push(
  6084. "<span " +
  6085. role +
  6086. " class='fancytree-expander fancytree-expander-fixed'></span>"
  6087. );
  6088. }
  6089. // .. else (i.e. for root level) skip expander/connector alltogether
  6090. } else {
  6091. ares.push(
  6092. "<span " + role + " class='fancytree-expander'></span>"
  6093. );
  6094. }
  6095. // Checkbox mode
  6096. checkbox = FT.evalOption("checkbox", node, node, opts, false);
  6097. if (checkbox && !node.isStatusNode()) {
  6098. role = aria ? " role='checkbox'" : "";
  6099. className = "fancytree-checkbox";
  6100. if (
  6101. checkbox === "radio" ||
  6102. (node.parent && node.parent.radiogroup)
  6103. ) {
  6104. className += " fancytree-radio";
  6105. }
  6106. ares.push(
  6107. "<span " + role + " class='" + className + "'></span>"
  6108. );
  6109. }
  6110. // Folder or doctype icon
  6111. if (node.data.iconClass !== undefined) {
  6112. // 2015-11-16
  6113. // Handle / warn about backward compatibility
  6114. if (node.icon) {
  6115. $.error(
  6116. "'iconClass' node option is deprecated since v2.14.0: use 'icon' only instead"
  6117. );
  6118. } else {
  6119. node.warn(
  6120. "'iconClass' node option is deprecated since v2.14.0: use 'icon' instead"
  6121. );
  6122. node.icon = node.data.iconClass;
  6123. }
  6124. }
  6125. // If opts.icon is a callback and returns something other than undefined, use that
  6126. // else if node.icon is a boolean or string, use that
  6127. // else if opts.icon is a boolean or string, use that
  6128. // else show standard icon (which may be different for folders or documents)
  6129. icon = FT.evalOption("icon", node, node, opts, true);
  6130. // if( typeof icon !== "boolean" ) {
  6131. // // icon is defined, but not true/false: must be a string
  6132. // icon = "" + icon;
  6133. // }
  6134. if (icon !== false) {
  6135. role = aria ? " role='presentation'" : "";
  6136. iconTooltip = FT.evalOption(
  6137. "iconTooltip",
  6138. node,
  6139. node,
  6140. opts,
  6141. null
  6142. );
  6143. iconTooltip = iconTooltip
  6144. ? " title='" + _escapeTooltip(iconTooltip) + "'"
  6145. : "";
  6146. if (typeof icon === "string") {
  6147. if (TEST_IMG.test(icon)) {
  6148. // node.icon is an image url. Prepend imagePath
  6149. icon =
  6150. icon.charAt(0) === "/"
  6151. ? icon
  6152. : (opts.imagePath || "") + icon;
  6153. ares.push(
  6154. "<img src='" +
  6155. icon +
  6156. "' class='fancytree-icon'" +
  6157. iconTooltip +
  6158. " alt='' />"
  6159. );
  6160. } else {
  6161. ares.push(
  6162. "<span " +
  6163. role +
  6164. " class='fancytree-custom-icon " +
  6165. icon +
  6166. "'" +
  6167. iconTooltip +
  6168. "></span>"
  6169. );
  6170. }
  6171. } else if (icon.text) {
  6172. ares.push(
  6173. "<span " +
  6174. role +
  6175. " class='fancytree-custom-icon " +
  6176. (icon.addClass || "") +
  6177. "'" +
  6178. iconTooltip +
  6179. ">" +
  6180. FT.escapeHtml(icon.text) +
  6181. "</span>"
  6182. );
  6183. } else if (icon.html) {
  6184. ares.push(
  6185. "<span " +
  6186. role +
  6187. " class='fancytree-custom-icon " +
  6188. (icon.addClass || "") +
  6189. "'" +
  6190. iconTooltip +
  6191. ">" +
  6192. icon.html +
  6193. "</span>"
  6194. );
  6195. } else {
  6196. // standard icon: theme css will take care of this
  6197. ares.push(
  6198. "<span " +
  6199. role +
  6200. " class='fancytree-icon'" +
  6201. iconTooltip +
  6202. "></span>"
  6203. );
  6204. }
  6205. }
  6206. // Node title
  6207. nodeTitle = "";
  6208. if (opts.renderTitle) {
  6209. nodeTitle =
  6210. opts.renderTitle.call(
  6211. tree,
  6212. { type: "renderTitle" },
  6213. ctx
  6214. ) || "";
  6215. }
  6216. if (!nodeTitle) {
  6217. tooltip = FT.evalOption("tooltip", node, node, opts, null);
  6218. if (tooltip === true) {
  6219. tooltip = node.title;
  6220. }
  6221. // if( node.tooltip ) {
  6222. // tooltip = node.tooltip;
  6223. // } else if ( opts.tooltip ) {
  6224. // tooltip = opts.tooltip === true ? node.title : opts.tooltip.call(tree, node);
  6225. // }
  6226. tooltip = tooltip
  6227. ? " title='" + _escapeTooltip(tooltip) + "'"
  6228. : "";
  6229. tabindex = opts.titlesTabbable ? " tabindex='0'" : "";
  6230. nodeTitle =
  6231. "<span class='fancytree-title'" +
  6232. tooltip +
  6233. tabindex +
  6234. ">" +
  6235. (opts.escapeTitles
  6236. ? FT.escapeHtml(node.title)
  6237. : node.title) +
  6238. "</span>";
  6239. }
  6240. ares.push(nodeTitle);
  6241. // Note: this will trigger focusout, if node had the focus
  6242. //$(node.span).html(ares.join("")); // it will cleanup the jQuery data currently associated with SPAN (if any), but it executes more slowly
  6243. node.span.innerHTML = ares.join("");
  6244. // Update CSS classes
  6245. this.nodeRenderStatus(ctx);
  6246. if (opts.enhanceTitle) {
  6247. ctx.$title = $(">span.fancytree-title", node.span);
  6248. nodeTitle =
  6249. opts.enhanceTitle.call(
  6250. tree,
  6251. { type: "enhanceTitle" },
  6252. ctx
  6253. ) || "";
  6254. }
  6255. },
  6256. /** Update element classes according to node state.
  6257. * @param {EventData} ctx
  6258. */
  6259. nodeRenderStatus: function (ctx) {
  6260. // Set classes for current status
  6261. var $ariaElem,
  6262. node = ctx.node,
  6263. tree = ctx.tree,
  6264. opts = ctx.options,
  6265. // nodeContainer = node[tree.nodeContainerAttrName],
  6266. hasChildren = node.hasChildren(),
  6267. isLastSib = node.isLastSibling(),
  6268. aria = opts.aria,
  6269. cn = opts._classNames,
  6270. cnList = [],
  6271. statusElem = node[tree.statusClassPropName];
  6272. if (!statusElem || tree._enableUpdate === false) {
  6273. // if this function is called for an unrendered node, ignore it (will be updated on nect render anyway)
  6274. return;
  6275. }
  6276. if (aria) {
  6277. $ariaElem = $(node.tr || node.li);
  6278. }
  6279. // Build a list of class names that we will add to the node <span>
  6280. cnList.push(cn.node);
  6281. if (tree.activeNode === node) {
  6282. cnList.push(cn.active);
  6283. // $(">span.fancytree-title", statusElem).attr("tabindex", "0");
  6284. // tree.$container.removeAttr("tabindex");
  6285. // }else{
  6286. // $(">span.fancytree-title", statusElem).removeAttr("tabindex");
  6287. // tree.$container.attr("tabindex", "0");
  6288. }
  6289. if (tree.focusNode === node) {
  6290. cnList.push(cn.focused);
  6291. }
  6292. if (node.expanded) {
  6293. cnList.push(cn.expanded);
  6294. }
  6295. if (aria) {
  6296. if (hasChildren === false) {
  6297. $ariaElem.removeAttr("aria-expanded");
  6298. } else {
  6299. $ariaElem.attr("aria-expanded", Boolean(node.expanded));
  6300. }
  6301. }
  6302. if (node.folder) {
  6303. cnList.push(cn.folder);
  6304. }
  6305. if (hasChildren !== false) {
  6306. cnList.push(cn.hasChildren);
  6307. }
  6308. // TODO: required?
  6309. if (isLastSib) {
  6310. cnList.push(cn.lastsib);
  6311. }
  6312. if (node.lazy && node.children == null) {
  6313. cnList.push(cn.lazy);
  6314. }
  6315. if (node.partload) {
  6316. cnList.push(cn.partload);
  6317. }
  6318. if (node.partsel) {
  6319. cnList.push(cn.partsel);
  6320. }
  6321. if (FT.evalOption("unselectable", node, node, opts, false)) {
  6322. cnList.push(cn.unselectable);
  6323. }
  6324. if (node._isLoading) {
  6325. cnList.push(cn.loading);
  6326. }
  6327. if (node._error) {
  6328. cnList.push(cn.error);
  6329. }
  6330. if (node.statusNodeType) {
  6331. cnList.push(cn.statusNodePrefix + node.statusNodeType);
  6332. }
  6333. if (node.selected) {
  6334. cnList.push(cn.selected);
  6335. if (aria) {
  6336. $ariaElem.attr("aria-selected", true);
  6337. }
  6338. } else if (aria) {
  6339. $ariaElem.attr("aria-selected", false);
  6340. }
  6341. if (node.extraClasses) {
  6342. cnList.push(node.extraClasses);
  6343. }
  6344. // IE6 doesn't correctly evaluate multiple class names,
  6345. // so we create combined class names that can be used in the CSS
  6346. if (hasChildren === false) {
  6347. cnList.push(
  6348. cn.combinedExpanderPrefix + "n" + (isLastSib ? "l" : "")
  6349. );
  6350. } else {
  6351. cnList.push(
  6352. cn.combinedExpanderPrefix +
  6353. (node.expanded ? "e" : "c") +
  6354. (node.lazy && node.children == null ? "d" : "") +
  6355. (isLastSib ? "l" : "")
  6356. );
  6357. }
  6358. cnList.push(
  6359. cn.combinedIconPrefix +
  6360. (node.expanded ? "e" : "c") +
  6361. (node.folder ? "f" : "")
  6362. );
  6363. // node.span.className = cnList.join(" ");
  6364. statusElem.className = cnList.join(" ");
  6365. // TODO: we should not set this in the <span> tag also, if we set it here:
  6366. // Maybe most (all) of the classes should be set in LI instead of SPAN?
  6367. if (node.li) {
  6368. // #719: we have to consider that there may be already other classes:
  6369. $(node.li).toggleClass(cn.lastsib, isLastSib);
  6370. }
  6371. },
  6372. /** Activate node.
  6373. * flag defaults to true.
  6374. * If flag is true, the node is activated (must be a synchronous operation)
  6375. * If flag is false, the node is deactivated (must be a synchronous operation)
  6376. * @param {EventData} ctx
  6377. * @param {boolean} [flag=true]
  6378. * @param {object} [opts] additional options. Defaults to {noEvents: false, noFocus: false}
  6379. * @returns {$.Promise}
  6380. */
  6381. nodeSetActive: function (ctx, flag, callOpts) {
  6382. // Handle user click / [space] / [enter], according to clickFolderMode.
  6383. callOpts = callOpts || {};
  6384. var subCtx,
  6385. node = ctx.node,
  6386. tree = ctx.tree,
  6387. opts = ctx.options,
  6388. noEvents = callOpts.noEvents === true,
  6389. noFocus = callOpts.noFocus === true,
  6390. scroll = callOpts.scrollIntoView !== false,
  6391. isActive = node === tree.activeNode;
  6392. // flag defaults to true
  6393. flag = flag !== false;
  6394. // node.debug("nodeSetActive", flag);
  6395. if (isActive === flag) {
  6396. // Nothing to do
  6397. return _getResolvedPromise(node);
  6398. }
  6399. // #1042: don't scroll between mousedown/-up when clicking an embedded link
  6400. if (
  6401. scroll &&
  6402. ctx.originalEvent &&
  6403. $(ctx.originalEvent.target).is("a,:checkbox")
  6404. ) {
  6405. node.info("Not scrolling while clicking an embedded link.");
  6406. scroll = false;
  6407. }
  6408. if (
  6409. flag &&
  6410. !noEvents &&
  6411. this._triggerNodeEvent(
  6412. "beforeActivate",
  6413. node,
  6414. ctx.originalEvent
  6415. ) === false
  6416. ) {
  6417. // Callback returned false
  6418. return _getRejectedPromise(node, ["rejected"]);
  6419. }
  6420. if (flag) {
  6421. if (tree.activeNode) {
  6422. _assert(
  6423. tree.activeNode !== node,
  6424. "node was active (inconsistency)"
  6425. );
  6426. subCtx = $.extend({}, ctx, { node: tree.activeNode });
  6427. tree.nodeSetActive(subCtx, false);
  6428. _assert(
  6429. tree.activeNode === null,
  6430. "deactivate was out of sync?"
  6431. );
  6432. }
  6433. if (opts.activeVisible) {
  6434. // If no focus is set (noFocus: true) and there is no focused node, this node is made visible.
  6435. // scroll = noFocus && tree.focusNode == null;
  6436. // #863: scroll by default (unless `scrollIntoView: false` was passed)
  6437. node.makeVisible({ scrollIntoView: scroll });
  6438. }
  6439. tree.activeNode = node;
  6440. tree.nodeRenderStatus(ctx);
  6441. if (!noFocus) {
  6442. tree.nodeSetFocus(ctx);
  6443. }
  6444. if (!noEvents) {
  6445. tree._triggerNodeEvent(
  6446. "activate",
  6447. node,
  6448. ctx.originalEvent
  6449. );
  6450. }
  6451. } else {
  6452. _assert(
  6453. tree.activeNode === node,
  6454. "node was not active (inconsistency)"
  6455. );
  6456. tree.activeNode = null;
  6457. this.nodeRenderStatus(ctx);
  6458. if (!noEvents) {
  6459. ctx.tree._triggerNodeEvent(
  6460. "deactivate",
  6461. node,
  6462. ctx.originalEvent
  6463. );
  6464. }
  6465. }
  6466. return _getResolvedPromise(node);
  6467. },
  6468. /** Expand or collapse node, return Deferred.promise.
  6469. *
  6470. * @param {EventData} ctx
  6471. * @param {boolean} [flag=true]
  6472. * @param {object} [opts] additional options. Defaults to `{noAnimation: false, noEvents: false}`
  6473. * @returns {$.Promise} The deferred will be resolved as soon as the (lazy)
  6474. * data was retrieved, rendered, and the expand animation finished.
  6475. */
  6476. nodeSetExpanded: function (ctx, flag, callOpts) {
  6477. callOpts = callOpts || {};
  6478. var _afterLoad,
  6479. dfd,
  6480. i,
  6481. l,
  6482. parents,
  6483. prevAC,
  6484. node = ctx.node,
  6485. tree = ctx.tree,
  6486. opts = ctx.options,
  6487. noAnimation = callOpts.noAnimation === true,
  6488. noEvents = callOpts.noEvents === true;
  6489. // flag defaults to true
  6490. flag = flag !== false;
  6491. // node.debug("nodeSetExpanded(" + flag + ")");
  6492. if ($(node.li).hasClass(opts._classNames.animating)) {
  6493. node.warn(
  6494. "setExpanded(" + flag + ") while animating: ignored."
  6495. );
  6496. return _getRejectedPromise(node, ["recursion"]);
  6497. }
  6498. if ((node.expanded && flag) || (!node.expanded && !flag)) {
  6499. // Nothing to do
  6500. // node.debug("nodeSetExpanded(" + flag + "): nothing to do");
  6501. return _getResolvedPromise(node);
  6502. } else if (flag && !node.lazy && !node.hasChildren()) {
  6503. // Prevent expanding of empty nodes
  6504. // return _getRejectedPromise(node, ["empty"]);
  6505. return _getResolvedPromise(node);
  6506. } else if (!flag && node.getLevel() < opts.minExpandLevel) {
  6507. // Prevent collapsing locked levels
  6508. return _getRejectedPromise(node, ["locked"]);
  6509. } else if (
  6510. !noEvents &&
  6511. this._triggerNodeEvent(
  6512. "beforeExpand",
  6513. node,
  6514. ctx.originalEvent
  6515. ) === false
  6516. ) {
  6517. // Callback returned false
  6518. return _getRejectedPromise(node, ["rejected"]);
  6519. }
  6520. // If this node inside a collpased node, no animation and scrolling is needed
  6521. if (!noAnimation && !node.isVisible()) {
  6522. noAnimation = callOpts.noAnimation = true;
  6523. }
  6524. dfd = new $.Deferred();
  6525. // Auto-collapse mode: collapse all siblings
  6526. if (flag && !node.expanded && opts.autoCollapse) {
  6527. parents = node.getParentList(false, true);
  6528. prevAC = opts.autoCollapse;
  6529. try {
  6530. opts.autoCollapse = false;
  6531. for (i = 0, l = parents.length; i < l; i++) {
  6532. // TODO: should return promise?
  6533. this._callHook(
  6534. "nodeCollapseSiblings",
  6535. parents[i],
  6536. callOpts
  6537. );
  6538. }
  6539. } finally {
  6540. opts.autoCollapse = prevAC;
  6541. }
  6542. }
  6543. // Trigger expand/collapse after expanding
  6544. dfd.done(function () {
  6545. var lastChild = node.getLastChild();
  6546. if (
  6547. flag &&
  6548. opts.autoScroll &&
  6549. !noAnimation &&
  6550. lastChild &&
  6551. tree._enableUpdate
  6552. ) {
  6553. // Scroll down to last child, but keep current node visible
  6554. lastChild
  6555. .scrollIntoView(true, { topNode: node })
  6556. .always(function () {
  6557. if (!noEvents) {
  6558. ctx.tree._triggerNodeEvent(
  6559. flag ? "expand" : "collapse",
  6560. ctx
  6561. );
  6562. }
  6563. });
  6564. } else {
  6565. if (!noEvents) {
  6566. ctx.tree._triggerNodeEvent(
  6567. flag ? "expand" : "collapse",
  6568. ctx
  6569. );
  6570. }
  6571. }
  6572. });
  6573. // vvv Code below is executed after loading finished:
  6574. _afterLoad = function (callback) {
  6575. var cn = opts._classNames,
  6576. isVisible,
  6577. isExpanded,
  6578. effect = opts.toggleEffect;
  6579. node.expanded = flag;
  6580. tree._callHook(
  6581. "treeStructureChanged",
  6582. ctx,
  6583. flag ? "expand" : "collapse"
  6584. );
  6585. // Create required markup, but make sure the top UL is hidden, so we
  6586. // can animate later
  6587. tree._callHook("nodeRender", ctx, false, false, true);
  6588. // Hide children, if node is collapsed
  6589. if (node.ul) {
  6590. isVisible = node.ul.style.display !== "none";
  6591. isExpanded = !!node.expanded;
  6592. if (isVisible === isExpanded) {
  6593. node.warn(
  6594. "nodeSetExpanded: UL.style.display already set"
  6595. );
  6596. } else if (!effect || noAnimation) {
  6597. node.ul.style.display =
  6598. node.expanded || !parent ? "" : "none";
  6599. } else {
  6600. // The UI toggle() effect works with the ext-wide extension,
  6601. // while jQuery.animate() has problems when the title span
  6602. // has position: absolute.
  6603. // Since jQuery UI 1.12, the blind effect requires the parent
  6604. // element to have 'position: relative'.
  6605. // See #716, #717
  6606. $(node.li).addClass(cn.animating); // #717
  6607. if (_isFunction($(node.ul)[effect.effect])) {
  6608. // tree.debug( "use jquery." + effect.effect + " method" );
  6609. $(node.ul)[effect.effect]({
  6610. duration: effect.duration,
  6611. always: function () {
  6612. // node.debug("fancytree-animating end: " + node.li.className);
  6613. $(this).removeClass(cn.animating); // #716
  6614. $(node.li).removeClass(cn.animating); // #717
  6615. callback();
  6616. },
  6617. });
  6618. } else {
  6619. // The UI toggle() effect works with the ext-wide extension,
  6620. // while jQuery.animate() has problems when the title span
  6621. // has positon: absolute.
  6622. // Since jQuery UI 1.12, the blind effect requires the parent
  6623. // element to have 'position: relative'.
  6624. // See #716, #717
  6625. // tree.debug("use specified effect (" + effect.effect + ") with the jqueryui.toggle method");
  6626. // try to stop an animation that might be already in progress
  6627. $(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"
  6628. // dirty fix to remove a defunct animation (effect: "blind") after resetLazy has been called
  6629. $(node.ul)
  6630. .parent()
  6631. .find(".ui-effects-placeholder")
  6632. .remove();
  6633. $(node.ul).toggle(
  6634. effect.effect,
  6635. effect.options,
  6636. effect.duration,
  6637. function () {
  6638. // node.debug("fancytree-animating end: " + node.li.className);
  6639. $(this).removeClass(cn.animating); // #716
  6640. $(node.li).removeClass(cn.animating); // #717
  6641. callback();
  6642. }
  6643. );
  6644. }
  6645. return;
  6646. }
  6647. }
  6648. callback();
  6649. };
  6650. // ^^^ Code above is executed after loading finshed.
  6651. // Load lazy nodes, if any. Then continue with _afterLoad()
  6652. if (flag && node.lazy && node.hasChildren() === undefined) {
  6653. // node.debug("nodeSetExpanded: load start...");
  6654. node.load()
  6655. .done(function () {
  6656. // node.debug("nodeSetExpanded: load done");
  6657. if (dfd.notifyWith) {
  6658. // requires jQuery 1.6+
  6659. dfd.notifyWith(node, ["loaded"]);
  6660. }
  6661. _afterLoad(function () {
  6662. dfd.resolveWith(node);
  6663. });
  6664. })
  6665. .fail(function (errMsg) {
  6666. _afterLoad(function () {
  6667. dfd.rejectWith(node, [
  6668. "load failed (" + errMsg + ")",
  6669. ]);
  6670. });
  6671. });
  6672. /*
  6673. var source = tree._triggerNodeEvent("lazyLoad", node, ctx.originalEvent);
  6674. _assert(typeof source !== "boolean", "lazyLoad event must return source in data.result");
  6675. node.debug("nodeSetExpanded: load start...");
  6676. this._callHook("nodeLoadChildren", ctx, source).done(function(){
  6677. node.debug("nodeSetExpanded: load done");
  6678. if(dfd.notifyWith){ // requires jQuery 1.6+
  6679. dfd.notifyWith(node, ["loaded"]);
  6680. }
  6681. _afterLoad.call(tree);
  6682. }).fail(function(errMsg){
  6683. dfd.rejectWith(node, ["load failed (" + errMsg + ")"]);
  6684. });
  6685. */
  6686. } else {
  6687. _afterLoad(function () {
  6688. dfd.resolveWith(node);
  6689. });
  6690. }
  6691. // node.debug("nodeSetExpanded: returns");
  6692. return dfd.promise();
  6693. },
  6694. /** Focus or blur this node.
  6695. * @param {EventData} ctx
  6696. * @param {boolean} [flag=true]
  6697. */
  6698. nodeSetFocus: function (ctx, flag) {
  6699. // ctx.node.debug("nodeSetFocus(" + flag + ")");
  6700. var ctx2,
  6701. tree = ctx.tree,
  6702. node = ctx.node,
  6703. opts = tree.options,
  6704. // et = ctx.originalEvent && ctx.originalEvent.type,
  6705. isInput = ctx.originalEvent
  6706. ? $(ctx.originalEvent.target).is(":input")
  6707. : false;
  6708. flag = flag !== false;
  6709. // (node || tree).debug("nodeSetFocus(" + flag + "), event: " + et + ", isInput: "+ isInput);
  6710. // Blur previous node if any
  6711. if (tree.focusNode) {
  6712. if (tree.focusNode === node && flag) {
  6713. // node.debug("nodeSetFocus(" + flag + "): nothing to do");
  6714. return;
  6715. }
  6716. ctx2 = $.extend({}, ctx, { node: tree.focusNode });
  6717. tree.focusNode = null;
  6718. this._triggerNodeEvent("blur", ctx2);
  6719. this._callHook("nodeRenderStatus", ctx2);
  6720. }
  6721. // Set focus to container and node
  6722. if (flag) {
  6723. if (!this.hasFocus()) {
  6724. node.debug("nodeSetFocus: forcing container focus");
  6725. this._callHook("treeSetFocus", ctx, true, {
  6726. calledByNode: true,
  6727. });
  6728. }
  6729. node.makeVisible({ scrollIntoView: false });
  6730. tree.focusNode = node;
  6731. if (opts.titlesTabbable) {
  6732. if (!isInput) {
  6733. // #621
  6734. $(node.span).find(".fancytree-title").focus();
  6735. }
  6736. }
  6737. if (opts.aria) {
  6738. // Set active descendant to node's span ID (create one, if needed)
  6739. $(tree.$container).attr(
  6740. "aria-activedescendant",
  6741. $(node.tr || node.li)
  6742. .uniqueId()
  6743. .attr("id")
  6744. );
  6745. // "ftal_" + opts.idPrefix + node.key);
  6746. }
  6747. // $(node.span).find(".fancytree-title").focus();
  6748. this._triggerNodeEvent("focus", ctx);
  6749. // determine if we have focus on or inside tree container
  6750. var hasFancytreeFocus =
  6751. document.activeElement === tree.$container.get(0) ||
  6752. $(document.activeElement, tree.$container).length >= 1;
  6753. if (!hasFancytreeFocus) {
  6754. // We cannot set KB focus to a node, so use the tree container
  6755. // #563, #570: IE scrolls on every call to .focus(), if the container
  6756. // is partially outside the viewport. So do it only, when absolutely
  6757. // necessary.
  6758. $(tree.$container).focus();
  6759. }
  6760. // if( opts.autoActivate ){
  6761. // tree.nodeSetActive(ctx, true);
  6762. // }
  6763. if (opts.autoScroll) {
  6764. node.scrollIntoView();
  6765. }
  6766. this._callHook("nodeRenderStatus", ctx);
  6767. }
  6768. },
  6769. /** (De)Select node, return new status (sync).
  6770. *
  6771. * @param {EventData} ctx
  6772. * @param {boolean} [flag=true]
  6773. * @param {object} [opts] additional options. Defaults to {noEvents: false,
  6774. * propagateDown: null, propagateUp: null,
  6775. * callback: null,
  6776. * }
  6777. * @returns {boolean} previous status
  6778. */
  6779. nodeSetSelected: function (ctx, flag, callOpts) {
  6780. callOpts = callOpts || {};
  6781. var node = ctx.node,
  6782. tree = ctx.tree,
  6783. opts = ctx.options,
  6784. noEvents = callOpts.noEvents === true,
  6785. parent = node.parent;
  6786. // flag defaults to true
  6787. flag = flag !== false;
  6788. // node.debug("nodeSetSelected(" + flag + ")", ctx);
  6789. // Cannot (de)select unselectable nodes directly (only by propagation or
  6790. // by setting the `.selected` property)
  6791. if (FT.evalOption("unselectable", node, node, opts, false)) {
  6792. return;
  6793. }
  6794. // Remember the user's intent, in case down -> up propagation prevents
  6795. // applying it to node.selected
  6796. node._lastSelectIntent = flag; // Confusing use of '!'
  6797. // Nothing to do?
  6798. if (!!node.selected === flag) {
  6799. if (opts.selectMode === 3 && node.partsel && !flag) {
  6800. // If propagation prevented selecting this node last time, we still
  6801. // want to allow to apply setSelected(false) now
  6802. } else {
  6803. return flag;
  6804. }
  6805. }
  6806. if (
  6807. !noEvents &&
  6808. this._triggerNodeEvent(
  6809. "beforeSelect",
  6810. node,
  6811. ctx.originalEvent
  6812. ) === false
  6813. ) {
  6814. return !!node.selected;
  6815. }
  6816. if (flag && opts.selectMode === 1) {
  6817. // single selection mode (we don't uncheck all tree nodes, for performance reasons)
  6818. if (tree.lastSelectedNode) {
  6819. tree.lastSelectedNode.setSelected(false);
  6820. }
  6821. node.selected = flag;
  6822. } else if (
  6823. opts.selectMode === 3 &&
  6824. parent &&
  6825. !parent.radiogroup &&
  6826. !node.radiogroup
  6827. ) {
  6828. // multi-hierarchical selection mode
  6829. node.selected = flag;
  6830. node.fixSelection3AfterClick(callOpts);
  6831. } else if (parent && parent.radiogroup) {
  6832. node.visitSiblings(function (n) {
  6833. n._changeSelectStatusAttrs(flag && n === node);
  6834. }, true);
  6835. } else {
  6836. // default: selectMode: 2, multi selection mode
  6837. node.selected = flag;
  6838. }
  6839. this.nodeRenderStatus(ctx);
  6840. tree.lastSelectedNode = flag ? node : null;
  6841. if (!noEvents) {
  6842. tree._triggerNodeEvent("select", ctx);
  6843. }
  6844. },
  6845. /** Show node status (ok, loading, error, nodata) using styles and a dummy child node.
  6846. *
  6847. * @param {EventData} ctx
  6848. * @param status
  6849. * @param message
  6850. * @param details
  6851. * @since 2.3
  6852. */
  6853. nodeSetStatus: function (ctx, status, message, details) {
  6854. var node = ctx.node,
  6855. tree = ctx.tree;
  6856. function _clearStatusNode() {
  6857. // Remove dedicated dummy node, if any
  6858. var firstChild = node.children ? node.children[0] : null;
  6859. if (firstChild && firstChild.isStatusNode()) {
  6860. try {
  6861. // I've seen exceptions here with loadKeyPath...
  6862. if (node.ul) {
  6863. node.ul.removeChild(firstChild.li);
  6864. firstChild.li = null; // avoid leaks (DT issue 215)
  6865. }
  6866. } catch (e) {}
  6867. if (node.children.length === 1) {
  6868. node.children = [];
  6869. } else {
  6870. node.children.shift();
  6871. }
  6872. tree._callHook(
  6873. "treeStructureChanged",
  6874. ctx,
  6875. "clearStatusNode"
  6876. );
  6877. }
  6878. }
  6879. function _setStatusNode(data, type) {
  6880. // Create/modify the dedicated dummy node for 'loading...' or
  6881. // 'error!' status. (only called for direct child of the invisible
  6882. // system root)
  6883. var firstChild = node.children ? node.children[0] : null;
  6884. if (firstChild && firstChild.isStatusNode()) {
  6885. $.extend(firstChild, data);
  6886. firstChild.statusNodeType = type;
  6887. tree._callHook("nodeRenderTitle", firstChild);
  6888. } else {
  6889. node._setChildren([data]);
  6890. tree._callHook(
  6891. "treeStructureChanged",
  6892. ctx,
  6893. "setStatusNode"
  6894. );
  6895. node.children[0].statusNodeType = type;
  6896. tree.render();
  6897. }
  6898. return node.children[0];
  6899. }
  6900. switch (status) {
  6901. case "ok":
  6902. _clearStatusNode();
  6903. node._isLoading = false;
  6904. node._error = null;
  6905. node.renderStatus();
  6906. break;
  6907. case "loading":
  6908. if (!node.parent) {
  6909. _setStatusNode(
  6910. {
  6911. title:
  6912. tree.options.strings.loading +
  6913. (message ? " (" + message + ")" : ""),
  6914. // icon: true, // needed for 'loding' icon
  6915. checkbox: false,
  6916. tooltip: details,
  6917. },
  6918. status
  6919. );
  6920. }
  6921. node._isLoading = true;
  6922. node._error = null;
  6923. node.renderStatus();
  6924. break;
  6925. case "error":
  6926. _setStatusNode(
  6927. {
  6928. title:
  6929. tree.options.strings.loadError +
  6930. (message ? " (" + message + ")" : ""),
  6931. // icon: false,
  6932. checkbox: false,
  6933. tooltip: details,
  6934. },
  6935. status
  6936. );
  6937. node._isLoading = false;
  6938. node._error = { message: message, details: details };
  6939. node.renderStatus();
  6940. break;
  6941. case "nodata":
  6942. _setStatusNode(
  6943. {
  6944. title: message || tree.options.strings.noData,
  6945. // icon: false,
  6946. checkbox: false,
  6947. tooltip: details,
  6948. },
  6949. status
  6950. );
  6951. node._isLoading = false;
  6952. node._error = null;
  6953. node.renderStatus();
  6954. break;
  6955. default:
  6956. $.error("invalid node status " + status);
  6957. }
  6958. },
  6959. /**
  6960. *
  6961. * @param {EventData} ctx
  6962. */
  6963. nodeToggleExpanded: function (ctx) {
  6964. return this.nodeSetExpanded(ctx, !ctx.node.expanded);
  6965. },
  6966. /**
  6967. * @param {EventData} ctx
  6968. */
  6969. nodeToggleSelected: function (ctx) {
  6970. var node = ctx.node,
  6971. flag = !node.selected;
  6972. // In selectMode: 3 this node may be unselected+partsel, even if
  6973. // setSelected(true) was called before, due to `unselectable` children.
  6974. // In this case, we now toggle as `setSelected(false)`
  6975. if (
  6976. node.partsel &&
  6977. !node.selected &&
  6978. node._lastSelectIntent === true
  6979. ) {
  6980. flag = false;
  6981. node.selected = true; // so it is not considered 'nothing to do'
  6982. }
  6983. node._lastSelectIntent = flag;
  6984. return this.nodeSetSelected(ctx, flag);
  6985. },
  6986. /** Remove all nodes.
  6987. * @param {EventData} ctx
  6988. */
  6989. treeClear: function (ctx) {
  6990. var tree = ctx.tree;
  6991. tree.activeNode = null;
  6992. tree.focusNode = null;
  6993. tree.$div.find(">ul.fancytree-container").empty();
  6994. // TODO: call destructors and remove reference loops
  6995. tree.rootNode.children = null;
  6996. tree._callHook("treeStructureChanged", ctx, "clear");
  6997. },
  6998. /** Widget was created (called only once, even it re-initialized).
  6999. * @param {EventData} ctx
  7000. */
  7001. treeCreate: function (ctx) {},
  7002. /** Widget was destroyed.
  7003. * @param {EventData} ctx
  7004. */
  7005. treeDestroy: function (ctx) {
  7006. this.$div.find(">ul.fancytree-container").remove();
  7007. if (this.$source) {
  7008. this.$source.removeClass("fancytree-helper-hidden");
  7009. }
  7010. },
  7011. /** Widget was (re-)initialized.
  7012. * @param {EventData} ctx
  7013. */
  7014. treeInit: function (ctx) {
  7015. var tree = ctx.tree,
  7016. opts = tree.options;
  7017. //this.debug("Fancytree.treeInit()");
  7018. // Add container to the TAB chain
  7019. // See http://www.w3.org/TR/wai-aria-practices/#focus_activedescendant
  7020. // #577: Allow to set tabindex to "0", "-1" and ""
  7021. tree.$container.attr("tabindex", opts.tabindex);
  7022. // Copy some attributes to tree.data
  7023. $.each(TREE_ATTRS, function (i, attr) {
  7024. if (opts[attr] !== undefined) {
  7025. tree.info("Move option " + attr + " to tree");
  7026. tree[attr] = opts[attr];
  7027. delete opts[attr];
  7028. }
  7029. });
  7030. if (opts.checkboxAutoHide) {
  7031. tree.$container.addClass("fancytree-checkbox-auto-hide");
  7032. }
  7033. if (opts.rtl) {
  7034. tree.$container
  7035. .attr("DIR", "RTL")
  7036. .addClass("fancytree-rtl");
  7037. } else {
  7038. tree.$container
  7039. .removeAttr("DIR")
  7040. .removeClass("fancytree-rtl");
  7041. }
  7042. if (opts.aria) {
  7043. tree.$container.attr("role", "tree");
  7044. if (opts.selectMode !== 1) {
  7045. tree.$container.attr("aria-multiselectable", true);
  7046. }
  7047. }
  7048. this.treeLoad(ctx);
  7049. },
  7050. /** Parse Fancytree from source, as configured in the options.
  7051. * @param {EventData} ctx
  7052. * @param {object} [source] optional new source (use last data otherwise)
  7053. */
  7054. treeLoad: function (ctx, source) {
  7055. var metaData,
  7056. type,
  7057. $ul,
  7058. tree = ctx.tree,
  7059. $container = ctx.widget.element,
  7060. dfd,
  7061. // calling context for root node
  7062. rootCtx = $.extend({}, ctx, { node: this.rootNode });
  7063. if (tree.rootNode.children) {
  7064. this.treeClear(ctx);
  7065. }
  7066. source = source || this.options.source;
  7067. if (!source) {
  7068. type = $container.data("type") || "html";
  7069. switch (type) {
  7070. case "html":
  7071. // There should be an embedded `<ul>` with initial nodes,
  7072. // but another `<ul class='fancytree-container'>` is appended
  7073. // to the tree's <div> on startup anyway.
  7074. $ul = $container
  7075. .find(">ul")
  7076. .not(".fancytree-container")
  7077. .first();
  7078. if ($ul.length) {
  7079. $ul.addClass(
  7080. "ui-fancytree-source fancytree-helper-hidden"
  7081. );
  7082. source = $.ui.fancytree.parseHtml($ul);
  7083. // allow to init tree.data.foo from <ul data-foo=''>
  7084. this.data = $.extend(
  7085. this.data,
  7086. _getElementDataAsDict($ul)
  7087. );
  7088. } else {
  7089. FT.warn(
  7090. "No `source` option was passed and container does not contain `<ul>`: assuming `source: []`."
  7091. );
  7092. source = [];
  7093. }
  7094. break;
  7095. case "json":
  7096. source = $.parseJSON($container.text());
  7097. // $container already contains the <ul>, but we remove the plain (json) text
  7098. // $container.empty();
  7099. $container
  7100. .contents()
  7101. .filter(function () {
  7102. return this.nodeType === 3;
  7103. })
  7104. .remove();
  7105. if ($.isPlainObject(source)) {
  7106. // We got {foo: 'abc', children: [...]}
  7107. _assert(
  7108. _isArray(source.children),
  7109. "if an object is passed as source, it must contain a 'children' array (all other properties are added to 'tree.data')"
  7110. );
  7111. metaData = source;
  7112. source = source.children;
  7113. delete metaData.children;
  7114. // Copy some attributes to tree.data
  7115. $.each(TREE_ATTRS, function (i, attr) {
  7116. if (metaData[attr] !== undefined) {
  7117. tree[attr] = metaData[attr];
  7118. delete metaData[attr];
  7119. }
  7120. });
  7121. // Copy extra properties to tree.data.foo
  7122. $.extend(tree.data, metaData);
  7123. }
  7124. break;
  7125. default:
  7126. $.error("Invalid data-type: " + type);
  7127. }
  7128. } else if (typeof source === "string") {
  7129. // TODO: source is an element ID
  7130. $.error("Not implemented");
  7131. }
  7132. // preInit is fired when the widget markup is created, but nodes
  7133. // not yet loaded
  7134. tree._triggerTreeEvent("preInit", null);
  7135. // Trigger fancytreeinit after nodes have been loaded
  7136. dfd = this.nodeLoadChildren(rootCtx, source)
  7137. .done(function () {
  7138. tree._callHook(
  7139. "treeStructureChanged",
  7140. ctx,
  7141. "loadChildren"
  7142. );
  7143. tree.render();
  7144. if (ctx.options.selectMode === 3) {
  7145. tree.rootNode.fixSelection3FromEndNodes();
  7146. }
  7147. if (tree.activeNode && tree.options.activeVisible) {
  7148. tree.activeNode.makeVisible();
  7149. }
  7150. tree._triggerTreeEvent("init", null, { status: true });
  7151. })
  7152. .fail(function () {
  7153. tree.render();
  7154. tree._triggerTreeEvent("init", null, { status: false });
  7155. });
  7156. return dfd;
  7157. },
  7158. /** Node was inserted into or removed from the tree.
  7159. * @param {EventData} ctx
  7160. * @param {boolean} add
  7161. * @param {FancytreeNode} node
  7162. */
  7163. treeRegisterNode: function (ctx, add, node) {
  7164. ctx.tree._callHook(
  7165. "treeStructureChanged",
  7166. ctx,
  7167. add ? "addNode" : "removeNode"
  7168. );
  7169. },
  7170. /** Widget got focus.
  7171. * @param {EventData} ctx
  7172. * @param {boolean} [flag=true]
  7173. */
  7174. treeSetFocus: function (ctx, flag, callOpts) {
  7175. var targetNode;
  7176. flag = flag !== false;
  7177. // this.debug("treeSetFocus(" + flag + "), callOpts: ", callOpts, this.hasFocus());
  7178. // this.debug(" focusNode: " + this.focusNode);
  7179. // this.debug(" activeNode: " + this.activeNode);
  7180. if (flag !== this.hasFocus()) {
  7181. this._hasFocus = flag;
  7182. if (!flag && this.focusNode) {
  7183. // Node also looses focus if widget blurs
  7184. this.focusNode.setFocus(false);
  7185. } else if (flag && (!callOpts || !callOpts.calledByNode)) {
  7186. $(this.$container).focus();
  7187. }
  7188. this.$container.toggleClass("fancytree-treefocus", flag);
  7189. this._triggerTreeEvent(flag ? "focusTree" : "blurTree");
  7190. if (flag && !this.activeNode) {
  7191. // #712: Use last mousedowned node ('click' event fires after focusin)
  7192. targetNode =
  7193. this._lastMousedownNode || this.getFirstChild();
  7194. if (targetNode) {
  7195. targetNode.setFocus();
  7196. }
  7197. }
  7198. }
  7199. },
  7200. /** Widget option was set using `$().fancytree("option", "KEY", VALUE)`.
  7201. *
  7202. * Note: `key` may reference a nested option, e.g. 'dnd5.scroll'.
  7203. * In this case `value`contains the complete, modified `dnd5` option hash.
  7204. * We can check for changed values like
  7205. * if( value.scroll !== tree.options.dnd5.scroll ) {...}
  7206. *
  7207. * @param {EventData} ctx
  7208. * @param {string} key option name
  7209. * @param {any} value option value
  7210. */
  7211. treeSetOption: function (ctx, key, value) {
  7212. var tree = ctx.tree,
  7213. callDefault = true,
  7214. callCreate = false,
  7215. callRender = false;
  7216. switch (key) {
  7217. case "aria":
  7218. case "checkbox":
  7219. case "icon":
  7220. case "minExpandLevel":
  7221. case "tabindex":
  7222. // tree._callHook("treeCreate", tree);
  7223. callCreate = true;
  7224. callRender = true;
  7225. break;
  7226. case "checkboxAutoHide":
  7227. tree.$container.toggleClass(
  7228. "fancytree-checkbox-auto-hide",
  7229. !!value
  7230. );
  7231. break;
  7232. case "escapeTitles":
  7233. case "tooltip":
  7234. callRender = true;
  7235. break;
  7236. case "rtl":
  7237. if (value === false) {
  7238. tree.$container
  7239. .removeAttr("DIR")
  7240. .removeClass("fancytree-rtl");
  7241. } else {
  7242. tree.$container
  7243. .attr("DIR", "RTL")
  7244. .addClass("fancytree-rtl");
  7245. }
  7246. callRender = true;
  7247. break;
  7248. case "source":
  7249. callDefault = false;
  7250. tree._callHook("treeLoad", tree, value);
  7251. callRender = true;
  7252. break;
  7253. }
  7254. tree.debug(
  7255. "set option " +
  7256. key +
  7257. "=" +
  7258. value +
  7259. " <" +
  7260. typeof value +
  7261. ">"
  7262. );
  7263. if (callDefault) {
  7264. if (this.widget._super) {
  7265. // jQuery UI 1.9+
  7266. this.widget._super.call(this.widget, key, value);
  7267. } else {
  7268. // jQuery UI <= 1.8, we have to manually invoke the _setOption method from the base widget
  7269. $.Widget.prototype._setOption.call(
  7270. this.widget,
  7271. key,
  7272. value
  7273. );
  7274. }
  7275. }
  7276. if (callCreate) {
  7277. tree._callHook("treeCreate", tree);
  7278. }
  7279. if (callRender) {
  7280. tree.render(true, false); // force, not-deep
  7281. }
  7282. },
  7283. /** A Node was added, removed, moved, or it's visibility changed.
  7284. * @param {EventData} ctx
  7285. */
  7286. treeStructureChanged: function (ctx, type) {},
  7287. }
  7288. );
  7289. /*******************************************************************************
  7290. * jQuery UI widget boilerplate
  7291. */
  7292. /**
  7293. * The plugin (derrived from [jQuery.Widget](http://api.jqueryui.com/jQuery.widget/)).
  7294. *
  7295. * **Note:**
  7296. * These methods implement the standard jQuery UI widget API.
  7297. * It is recommended to use methods of the {Fancytree} instance instead
  7298. *
  7299. * @example
  7300. * // DEPRECATED: Access jQuery UI widget methods and members:
  7301. * var tree = $("#tree").fancytree("getTree");
  7302. * var node = $("#tree").fancytree("getActiveNode");
  7303. *
  7304. * // RECOMMENDED: Use the Fancytree object API
  7305. * var tree = $.ui.fancytree.getTree("#tree");
  7306. * var node = tree.getActiveNode();
  7307. *
  7308. * // or you may already have stored the tree instance upon creation:
  7309. * import {createTree, version} from 'jquery.fancytree'
  7310. * const tree = createTree('#tree', { ... });
  7311. * var node = tree.getActiveNode();
  7312. *
  7313. * @see {Fancytree_Static#getTree}
  7314. * @deprecated Use methods of the {Fancytree} instance instead
  7315. * @mixin Fancytree_Widget
  7316. */
  7317. $.widget(
  7318. "ui.fancytree",
  7319. /** @lends Fancytree_Widget# */
  7320. {
  7321. /**These options will be used as defaults
  7322. * @type {FancytreeOptions}
  7323. */
  7324. options: {
  7325. activeVisible: true,
  7326. ajax: {
  7327. type: "GET",
  7328. cache: false, // false: Append random '_' argument to the request url to prevent caching.
  7329. // timeout: 0, // >0: Make sure we get an ajax error if server is unreachable
  7330. dataType: "json", // Expect json format and pass json object to callbacks.
  7331. },
  7332. aria: true,
  7333. autoActivate: true,
  7334. autoCollapse: false,
  7335. autoScroll: false,
  7336. checkbox: false,
  7337. clickFolderMode: 4,
  7338. copyFunctionsToData: false,
  7339. debugLevel: null, // 0..4 (null: use global setting $.ui.fancytree.debugLevel)
  7340. disabled: false, // TODO: required anymore?
  7341. enableAspx: 42, // TODO: this is truethy, but distinguishable from true: default will change to false in the future
  7342. escapeTitles: false,
  7343. extensions: [],
  7344. focusOnSelect: false,
  7345. generateIds: false,
  7346. icon: true,
  7347. idPrefix: "ft_",
  7348. keyboard: true,
  7349. keyPathSeparator: "/",
  7350. minExpandLevel: 1,
  7351. nodata: true, // (bool, string, or callback) display message, when no data available
  7352. quicksearch: false,
  7353. rtl: false,
  7354. scrollOfs: { top: 0, bottom: 0 },
  7355. scrollParent: null,
  7356. selectMode: 2,
  7357. strings: {
  7358. loading: "Loading...", // &#8230; would be escaped when escapeTitles is true
  7359. loadError: "Load error!",
  7360. moreData: "More...",
  7361. noData: "No data.",
  7362. },
  7363. tabindex: "0",
  7364. titlesTabbable: false,
  7365. toggleEffect: { effect: "slideToggle", duration: 200 }, //< "toggle" or "slideToggle" to use jQuery instead of jQueryUI for toggleEffect animation
  7366. tooltip: false,
  7367. treeId: null,
  7368. _classNames: {
  7369. active: "fancytree-active",
  7370. animating: "fancytree-animating",
  7371. combinedExpanderPrefix: "fancytree-exp-",
  7372. combinedIconPrefix: "fancytree-ico-",
  7373. error: "fancytree-error",
  7374. expanded: "fancytree-expanded",
  7375. focused: "fancytree-focused",
  7376. folder: "fancytree-folder",
  7377. hasChildren: "fancytree-has-children",
  7378. lastsib: "fancytree-lastsib",
  7379. lazy: "fancytree-lazy",
  7380. loading: "fancytree-loading",
  7381. node: "fancytree-node",
  7382. partload: "fancytree-partload",
  7383. partsel: "fancytree-partsel",
  7384. radio: "fancytree-radio",
  7385. selected: "fancytree-selected",
  7386. statusNodePrefix: "fancytree-statusnode-",
  7387. unselectable: "fancytree-unselectable",
  7388. },
  7389. // events
  7390. lazyLoad: null,
  7391. postProcess: null,
  7392. },
  7393. _deprecationWarning: function (name) {
  7394. var tree = this.tree;
  7395. if (tree && tree.options.debugLevel >= 3) {
  7396. tree.warn(
  7397. "$().fancytree('" +
  7398. name +
  7399. "') is deprecated (see https://wwwendt.de/tech/fancytree/doc/jsdoc/Fancytree_Widget.html"
  7400. );
  7401. }
  7402. },
  7403. /* Set up the widget, Called on first $().fancytree() */
  7404. _create: function () {
  7405. this.tree = new Fancytree(this);
  7406. this.$source =
  7407. this.source || this.element.data("type") === "json"
  7408. ? this.element
  7409. : this.element.find(">ul").first();
  7410. // Subclass Fancytree instance with all enabled extensions
  7411. var extension,
  7412. extName,
  7413. i,
  7414. opts = this.options,
  7415. extensions = opts.extensions,
  7416. base = this.tree;
  7417. for (i = 0; i < extensions.length; i++) {
  7418. extName = extensions[i];
  7419. extension = $.ui.fancytree._extensions[extName];
  7420. if (!extension) {
  7421. $.error(
  7422. "Could not apply extension '" +
  7423. extName +
  7424. "' (it is not registered, did you forget to include it?)"
  7425. );
  7426. }
  7427. // Add extension options as tree.options.EXTENSION
  7428. // _assert(!this.tree.options[extName], "Extension name must not exist as option name: " + extName);
  7429. // console.info("extend " + extName, extension.options, this.tree.options[extName])
  7430. // issue #876: we want to replace custom array-options, not merge them
  7431. this.tree.options[extName] = _simpleDeepMerge(
  7432. {},
  7433. extension.options,
  7434. this.tree.options[extName]
  7435. );
  7436. // this.tree.options[extName] = $.extend(true, {}, extension.options, this.tree.options[extName]);
  7437. // console.info("extend " + extName + " =>", this.tree.options[extName])
  7438. // console.info("extend " + extName + " org default =>", extension.options)
  7439. // Add a namespace tree.ext.EXTENSION, to hold instance data
  7440. _assert(
  7441. this.tree.ext[extName] === undefined,
  7442. "Extension name must not exist as Fancytree.ext attribute: '" +
  7443. extName +
  7444. "'"
  7445. );
  7446. // this.tree[extName] = extension;
  7447. this.tree.ext[extName] = {};
  7448. // Subclass Fancytree methods using proxies.
  7449. _subclassObject(this.tree, base, extension, extName);
  7450. // current extension becomes base for the next extension
  7451. base = extension;
  7452. }
  7453. //
  7454. if (opts.icons !== undefined) {
  7455. // 2015-11-16
  7456. if (opts.icon === true) {
  7457. this.tree.warn(
  7458. "'icons' tree option is deprecated since v2.14.0: use 'icon' instead"
  7459. );
  7460. opts.icon = opts.icons;
  7461. } else {
  7462. $.error(
  7463. "'icons' tree option is deprecated since v2.14.0: use 'icon' only instead"
  7464. );
  7465. }
  7466. }
  7467. if (opts.iconClass !== undefined) {
  7468. // 2015-11-16
  7469. if (opts.icon) {
  7470. $.error(
  7471. "'iconClass' tree option is deprecated since v2.14.0: use 'icon' only instead"
  7472. );
  7473. } else {
  7474. this.tree.warn(
  7475. "'iconClass' tree option is deprecated since v2.14.0: use 'icon' instead"
  7476. );
  7477. opts.icon = opts.iconClass;
  7478. }
  7479. }
  7480. if (opts.tabbable !== undefined) {
  7481. // 2016-04-04
  7482. opts.tabindex = opts.tabbable ? "0" : "-1";
  7483. this.tree.warn(
  7484. "'tabbable' tree option is deprecated since v2.17.0: use 'tabindex='" +
  7485. opts.tabindex +
  7486. "' instead"
  7487. );
  7488. }
  7489. //
  7490. this.tree._callHook("treeCreate", this.tree);
  7491. // Note: 'fancytreecreate' event is fired by widget base class
  7492. // this.tree._triggerTreeEvent("create");
  7493. },
  7494. /* Called on every $().fancytree() */
  7495. _init: function () {
  7496. this.tree._callHook("treeInit", this.tree);
  7497. // TODO: currently we call bind after treeInit, because treeInit
  7498. // might change tree.$container.
  7499. // It would be better, to move event binding into hooks altogether
  7500. this._bind();
  7501. },
  7502. /* Use the _setOption method to respond to changes to options. */
  7503. _setOption: function (key, value) {
  7504. return this.tree._callHook(
  7505. "treeSetOption",
  7506. this.tree,
  7507. key,
  7508. value
  7509. );
  7510. },
  7511. /** Use the destroy method to clean up any modifications your widget has made to the DOM */
  7512. _destroy: function () {
  7513. this._unbind();
  7514. this.tree._callHook("treeDestroy", this.tree);
  7515. // In jQuery UI 1.8, you must invoke the destroy method from the base widget
  7516. // $.Widget.prototype.destroy.call(this);
  7517. // TODO: delete tree and nodes to make garbage collect easier?
  7518. // TODO: In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method
  7519. },
  7520. // -------------------------------------------------------------------------
  7521. /* Remove all event handlers for our namespace */
  7522. _unbind: function () {
  7523. var ns = this.tree._ns;
  7524. this.element.off(ns);
  7525. this.tree.$container.off(ns);
  7526. $(document).off(ns);
  7527. },
  7528. /* Add mouse and kyboard handlers to the container */
  7529. _bind: function () {
  7530. var self = this,
  7531. opts = this.options,
  7532. tree = this.tree,
  7533. ns = tree._ns;
  7534. // selstartEvent = ( $.support.selectstart ? "selectstart" : "mousedown" )
  7535. // Remove all previuous handlers for this tree
  7536. this._unbind();
  7537. //alert("keydown" + ns + "foc=" + tree.hasFocus() + tree.$container);
  7538. // tree.debug("bind events; container: ", tree.$container);
  7539. tree.$container
  7540. .on("focusin" + ns + " focusout" + ns, function (event) {
  7541. var node = FT.getNode(event),
  7542. flag = event.type === "focusin";
  7543. if (!flag && node && $(event.target).is("a")) {
  7544. // #764
  7545. node.debug(
  7546. "Ignored focusout on embedded <a> element."
  7547. );
  7548. return;
  7549. }
  7550. // tree.treeOnFocusInOut.call(tree, event);
  7551. // tree.debug("Tree container got event " + event.type, node, event, FT.getEventTarget(event));
  7552. if (flag) {
  7553. if (tree._getExpiringValue("focusin")) {
  7554. // #789: IE 11 may send duplicate focusin events
  7555. tree.debug("Ignored double focusin.");
  7556. return;
  7557. }
  7558. tree._setExpiringValue("focusin", true, 50);
  7559. if (!node) {
  7560. // #789: IE 11 may send focusin before mousdown(?)
  7561. node = tree._getExpiringValue("mouseDownNode");
  7562. if (node) {
  7563. tree.debug(
  7564. "Reconstruct mouse target for focusin from recent event."
  7565. );
  7566. }
  7567. }
  7568. }
  7569. if (node) {
  7570. // For example clicking into an <input> that is part of a node
  7571. tree._callHook(
  7572. "nodeSetFocus",
  7573. tree._makeHookContext(node, event),
  7574. flag
  7575. );
  7576. } else {
  7577. if (
  7578. tree.tbody &&
  7579. $(event.target).parents(
  7580. "table.fancytree-container > thead"
  7581. ).length
  7582. ) {
  7583. // #767: ignore events in the table's header
  7584. tree.debug(
  7585. "Ignore focus event outside table body.",
  7586. event
  7587. );
  7588. } else {
  7589. tree._callHook("treeSetFocus", tree, flag);
  7590. }
  7591. }
  7592. })
  7593. .on(
  7594. "selectstart" + ns,
  7595. "span.fancytree-title",
  7596. function (event) {
  7597. // prevent mouse-drags to select text ranges
  7598. // tree.debug("<span title> got event " + event.type);
  7599. event.preventDefault();
  7600. }
  7601. )
  7602. .on("keydown" + ns, function (event) {
  7603. // TODO: also bind keyup and keypress
  7604. // tree.debug("got event " + event.type + ", hasFocus:" + tree.hasFocus());
  7605. // if(opts.disabled || opts.keyboard === false || !tree.hasFocus() ){
  7606. if (opts.disabled || opts.keyboard === false) {
  7607. return true;
  7608. }
  7609. var res,
  7610. node = tree.focusNode, // node may be null
  7611. ctx = tree._makeHookContext(node || tree, event),
  7612. prevPhase = tree.phase;
  7613. try {
  7614. tree.phase = "userEvent";
  7615. // If a 'fancytreekeydown' handler returns false, skip the default
  7616. // handling (implemented by tree.nodeKeydown()).
  7617. if (node) {
  7618. res = tree._triggerNodeEvent(
  7619. "keydown",
  7620. node,
  7621. event
  7622. );
  7623. } else {
  7624. res = tree._triggerTreeEvent("keydown", event);
  7625. }
  7626. if (res === "preventNav") {
  7627. res = true; // prevent keyboard navigation, but don't prevent default handling of embedded input controls
  7628. } else if (res !== false) {
  7629. res = tree._callHook("nodeKeydown", ctx);
  7630. }
  7631. return res;
  7632. } finally {
  7633. tree.phase = prevPhase;
  7634. }
  7635. })
  7636. .on("mousedown" + ns, function (event) {
  7637. var et = FT.getEventTarget(event);
  7638. // self.tree.debug("event(" + event.type + "): node: ", et.node);
  7639. // #712: Store the clicked node, so we can use it when we get a focusin event
  7640. // ('click' event fires after focusin)
  7641. // tree.debug("event(" + event.type + "): node: ", et.node);
  7642. tree._lastMousedownNode = et ? et.node : null;
  7643. // #789: Store the node also for a short period, so we can use it
  7644. // in a *resulting* focusin event
  7645. tree._setExpiringValue(
  7646. "mouseDownNode",
  7647. tree._lastMousedownNode
  7648. );
  7649. })
  7650. .on("click" + ns + " dblclick" + ns, function (event) {
  7651. if (opts.disabled) {
  7652. return true;
  7653. }
  7654. var ctx,
  7655. et = FT.getEventTarget(event),
  7656. node = et.node,
  7657. tree = self.tree,
  7658. prevPhase = tree.phase;
  7659. // self.tree.debug("event(" + event.type + "): node: ", node);
  7660. if (!node) {
  7661. return true; // Allow bubbling of other events
  7662. }
  7663. ctx = tree._makeHookContext(node, event);
  7664. // self.tree.debug("event(" + event.type + "): node: ", node);
  7665. try {
  7666. tree.phase = "userEvent";
  7667. switch (event.type) {
  7668. case "click":
  7669. ctx.targetType = et.type;
  7670. if (node.isPagingNode()) {
  7671. return (
  7672. tree._triggerNodeEvent(
  7673. "clickPaging",
  7674. ctx,
  7675. event
  7676. ) === true
  7677. );
  7678. }
  7679. return tree._triggerNodeEvent(
  7680. "click",
  7681. ctx,
  7682. event
  7683. ) === false
  7684. ? false
  7685. : tree._callHook("nodeClick", ctx);
  7686. case "dblclick":
  7687. ctx.targetType = et.type;
  7688. return tree._triggerNodeEvent(
  7689. "dblclick",
  7690. ctx,
  7691. event
  7692. ) === false
  7693. ? false
  7694. : tree._callHook("nodeDblclick", ctx);
  7695. }
  7696. } finally {
  7697. tree.phase = prevPhase;
  7698. }
  7699. });
  7700. },
  7701. /** Return the active node or null.
  7702. * @returns {FancytreeNode}
  7703. * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>).
  7704. */
  7705. getActiveNode: function () {
  7706. this._deprecationWarning("getActiveNode");
  7707. return this.tree.activeNode;
  7708. },
  7709. /** Return the matching node or null.
  7710. * @param {string} key
  7711. * @returns {FancytreeNode}
  7712. * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>).
  7713. */
  7714. getNodeByKey: function (key) {
  7715. this._deprecationWarning("getNodeByKey");
  7716. return this.tree.getNodeByKey(key);
  7717. },
  7718. /** Return the invisible system root node.
  7719. * @returns {FancytreeNode}
  7720. * @deprecated Use methods of the Fancytree instance instead (<a href="Fancytree_Widget.html">example above</a>).
  7721. */
  7722. getRootNode: function () {
  7723. this._deprecationWarning("getRootNode");
  7724. return this.tree.rootNode;
  7725. },
  7726. /** Return the current tree instance.
  7727. * @returns {Fancytree}
  7728. * @deprecated Use `$.ui.fancytree.getTree()` instead (<a href="Fancytree_Widget.html">example above</a>).
  7729. */
  7730. getTree: function () {
  7731. this._deprecationWarning("getTree");
  7732. return this.tree;
  7733. },
  7734. }
  7735. );
  7736. // $.ui.fancytree was created by the widget factory. Create a local shortcut:
  7737. FT = $.ui.fancytree;
  7738. /**
  7739. * Static members in the `$.ui.fancytree` namespace.
  7740. * This properties and methods can be accessed without instantiating a concrete
  7741. * Fancytree instance.
  7742. *
  7743. * @example
  7744. * // Access static members:
  7745. * var node = $.ui.fancytree.getNode(element);
  7746. * alert($.ui.fancytree.version);
  7747. *
  7748. * @mixin Fancytree_Static
  7749. */
  7750. $.extend(
  7751. $.ui.fancytree,
  7752. /** @lends Fancytree_Static# */
  7753. {
  7754. /** Version number `"MAJOR.MINOR.PATCH"`
  7755. * @type {string} */
  7756. version: "2.38.3", // Set to semver by 'grunt release'
  7757. /** @type {string}
  7758. * @description `"production" for release builds` */
  7759. buildType: "production", // Set to 'production' by 'grunt build'
  7760. /** @type {int}
  7761. * @description 0: silent .. 5: verbose (default: 3 for release builds). */
  7762. debugLevel: 3, // Set to 3 by 'grunt build'
  7763. // Used by $.ui.fancytree.debug() and as default for tree.options.debugLevel
  7764. _nextId: 1,
  7765. _nextNodeKey: 1,
  7766. _extensions: {},
  7767. // focusTree: null,
  7768. /** Expose class object as `$.ui.fancytree._FancytreeClass`.
  7769. * Useful to extend `$.ui.fancytree._FancytreeClass.prototype`.
  7770. * @type {Fancytree}
  7771. */
  7772. _FancytreeClass: Fancytree,
  7773. /** Expose class object as $.ui.fancytree._FancytreeNodeClass
  7774. * Useful to extend `$.ui.fancytree._FancytreeNodeClass.prototype`.
  7775. * @type {FancytreeNode}
  7776. */
  7777. _FancytreeNodeClass: FancytreeNode,
  7778. /* Feature checks to provide backwards compatibility */
  7779. jquerySupports: {
  7780. // http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at
  7781. positionMyOfs: isVersionAtLeast($.ui.version, 1, 9),
  7782. },
  7783. /** Throw an error if condition fails (debug method).
  7784. * @param {boolean} cond
  7785. * @param {string} msg
  7786. */
  7787. assert: function (cond, msg) {
  7788. return _assert(cond, msg);
  7789. },
  7790. /** Create a new Fancytree instance on a target element.
  7791. *
  7792. * @param {Element | jQueryObject | string} el Target DOM element or selector
  7793. * @param {FancytreeOptions} [opts] Fancytree options
  7794. * @returns {Fancytree} new tree instance
  7795. * @example
  7796. * var tree = $.ui.fancytree.createTree("#tree", {
  7797. * source: {url: "my/webservice"}
  7798. * }); // Create tree for this matching element
  7799. *
  7800. * @since 2.25
  7801. */
  7802. createTree: function (el, opts) {
  7803. var $tree = $(el).fancytree(opts);
  7804. return FT.getTree($tree);
  7805. },
  7806. /** Return a function that executes *fn* at most every *timeout* ms.
  7807. * @param {integer} timeout
  7808. * @param {function} fn
  7809. * @param {boolean} [invokeAsap=false]
  7810. * @param {any} [ctx]
  7811. */
  7812. debounce: function (timeout, fn, invokeAsap, ctx) {
  7813. var timer;
  7814. if (arguments.length === 3 && typeof invokeAsap !== "boolean") {
  7815. ctx = invokeAsap;
  7816. invokeAsap = false;
  7817. }
  7818. return function () {
  7819. var args = arguments;
  7820. ctx = ctx || this;
  7821. // eslint-disable-next-line no-unused-expressions
  7822. invokeAsap && !timer && fn.apply(ctx, args);
  7823. clearTimeout(timer);
  7824. timer = setTimeout(function () {
  7825. // eslint-disable-next-line no-unused-expressions
  7826. invokeAsap || fn.apply(ctx, args);
  7827. timer = null;
  7828. }, timeout);
  7829. };
  7830. },
  7831. /** Write message to console if debugLevel >= 4
  7832. * @param {string} msg
  7833. */
  7834. debug: function (msg) {
  7835. if ($.ui.fancytree.debugLevel >= 4) {
  7836. consoleApply("log", arguments);
  7837. }
  7838. },
  7839. /** Write error message to console if debugLevel >= 1.
  7840. * @param {string} msg
  7841. */
  7842. error: function (msg) {
  7843. if ($.ui.fancytree.debugLevel >= 1) {
  7844. consoleApply("error", arguments);
  7845. }
  7846. },
  7847. /** Convert `<`, `>`, `&`, `"`, `'`, and `/` to the equivalent entities.
  7848. *
  7849. * @param {string} s
  7850. * @returns {string}
  7851. */
  7852. escapeHtml: function (s) {
  7853. return ("" + s).replace(REX_HTML, function (s) {
  7854. return ENTITY_MAP[s];
  7855. });
  7856. },
  7857. /** Make jQuery.position() arguments backwards compatible, i.e. if
  7858. * jQuery UI version <= 1.8, convert
  7859. * { my: "left+3 center", at: "left bottom", of: $target }
  7860. * to
  7861. * { my: "left center", at: "left bottom", of: $target, offset: "3 0" }
  7862. *
  7863. * See http://jqueryui.com/upgrade-guide/1.9/#deprecated-offset-option-merged-into-my-and-at
  7864. * and http://jsfiddle.net/mar10/6xtu9a4e/
  7865. *
  7866. * @param {object} opts
  7867. * @returns {object} the (potentially modified) original opts hash object
  7868. */
  7869. fixPositionOptions: function (opts) {
  7870. if (opts.offset || ("" + opts.my + opts.at).indexOf("%") >= 0) {
  7871. $.error(
  7872. "expected new position syntax (but '%' is not supported)"
  7873. );
  7874. }
  7875. if (!$.ui.fancytree.jquerySupports.positionMyOfs) {
  7876. var // parse 'left+3 center' into ['left+3 center', 'left', '+3', 'center', undefined]
  7877. myParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec(
  7878. opts.my
  7879. ),
  7880. atParts = /(\w+)([+-]?\d+)?\s+(\w+)([+-]?\d+)?/.exec(
  7881. opts.at
  7882. ),
  7883. // convert to numbers
  7884. dx =
  7885. (myParts[2] ? +myParts[2] : 0) +
  7886. (atParts[2] ? +atParts[2] : 0),
  7887. dy =
  7888. (myParts[4] ? +myParts[4] : 0) +
  7889. (atParts[4] ? +atParts[4] : 0);
  7890. opts = $.extend({}, opts, {
  7891. // make a copy and overwrite
  7892. my: myParts[1] + " " + myParts[3],
  7893. at: atParts[1] + " " + atParts[3],
  7894. });
  7895. if (dx || dy) {
  7896. opts.offset = "" + dx + " " + dy;
  7897. }
  7898. }
  7899. return opts;
  7900. },
  7901. /** Return a {node: FancytreeNode, type: TYPE} object for a mouse event.
  7902. *
  7903. * @param {Event} event Mouse event, e.g. click, ...
  7904. * @returns {object} Return a {node: FancytreeNode, type: TYPE} object
  7905. * TYPE: 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
  7906. */
  7907. getEventTarget: function (event) {
  7908. var $target,
  7909. tree,
  7910. tcn = event && event.target ? event.target.className : "",
  7911. res = { node: this.getNode(event.target), type: undefined };
  7912. // We use a fast version of $(res.node).hasClass()
  7913. // See http://jsperf.com/test-for-classname/2
  7914. if (/\bfancytree-title\b/.test(tcn)) {
  7915. res.type = "title";
  7916. } else if (/\bfancytree-expander\b/.test(tcn)) {
  7917. res.type =
  7918. res.node.hasChildren() === false
  7919. ? "prefix"
  7920. : "expander";
  7921. // }else if( /\bfancytree-checkbox\b/.test(tcn) || /\bfancytree-radio\b/.test(tcn) ){
  7922. } else if (/\bfancytree-checkbox\b/.test(tcn)) {
  7923. res.type = "checkbox";
  7924. } else if (/\bfancytree(-custom)?-icon\b/.test(tcn)) {
  7925. res.type = "icon";
  7926. } else if (/\bfancytree-node\b/.test(tcn)) {
  7927. // Somewhere near the title
  7928. res.type = "title";
  7929. } else if (event && event.target) {
  7930. $target = $(event.target);
  7931. if ($target.is("ul[role=group]")) {
  7932. // #nnn: Clicking right to a node may hit the surrounding UL
  7933. tree = res.node && res.node.tree;
  7934. (tree || FT).debug("Ignoring click on outer UL.");
  7935. res.node = null;
  7936. } else if ($target.closest(".fancytree-title").length) {
  7937. // #228: clicking an embedded element inside a title
  7938. res.type = "title";
  7939. } else if ($target.closest(".fancytree-checkbox").length) {
  7940. // E.g. <svg> inside checkbox span
  7941. res.type = "checkbox";
  7942. } else if ($target.closest(".fancytree-expander").length) {
  7943. res.type = "expander";
  7944. }
  7945. }
  7946. return res;
  7947. },
  7948. /** Return a string describing the affected node region for a mouse event.
  7949. *
  7950. * @param {Event} event Mouse event, e.g. click, mousemove, ...
  7951. * @returns {string} 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon' | undefined
  7952. */
  7953. getEventTargetType: function (event) {
  7954. return this.getEventTarget(event).type;
  7955. },
  7956. /** Return a FancytreeNode instance from element, event, or jQuery object.
  7957. *
  7958. * @param {Element | jQueryObject | Event} el
  7959. * @returns {FancytreeNode} matching node or null
  7960. */
  7961. getNode: function (el) {
  7962. if (el instanceof FancytreeNode) {
  7963. return el; // el already was a FancytreeNode
  7964. } else if (el instanceof $) {
  7965. el = el[0]; // el was a jQuery object: use the DOM element
  7966. } else if (el.originalEvent !== undefined) {
  7967. el = el.target; // el was an Event
  7968. }
  7969. while (el) {
  7970. if (el.ftnode) {
  7971. return el.ftnode;
  7972. }
  7973. el = el.parentNode;
  7974. }
  7975. return null;
  7976. },
  7977. /** Return a Fancytree instance, from element, index, event, or jQueryObject.
  7978. *
  7979. * @param {Element | jQueryObject | Event | integer | string} [el]
  7980. * @returns {Fancytree} matching tree or null
  7981. * @example
  7982. * $.ui.fancytree.getTree(); // Get first Fancytree instance on page
  7983. * $.ui.fancytree.getTree(1); // Get second Fancytree instance on page
  7984. * $.ui.fancytree.getTree(event); // Get tree for this mouse- or keyboard event
  7985. * $.ui.fancytree.getTree("foo"); // Get tree for this `opts.treeId`
  7986. * $.ui.fancytree.getTree("#tree"); // Get tree for this matching element
  7987. *
  7988. * @since 2.13
  7989. */
  7990. getTree: function (el) {
  7991. var widget,
  7992. orgEl = el;
  7993. if (el instanceof Fancytree) {
  7994. return el; // el already was a Fancytree
  7995. }
  7996. if (el === undefined) {
  7997. el = 0; // get first tree
  7998. }
  7999. if (typeof el === "number") {
  8000. el = $(".fancytree-container").eq(el); // el was an integer: return nth instance
  8001. } else if (typeof el === "string") {
  8002. // `el` may be a treeId or a selector:
  8003. el = $("#ft-id-" + orgEl).eq(0);
  8004. if (!el.length) {
  8005. el = $(orgEl).eq(0); // el was a selector: use first match
  8006. }
  8007. } else if (
  8008. el instanceof Element ||
  8009. el instanceof HTMLDocument
  8010. ) {
  8011. el = $(el);
  8012. } else if (el instanceof $) {
  8013. el = el.eq(0); // el was a jQuery object: use the first
  8014. } else if (el.originalEvent !== undefined) {
  8015. el = $(el.target); // el was an Event
  8016. }
  8017. // el is a jQuery object wit one element here
  8018. el = el.closest(":ui-fancytree");
  8019. widget = el.data("ui-fancytree") || el.data("fancytree"); // the latter is required by jQuery <= 1.8
  8020. return widget ? widget.tree : null;
  8021. },
  8022. /** Return an option value that has a default, but may be overridden by a
  8023. * callback or a node instance attribute.
  8024. *
  8025. * Evaluation sequence:
  8026. *
  8027. * If `tree.options.<optionName>` is a callback that returns something, use that.
  8028. * Else if `node.<optionName>` is defined, use that.
  8029. * Else if `tree.options.<optionName>` is a value, use that.
  8030. * Else use `defaultValue`.
  8031. *
  8032. * @param {string} optionName name of the option property (on node and tree)
  8033. * @param {FancytreeNode} node passed to the callback
  8034. * @param {object} nodeObject where to look for the local option property, e.g. `node` or `node.data`
  8035. * @param {object} treeOption where to look for the tree option, e.g. `tree.options` or `tree.options.dnd5`
  8036. * @param {any} [defaultValue]
  8037. * @returns {any}
  8038. *
  8039. * @example
  8040. * // Check for node.foo, tree,options.foo(), and tree.options.foo:
  8041. * $.ui.fancytree.evalOption("foo", node, node, tree.options);
  8042. * // Check for node.data.bar, tree,options.qux.bar(), and tree.options.qux.bar:
  8043. * $.ui.fancytree.evalOption("bar", node, node.data, tree.options.qux);
  8044. *
  8045. * @since 2.22
  8046. */
  8047. evalOption: function (
  8048. optionName,
  8049. node,
  8050. nodeObject,
  8051. treeOptions,
  8052. defaultValue
  8053. ) {
  8054. var ctx,
  8055. res,
  8056. tree = node.tree,
  8057. treeOpt = treeOptions[optionName],
  8058. nodeOpt = nodeObject[optionName];
  8059. if (_isFunction(treeOpt)) {
  8060. ctx = {
  8061. node: node,
  8062. tree: tree,
  8063. widget: tree.widget,
  8064. options: tree.widget.options,
  8065. typeInfo: tree.types[node.type] || {},
  8066. };
  8067. res = treeOpt.call(tree, { type: optionName }, ctx);
  8068. if (res == null) {
  8069. res = nodeOpt;
  8070. }
  8071. } else {
  8072. res = nodeOpt == null ? treeOpt : nodeOpt;
  8073. }
  8074. if (res == null) {
  8075. res = defaultValue; // no option set at all: return default
  8076. }
  8077. return res;
  8078. },
  8079. /** Set expander, checkbox, or node icon, supporting string and object format.
  8080. *
  8081. * @param {Element | jQueryObject} span
  8082. * @param {string} baseClass
  8083. * @param {string | object} icon
  8084. * @since 2.27
  8085. */
  8086. setSpanIcon: function (span, baseClass, icon) {
  8087. var $span = $(span);
  8088. if (typeof icon === "string") {
  8089. $span.attr("class", baseClass + " " + icon);
  8090. } else {
  8091. // support object syntax: { text: ligature, addClasse: classname }
  8092. if (icon.text) {
  8093. $span.text("" + icon.text);
  8094. } else if (icon.html) {
  8095. span.innerHTML = icon.html;
  8096. }
  8097. $span.attr(
  8098. "class",
  8099. baseClass + " " + (icon.addClass || "")
  8100. );
  8101. }
  8102. },
  8103. /** Convert a keydown or mouse event to a canonical string like 'ctrl+a',
  8104. * 'ctrl+shift+f2', 'shift+leftdblclick'.
  8105. *
  8106. * This is especially handy for switch-statements in event handlers.
  8107. *
  8108. * @param {event}
  8109. * @returns {string}
  8110. *
  8111. * @example
  8112. switch( $.ui.fancytree.eventToString(event) ) {
  8113. case "-":
  8114. tree.nodeSetExpanded(ctx, false);
  8115. break;
  8116. case "shift+return":
  8117. tree.nodeSetActive(ctx, true);
  8118. break;
  8119. case "down":
  8120. res = node.navigate(event.which, activate);
  8121. break;
  8122. default:
  8123. handled = false;
  8124. }
  8125. if( handled ){
  8126. event.preventDefault();
  8127. }
  8128. */
  8129. eventToString: function (event) {
  8130. // Poor-man's hotkeys. See here for a complete implementation:
  8131. // https://github.com/jeresig/jquery.hotkeys
  8132. var which = event.which,
  8133. et = event.type,
  8134. s = [];
  8135. if (event.altKey) {
  8136. s.push("alt");
  8137. }
  8138. if (event.ctrlKey) {
  8139. s.push("ctrl");
  8140. }
  8141. if (event.metaKey) {
  8142. s.push("meta");
  8143. }
  8144. if (event.shiftKey) {
  8145. s.push("shift");
  8146. }
  8147. if (et === "click" || et === "dblclick") {
  8148. s.push(MOUSE_BUTTONS[event.button] + et);
  8149. } else if (et === "wheel") {
  8150. s.push(et);
  8151. } else if (!IGNORE_KEYCODES[which]) {
  8152. s.push(
  8153. SPECIAL_KEYCODES[which] ||
  8154. String.fromCharCode(which).toLowerCase()
  8155. );
  8156. }
  8157. return s.join("+");
  8158. },
  8159. /** Write message to console if debugLevel >= 3
  8160. * @param {string} msg
  8161. */
  8162. info: function (msg) {
  8163. if ($.ui.fancytree.debugLevel >= 3) {
  8164. consoleApply("info", arguments);
  8165. }
  8166. },
  8167. /* @deprecated: use eventToString(event) instead.
  8168. */
  8169. keyEventToString: function (event) {
  8170. this.warn(
  8171. "keyEventToString() is deprecated: use eventToString()"
  8172. );
  8173. return this.eventToString(event);
  8174. },
  8175. /** Return a wrapped handler method, that provides `this._super`.
  8176. *
  8177. * @example
  8178. // Implement `opts.createNode` event to add the 'draggable' attribute
  8179. $.ui.fancytree.overrideMethod(ctx.options, "createNode", function(event, data) {
  8180. // Default processing if any
  8181. this._super.apply(this, arguments);
  8182. // Add 'draggable' attribute
  8183. data.node.span.draggable = true;
  8184. });
  8185. *
  8186. * @param {object} instance
  8187. * @param {string} methodName
  8188. * @param {function} handler
  8189. * @param {object} [context] optional context
  8190. */
  8191. overrideMethod: function (instance, methodName, handler, context) {
  8192. var prevSuper,
  8193. _super = instance[methodName] || $.noop;
  8194. instance[methodName] = function () {
  8195. var self = context || this;
  8196. try {
  8197. prevSuper = self._super;
  8198. self._super = _super;
  8199. return handler.apply(self, arguments);
  8200. } finally {
  8201. self._super = prevSuper;
  8202. }
  8203. };
  8204. },
  8205. /**
  8206. * Parse tree data from HTML <ul> markup
  8207. *
  8208. * @param {jQueryObject} $ul
  8209. * @returns {NodeData[]}
  8210. */
  8211. parseHtml: function ($ul) {
  8212. var classes,
  8213. className,
  8214. extraClasses,
  8215. i,
  8216. iPos,
  8217. l,
  8218. tmp,
  8219. tmp2,
  8220. $children = $ul.find(">li"),
  8221. children = [];
  8222. $children.each(function () {
  8223. var allData,
  8224. lowerCaseAttr,
  8225. $li = $(this),
  8226. $liSpan = $li.find(">span", this).first(),
  8227. $liA = $liSpan.length ? null : $li.find(">a").first(),
  8228. d = { tooltip: null, data: {} };
  8229. if ($liSpan.length) {
  8230. d.title = $liSpan.html();
  8231. } else if ($liA && $liA.length) {
  8232. // If a <li><a> tag is specified, use it literally and extract href/target.
  8233. d.title = $liA.html();
  8234. d.data.href = $liA.attr("href");
  8235. d.data.target = $liA.attr("target");
  8236. d.tooltip = $liA.attr("title");
  8237. } else {
  8238. // If only a <li> tag is specified, use the trimmed string up to
  8239. // the next child <ul> tag.
  8240. d.title = $li.html();
  8241. iPos = d.title.search(/<ul/i);
  8242. if (iPos >= 0) {
  8243. d.title = d.title.substring(0, iPos);
  8244. }
  8245. }
  8246. d.title = _trim(d.title);
  8247. // Make sure all fields exist
  8248. for (i = 0, l = CLASS_ATTRS.length; i < l; i++) {
  8249. d[CLASS_ATTRS[i]] = undefined;
  8250. }
  8251. // Initialize to `true`, if class is set and collect extraClasses
  8252. classes = this.className.split(" ");
  8253. extraClasses = [];
  8254. for (i = 0, l = classes.length; i < l; i++) {
  8255. className = classes[i];
  8256. if (CLASS_ATTR_MAP[className]) {
  8257. d[className] = true;
  8258. } else {
  8259. extraClasses.push(className);
  8260. }
  8261. }
  8262. d.extraClasses = extraClasses.join(" ");
  8263. // Parse node options from ID, title and class attributes
  8264. tmp = $li.attr("title");
  8265. if (tmp) {
  8266. d.tooltip = tmp; // overrides <a title='...'>
  8267. }
  8268. tmp = $li.attr("id");
  8269. if (tmp) {
  8270. d.key = tmp;
  8271. }
  8272. // Translate hideCheckbox -> checkbox:false
  8273. if ($li.attr("hideCheckbox")) {
  8274. d.checkbox = false;
  8275. }
  8276. // Add <li data-NAME='...'> as node.data.NAME
  8277. allData = _getElementDataAsDict($li);
  8278. if (allData && !$.isEmptyObject(allData)) {
  8279. // #507: convert data-hidecheckbox (lower case) to hideCheckbox
  8280. for (lowerCaseAttr in NODE_ATTR_LOWERCASE_MAP) {
  8281. if (_hasProp(allData, lowerCaseAttr)) {
  8282. allData[
  8283. NODE_ATTR_LOWERCASE_MAP[lowerCaseAttr]
  8284. ] = allData[lowerCaseAttr];
  8285. delete allData[lowerCaseAttr];
  8286. }
  8287. }
  8288. // #56: Allow to set special node.attributes from data-...
  8289. for (i = 0, l = NODE_ATTRS.length; i < l; i++) {
  8290. tmp = NODE_ATTRS[i];
  8291. tmp2 = allData[tmp];
  8292. if (tmp2 != null) {
  8293. delete allData[tmp];
  8294. d[tmp] = tmp2;
  8295. }
  8296. }
  8297. // All other data-... goes to node.data...
  8298. $.extend(d.data, allData);
  8299. }
  8300. // Recursive reading of child nodes, if LI tag contains an UL tag
  8301. $ul = $li.find(">ul").first();
  8302. if ($ul.length) {
  8303. d.children = $.ui.fancytree.parseHtml($ul);
  8304. } else {
  8305. d.children = d.lazy ? undefined : null;
  8306. }
  8307. children.push(d);
  8308. // FT.debug("parse ", d, children);
  8309. });
  8310. return children;
  8311. },
  8312. /** Add Fancytree extension definition to the list of globally available extensions.
  8313. *
  8314. * @param {object} definition
  8315. */
  8316. registerExtension: function (definition) {
  8317. _assert(
  8318. definition.name != null,
  8319. "extensions must have a `name` property."
  8320. );
  8321. _assert(
  8322. definition.version != null,
  8323. "extensions must have a `version` property."
  8324. );
  8325. $.ui.fancytree._extensions[definition.name] = definition;
  8326. },
  8327. /** Replacement for the deprecated `jQuery.trim()`.
  8328. *
  8329. * @param {string} text
  8330. */
  8331. trim: _trim,
  8332. /** Inverse of escapeHtml().
  8333. *
  8334. * @param {string} s
  8335. * @returns {string}
  8336. */
  8337. unescapeHtml: function (s) {
  8338. var e = document.createElement("div");
  8339. e.innerHTML = s;
  8340. return e.childNodes.length === 0
  8341. ? ""
  8342. : e.childNodes[0].nodeValue;
  8343. },
  8344. /** Write warning message to console if debugLevel >= 2.
  8345. * @param {string} msg
  8346. */
  8347. warn: function (msg) {
  8348. if ($.ui.fancytree.debugLevel >= 2) {
  8349. consoleApply("warn", arguments);
  8350. }
  8351. },
  8352. }
  8353. );
  8354. // Value returned by `require('jquery.fancytree')`
  8355. return $.ui.fancytree;
  8356. }); // End of closure
  8357. /*! Extension 'jquery.fancytree.childcounter.js' */// Extending Fancytree
  8358. // ===================
  8359. //
  8360. // See also the [live demo](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html) of this code.
  8361. //
  8362. // Every extension should have a comment header containing some information
  8363. // about the author, copyright and licensing. Also a pointer to the latest
  8364. // source code.
  8365. // Prefix with `/*!` so the comment is not removed by the minifier.
  8366. /*!
  8367. * jquery.fancytree.childcounter.js
  8368. *
  8369. * Add a child counter bubble to tree nodes.
  8370. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  8371. *
  8372. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  8373. *
  8374. * Released under the MIT license
  8375. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  8376. *
  8377. * @version 2.38.3
  8378. * @date 2023-02-01T20:52:50Z
  8379. */
  8380. // To keep the global namespace clean, we wrap everything in a closure.
  8381. // The UMD wrapper pattern defines the dependencies on jQuery and the
  8382. // Fancytree core module, and makes sure that we can use the `require()`
  8383. // syntax with package loaders.
  8384. (function (factory) {
  8385. if (typeof define === "function" && define.amd) {
  8386. // AMD. Register as an anonymous module.
  8387. define(["jquery", "./jquery.fancytree"], factory);
  8388. } else if (typeof module === "object" && module.exports) {
  8389. // Node/CommonJS
  8390. require("./jquery.fancytree");
  8391. module.exports = factory(require("jquery"));
  8392. } else {
  8393. // Browser globals
  8394. factory(jQuery);
  8395. }
  8396. })(function ($) {
  8397. // Consider to use [strict mode](http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/)
  8398. "use strict";
  8399. // The [coding guidelines](http://contribute.jquery.org/style-guide/js/)
  8400. // require jshint /eslint compliance.
  8401. // But for this sample, we want to allow unused variables for demonstration purpose.
  8402. /*eslint-disable no-unused-vars */
  8403. // Adding methods
  8404. // --------------
  8405. // New member functions can be added to the `Fancytree` class.
  8406. // This function will be available for every tree instance:
  8407. //
  8408. // var tree = $.ui.fancytree.getTree("#tree");
  8409. // tree.countSelected(false);
  8410. $.ui.fancytree._FancytreeClass.prototype.countSelected = function (
  8411. topOnly
  8412. ) {
  8413. var tree = this,
  8414. treeOptions = tree.options;
  8415. return tree.getSelectedNodes(topOnly).length;
  8416. };
  8417. // The `FancytreeNode` class can also be easily extended. This would be called
  8418. // like
  8419. // node.updateCounters();
  8420. //
  8421. // It is also good practice to add a docstring comment.
  8422. /**
  8423. * [ext-childcounter] Update counter badges for `node` and its parents.
  8424. * May be called in the `loadChildren` event, to update parents of lazy loaded
  8425. * nodes.
  8426. * @alias FancytreeNode#updateCounters
  8427. * @requires jquery.fancytree.childcounters.js
  8428. */
  8429. $.ui.fancytree._FancytreeNodeClass.prototype.updateCounters = function () {
  8430. var node = this,
  8431. $badge = $("span.fancytree-childcounter", node.span),
  8432. extOpts = node.tree.options.childcounter,
  8433. count = node.countChildren(extOpts.deep);
  8434. node.data.childCounter = count;
  8435. if (
  8436. (count || !extOpts.hideZeros) &&
  8437. (!node.isExpanded() || !extOpts.hideExpanded)
  8438. ) {
  8439. if (!$badge.length) {
  8440. $badge = $("<span class='fancytree-childcounter'/>").appendTo(
  8441. $(
  8442. "span.fancytree-icon,span.fancytree-custom-icon",
  8443. node.span
  8444. )
  8445. );
  8446. }
  8447. $badge.text(count);
  8448. } else {
  8449. $badge.remove();
  8450. }
  8451. if (extOpts.deep && !node.isTopLevel() && !node.isRootNode()) {
  8452. node.parent.updateCounters();
  8453. }
  8454. };
  8455. // Finally, we can extend the widget API and create functions that are called
  8456. // like so:
  8457. //
  8458. // $("#tree").fancytree("widgetMethod1", "abc");
  8459. $.ui.fancytree.prototype.widgetMethod1 = function (arg1) {
  8460. var tree = this.tree;
  8461. return arg1;
  8462. };
  8463. // Register a Fancytree extension
  8464. // ------------------------------
  8465. // A full blown extension, extension is available for all trees and can be
  8466. // enabled like so (see also the [live demo](https://wwWendt.de/tech/fancytree/demo/sample-ext-childcounter.html)):
  8467. //
  8468. // <script src="../src/jquery.fancytree.js"></script>
  8469. // <script src="../src/jquery.fancytree.childcounter.js"></script>
  8470. // ...
  8471. //
  8472. // $("#tree").fancytree({
  8473. // extensions: ["childcounter"],
  8474. // childcounter: {
  8475. // hideExpanded: true
  8476. // },
  8477. // ...
  8478. // });
  8479. //
  8480. /* 'childcounter' extension */
  8481. $.ui.fancytree.registerExtension({
  8482. // Every extension must be registered by a unique name.
  8483. name: "childcounter",
  8484. // Version information should be compliant with [semver](http://semver.org)
  8485. version: "2.38.3",
  8486. // Extension specific options and their defaults.
  8487. // This options will be available as `tree.options.childcounter.hideExpanded`
  8488. options: {
  8489. deep: true,
  8490. hideZeros: true,
  8491. hideExpanded: false,
  8492. },
  8493. // Attributes other than `options` (or functions) can be defined here, and
  8494. // will be added to the tree.ext.EXTNAME namespace, in this case `tree.ext.childcounter.foo`.
  8495. // They can also be accessed as `this._local.foo` from within the extension
  8496. // methods.
  8497. foo: 42,
  8498. // Local functions are prefixed with an underscore '_'.
  8499. // Callable as `this._local._appendCounter()`.
  8500. _appendCounter: function (bar) {
  8501. var tree = this;
  8502. },
  8503. // **Override virtual methods for this extension.**
  8504. //
  8505. // Fancytree implements a number of 'hook methods', prefixed by 'node...' or 'tree...'.
  8506. // with a `ctx` argument (see [EventData](https://wwWendt.de/tech/fancytree/doc/jsdoc/global.html#EventData)
  8507. // for details) and an extended calling context:<br>
  8508. // `this` : the Fancytree instance<br>
  8509. // `this._local`: the namespace that contains extension attributes and private methods (same as this.ext.EXTNAME)<br>
  8510. // `this._super`: the virtual function that was overridden (member of previous extension or Fancytree)
  8511. //
  8512. // See also the [complete list of available hook functions](https://wwWendt.de/tech/fancytree/doc/jsdoc/Fancytree_Hooks.html).
  8513. /* Init */
  8514. // `treeInit` is triggered when a tree is initalized. We can set up classes or
  8515. // bind event handlers here...
  8516. treeInit: function (ctx) {
  8517. var tree = this, // same as ctx.tree,
  8518. opts = ctx.options,
  8519. extOpts = ctx.options.childcounter;
  8520. // Optionally check for dependencies with other extensions
  8521. /* this._requireExtension("glyph", false, false); */
  8522. // Call the base implementation
  8523. this._superApply(arguments);
  8524. // Add a class to the tree container
  8525. this.$container.addClass("fancytree-ext-childcounter");
  8526. },
  8527. // Destroy this tree instance (we only call the default implementation, so
  8528. // this method could as well be omitted).
  8529. treeDestroy: function (ctx) {
  8530. this._superApply(arguments);
  8531. },
  8532. // Overload the `renderTitle` hook, to append a counter badge
  8533. nodeRenderTitle: function (ctx, title) {
  8534. var node = ctx.node,
  8535. extOpts = ctx.options.childcounter,
  8536. count =
  8537. node.data.childCounter == null
  8538. ? node.countChildren(extOpts.deep)
  8539. : +node.data.childCounter;
  8540. // Let the base implementation render the title
  8541. // We use `_super()` instead of `_superApply()` here, since it is a little bit
  8542. // more performant when called often
  8543. this._super(ctx, title);
  8544. // Append a counter badge
  8545. if (
  8546. (count || !extOpts.hideZeros) &&
  8547. (!node.isExpanded() || !extOpts.hideExpanded)
  8548. ) {
  8549. $(
  8550. "span.fancytree-icon,span.fancytree-custom-icon",
  8551. node.span
  8552. ).append(
  8553. $("<span class='fancytree-childcounter'/>").text(count)
  8554. );
  8555. }
  8556. },
  8557. // Overload the `setExpanded` hook, so the counters are updated
  8558. nodeSetExpanded: function (ctx, flag, callOpts) {
  8559. var tree = ctx.tree,
  8560. node = ctx.node;
  8561. // Let the base implementation expand/collapse the node, then redraw the title
  8562. // after the animation has finished
  8563. return this._superApply(arguments).always(function () {
  8564. tree.nodeRenderTitle(ctx);
  8565. });
  8566. },
  8567. // End of extension definition
  8568. });
  8569. // Value returned by `require('jquery.fancytree..')`
  8570. return $.ui.fancytree;
  8571. }); // End of closure
  8572. /*! Extension 'jquery.fancytree.clones.js' *//*!
  8573. *
  8574. * jquery.fancytree.clones.js
  8575. * Support faster lookup of nodes by key and shared ref-ids.
  8576. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  8577. *
  8578. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  8579. *
  8580. * Released under the MIT license
  8581. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  8582. *
  8583. * @version 2.38.3
  8584. * @date 2023-02-01T20:52:50Z
  8585. */
  8586. (function (factory) {
  8587. if (typeof define === "function" && define.amd) {
  8588. // AMD. Register as an anonymous module.
  8589. define(["jquery", "./jquery.fancytree"], factory);
  8590. } else if (typeof module === "object" && module.exports) {
  8591. // Node/CommonJS
  8592. require("./jquery.fancytree");
  8593. module.exports = factory(require("jquery"));
  8594. } else {
  8595. // Browser globals
  8596. factory(jQuery);
  8597. }
  8598. })(function ($) {
  8599. "use strict";
  8600. /*******************************************************************************
  8601. * Private functions and variables
  8602. */
  8603. var _assert = $.ui.fancytree.assert;
  8604. /* Return first occurrence of member from array. */
  8605. function _removeArrayMember(arr, elem) {
  8606. // TODO: use Array.indexOf for IE >= 9
  8607. var i;
  8608. for (i = arr.length - 1; i >= 0; i--) {
  8609. if (arr[i] === elem) {
  8610. arr.splice(i, 1);
  8611. return true;
  8612. }
  8613. }
  8614. return false;
  8615. }
  8616. /**
  8617. * JS Implementation of MurmurHash3 (r136) (as of May 20, 2011)
  8618. *
  8619. * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
  8620. * @see http://github.com/garycourt/murmurhash-js
  8621. * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
  8622. * @see http://sites.google.com/site/murmurhash/
  8623. *
  8624. * @param {string} key ASCII only
  8625. * @param {boolean} [asString=false]
  8626. * @param {number} seed Positive integer only
  8627. * @return {number} 32-bit positive integer hash
  8628. */
  8629. function hashMurmur3(key, asString, seed) {
  8630. /*eslint-disable no-bitwise */
  8631. var h1b,
  8632. k1,
  8633. remainder = key.length & 3,
  8634. bytes = key.length - remainder,
  8635. h1 = seed,
  8636. c1 = 0xcc9e2d51,
  8637. c2 = 0x1b873593,
  8638. i = 0;
  8639. while (i < bytes) {
  8640. k1 =
  8641. (key.charCodeAt(i) & 0xff) |
  8642. ((key.charCodeAt(++i) & 0xff) << 8) |
  8643. ((key.charCodeAt(++i) & 0xff) << 16) |
  8644. ((key.charCodeAt(++i) & 0xff) << 24);
  8645. ++i;
  8646. k1 =
  8647. ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
  8648. 0xffffffff;
  8649. k1 = (k1 << 15) | (k1 >>> 17);
  8650. k1 =
  8651. ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
  8652. 0xffffffff;
  8653. h1 ^= k1;
  8654. h1 = (h1 << 13) | (h1 >>> 19);
  8655. h1b =
  8656. ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) &
  8657. 0xffffffff;
  8658. h1 =
  8659. (h1b & 0xffff) +
  8660. 0x6b64 +
  8661. ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
  8662. }
  8663. k1 = 0;
  8664. switch (remainder) {
  8665. case 3:
  8666. k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
  8667. // fall through
  8668. case 2:
  8669. k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
  8670. // fall through
  8671. case 1:
  8672. k1 ^= key.charCodeAt(i) & 0xff;
  8673. k1 =
  8674. ((k1 & 0xffff) * c1 +
  8675. ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
  8676. 0xffffffff;
  8677. k1 = (k1 << 15) | (k1 >>> 17);
  8678. k1 =
  8679. ((k1 & 0xffff) * c2 +
  8680. ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
  8681. 0xffffffff;
  8682. h1 ^= k1;
  8683. }
  8684. h1 ^= key.length;
  8685. h1 ^= h1 >>> 16;
  8686. h1 =
  8687. ((h1 & 0xffff) * 0x85ebca6b +
  8688. ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
  8689. 0xffffffff;
  8690. h1 ^= h1 >>> 13;
  8691. h1 =
  8692. ((h1 & 0xffff) * 0xc2b2ae35 +
  8693. ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
  8694. 0xffffffff;
  8695. h1 ^= h1 >>> 16;
  8696. if (asString) {
  8697. // Convert to 8 digit hex string
  8698. return ("0000000" + (h1 >>> 0).toString(16)).substr(-8);
  8699. }
  8700. return h1 >>> 0;
  8701. /*eslint-enable no-bitwise */
  8702. }
  8703. /*
  8704. * Return a unique key for node by calculating the hash of the parents refKey-list.
  8705. */
  8706. function calcUniqueKey(node) {
  8707. var key,
  8708. h1,
  8709. path = $.map(node.getParentList(false, true), function (e) {
  8710. return e.refKey || e.key;
  8711. });
  8712. path = path.join("/");
  8713. // 32-bit has a high probability of collisions, so we pump up to 64-bit
  8714. // https://security.stackexchange.com/q/209882/207588
  8715. h1 = hashMurmur3(path, true);
  8716. key = "id_" + h1 + hashMurmur3(h1 + path, true);
  8717. return key;
  8718. }
  8719. /**
  8720. * [ext-clones] Return a list of clone-nodes (i.e. same refKey) or null.
  8721. * @param {boolean} [includeSelf=false]
  8722. * @returns {FancytreeNode[] | null}
  8723. *
  8724. * @alias FancytreeNode#getCloneList
  8725. * @requires jquery.fancytree.clones.js
  8726. */
  8727. $.ui.fancytree._FancytreeNodeClass.prototype.getCloneList = function (
  8728. includeSelf
  8729. ) {
  8730. var key,
  8731. tree = this.tree,
  8732. refList = tree.refMap[this.refKey] || null,
  8733. keyMap = tree.keyMap;
  8734. if (refList) {
  8735. key = this.key;
  8736. // Convert key list to node list
  8737. if (includeSelf) {
  8738. refList = $.map(refList, function (val) {
  8739. return keyMap[val];
  8740. });
  8741. } else {
  8742. refList = $.map(refList, function (val) {
  8743. return val === key ? null : keyMap[val];
  8744. });
  8745. if (refList.length < 1) {
  8746. refList = null;
  8747. }
  8748. }
  8749. }
  8750. return refList;
  8751. };
  8752. /**
  8753. * [ext-clones] Return true if this node has at least another clone with same refKey.
  8754. * @returns {boolean}
  8755. *
  8756. * @alias FancytreeNode#isClone
  8757. * @requires jquery.fancytree.clones.js
  8758. */
  8759. $.ui.fancytree._FancytreeNodeClass.prototype.isClone = function () {
  8760. var refKey = this.refKey || null,
  8761. refList = (refKey && this.tree.refMap[refKey]) || null;
  8762. return !!(refList && refList.length > 1);
  8763. };
  8764. /**
  8765. * [ext-clones] Update key and/or refKey for an existing node.
  8766. * @param {string} key
  8767. * @param {string} refKey
  8768. * @returns {boolean}
  8769. *
  8770. * @alias FancytreeNode#reRegister
  8771. * @requires jquery.fancytree.clones.js
  8772. */
  8773. $.ui.fancytree._FancytreeNodeClass.prototype.reRegister = function (
  8774. key,
  8775. refKey
  8776. ) {
  8777. key = key == null ? null : "" + key;
  8778. refKey = refKey == null ? null : "" + refKey;
  8779. // this.debug("reRegister", key, refKey);
  8780. var tree = this.tree,
  8781. prevKey = this.key,
  8782. prevRefKey = this.refKey,
  8783. keyMap = tree.keyMap,
  8784. refMap = tree.refMap,
  8785. refList = refMap[prevRefKey] || null,
  8786. // curCloneKeys = refList ? node.getCloneList(true),
  8787. modified = false;
  8788. // Key has changed: update all references
  8789. if (key != null && key !== this.key) {
  8790. if (keyMap[key]) {
  8791. $.error(
  8792. "[ext-clones] reRegister(" +
  8793. key +
  8794. "): already exists: " +
  8795. this
  8796. );
  8797. }
  8798. // Update keyMap
  8799. delete keyMap[prevKey];
  8800. keyMap[key] = this;
  8801. // Update refMap
  8802. if (refList) {
  8803. refMap[prevRefKey] = $.map(refList, function (e) {
  8804. return e === prevKey ? key : e;
  8805. });
  8806. }
  8807. this.key = key;
  8808. modified = true;
  8809. }
  8810. // refKey has changed
  8811. if (refKey != null && refKey !== this.refKey) {
  8812. // Remove previous refKeys
  8813. if (refList) {
  8814. if (refList.length === 1) {
  8815. delete refMap[prevRefKey];
  8816. } else {
  8817. refMap[prevRefKey] = $.map(refList, function (e) {
  8818. return e === prevKey ? null : e;
  8819. });
  8820. }
  8821. }
  8822. // Add refKey
  8823. if (refMap[refKey]) {
  8824. refMap[refKey].append(key);
  8825. } else {
  8826. refMap[refKey] = [this.key];
  8827. }
  8828. this.refKey = refKey;
  8829. modified = true;
  8830. }
  8831. return modified;
  8832. };
  8833. /**
  8834. * [ext-clones] Define a refKey for an existing node.
  8835. * @param {string} refKey
  8836. * @returns {boolean}
  8837. *
  8838. * @alias FancytreeNode#setRefKey
  8839. * @requires jquery.fancytree.clones.js
  8840. * @since 2.16
  8841. */
  8842. $.ui.fancytree._FancytreeNodeClass.prototype.setRefKey = function (refKey) {
  8843. return this.reRegister(null, refKey);
  8844. };
  8845. /**
  8846. * [ext-clones] Return all nodes with a given refKey (null if not found).
  8847. * @param {string} refKey
  8848. * @param {FancytreeNode} [rootNode] optionally restrict results to descendants of this node
  8849. * @returns {FancytreeNode[] | null}
  8850. * @alias Fancytree#getNodesByRef
  8851. * @requires jquery.fancytree.clones.js
  8852. */
  8853. $.ui.fancytree._FancytreeClass.prototype.getNodesByRef = function (
  8854. refKey,
  8855. rootNode
  8856. ) {
  8857. var keyMap = this.keyMap,
  8858. refList = this.refMap[refKey] || null;
  8859. if (refList) {
  8860. // Convert key list to node list
  8861. if (rootNode) {
  8862. refList = $.map(refList, function (val) {
  8863. var node = keyMap[val];
  8864. return node.isDescendantOf(rootNode) ? node : null;
  8865. });
  8866. } else {
  8867. refList = $.map(refList, function (val) {
  8868. return keyMap[val];
  8869. });
  8870. }
  8871. if (refList.length < 1) {
  8872. refList = null;
  8873. }
  8874. }
  8875. return refList;
  8876. };
  8877. /**
  8878. * [ext-clones] Replace a refKey with a new one.
  8879. * @param {string} oldRefKey
  8880. * @param {string} newRefKey
  8881. * @alias Fancytree#changeRefKey
  8882. * @requires jquery.fancytree.clones.js
  8883. */
  8884. $.ui.fancytree._FancytreeClass.prototype.changeRefKey = function (
  8885. oldRefKey,
  8886. newRefKey
  8887. ) {
  8888. var i,
  8889. node,
  8890. keyMap = this.keyMap,
  8891. refList = this.refMap[oldRefKey] || null;
  8892. if (refList) {
  8893. for (i = 0; i < refList.length; i++) {
  8894. node = keyMap[refList[i]];
  8895. node.refKey = newRefKey;
  8896. }
  8897. delete this.refMap[oldRefKey];
  8898. this.refMap[newRefKey] = refList;
  8899. }
  8900. };
  8901. /*******************************************************************************
  8902. * Extension code
  8903. */
  8904. $.ui.fancytree.registerExtension({
  8905. name: "clones",
  8906. version: "2.38.3",
  8907. // Default options for this extension.
  8908. options: {
  8909. highlightActiveClones: true, // set 'fancytree-active-clone' on active clones and all peers
  8910. highlightClones: false, // set 'fancytree-clone' class on any node that has at least one clone
  8911. },
  8912. treeCreate: function (ctx) {
  8913. this._superApply(arguments);
  8914. ctx.tree.refMap = {};
  8915. ctx.tree.keyMap = {};
  8916. },
  8917. treeInit: function (ctx) {
  8918. this.$container.addClass("fancytree-ext-clones");
  8919. _assert(ctx.options.defaultKey == null);
  8920. // Generate unique / reproducible default keys
  8921. ctx.options.defaultKey = function (node) {
  8922. return calcUniqueKey(node);
  8923. };
  8924. // The default implementation loads initial data
  8925. this._superApply(arguments);
  8926. },
  8927. treeClear: function (ctx) {
  8928. ctx.tree.refMap = {};
  8929. ctx.tree.keyMap = {};
  8930. return this._superApply(arguments);
  8931. },
  8932. treeRegisterNode: function (ctx, add, node) {
  8933. var refList,
  8934. len,
  8935. tree = ctx.tree,
  8936. keyMap = tree.keyMap,
  8937. refMap = tree.refMap,
  8938. key = node.key,
  8939. refKey = node && node.refKey != null ? "" + node.refKey : null;
  8940. // ctx.tree.debug("clones.treeRegisterNode", add, node);
  8941. if (node.isStatusNode()) {
  8942. return this._super(ctx, add, node);
  8943. }
  8944. if (add) {
  8945. if (keyMap[node.key] != null) {
  8946. var other = keyMap[node.key],
  8947. msg =
  8948. "clones.treeRegisterNode: duplicate key '" +
  8949. node.key +
  8950. "': /" +
  8951. node.getPath(true) +
  8952. " => " +
  8953. other.getPath(true);
  8954. // Sometimes this exception is not visible in the console,
  8955. // so we also write it:
  8956. tree.error(msg);
  8957. $.error(msg);
  8958. }
  8959. keyMap[key] = node;
  8960. if (refKey) {
  8961. refList = refMap[refKey];
  8962. if (refList) {
  8963. refList.push(key);
  8964. if (
  8965. refList.length === 2 &&
  8966. ctx.options.clones.highlightClones
  8967. ) {
  8968. // Mark peer node, if it just became a clone (no need to
  8969. // mark current node, since it will be rendered later anyway)
  8970. keyMap[refList[0]].renderStatus();
  8971. }
  8972. } else {
  8973. refMap[refKey] = [key];
  8974. }
  8975. // node.debug("clones.treeRegisterNode: add clone =>", refMap[refKey]);
  8976. }
  8977. } else {
  8978. if (keyMap[key] == null) {
  8979. $.error(
  8980. "clones.treeRegisterNode: node.key not registered: " +
  8981. node.key
  8982. );
  8983. }
  8984. delete keyMap[key];
  8985. if (refKey) {
  8986. refList = refMap[refKey];
  8987. // node.debug("clones.treeRegisterNode: remove clone BEFORE =>", refMap[refKey]);
  8988. if (refList) {
  8989. len = refList.length;
  8990. if (len <= 1) {
  8991. _assert(len === 1);
  8992. _assert(refList[0] === key);
  8993. delete refMap[refKey];
  8994. } else {
  8995. _removeArrayMember(refList, key);
  8996. // Unmark peer node, if this was the only clone
  8997. if (
  8998. len === 2 &&
  8999. ctx.options.clones.highlightClones
  9000. ) {
  9001. // node.debug("clones.treeRegisterNode: last =>", node.getCloneList());
  9002. keyMap[refList[0]].renderStatus();
  9003. }
  9004. }
  9005. // node.debug("clones.treeRegisterNode: remove clone =>", refMap[refKey]);
  9006. }
  9007. }
  9008. }
  9009. return this._super(ctx, add, node);
  9010. },
  9011. nodeRenderStatus: function (ctx) {
  9012. var $span,
  9013. res,
  9014. node = ctx.node;
  9015. res = this._super(ctx);
  9016. if (ctx.options.clones.highlightClones) {
  9017. $span = $(node[ctx.tree.statusClassPropName]);
  9018. // Only if span already exists
  9019. if ($span.length && node.isClone()) {
  9020. // node.debug("clones.nodeRenderStatus: ", ctx.options.clones.highlightClones);
  9021. $span.addClass("fancytree-clone");
  9022. }
  9023. }
  9024. return res;
  9025. },
  9026. nodeSetActive: function (ctx, flag, callOpts) {
  9027. var res,
  9028. scpn = ctx.tree.statusClassPropName,
  9029. node = ctx.node;
  9030. res = this._superApply(arguments);
  9031. if (ctx.options.clones.highlightActiveClones && node.isClone()) {
  9032. $.each(node.getCloneList(true), function (idx, n) {
  9033. // n.debug("clones.nodeSetActive: ", flag !== false);
  9034. $(n[scpn]).toggleClass(
  9035. "fancytree-active-clone",
  9036. flag !== false
  9037. );
  9038. });
  9039. }
  9040. return res;
  9041. },
  9042. });
  9043. // Value returned by `require('jquery.fancytree..')`
  9044. return $.ui.fancytree;
  9045. }); // End of closure
  9046. /*! Extension 'jquery.fancytree.dnd5.js' *//*!
  9047. * jquery.fancytree.dnd5.js
  9048. *
  9049. * Drag-and-drop support (native HTML5).
  9050. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  9051. *
  9052. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  9053. *
  9054. * Released under the MIT license
  9055. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  9056. *
  9057. * @version 2.38.3
  9058. * @date 2023-02-01T20:52:50Z
  9059. */
  9060. /*
  9061. #TODO
  9062. Compatiblity when dragging between *separate* windows:
  9063. Drag from Chrome Edge FF IE11 Safari
  9064. To Chrome ok ok ok NO ?
  9065. Edge ok ok ok NO ?
  9066. FF ok ok ok NO ?
  9067. IE 11 ok ok ok ok ?
  9068. Safari ? ? ? ? ok
  9069. */
  9070. (function (factory) {
  9071. if (typeof define === "function" && define.amd) {
  9072. // AMD. Register as an anonymous module.
  9073. define(["jquery", "./jquery.fancytree"], factory);
  9074. } else if (typeof module === "object" && module.exports) {
  9075. // Node/CommonJS
  9076. require("./jquery.fancytree");
  9077. module.exports = factory(require("jquery"));
  9078. } else {
  9079. // Browser globals
  9080. factory(jQuery);
  9081. }
  9082. })(function ($) {
  9083. "use strict";
  9084. /******************************************************************************
  9085. * Private functions and variables
  9086. */
  9087. var FT = $.ui.fancytree,
  9088. isMac = /Mac/.test(navigator.platform),
  9089. classDragSource = "fancytree-drag-source",
  9090. classDragRemove = "fancytree-drag-remove",
  9091. classDropAccept = "fancytree-drop-accept",
  9092. classDropAfter = "fancytree-drop-after",
  9093. classDropBefore = "fancytree-drop-before",
  9094. classDropOver = "fancytree-drop-over",
  9095. classDropReject = "fancytree-drop-reject",
  9096. classDropTarget = "fancytree-drop-target",
  9097. nodeMimeType = "application/x-fancytree-node",
  9098. $dropMarker = null,
  9099. $dragImage,
  9100. $extraHelper,
  9101. SOURCE_NODE = null,
  9102. SOURCE_NODE_LIST = null,
  9103. $sourceList = null,
  9104. DRAG_ENTER_RESPONSE = null,
  9105. // SESSION_DATA = null, // plain object passed to events as `data`
  9106. SUGGESTED_DROP_EFFECT = null,
  9107. REQUESTED_DROP_EFFECT = null,
  9108. REQUESTED_EFFECT_ALLOWED = null,
  9109. LAST_HIT_MODE = null,
  9110. DRAG_OVER_STAMP = null; // Time when a node entered the 'over' hitmode
  9111. /* */
  9112. function _clearGlobals() {
  9113. DRAG_ENTER_RESPONSE = null;
  9114. DRAG_OVER_STAMP = null;
  9115. REQUESTED_DROP_EFFECT = null;
  9116. REQUESTED_EFFECT_ALLOWED = null;
  9117. SUGGESTED_DROP_EFFECT = null;
  9118. SOURCE_NODE = null;
  9119. SOURCE_NODE_LIST = null;
  9120. if ($sourceList) {
  9121. $sourceList.removeClass(classDragSource + " " + classDragRemove);
  9122. }
  9123. $sourceList = null;
  9124. if ($dropMarker) {
  9125. $dropMarker.hide();
  9126. }
  9127. // Take this badge off of me - I can't use it anymore:
  9128. if ($extraHelper) {
  9129. $extraHelper.remove();
  9130. $extraHelper = null;
  9131. }
  9132. }
  9133. /* Convert number to string and prepend +/-; return empty string for 0.*/
  9134. function offsetString(n) {
  9135. // eslint-disable-next-line no-nested-ternary
  9136. return n === 0 ? "" : n > 0 ? "+" + n : "" + n;
  9137. }
  9138. /* Convert a dragEnter() or dragOver() response to a canonical form.
  9139. * Return false or plain object
  9140. * @param {string|object|boolean} r
  9141. * @return {object|false}
  9142. */
  9143. function normalizeDragEnterResponse(r) {
  9144. var res;
  9145. if (!r) {
  9146. return false;
  9147. }
  9148. if ($.isPlainObject(r)) {
  9149. res = {
  9150. over: !!r.over,
  9151. before: !!r.before,
  9152. after: !!r.after,
  9153. };
  9154. } else if (Array.isArray(r)) {
  9155. res = {
  9156. over: $.inArray("over", r) >= 0,
  9157. before: $.inArray("before", r) >= 0,
  9158. after: $.inArray("after", r) >= 0,
  9159. };
  9160. } else {
  9161. res = {
  9162. over: r === true || r === "over",
  9163. before: r === true || r === "before",
  9164. after: r === true || r === "after",
  9165. };
  9166. }
  9167. if (Object.keys(res).length === 0) {
  9168. return false;
  9169. }
  9170. // if( Object.keys(res).length === 1 ) {
  9171. // res.unique = res[0];
  9172. // }
  9173. return res;
  9174. }
  9175. /* Convert a dataTransfer.effectAllowed to a canonical form.
  9176. * Return false or plain object
  9177. * @param {string|boolean} r
  9178. * @return {object|false}
  9179. */
  9180. // function normalizeEffectAllowed(r) {
  9181. // if (!r || r === "none") {
  9182. // return false;
  9183. // }
  9184. // var all = r === "all",
  9185. // res = {
  9186. // copy: all || /copy/i.test(r),
  9187. // link: all || /link/i.test(r),
  9188. // move: all || /move/i.test(r),
  9189. // };
  9190. // return res;
  9191. // }
  9192. /* Implement auto scrolling when drag cursor is in top/bottom area of scroll parent. */
  9193. function autoScroll(tree, event) {
  9194. var spOfs,
  9195. scrollTop,
  9196. delta,
  9197. dndOpts = tree.options.dnd5,
  9198. sp = tree.$scrollParent[0],
  9199. sensitivity = dndOpts.scrollSensitivity,
  9200. speed = dndOpts.scrollSpeed,
  9201. scrolled = 0;
  9202. if (sp !== document && sp.tagName !== "HTML") {
  9203. spOfs = tree.$scrollParent.offset();
  9204. scrollTop = sp.scrollTop;
  9205. if (spOfs.top + sp.offsetHeight - event.pageY < sensitivity) {
  9206. delta =
  9207. sp.scrollHeight -
  9208. tree.$scrollParent.innerHeight() -
  9209. scrollTop;
  9210. // console.log ("sp.offsetHeight: " + sp.offsetHeight
  9211. // + ", spOfs.top: " + spOfs.top
  9212. // + ", scrollTop: " + scrollTop
  9213. // + ", innerHeight: " + tree.$scrollParent.innerHeight()
  9214. // + ", scrollHeight: " + sp.scrollHeight
  9215. // + ", delta: " + delta
  9216. // );
  9217. if (delta > 0) {
  9218. sp.scrollTop = scrolled = scrollTop + speed;
  9219. }
  9220. } else if (scrollTop > 0 && event.pageY - spOfs.top < sensitivity) {
  9221. sp.scrollTop = scrolled = scrollTop - speed;
  9222. }
  9223. } else {
  9224. scrollTop = $(document).scrollTop();
  9225. if (scrollTop > 0 && event.pageY - scrollTop < sensitivity) {
  9226. scrolled = scrollTop - speed;
  9227. $(document).scrollTop(scrolled);
  9228. } else if (
  9229. $(window).height() - (event.pageY - scrollTop) <
  9230. sensitivity
  9231. ) {
  9232. scrolled = scrollTop + speed;
  9233. $(document).scrollTop(scrolled);
  9234. }
  9235. }
  9236. if (scrolled) {
  9237. tree.debug("autoScroll: " + scrolled + "px");
  9238. }
  9239. return scrolled;
  9240. }
  9241. /* Guess dropEffect from modifier keys.
  9242. * Using rules suggested here:
  9243. * https://ux.stackexchange.com/a/83769
  9244. * @returns
  9245. * 'copy', 'link', 'move', or 'none'
  9246. */
  9247. function evalEffectModifiers(tree, event, effectDefault) {
  9248. var res = effectDefault;
  9249. if (isMac) {
  9250. if (event.metaKey && event.altKey) {
  9251. // Mac: [Control] + [Option]
  9252. res = "link";
  9253. } else if (event.ctrlKey) {
  9254. // Chrome on Mac: [Control]
  9255. res = "link";
  9256. } else if (event.metaKey) {
  9257. // Mac: [Command]
  9258. res = "move";
  9259. } else if (event.altKey) {
  9260. // Mac: [Option]
  9261. res = "copy";
  9262. }
  9263. } else {
  9264. if (event.ctrlKey) {
  9265. // Windows: [Ctrl]
  9266. res = "copy";
  9267. } else if (event.shiftKey) {
  9268. // Windows: [Shift]
  9269. res = "move";
  9270. } else if (event.altKey) {
  9271. // Windows: [Alt]
  9272. res = "link";
  9273. }
  9274. }
  9275. if (res !== SUGGESTED_DROP_EFFECT) {
  9276. tree.info(
  9277. "evalEffectModifiers: " +
  9278. event.type +
  9279. " - evalEffectModifiers(): " +
  9280. SUGGESTED_DROP_EFFECT +
  9281. " -> " +
  9282. res
  9283. );
  9284. }
  9285. SUGGESTED_DROP_EFFECT = res;
  9286. // tree.debug("evalEffectModifiers: " + res);
  9287. return res;
  9288. }
  9289. /*
  9290. * Check if the previous callback (dragEnter, dragOver, ...) has changed
  9291. * the `data` object and apply those settings.
  9292. *
  9293. * Safari:
  9294. * It seems that `dataTransfer.dropEffect` can only be set on dragStart, and will remain
  9295. * even if the cursor changes when [Alt] or [Ctrl] are pressed (?)
  9296. * Using rules suggested here:
  9297. * https://ux.stackexchange.com/a/83769
  9298. * @returns
  9299. * 'copy', 'link', 'move', or 'none'
  9300. */
  9301. function prepareDropEffectCallback(event, data) {
  9302. var tree = data.tree,
  9303. dataTransfer = data.dataTransfer;
  9304. if (event.type === "dragstart") {
  9305. data.effectAllowed = tree.options.dnd5.effectAllowed;
  9306. data.dropEffect = tree.options.dnd5.dropEffectDefault;
  9307. } else {
  9308. data.effectAllowed = REQUESTED_EFFECT_ALLOWED;
  9309. data.dropEffect = REQUESTED_DROP_EFFECT;
  9310. }
  9311. data.dropEffectSuggested = evalEffectModifiers(
  9312. tree,
  9313. event,
  9314. tree.options.dnd5.dropEffectDefault
  9315. );
  9316. data.isMove = data.dropEffect === "move";
  9317. data.files = dataTransfer.files || [];
  9318. // if (REQUESTED_EFFECT_ALLOWED !== dataTransfer.effectAllowed) {
  9319. // tree.warn(
  9320. // "prepareDropEffectCallback(" +
  9321. // event.type +
  9322. // "): dataTransfer.effectAllowed changed from " +
  9323. // REQUESTED_EFFECT_ALLOWED +
  9324. // " -> " +
  9325. // dataTransfer.effectAllowed
  9326. // );
  9327. // }
  9328. // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
  9329. // tree.warn(
  9330. // "prepareDropEffectCallback(" +
  9331. // event.type +
  9332. // "): dataTransfer.dropEffect changed from requested " +
  9333. // REQUESTED_DROP_EFFECT +
  9334. // " to " +
  9335. // dataTransfer.dropEffect
  9336. // );
  9337. // }
  9338. }
  9339. function applyDropEffectCallback(event, data, allowDrop) {
  9340. var tree = data.tree,
  9341. dataTransfer = data.dataTransfer;
  9342. if (
  9343. event.type !== "dragstart" &&
  9344. REQUESTED_EFFECT_ALLOWED !== data.effectAllowed
  9345. ) {
  9346. tree.warn(
  9347. "effectAllowed should only be changed in dragstart event: " +
  9348. event.type +
  9349. ": data.effectAllowed changed from " +
  9350. REQUESTED_EFFECT_ALLOWED +
  9351. " -> " +
  9352. data.effectAllowed
  9353. );
  9354. }
  9355. if (allowDrop === false) {
  9356. tree.info("applyDropEffectCallback: allowDrop === false");
  9357. data.effectAllowed = "none";
  9358. data.dropEffect = "none";
  9359. }
  9360. // if (REQUESTED_DROP_EFFECT !== data.dropEffect) {
  9361. // tree.debug(
  9362. // "applyDropEffectCallback(" +
  9363. // event.type +
  9364. // "): data.dropEffect changed from previous " +
  9365. // REQUESTED_DROP_EFFECT +
  9366. // " to " +
  9367. // data.dropEffect
  9368. // );
  9369. // }
  9370. data.isMove = data.dropEffect === "move";
  9371. // data.isMove = data.dropEffectSuggested === "move";
  9372. // `effectAllowed` must only be defined in dragstart event, so we
  9373. // store it in a global variable for reference
  9374. if (event.type === "dragstart") {
  9375. REQUESTED_EFFECT_ALLOWED = data.effectAllowed;
  9376. REQUESTED_DROP_EFFECT = data.dropEffect;
  9377. }
  9378. // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
  9379. // data.tree.info(
  9380. // "applyDropEffectCallback(" +
  9381. // event.type +
  9382. // "): dataTransfer.dropEffect changed from " +
  9383. // REQUESTED_DROP_EFFECT +
  9384. // " -> " +
  9385. // dataTransfer.dropEffect
  9386. // );
  9387. // }
  9388. dataTransfer.effectAllowed = REQUESTED_EFFECT_ALLOWED;
  9389. dataTransfer.dropEffect = REQUESTED_DROP_EFFECT;
  9390. // tree.debug(
  9391. // "applyDropEffectCallback(" +
  9392. // event.type +
  9393. // "): set " +
  9394. // dataTransfer.dropEffect +
  9395. // "/" +
  9396. // dataTransfer.effectAllowed
  9397. // );
  9398. // if (REQUESTED_DROP_EFFECT !== dataTransfer.dropEffect) {
  9399. // data.tree.warn(
  9400. // "applyDropEffectCallback(" +
  9401. // event.type +
  9402. // "): could not set dataTransfer.dropEffect to " +
  9403. // REQUESTED_DROP_EFFECT +
  9404. // ": got " +
  9405. // dataTransfer.dropEffect
  9406. // );
  9407. // }
  9408. return REQUESTED_DROP_EFFECT;
  9409. }
  9410. /* Handle dragover event (fired every x ms) on valid drop targets.
  9411. *
  9412. * - Auto-scroll when cursor is in border regions
  9413. * - Apply restrictioan like 'preventVoidMoves'
  9414. * - Calculate hit mode
  9415. * - Calculate drop effect
  9416. * - Trigger dragOver() callback to let user modify hit mode and drop effect
  9417. * - Adjust the drop marker accordingly
  9418. *
  9419. * @returns hitMode
  9420. */
  9421. function handleDragOver(event, data) {
  9422. // Implement auto-scrolling
  9423. if (data.options.dnd5.scroll) {
  9424. autoScroll(data.tree, event);
  9425. }
  9426. // Bail out with previous response if we get an invalid dragover
  9427. if (!data.node) {
  9428. data.tree.warn("Ignored dragover for non-node"); //, event, data);
  9429. return LAST_HIT_MODE;
  9430. }
  9431. var markerOffsetX,
  9432. nodeOfs,
  9433. pos,
  9434. relPosY,
  9435. hitMode = null,
  9436. tree = data.tree,
  9437. options = tree.options,
  9438. dndOpts = options.dnd5,
  9439. targetNode = data.node,
  9440. sourceNode = data.otherNode,
  9441. markerAt = "center",
  9442. $target = $(targetNode.span),
  9443. $targetTitle = $target.find("span.fancytree-title");
  9444. if (DRAG_ENTER_RESPONSE === false) {
  9445. tree.debug("Ignored dragover, since dragenter returned false.");
  9446. return false;
  9447. } else if (typeof DRAG_ENTER_RESPONSE === "string") {
  9448. $.error("assert failed: dragenter returned string");
  9449. }
  9450. // Calculate hitMode from relative cursor position.
  9451. nodeOfs = $target.offset();
  9452. relPosY = (event.pageY - nodeOfs.top) / $target.height();
  9453. if (event.pageY === undefined) {
  9454. tree.warn("event.pageY is undefined: see issue #1013.");
  9455. }
  9456. if (DRAG_ENTER_RESPONSE.after && relPosY > 0.75) {
  9457. hitMode = "after";
  9458. } else if (
  9459. !DRAG_ENTER_RESPONSE.over &&
  9460. DRAG_ENTER_RESPONSE.after &&
  9461. relPosY > 0.5
  9462. ) {
  9463. hitMode = "after";
  9464. } else if (DRAG_ENTER_RESPONSE.before && relPosY <= 0.25) {
  9465. hitMode = "before";
  9466. } else if (
  9467. !DRAG_ENTER_RESPONSE.over &&
  9468. DRAG_ENTER_RESPONSE.before &&
  9469. relPosY <= 0.5
  9470. ) {
  9471. hitMode = "before";
  9472. } else if (DRAG_ENTER_RESPONSE.over) {
  9473. hitMode = "over";
  9474. }
  9475. // Prevent no-ops like 'before source node'
  9476. // TODO: these are no-ops when moving nodes, but not in copy mode
  9477. if (dndOpts.preventVoidMoves && data.dropEffect === "move") {
  9478. if (targetNode === sourceNode) {
  9479. targetNode.debug("Drop over source node prevented.");
  9480. hitMode = null;
  9481. } else if (
  9482. hitMode === "before" &&
  9483. sourceNode &&
  9484. targetNode === sourceNode.getNextSibling()
  9485. ) {
  9486. targetNode.debug("Drop after source node prevented.");
  9487. hitMode = null;
  9488. } else if (
  9489. hitMode === "after" &&
  9490. sourceNode &&
  9491. targetNode === sourceNode.getPrevSibling()
  9492. ) {
  9493. targetNode.debug("Drop before source node prevented.");
  9494. hitMode = null;
  9495. } else if (
  9496. hitMode === "over" &&
  9497. sourceNode &&
  9498. sourceNode.parent === targetNode &&
  9499. sourceNode.isLastSibling()
  9500. ) {
  9501. targetNode.debug("Drop last child over own parent prevented.");
  9502. hitMode = null;
  9503. }
  9504. }
  9505. // Let callback modify the calculated hitMode
  9506. data.hitMode = hitMode;
  9507. if (hitMode && dndOpts.dragOver) {
  9508. prepareDropEffectCallback(event, data);
  9509. dndOpts.dragOver(targetNode, data);
  9510. var allowDrop = !!hitMode;
  9511. applyDropEffectCallback(event, data, allowDrop);
  9512. hitMode = data.hitMode;
  9513. }
  9514. LAST_HIT_MODE = hitMode;
  9515. //
  9516. if (hitMode === "after" || hitMode === "before" || hitMode === "over") {
  9517. markerOffsetX = dndOpts.dropMarkerOffsetX || 0;
  9518. switch (hitMode) {
  9519. case "before":
  9520. markerAt = "top";
  9521. markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0;
  9522. break;
  9523. case "after":
  9524. markerAt = "bottom";
  9525. markerOffsetX += dndOpts.dropMarkerInsertOffsetX || 0;
  9526. break;
  9527. }
  9528. pos = {
  9529. my: "left" + offsetString(markerOffsetX) + " center",
  9530. at: "left " + markerAt,
  9531. of: $targetTitle,
  9532. };
  9533. if (options.rtl) {
  9534. pos.my = "right" + offsetString(-markerOffsetX) + " center";
  9535. pos.at = "right " + markerAt;
  9536. // console.log("rtl", pos);
  9537. }
  9538. $dropMarker
  9539. .toggleClass(classDropAfter, hitMode === "after")
  9540. .toggleClass(classDropOver, hitMode === "over")
  9541. .toggleClass(classDropBefore, hitMode === "before")
  9542. .show()
  9543. .position(FT.fixPositionOptions(pos));
  9544. } else {
  9545. $dropMarker.hide();
  9546. // console.log("hide dropmarker")
  9547. }
  9548. $(targetNode.span)
  9549. .toggleClass(
  9550. classDropTarget,
  9551. hitMode === "after" ||
  9552. hitMode === "before" ||
  9553. hitMode === "over"
  9554. )
  9555. .toggleClass(classDropAfter, hitMode === "after")
  9556. .toggleClass(classDropBefore, hitMode === "before")
  9557. .toggleClass(classDropAccept, hitMode === "over")
  9558. .toggleClass(classDropReject, hitMode === false);
  9559. return hitMode;
  9560. }
  9561. /*
  9562. * Handle dragstart drag dragend events on the container
  9563. */
  9564. function onDragEvent(event) {
  9565. var json,
  9566. tree = this,
  9567. dndOpts = tree.options.dnd5,
  9568. node = FT.getNode(event),
  9569. dataTransfer =
  9570. event.dataTransfer || event.originalEvent.dataTransfer,
  9571. data = {
  9572. tree: tree,
  9573. node: node,
  9574. options: tree.options,
  9575. originalEvent: event.originalEvent,
  9576. widget: tree.widget,
  9577. dataTransfer: dataTransfer,
  9578. useDefaultImage: true,
  9579. dropEffect: undefined,
  9580. dropEffectSuggested: undefined,
  9581. effectAllowed: undefined, // set by dragstart
  9582. files: undefined, // only for drop events
  9583. isCancelled: undefined, // set by dragend
  9584. isMove: undefined,
  9585. };
  9586. switch (event.type) {
  9587. case "dragstart":
  9588. if (!node) {
  9589. tree.info("Ignored dragstart on a non-node.");
  9590. return false;
  9591. }
  9592. // Store current source node in different formats
  9593. SOURCE_NODE = node;
  9594. // Also optionally store selected nodes
  9595. if (dndOpts.multiSource === false) {
  9596. SOURCE_NODE_LIST = [node];
  9597. } else if (dndOpts.multiSource === true) {
  9598. if (node.isSelected()) {
  9599. SOURCE_NODE_LIST = tree.getSelectedNodes();
  9600. } else {
  9601. SOURCE_NODE_LIST = [node];
  9602. }
  9603. } else {
  9604. SOURCE_NODE_LIST = dndOpts.multiSource(node, data);
  9605. }
  9606. // Cache as array of jQuery objects for faster access:
  9607. $sourceList = $(
  9608. $.map(SOURCE_NODE_LIST, function (n) {
  9609. return n.span;
  9610. })
  9611. );
  9612. // Set visual feedback
  9613. $sourceList.addClass(classDragSource);
  9614. // Set payload
  9615. // Note:
  9616. // Transfer data is only accessible on dragstart and drop!
  9617. // For all other events the formats and kinds in the drag
  9618. // data store list of items representing dragged data can be
  9619. // enumerated, but the data itself is unavailable and no new
  9620. // data can be added.
  9621. var nodeData = node.toDict(true, dndOpts.sourceCopyHook);
  9622. nodeData.treeId = node.tree._id;
  9623. json = JSON.stringify(nodeData);
  9624. try {
  9625. dataTransfer.setData(nodeMimeType, json);
  9626. dataTransfer.setData("text/html", $(node.span).html());
  9627. dataTransfer.setData("text/plain", node.title);
  9628. } catch (ex) {
  9629. // IE only accepts 'text' type
  9630. tree.warn(
  9631. "Could not set data (IE only accepts 'text') - " + ex
  9632. );
  9633. }
  9634. // We always need to set the 'text' type if we want to drag
  9635. // Because IE 11 only accepts this single type.
  9636. // If we pass JSON here, IE can can access all node properties,
  9637. // even when the source lives in another window. (D'n'd inside
  9638. // the same window will always work.)
  9639. // The drawback is, that in this case ALL browsers will see
  9640. // the JSON representation as 'text', so dragging
  9641. // to a text field will insert the JSON string instead of
  9642. // the node title.
  9643. if (dndOpts.setTextTypeJson) {
  9644. dataTransfer.setData("text", json);
  9645. } else {
  9646. dataTransfer.setData("text", node.title);
  9647. }
  9648. // Set the allowed drag modes (combinations of move, copy, and link)
  9649. // (effectAllowed can only be set in the dragstart event.)
  9650. // This can be overridden in the dragStart() callback
  9651. prepareDropEffectCallback(event, data);
  9652. // Let user cancel or modify above settings
  9653. // Realize potential changes by previous callback
  9654. if (dndOpts.dragStart(node, data) === false) {
  9655. // Cancel dragging
  9656. // dataTransfer.dropEffect = "none";
  9657. _clearGlobals();
  9658. return false;
  9659. }
  9660. applyDropEffectCallback(event, data);
  9661. // Unless user set `data.useDefaultImage` to false in dragStart,
  9662. // generata a default drag image now:
  9663. $extraHelper = null;
  9664. if (data.useDefaultImage) {
  9665. // Set the title as drag image (otherwise it would contain the expander)
  9666. $dragImage = $(node.span).find(".fancytree-title");
  9667. if (SOURCE_NODE_LIST && SOURCE_NODE_LIST.length > 1) {
  9668. // Add a counter badge to node title if dragging more than one node.
  9669. // We want this, because the element that is used as drag image
  9670. // must be *visible* in the DOM, so we cannot create some hidden
  9671. // custom markup.
  9672. // See https://kryogenix.org/code/browser/custom-drag-image.html
  9673. // Also, since IE 11 and Edge don't support setDragImage() alltogether,
  9674. // it gives som feedback to the user.
  9675. // The badge will be removed later on drag end.
  9676. $extraHelper = $(
  9677. "<span class='fancytree-childcounter'/>"
  9678. )
  9679. .text("+" + (SOURCE_NODE_LIST.length - 1))
  9680. .appendTo($dragImage);
  9681. }
  9682. if (dataTransfer.setDragImage) {
  9683. // IE 11 and Edge do not support this
  9684. dataTransfer.setDragImage($dragImage[0], -10, -10);
  9685. }
  9686. }
  9687. return true;
  9688. case "drag":
  9689. // Called every few milliseconds (no matter if the
  9690. // cursor is over a valid drop target)
  9691. // data.tree.info("drag", SOURCE_NODE)
  9692. prepareDropEffectCallback(event, data);
  9693. dndOpts.dragDrag(node, data);
  9694. applyDropEffectCallback(event, data);
  9695. $sourceList.toggleClass(classDragRemove, data.isMove);
  9696. break;
  9697. case "dragend":
  9698. // Called at the end of a d'n'd process (after drop)
  9699. // Note caveat: If drop removed the dragged source element,
  9700. // we may not get this event, since the target does not exist
  9701. // anymore
  9702. prepareDropEffectCallback(event, data);
  9703. _clearGlobals();
  9704. data.isCancelled = !LAST_HIT_MODE;
  9705. dndOpts.dragEnd(node, data, !LAST_HIT_MODE);
  9706. // applyDropEffectCallback(event, data);
  9707. break;
  9708. }
  9709. }
  9710. /*
  9711. * Handle dragenter dragover dragleave drop events on the container
  9712. */
  9713. function onDropEvent(event) {
  9714. var json,
  9715. allowAutoExpand,
  9716. nodeData,
  9717. isSourceFtNode,
  9718. r,
  9719. res,
  9720. tree = this,
  9721. dndOpts = tree.options.dnd5,
  9722. allowDrop = null,
  9723. node = FT.getNode(event),
  9724. dataTransfer =
  9725. event.dataTransfer || event.originalEvent.dataTransfer,
  9726. data = {
  9727. tree: tree,
  9728. node: node,
  9729. options: tree.options,
  9730. originalEvent: event.originalEvent,
  9731. widget: tree.widget,
  9732. hitMode: DRAG_ENTER_RESPONSE,
  9733. dataTransfer: dataTransfer,
  9734. otherNode: SOURCE_NODE || null,
  9735. otherNodeList: SOURCE_NODE_LIST || null,
  9736. otherNodeData: null, // set by drop event
  9737. useDefaultImage: true,
  9738. dropEffect: undefined,
  9739. dropEffectSuggested: undefined,
  9740. effectAllowed: undefined, // set by dragstart
  9741. files: null, // list of File objects (may be [])
  9742. isCancelled: undefined, // set by drop event
  9743. isMove: undefined,
  9744. };
  9745. // data.isMove = dropEffect === "move";
  9746. switch (event.type) {
  9747. case "dragenter":
  9748. // The dragenter event is fired when a dragged element or
  9749. // text selection enters a valid drop target.
  9750. DRAG_OVER_STAMP = null;
  9751. if (!node) {
  9752. // Sometimes we get dragenter for the container element
  9753. tree.debug(
  9754. "Ignore non-node " +
  9755. event.type +
  9756. ": " +
  9757. event.target.tagName +
  9758. "." +
  9759. event.target.className
  9760. );
  9761. DRAG_ENTER_RESPONSE = false;
  9762. break;
  9763. }
  9764. $(node.span)
  9765. .addClass(classDropOver)
  9766. .removeClass(classDropAccept + " " + classDropReject);
  9767. // Data is only readable in the dragstart and drop event,
  9768. // but we can check for the type:
  9769. isSourceFtNode =
  9770. $.inArray(nodeMimeType, dataTransfer.types) >= 0;
  9771. if (dndOpts.preventNonNodes && !isSourceFtNode) {
  9772. node.debug("Reject dropping a non-node.");
  9773. DRAG_ENTER_RESPONSE = false;
  9774. break;
  9775. } else if (
  9776. dndOpts.preventForeignNodes &&
  9777. (!SOURCE_NODE || SOURCE_NODE.tree !== node.tree)
  9778. ) {
  9779. node.debug("Reject dropping a foreign node.");
  9780. DRAG_ENTER_RESPONSE = false;
  9781. break;
  9782. } else if (
  9783. dndOpts.preventSameParent &&
  9784. data.otherNode &&
  9785. data.otherNode.tree === node.tree &&
  9786. node.parent === data.otherNode.parent
  9787. ) {
  9788. node.debug("Reject dropping as sibling (same parent).");
  9789. DRAG_ENTER_RESPONSE = false;
  9790. break;
  9791. } else if (
  9792. dndOpts.preventRecursion &&
  9793. data.otherNode &&
  9794. data.otherNode.tree === node.tree &&
  9795. node.isDescendantOf(data.otherNode)
  9796. ) {
  9797. node.debug("Reject dropping below own ancestor.");
  9798. DRAG_ENTER_RESPONSE = false;
  9799. break;
  9800. } else if (dndOpts.preventLazyParents && !node.isLoaded()) {
  9801. node.warn("Drop over unloaded target node prevented.");
  9802. DRAG_ENTER_RESPONSE = false;
  9803. break;
  9804. }
  9805. $dropMarker.show();
  9806. // Call dragEnter() to figure out if (and where) dropping is allowed
  9807. prepareDropEffectCallback(event, data);
  9808. r = dndOpts.dragEnter(node, data);
  9809. res = normalizeDragEnterResponse(r);
  9810. // alert("res:" + JSON.stringify(res))
  9811. DRAG_ENTER_RESPONSE = res;
  9812. allowDrop = res && (res.over || res.before || res.after);
  9813. applyDropEffectCallback(event, data, allowDrop);
  9814. break;
  9815. case "dragover":
  9816. if (!node) {
  9817. tree.debug(
  9818. "Ignore non-node " +
  9819. event.type +
  9820. ": " +
  9821. event.target.tagName +
  9822. "." +
  9823. event.target.className
  9824. );
  9825. break;
  9826. }
  9827. // The dragover event is fired when an element or text
  9828. // selection is being dragged over a valid drop target
  9829. // (every few hundred milliseconds).
  9830. // tree.debug(
  9831. // event.type +
  9832. // ": dropEffect: " +
  9833. // dataTransfer.dropEffect
  9834. // );
  9835. prepareDropEffectCallback(event, data);
  9836. LAST_HIT_MODE = handleDragOver(event, data);
  9837. // The flag controls the preventDefault() below:
  9838. allowDrop = !!LAST_HIT_MODE;
  9839. allowAutoExpand =
  9840. LAST_HIT_MODE === "over" || LAST_HIT_MODE === false;
  9841. if (
  9842. allowAutoExpand &&
  9843. !node.expanded &&
  9844. node.hasChildren() !== false
  9845. ) {
  9846. if (!DRAG_OVER_STAMP) {
  9847. DRAG_OVER_STAMP = Date.now();
  9848. } else if (
  9849. dndOpts.autoExpandMS &&
  9850. Date.now() - DRAG_OVER_STAMP > dndOpts.autoExpandMS &&
  9851. !node.isLoading() &&
  9852. (!dndOpts.dragExpand ||
  9853. dndOpts.dragExpand(node, data) !== false)
  9854. ) {
  9855. node.setExpanded();
  9856. }
  9857. } else {
  9858. DRAG_OVER_STAMP = null;
  9859. }
  9860. break;
  9861. case "dragleave":
  9862. // NOTE: dragleave is fired AFTER the dragenter event of the
  9863. // FOLLOWING element.
  9864. if (!node) {
  9865. tree.debug(
  9866. "Ignore non-node " +
  9867. event.type +
  9868. ": " +
  9869. event.target.tagName +
  9870. "." +
  9871. event.target.className
  9872. );
  9873. break;
  9874. }
  9875. if (!$(node.span).hasClass(classDropOver)) {
  9876. node.debug("Ignore dragleave (multi).");
  9877. break;
  9878. }
  9879. $(node.span).removeClass(
  9880. classDropOver +
  9881. " " +
  9882. classDropAccept +
  9883. " " +
  9884. classDropReject
  9885. );
  9886. node.scheduleAction("cancel");
  9887. dndOpts.dragLeave(node, data);
  9888. $dropMarker.hide();
  9889. break;
  9890. case "drop":
  9891. // Data is only readable in the (dragstart and) drop event:
  9892. if ($.inArray(nodeMimeType, dataTransfer.types) >= 0) {
  9893. nodeData = dataTransfer.getData(nodeMimeType);
  9894. tree.info(
  9895. event.type +
  9896. ": getData('application/x-fancytree-node'): '" +
  9897. nodeData +
  9898. "'"
  9899. );
  9900. }
  9901. if (!nodeData) {
  9902. // 1. Source is not a Fancytree node, or
  9903. // 2. If the FT mime type was set, but returns '', this
  9904. // is probably IE 11 (which only supports 'text')
  9905. nodeData = dataTransfer.getData("text");
  9906. tree.info(
  9907. event.type + ": getData('text'): '" + nodeData + "'"
  9908. );
  9909. }
  9910. if (nodeData) {
  9911. try {
  9912. // 'text' type may contain JSON if IE is involved
  9913. // and setTextTypeJson option was set
  9914. json = JSON.parse(nodeData);
  9915. if (json.title !== undefined) {
  9916. data.otherNodeData = json;
  9917. }
  9918. } catch (ex) {
  9919. // assume 'text' type contains plain text, so `otherNodeData`
  9920. // should not be set
  9921. }
  9922. }
  9923. tree.debug(
  9924. event.type +
  9925. ": nodeData: '" +
  9926. nodeData +
  9927. "', otherNodeData: ",
  9928. data.otherNodeData
  9929. );
  9930. $(node.span).removeClass(
  9931. classDropOver +
  9932. " " +
  9933. classDropAccept +
  9934. " " +
  9935. classDropReject
  9936. );
  9937. // Let user implement the actual drop operation
  9938. data.hitMode = LAST_HIT_MODE;
  9939. prepareDropEffectCallback(event, data, !LAST_HIT_MODE);
  9940. data.isCancelled = !LAST_HIT_MODE;
  9941. var orgSourceElem = SOURCE_NODE && SOURCE_NODE.span,
  9942. orgSourceTree = SOURCE_NODE && SOURCE_NODE.tree;
  9943. dndOpts.dragDrop(node, data);
  9944. // applyDropEffectCallback(event, data);
  9945. // Prevent browser's default drop handling, i.e. open as link, ...
  9946. event.preventDefault();
  9947. if (orgSourceElem && !document.body.contains(orgSourceElem)) {
  9948. // The drop handler removed the original drag source from
  9949. // the DOM, so the dragend event will probaly not fire.
  9950. if (orgSourceTree === tree) {
  9951. tree.debug(
  9952. "Drop handler removed source element: generating dragEnd."
  9953. );
  9954. dndOpts.dragEnd(SOURCE_NODE, data);
  9955. } else {
  9956. tree.warn(
  9957. "Drop handler removed source element: dragend event may be lost."
  9958. );
  9959. }
  9960. }
  9961. _clearGlobals();
  9962. break;
  9963. }
  9964. // Dnd API madness: we must PREVENT default handling to enable dropping
  9965. if (allowDrop) {
  9966. event.preventDefault();
  9967. return false;
  9968. }
  9969. }
  9970. /** [ext-dnd5] Return a Fancytree instance, from element, index, event, or jQueryObject.
  9971. *
  9972. * @returns {FancytreeNode[]} List of nodes (empty if no drag operation)
  9973. * @example
  9974. * $.ui.fancytree.getDragNodeList();
  9975. *
  9976. * @alias Fancytree_Static#getDragNodeList
  9977. * @requires jquery.fancytree.dnd5.js
  9978. * @since 2.31
  9979. */
  9980. $.ui.fancytree.getDragNodeList = function () {
  9981. return SOURCE_NODE_LIST || [];
  9982. };
  9983. /** [ext-dnd5] Return the FancytreeNode that is currently being dragged.
  9984. *
  9985. * If multiple nodes are dragged, only the first is returned.
  9986. *
  9987. * @returns {FancytreeNode | null} dragged nodes or null if no drag operation
  9988. * @example
  9989. * $.ui.fancytree.getDragNode();
  9990. *
  9991. * @alias Fancytree_Static#getDragNode
  9992. * @requires jquery.fancytree.dnd5.js
  9993. * @since 2.31
  9994. */
  9995. $.ui.fancytree.getDragNode = function () {
  9996. return SOURCE_NODE;
  9997. };
  9998. /******************************************************************************
  9999. *
  10000. */
  10001. $.ui.fancytree.registerExtension({
  10002. name: "dnd5",
  10003. version: "2.38.3",
  10004. // Default options for this extension.
  10005. options: {
  10006. autoExpandMS: 1500, // Expand nodes after n milliseconds of hovering
  10007. dropMarkerInsertOffsetX: -16, // Additional offset for drop-marker with hitMode = "before"/"after"
  10008. dropMarkerOffsetX: -24, // Absolute position offset for .fancytree-drop-marker relatively to ..fancytree-title (icon/img near a node accepting drop)
  10009. // #1021 `document.body` is not available yet
  10010. dropMarkerParent: "body", // Root Container used for drop marker (could be a shadow root)
  10011. multiSource: false, // true: Drag multiple (i.e. selected) nodes. Also a callback() is allowed
  10012. effectAllowed: "all", // Restrict the possible cursor shapes and modifier operations (can also be set in the dragStart event)
  10013. // dropEffect: "auto", // 'copy'|'link'|'move'|'auto'(calculate from `effectAllowed`+modifier keys) or callback(node, data) that returns such string.
  10014. dropEffectDefault: "move", // Default dropEffect ('copy', 'link', or 'move') when no modifier is pressed (overide in dragDrag, dragOver).
  10015. preventForeignNodes: false, // Prevent dropping nodes from different Fancytrees
  10016. preventLazyParents: true, // Prevent dropping items on unloaded lazy Fancytree nodes
  10017. preventNonNodes: false, // Prevent dropping items other than Fancytree nodes
  10018. preventRecursion: true, // Prevent dropping nodes on own descendants
  10019. preventSameParent: false, // Prevent dropping nodes under same direct parent
  10020. preventVoidMoves: true, // Prevent dropping nodes 'before self', etc.
  10021. scroll: true, // Enable auto-scrolling while dragging
  10022. scrollSensitivity: 20, // Active top/bottom margin in pixel
  10023. scrollSpeed: 5, // Pixel per event
  10024. setTextTypeJson: false, // Allow dragging of nodes to different IE windows
  10025. sourceCopyHook: null, // Optional callback passed to `toDict` on dragStart @since 2.38
  10026. // Events (drag support)
  10027. dragStart: null, // Callback(sourceNode, data), return true, to enable dnd drag
  10028. dragDrag: $.noop, // Callback(sourceNode, data)
  10029. dragEnd: $.noop, // Callback(sourceNode, data)
  10030. // Events (drop support)
  10031. dragEnter: null, // Callback(targetNode, data), return true, to enable dnd drop
  10032. dragOver: $.noop, // Callback(targetNode, data)
  10033. dragExpand: $.noop, // Callback(targetNode, data), return false to prevent autoExpand
  10034. dragDrop: $.noop, // Callback(targetNode, data)
  10035. dragLeave: $.noop, // Callback(targetNode, data)
  10036. },
  10037. treeInit: function (ctx) {
  10038. var $temp,
  10039. tree = ctx.tree,
  10040. opts = ctx.options,
  10041. glyph = opts.glyph || null,
  10042. dndOpts = opts.dnd5;
  10043. if ($.inArray("dnd", opts.extensions) >= 0) {
  10044. $.error("Extensions 'dnd' and 'dnd5' are mutually exclusive.");
  10045. }
  10046. if (dndOpts.dragStop) {
  10047. $.error(
  10048. "dragStop is not used by ext-dnd5. Use dragEnd instead."
  10049. );
  10050. }
  10051. if (dndOpts.preventRecursiveMoves != null) {
  10052. $.error(
  10053. "preventRecursiveMoves was renamed to preventRecursion."
  10054. );
  10055. }
  10056. // Implement `opts.createNode` event to add the 'draggable' attribute
  10057. // #680: this must happen before calling super.treeInit()
  10058. if (dndOpts.dragStart) {
  10059. FT.overrideMethod(
  10060. ctx.options,
  10061. "createNode",
  10062. function (event, data) {
  10063. // Default processing if any
  10064. this._super.apply(this, arguments);
  10065. if (data.node.span) {
  10066. data.node.span.draggable = true;
  10067. } else {
  10068. data.node.warn(
  10069. "Cannot add `draggable`: no span tag"
  10070. );
  10071. }
  10072. }
  10073. );
  10074. }
  10075. this._superApply(arguments);
  10076. this.$container.addClass("fancytree-ext-dnd5");
  10077. // Store the current scroll parent, which may be the tree
  10078. // container, any enclosing div, or the document.
  10079. // #761: scrollParent() always needs a container child
  10080. $temp = $("<span>").appendTo(this.$container);
  10081. this.$scrollParent = $temp.scrollParent();
  10082. $temp.remove();
  10083. $dropMarker = $("#fancytree-drop-marker");
  10084. if (!$dropMarker.length) {
  10085. $dropMarker = $("<div id='fancytree-drop-marker'></div>")
  10086. .hide()
  10087. .css({
  10088. "z-index": 1000,
  10089. // Drop marker should not steal dragenter/dragover events:
  10090. "pointer-events": "none",
  10091. })
  10092. .prependTo(dndOpts.dropMarkerParent);
  10093. if (glyph) {
  10094. FT.setSpanIcon(
  10095. $dropMarker[0],
  10096. glyph.map._addClass,
  10097. glyph.map.dropMarker
  10098. );
  10099. }
  10100. }
  10101. $dropMarker.toggleClass("fancytree-rtl", !!opts.rtl);
  10102. // Enable drag support if dragStart() is specified:
  10103. if (dndOpts.dragStart) {
  10104. // Bind drag event handlers
  10105. tree.$container.on(
  10106. "dragstart drag dragend",
  10107. onDragEvent.bind(tree)
  10108. );
  10109. }
  10110. // Enable drop support if dragEnter() is specified:
  10111. if (dndOpts.dragEnter) {
  10112. // Bind drop event handlers
  10113. tree.$container.on(
  10114. "dragenter dragover dragleave drop",
  10115. onDropEvent.bind(tree)
  10116. );
  10117. }
  10118. },
  10119. });
  10120. // Value returned by `require('jquery.fancytree..')`
  10121. return $.ui.fancytree;
  10122. }); // End of closure
  10123. /*! Extension 'jquery.fancytree.edit.js' *//*!
  10124. * jquery.fancytree.edit.js
  10125. *
  10126. * Make node titles editable.
  10127. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  10128. *
  10129. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  10130. *
  10131. * Released under the MIT license
  10132. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  10133. *
  10134. * @version 2.38.3
  10135. * @date 2023-02-01T20:52:50Z
  10136. */
  10137. (function (factory) {
  10138. if (typeof define === "function" && define.amd) {
  10139. // AMD. Register as an anonymous module.
  10140. define(["jquery", "./jquery.fancytree"], factory);
  10141. } else if (typeof module === "object" && module.exports) {
  10142. // Node/CommonJS
  10143. require("./jquery.fancytree");
  10144. module.exports = factory(require("jquery"));
  10145. } else {
  10146. // Browser globals
  10147. factory(jQuery);
  10148. }
  10149. })(function ($) {
  10150. "use strict";
  10151. /*******************************************************************************
  10152. * Private functions and variables
  10153. */
  10154. var isMac = /Mac/.test(navigator.platform),
  10155. escapeHtml = $.ui.fancytree.escapeHtml,
  10156. trim = $.ui.fancytree.trim,
  10157. unescapeHtml = $.ui.fancytree.unescapeHtml;
  10158. /**
  10159. * [ext-edit] Start inline editing of current node title.
  10160. *
  10161. * @alias FancytreeNode#editStart
  10162. * @requires Fancytree
  10163. */
  10164. $.ui.fancytree._FancytreeNodeClass.prototype.editStart = function () {
  10165. var $input,
  10166. node = this,
  10167. tree = this.tree,
  10168. local = tree.ext.edit,
  10169. instOpts = tree.options.edit,
  10170. $title = $(".fancytree-title", node.span),
  10171. eventData = {
  10172. node: node,
  10173. tree: tree,
  10174. options: tree.options,
  10175. isNew: $(node[tree.statusClassPropName]).hasClass(
  10176. "fancytree-edit-new"
  10177. ),
  10178. orgTitle: node.title,
  10179. input: null,
  10180. dirty: false,
  10181. };
  10182. // beforeEdit may want to modify the title before editing
  10183. if (
  10184. instOpts.beforeEdit.call(
  10185. node,
  10186. { type: "beforeEdit" },
  10187. eventData
  10188. ) === false
  10189. ) {
  10190. return false;
  10191. }
  10192. $.ui.fancytree.assert(!local.currentNode, "recursive edit");
  10193. local.currentNode = this;
  10194. local.eventData = eventData;
  10195. // Disable standard Fancytree mouse- and key handling
  10196. tree.widget._unbind();
  10197. local.lastDraggableAttrValue = node.span.draggable;
  10198. if (local.lastDraggableAttrValue) {
  10199. node.span.draggable = false;
  10200. }
  10201. // #116: ext-dnd prevents the blur event, so we have to catch outer clicks
  10202. $(document).on("mousedown.fancytree-edit", function (event) {
  10203. if (!$(event.target).hasClass("fancytree-edit-input")) {
  10204. node.editEnd(true, event);
  10205. }
  10206. });
  10207. // Replace node with <input>
  10208. $input = $("<input />", {
  10209. class: "fancytree-edit-input",
  10210. type: "text",
  10211. value: tree.options.escapeTitles
  10212. ? eventData.orgTitle
  10213. : unescapeHtml(eventData.orgTitle),
  10214. });
  10215. local.eventData.input = $input;
  10216. if (instOpts.adjustWidthOfs != null) {
  10217. $input.width($title.width() + instOpts.adjustWidthOfs);
  10218. }
  10219. if (instOpts.inputCss != null) {
  10220. $input.css(instOpts.inputCss);
  10221. }
  10222. $title.html($input);
  10223. // Focus <input> and bind keyboard handler
  10224. $input
  10225. .focus()
  10226. .change(function (event) {
  10227. $input.addClass("fancytree-edit-dirty");
  10228. })
  10229. .on("keydown", function (event) {
  10230. switch (event.which) {
  10231. case $.ui.keyCode.ESCAPE:
  10232. node.editEnd(false, event);
  10233. break;
  10234. case $.ui.keyCode.ENTER:
  10235. node.editEnd(true, event);
  10236. return false; // so we don't start editmode on Mac
  10237. }
  10238. event.stopPropagation();
  10239. })
  10240. .blur(function (event) {
  10241. return node.editEnd(true, event);
  10242. });
  10243. instOpts.edit.call(node, { type: "edit" }, eventData);
  10244. };
  10245. /**
  10246. * [ext-edit] Stop inline editing.
  10247. * @param {Boolean} [applyChanges=false] false: cancel edit, true: save (if modified)
  10248. * @alias FancytreeNode#editEnd
  10249. * @requires jquery.fancytree.edit.js
  10250. */
  10251. $.ui.fancytree._FancytreeNodeClass.prototype.editEnd = function (
  10252. applyChanges,
  10253. _event
  10254. ) {
  10255. var newVal,
  10256. node = this,
  10257. tree = this.tree,
  10258. local = tree.ext.edit,
  10259. eventData = local.eventData,
  10260. instOpts = tree.options.edit,
  10261. $title = $(".fancytree-title", node.span),
  10262. $input = $title.find("input.fancytree-edit-input");
  10263. if (instOpts.trim) {
  10264. $input.val(trim($input.val()));
  10265. }
  10266. newVal = $input.val();
  10267. eventData.dirty = newVal !== node.title;
  10268. eventData.originalEvent = _event;
  10269. // Find out, if saving is required
  10270. if (applyChanges === false) {
  10271. // If true/false was passed, honor this (except in rename mode, if unchanged)
  10272. eventData.save = false;
  10273. } else if (eventData.isNew) {
  10274. // In create mode, we save everything, except for empty text
  10275. eventData.save = newVal !== "";
  10276. } else {
  10277. // In rename mode, we save everyting, except for empty or unchanged text
  10278. eventData.save = eventData.dirty && newVal !== "";
  10279. }
  10280. // Allow to break (keep editor open), modify input, or re-define data.save
  10281. if (
  10282. instOpts.beforeClose.call(
  10283. node,
  10284. { type: "beforeClose" },
  10285. eventData
  10286. ) === false
  10287. ) {
  10288. return false;
  10289. }
  10290. if (
  10291. eventData.save &&
  10292. instOpts.save.call(node, { type: "save" }, eventData) === false
  10293. ) {
  10294. return false;
  10295. }
  10296. $input.removeClass("fancytree-edit-dirty").off();
  10297. // Unbind outer-click handler
  10298. $(document).off(".fancytree-edit");
  10299. if (eventData.save) {
  10300. // # 171: escape user input (not required if global escaping is on)
  10301. node.setTitle(
  10302. tree.options.escapeTitles ? newVal : escapeHtml(newVal)
  10303. );
  10304. node.setFocus();
  10305. } else {
  10306. if (eventData.isNew) {
  10307. node.remove();
  10308. node = eventData.node = null;
  10309. local.relatedNode.setFocus();
  10310. } else {
  10311. node.renderTitle();
  10312. node.setFocus();
  10313. }
  10314. }
  10315. local.eventData = null;
  10316. local.currentNode = null;
  10317. local.relatedNode = null;
  10318. // Re-enable mouse and keyboard handling
  10319. tree.widget._bind();
  10320. if (node && local.lastDraggableAttrValue) {
  10321. node.span.draggable = true;
  10322. }
  10323. // Set keyboard focus, even if setFocus() claims 'nothing to do'
  10324. tree.$container.get(0).focus({ preventScroll: true });
  10325. eventData.input = null;
  10326. instOpts.close.call(node, { type: "close" }, eventData);
  10327. return true;
  10328. };
  10329. /**
  10330. * [ext-edit] Create a new child or sibling node and start edit mode.
  10331. *
  10332. * @param {String} [mode='child'] 'before', 'after', or 'child'
  10333. * @param {Object} [init] NodeData (or simple title string)
  10334. * @alias FancytreeNode#editCreateNode
  10335. * @requires jquery.fancytree.edit.js
  10336. * @since 2.4
  10337. */
  10338. $.ui.fancytree._FancytreeNodeClass.prototype.editCreateNode = function (
  10339. mode,
  10340. init
  10341. ) {
  10342. var newNode,
  10343. tree = this.tree,
  10344. self = this;
  10345. mode = mode || "child";
  10346. if (init == null) {
  10347. init = { title: "" };
  10348. } else if (typeof init === "string") {
  10349. init = { title: init };
  10350. } else {
  10351. $.ui.fancytree.assert($.isPlainObject(init));
  10352. }
  10353. // Make sure node is expanded (and loaded) in 'child' mode
  10354. if (
  10355. mode === "child" &&
  10356. !this.isExpanded() &&
  10357. this.hasChildren() !== false
  10358. ) {
  10359. this.setExpanded().done(function () {
  10360. self.editCreateNode(mode, init);
  10361. });
  10362. return;
  10363. }
  10364. newNode = this.addNode(init, mode);
  10365. // #644: Don't filter new nodes.
  10366. newNode.match = true;
  10367. $(newNode[tree.statusClassPropName])
  10368. .removeClass("fancytree-hide")
  10369. .addClass("fancytree-match");
  10370. newNode.makeVisible(/*{noAnimation: true}*/).done(function () {
  10371. $(newNode[tree.statusClassPropName]).addClass("fancytree-edit-new");
  10372. self.tree.ext.edit.relatedNode = self;
  10373. newNode.editStart();
  10374. });
  10375. };
  10376. /**
  10377. * [ext-edit] Check if any node in this tree in edit mode.
  10378. *
  10379. * @returns {FancytreeNode | null}
  10380. * @alias Fancytree#isEditing
  10381. * @requires jquery.fancytree.edit.js
  10382. */
  10383. $.ui.fancytree._FancytreeClass.prototype.isEditing = function () {
  10384. return this.ext.edit ? this.ext.edit.currentNode : null;
  10385. };
  10386. /**
  10387. * [ext-edit] Check if this node is in edit mode.
  10388. * @returns {Boolean} true if node is currently beeing edited
  10389. * @alias FancytreeNode#isEditing
  10390. * @requires jquery.fancytree.edit.js
  10391. */
  10392. $.ui.fancytree._FancytreeNodeClass.prototype.isEditing = function () {
  10393. return this.tree.ext.edit
  10394. ? this.tree.ext.edit.currentNode === this
  10395. : false;
  10396. };
  10397. /*******************************************************************************
  10398. * Extension code
  10399. */
  10400. $.ui.fancytree.registerExtension({
  10401. name: "edit",
  10402. version: "2.38.3",
  10403. // Default options for this extension.
  10404. options: {
  10405. adjustWidthOfs: 4, // null: don't adjust input size to content
  10406. allowEmpty: false, // Prevent empty input
  10407. inputCss: { minWidth: "3em" },
  10408. // triggerCancel: ["esc", "tab", "click"],
  10409. triggerStart: ["f2", "mac+enter", "shift+click"],
  10410. trim: true, // Trim whitespace before save
  10411. // Events:
  10412. beforeClose: $.noop, // Return false to prevent cancel/save (data.input is available)
  10413. beforeEdit: $.noop, // Return false to prevent edit mode
  10414. close: $.noop, // Editor was removed
  10415. edit: $.noop, // Editor was opened (available as data.input)
  10416. // keypress: $.noop, // Not yet implemented
  10417. save: $.noop, // Save data.input.val() or return false to keep editor open
  10418. },
  10419. // Local attributes
  10420. currentNode: null,
  10421. treeInit: function (ctx) {
  10422. var tree = ctx.tree;
  10423. this._superApply(arguments);
  10424. this.$container
  10425. .addClass("fancytree-ext-edit")
  10426. .on("fancytreebeforeupdateviewport", function (event, data) {
  10427. var editNode = tree.isEditing();
  10428. // When scrolling, the TR may be re-used by another node, so the
  10429. // active cell marker an
  10430. if (editNode) {
  10431. editNode.info("Cancel edit due to scroll event.");
  10432. editNode.editEnd(false, event);
  10433. }
  10434. });
  10435. },
  10436. nodeClick: function (ctx) {
  10437. var eventStr = $.ui.fancytree.eventToString(ctx.originalEvent),
  10438. triggerStart = ctx.options.edit.triggerStart;
  10439. if (
  10440. eventStr === "shift+click" &&
  10441. $.inArray("shift+click", triggerStart) >= 0
  10442. ) {
  10443. if (ctx.originalEvent.shiftKey) {
  10444. ctx.node.editStart();
  10445. return false;
  10446. }
  10447. }
  10448. if (
  10449. eventStr === "click" &&
  10450. $.inArray("clickActive", triggerStart) >= 0
  10451. ) {
  10452. // Only when click was inside title text (not aynwhere else in the row)
  10453. if (
  10454. ctx.node.isActive() &&
  10455. !ctx.node.isEditing() &&
  10456. $(ctx.originalEvent.target).hasClass("fancytree-title")
  10457. ) {
  10458. ctx.node.editStart();
  10459. return false;
  10460. }
  10461. }
  10462. return this._superApply(arguments);
  10463. },
  10464. nodeDblclick: function (ctx) {
  10465. if ($.inArray("dblclick", ctx.options.edit.triggerStart) >= 0) {
  10466. ctx.node.editStart();
  10467. return false;
  10468. }
  10469. return this._superApply(arguments);
  10470. },
  10471. nodeKeydown: function (ctx) {
  10472. switch (ctx.originalEvent.which) {
  10473. case 113: // [F2]
  10474. if ($.inArray("f2", ctx.options.edit.triggerStart) >= 0) {
  10475. ctx.node.editStart();
  10476. return false;
  10477. }
  10478. break;
  10479. case $.ui.keyCode.ENTER:
  10480. if (
  10481. $.inArray("mac+enter", ctx.options.edit.triggerStart) >=
  10482. 0 &&
  10483. isMac
  10484. ) {
  10485. ctx.node.editStart();
  10486. return false;
  10487. }
  10488. break;
  10489. }
  10490. return this._superApply(arguments);
  10491. },
  10492. });
  10493. // Value returned by `require('jquery.fancytree..')`
  10494. return $.ui.fancytree;
  10495. }); // End of closure
  10496. /*! Extension 'jquery.fancytree.filter.js' *//*!
  10497. * jquery.fancytree.filter.js
  10498. *
  10499. * Remove or highlight tree nodes, based on a filter.
  10500. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  10501. *
  10502. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  10503. *
  10504. * Released under the MIT license
  10505. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  10506. *
  10507. * @version 2.38.3
  10508. * @date 2023-02-01T20:52:50Z
  10509. */
  10510. (function (factory) {
  10511. if (typeof define === "function" && define.amd) {
  10512. // AMD. Register as an anonymous module.
  10513. define(["jquery", "./jquery.fancytree"], factory);
  10514. } else if (typeof module === "object" && module.exports) {
  10515. // Node/CommonJS
  10516. require("./jquery.fancytree");
  10517. module.exports = factory(require("jquery"));
  10518. } else {
  10519. // Browser globals
  10520. factory(jQuery);
  10521. }
  10522. })(function ($) {
  10523. "use strict";
  10524. /*******************************************************************************
  10525. * Private functions and variables
  10526. */
  10527. var KeyNoData = "__not_found__",
  10528. escapeHtml = $.ui.fancytree.escapeHtml,
  10529. exoticStartChar = "\uFFF7",
  10530. exoticEndChar = "\uFFF8";
  10531. function _escapeRegex(str) {
  10532. return (str + "").replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
  10533. }
  10534. function extractHtmlText(s) {
  10535. if (s.indexOf(">") >= 0) {
  10536. return $("<div/>").html(s).text();
  10537. }
  10538. return s;
  10539. }
  10540. /**
  10541. * @description Marks the matching charecters of `text` either by `mark` or
  10542. * by exotic*Chars (if `escapeTitles` is `true`) based on `regexMatchArray`
  10543. * which is an array of matching groups.
  10544. * @param {string} text
  10545. * @param {RegExpMatchArray} regexMatchArray
  10546. */
  10547. function _markFuzzyMatchedChars(text, regexMatchArray, escapeTitles) {
  10548. // It is extremely infuriating that we can not use `let` or `const` or arrow functions.
  10549. // Damn you IE!!!
  10550. var matchingIndices = [];
  10551. // get the indices of matched characters (Iterate through `RegExpMatchArray`)
  10552. for (
  10553. var _matchingArrIdx = 1;
  10554. _matchingArrIdx < regexMatchArray.length;
  10555. _matchingArrIdx++
  10556. ) {
  10557. var _mIdx =
  10558. // get matching char index by cumulatively adding
  10559. // the matched group length
  10560. regexMatchArray[_matchingArrIdx].length +
  10561. (_matchingArrIdx === 1 ? 0 : 1) +
  10562. (matchingIndices[matchingIndices.length - 1] || 0);
  10563. matchingIndices.push(_mIdx);
  10564. }
  10565. // Map each `text` char to its position and store in `textPoses`.
  10566. var textPoses = text.split("");
  10567. if (escapeTitles) {
  10568. // If escaping the title, then wrap the matchng char within exotic chars
  10569. matchingIndices.forEach(function (v) {
  10570. textPoses[v] = exoticStartChar + textPoses[v] + exoticEndChar;
  10571. });
  10572. } else {
  10573. // Otherwise, Wrap the matching chars within `mark`.
  10574. matchingIndices.forEach(function (v) {
  10575. textPoses[v] = "<mark>" + textPoses[v] + "</mark>";
  10576. });
  10577. }
  10578. // Join back the modified `textPoses` to create final highlight markup.
  10579. return textPoses.join("");
  10580. }
  10581. $.ui.fancytree._FancytreeClass.prototype._applyFilterImpl = function (
  10582. filter,
  10583. branchMode,
  10584. _opts
  10585. ) {
  10586. var match,
  10587. statusNode,
  10588. re,
  10589. reHighlight,
  10590. reExoticStartChar,
  10591. reExoticEndChar,
  10592. temp,
  10593. prevEnableUpdate,
  10594. count = 0,
  10595. treeOpts = this.options,
  10596. escapeTitles = treeOpts.escapeTitles,
  10597. prevAutoCollapse = treeOpts.autoCollapse,
  10598. opts = $.extend({}, treeOpts.filter, _opts),
  10599. hideMode = opts.mode === "hide",
  10600. leavesOnly = !!opts.leavesOnly && !branchMode;
  10601. // Default to 'match title substring (not case sensitive)'
  10602. if (typeof filter === "string") {
  10603. if (filter === "") {
  10604. this.warn(
  10605. "Fancytree passing an empty string as a filter is handled as clearFilter()."
  10606. );
  10607. this.clearFilter();
  10608. return;
  10609. }
  10610. if (opts.fuzzy) {
  10611. // See https://codereview.stackexchange.com/questions/23899/faster-javascript-fuzzy-string-matching-function/23905#23905
  10612. // and http://www.quora.com/How-is-the-fuzzy-search-algorithm-in-Sublime-Text-designed
  10613. // and http://www.dustindiaz.com/autocomplete-fuzzy-matching
  10614. match = filter
  10615. .split("")
  10616. // Escaping the `filter` will not work because,
  10617. // it gets further split into individual characters. So,
  10618. // escape each character after splitting
  10619. .map(_escapeRegex)
  10620. .reduce(function (a, b) {
  10621. // create capture groups for parts that comes before
  10622. // the character
  10623. return a + "([^" + b + "]*)" + b;
  10624. }, "");
  10625. } else {
  10626. match = _escapeRegex(filter); // make sure a '.' is treated literally
  10627. }
  10628. re = new RegExp(match, "i");
  10629. reHighlight = new RegExp(_escapeRegex(filter), "gi");
  10630. if (escapeTitles) {
  10631. reExoticStartChar = new RegExp(
  10632. _escapeRegex(exoticStartChar),
  10633. "g"
  10634. );
  10635. reExoticEndChar = new RegExp(_escapeRegex(exoticEndChar), "g");
  10636. }
  10637. filter = function (node) {
  10638. if (!node.title) {
  10639. return false;
  10640. }
  10641. var text = escapeTitles
  10642. ? node.title
  10643. : extractHtmlText(node.title),
  10644. // `.match` instead of `.test` to get the capture groups
  10645. res = text.match(re);
  10646. if (res && opts.highlight) {
  10647. if (escapeTitles) {
  10648. if (opts.fuzzy) {
  10649. temp = _markFuzzyMatchedChars(
  10650. text,
  10651. res,
  10652. escapeTitles
  10653. );
  10654. } else {
  10655. // #740: we must not apply the marks to escaped entity names, e.g. `&quot;`
  10656. // Use some exotic characters to mark matches:
  10657. temp = text.replace(reHighlight, function (s) {
  10658. return exoticStartChar + s + exoticEndChar;
  10659. });
  10660. }
  10661. // now we can escape the title...
  10662. node.titleWithHighlight = escapeHtml(temp)
  10663. // ... and finally insert the desired `<mark>` tags
  10664. .replace(reExoticStartChar, "<mark>")
  10665. .replace(reExoticEndChar, "</mark>");
  10666. } else {
  10667. if (opts.fuzzy) {
  10668. node.titleWithHighlight = _markFuzzyMatchedChars(
  10669. text,
  10670. res
  10671. );
  10672. } else {
  10673. node.titleWithHighlight = text.replace(
  10674. reHighlight,
  10675. function (s) {
  10676. return "<mark>" + s + "</mark>";
  10677. }
  10678. );
  10679. }
  10680. }
  10681. // node.debug("filter", escapeTitles, text, node.titleWithHighlight);
  10682. }
  10683. return !!res;
  10684. };
  10685. }
  10686. this.enableFilter = true;
  10687. this.lastFilterArgs = arguments;
  10688. prevEnableUpdate = this.enableUpdate(false);
  10689. this.$div.addClass("fancytree-ext-filter");
  10690. if (hideMode) {
  10691. this.$div.addClass("fancytree-ext-filter-hide");
  10692. } else {
  10693. this.$div.addClass("fancytree-ext-filter-dimm");
  10694. }
  10695. this.$div.toggleClass(
  10696. "fancytree-ext-filter-hide-expanders",
  10697. !!opts.hideExpanders
  10698. );
  10699. // Reset current filter
  10700. this.rootNode.subMatchCount = 0;
  10701. this.visit(function (node) {
  10702. delete node.match;
  10703. delete node.titleWithHighlight;
  10704. node.subMatchCount = 0;
  10705. });
  10706. statusNode = this.getRootNode()._findDirectChild(KeyNoData);
  10707. if (statusNode) {
  10708. statusNode.remove();
  10709. }
  10710. // Adjust node.hide, .match, and .subMatchCount properties
  10711. treeOpts.autoCollapse = false; // #528
  10712. this.visit(function (node) {
  10713. if (leavesOnly && node.children != null) {
  10714. return;
  10715. }
  10716. var res = filter(node),
  10717. matchedByBranch = false;
  10718. if (res === "skip") {
  10719. node.visit(function (c) {
  10720. c.match = false;
  10721. }, true);
  10722. return "skip";
  10723. }
  10724. if (!res && (branchMode || res === "branch") && node.parent.match) {
  10725. res = true;
  10726. matchedByBranch = true;
  10727. }
  10728. if (res) {
  10729. count++;
  10730. node.match = true;
  10731. node.visitParents(function (p) {
  10732. if (p !== node) {
  10733. p.subMatchCount += 1;
  10734. }
  10735. // Expand match (unless this is no real match, but only a node in a matched branch)
  10736. if (opts.autoExpand && !matchedByBranch && !p.expanded) {
  10737. p.setExpanded(true, {
  10738. noAnimation: true,
  10739. noEvents: true,
  10740. scrollIntoView: false,
  10741. });
  10742. p._filterAutoExpanded = true;
  10743. }
  10744. }, true);
  10745. }
  10746. });
  10747. treeOpts.autoCollapse = prevAutoCollapse;
  10748. if (count === 0 && opts.nodata && hideMode) {
  10749. statusNode = opts.nodata;
  10750. if (typeof statusNode === "function") {
  10751. statusNode = statusNode();
  10752. }
  10753. if (statusNode === true) {
  10754. statusNode = {};
  10755. } else if (typeof statusNode === "string") {
  10756. statusNode = { title: statusNode };
  10757. }
  10758. statusNode = $.extend(
  10759. {
  10760. statusNodeType: "nodata",
  10761. key: KeyNoData,
  10762. title: this.options.strings.noData,
  10763. },
  10764. statusNode
  10765. );
  10766. this.getRootNode().addNode(statusNode).match = true;
  10767. }
  10768. // Redraw whole tree
  10769. this._callHook("treeStructureChanged", this, "applyFilter");
  10770. // this.render();
  10771. this.enableUpdate(prevEnableUpdate);
  10772. return count;
  10773. };
  10774. /**
  10775. * [ext-filter] Dimm or hide nodes.
  10776. *
  10777. * @param {function | string} filter
  10778. * @param {boolean} [opts={autoExpand: false, leavesOnly: false}]
  10779. * @returns {integer} count
  10780. * @alias Fancytree#filterNodes
  10781. * @requires jquery.fancytree.filter.js
  10782. */
  10783. $.ui.fancytree._FancytreeClass.prototype.filterNodes = function (
  10784. filter,
  10785. opts
  10786. ) {
  10787. if (typeof opts === "boolean") {
  10788. opts = { leavesOnly: opts };
  10789. this.warn(
  10790. "Fancytree.filterNodes() leavesOnly option is deprecated since 2.9.0 / 2015-04-19. Use opts.leavesOnly instead."
  10791. );
  10792. }
  10793. return this._applyFilterImpl(filter, false, opts);
  10794. };
  10795. /**
  10796. * [ext-filter] Dimm or hide whole branches.
  10797. *
  10798. * @param {function | string} filter
  10799. * @param {boolean} [opts={autoExpand: false}]
  10800. * @returns {integer} count
  10801. * @alias Fancytree#filterBranches
  10802. * @requires jquery.fancytree.filter.js
  10803. */
  10804. $.ui.fancytree._FancytreeClass.prototype.filterBranches = function (
  10805. filter,
  10806. opts
  10807. ) {
  10808. return this._applyFilterImpl(filter, true, opts);
  10809. };
  10810. /**
  10811. * [ext-filter] Re-apply current filter.
  10812. *
  10813. * @returns {integer} count
  10814. * @alias Fancytree#updateFilter
  10815. * @requires jquery.fancytree.filter.js
  10816. * @since 2.38
  10817. */
  10818. $.ui.fancytree._FancytreeClass.prototype.updateFilter = function () {
  10819. if (
  10820. this.enableFilter &&
  10821. this.lastFilterArgs &&
  10822. this.options.filter.autoApply
  10823. ) {
  10824. this._applyFilterImpl.apply(this, this.lastFilterArgs);
  10825. } else {
  10826. this.warn("updateFilter(): no filter active.");
  10827. }
  10828. };
  10829. /**
  10830. * [ext-filter] Reset the filter.
  10831. *
  10832. * @alias Fancytree#clearFilter
  10833. * @requires jquery.fancytree.filter.js
  10834. */
  10835. $.ui.fancytree._FancytreeClass.prototype.clearFilter = function () {
  10836. var $title,
  10837. statusNode = this.getRootNode()._findDirectChild(KeyNoData),
  10838. escapeTitles = this.options.escapeTitles,
  10839. enhanceTitle = this.options.enhanceTitle,
  10840. prevEnableUpdate = this.enableUpdate(false);
  10841. if (statusNode) {
  10842. statusNode.remove();
  10843. }
  10844. // we also counted root node's subMatchCount
  10845. delete this.rootNode.match;
  10846. delete this.rootNode.subMatchCount;
  10847. this.visit(function (node) {
  10848. if (node.match && node.span) {
  10849. // #491, #601
  10850. $title = $(node.span).find(">span.fancytree-title");
  10851. if (escapeTitles) {
  10852. $title.text(node.title);
  10853. } else {
  10854. $title.html(node.title);
  10855. }
  10856. if (enhanceTitle) {
  10857. enhanceTitle(
  10858. { type: "enhanceTitle" },
  10859. { node: node, $title: $title }
  10860. );
  10861. }
  10862. }
  10863. delete node.match;
  10864. delete node.subMatchCount;
  10865. delete node.titleWithHighlight;
  10866. if (node.$subMatchBadge) {
  10867. node.$subMatchBadge.remove();
  10868. delete node.$subMatchBadge;
  10869. }
  10870. if (node._filterAutoExpanded && node.expanded) {
  10871. node.setExpanded(false, {
  10872. noAnimation: true,
  10873. noEvents: true,
  10874. scrollIntoView: false,
  10875. });
  10876. }
  10877. delete node._filterAutoExpanded;
  10878. });
  10879. this.enableFilter = false;
  10880. this.lastFilterArgs = null;
  10881. this.$div.removeClass(
  10882. "fancytree-ext-filter fancytree-ext-filter-dimm fancytree-ext-filter-hide"
  10883. );
  10884. this._callHook("treeStructureChanged", this, "clearFilter");
  10885. // this.render();
  10886. this.enableUpdate(prevEnableUpdate);
  10887. };
  10888. /**
  10889. * [ext-filter] Return true if a filter is currently applied.
  10890. *
  10891. * @returns {Boolean}
  10892. * @alias Fancytree#isFilterActive
  10893. * @requires jquery.fancytree.filter.js
  10894. * @since 2.13
  10895. */
  10896. $.ui.fancytree._FancytreeClass.prototype.isFilterActive = function () {
  10897. return !!this.enableFilter;
  10898. };
  10899. /**
  10900. * [ext-filter] Return true if this node is matched by current filter (or no filter is active).
  10901. *
  10902. * @returns {Boolean}
  10903. * @alias FancytreeNode#isMatched
  10904. * @requires jquery.fancytree.filter.js
  10905. * @since 2.13
  10906. */
  10907. $.ui.fancytree._FancytreeNodeClass.prototype.isMatched = function () {
  10908. return !(this.tree.enableFilter && !this.match);
  10909. };
  10910. /*******************************************************************************
  10911. * Extension code
  10912. */
  10913. $.ui.fancytree.registerExtension({
  10914. name: "filter",
  10915. version: "2.38.3",
  10916. // Default options for this extension.
  10917. options: {
  10918. autoApply: true, // Re-apply last filter if lazy data is loaded
  10919. autoExpand: false, // Expand all branches that contain matches while filtered
  10920. counter: true, // Show a badge with number of matching child nodes near parent icons
  10921. fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
  10922. hideExpandedCounter: true, // Hide counter badge if parent is expanded
  10923. hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
  10924. highlight: true, // Highlight matches by wrapping inside <mark> tags
  10925. leavesOnly: false, // Match end nodes only
  10926. nodata: true, // Display a 'no data' status node if result is empty
  10927. mode: "dimm", // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
  10928. },
  10929. nodeLoadChildren: function (ctx, source) {
  10930. var tree = ctx.tree;
  10931. return this._superApply(arguments).done(function () {
  10932. if (
  10933. tree.enableFilter &&
  10934. tree.lastFilterArgs &&
  10935. ctx.options.filter.autoApply
  10936. ) {
  10937. tree._applyFilterImpl.apply(tree, tree.lastFilterArgs);
  10938. }
  10939. });
  10940. },
  10941. nodeSetExpanded: function (ctx, flag, callOpts) {
  10942. var node = ctx.node;
  10943. delete node._filterAutoExpanded;
  10944. // Make sure counter badge is displayed again, when node is beeing collapsed
  10945. if (
  10946. !flag &&
  10947. ctx.options.filter.hideExpandedCounter &&
  10948. node.$subMatchBadge
  10949. ) {
  10950. node.$subMatchBadge.show();
  10951. }
  10952. return this._superApply(arguments);
  10953. },
  10954. nodeRenderStatus: function (ctx) {
  10955. // Set classes for current status
  10956. var res,
  10957. node = ctx.node,
  10958. tree = ctx.tree,
  10959. opts = ctx.options.filter,
  10960. $title = $(node.span).find("span.fancytree-title"),
  10961. $span = $(node[tree.statusClassPropName]),
  10962. enhanceTitle = ctx.options.enhanceTitle,
  10963. escapeTitles = ctx.options.escapeTitles;
  10964. res = this._super(ctx);
  10965. // nothing to do, if node was not yet rendered
  10966. if (!$span.length || !tree.enableFilter) {
  10967. return res;
  10968. }
  10969. $span
  10970. .toggleClass("fancytree-match", !!node.match)
  10971. .toggleClass("fancytree-submatch", !!node.subMatchCount)
  10972. .toggleClass(
  10973. "fancytree-hide",
  10974. !(node.match || node.subMatchCount)
  10975. );
  10976. // Add/update counter badge
  10977. if (
  10978. opts.counter &&
  10979. node.subMatchCount &&
  10980. (!node.isExpanded() || !opts.hideExpandedCounter)
  10981. ) {
  10982. if (!node.$subMatchBadge) {
  10983. node.$subMatchBadge = $(
  10984. "<span class='fancytree-childcounter'/>"
  10985. );
  10986. $(
  10987. "span.fancytree-icon, span.fancytree-custom-icon",
  10988. node.span
  10989. ).append(node.$subMatchBadge);
  10990. }
  10991. node.$subMatchBadge.show().text(node.subMatchCount);
  10992. } else if (node.$subMatchBadge) {
  10993. node.$subMatchBadge.hide();
  10994. }
  10995. // node.debug("nodeRenderStatus", node.titleWithHighlight, node.title)
  10996. // #601: also check for $title.length, because we don't need to render
  10997. // if node.span is null (i.e. not rendered)
  10998. if (node.span && (!node.isEditing || !node.isEditing.call(node))) {
  10999. if (node.titleWithHighlight) {
  11000. $title.html(node.titleWithHighlight);
  11001. } else if (escapeTitles) {
  11002. $title.text(node.title);
  11003. } else {
  11004. $title.html(node.title);
  11005. }
  11006. if (enhanceTitle) {
  11007. enhanceTitle(
  11008. { type: "enhanceTitle" },
  11009. { node: node, $title: $title }
  11010. );
  11011. }
  11012. }
  11013. return res;
  11014. },
  11015. });
  11016. // Value returned by `require('jquery.fancytree..')`
  11017. return $.ui.fancytree;
  11018. }); // End of closure
  11019. /*! Extension 'jquery.fancytree.glyph.js' *//*!
  11020. * jquery.fancytree.glyph.js
  11021. *
  11022. * Use glyph-fonts, ligature-fonts, or SVG icons instead of icon sprites.
  11023. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  11024. *
  11025. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  11026. *
  11027. * Released under the MIT license
  11028. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  11029. *
  11030. * @version 2.38.3
  11031. * @date 2023-02-01T20:52:50Z
  11032. */
  11033. (function (factory) {
  11034. if (typeof define === "function" && define.amd) {
  11035. // AMD. Register as an anonymous module.
  11036. define(["jquery", "./jquery.fancytree"], factory);
  11037. } else if (typeof module === "object" && module.exports) {
  11038. // Node/CommonJS
  11039. require("./jquery.fancytree");
  11040. module.exports = factory(require("jquery"));
  11041. } else {
  11042. // Browser globals
  11043. factory(jQuery);
  11044. }
  11045. })(function ($) {
  11046. "use strict";
  11047. /******************************************************************************
  11048. * Private functions and variables
  11049. */
  11050. var FT = $.ui.fancytree,
  11051. PRESETS = {
  11052. awesome3: {
  11053. // Outdated!
  11054. _addClass: "",
  11055. checkbox: "icon-check-empty",
  11056. checkboxSelected: "icon-check",
  11057. checkboxUnknown: "icon-check icon-muted",
  11058. dragHelper: "icon-caret-right",
  11059. dropMarker: "icon-caret-right",
  11060. error: "icon-exclamation-sign",
  11061. expanderClosed: "icon-caret-right",
  11062. expanderLazy: "icon-angle-right",
  11063. expanderOpen: "icon-caret-down",
  11064. loading: "icon-refresh icon-spin",
  11065. nodata: "icon-meh",
  11066. noExpander: "",
  11067. radio: "icon-circle-blank",
  11068. radioSelected: "icon-circle",
  11069. // radioUnknown: "icon-circle icon-muted",
  11070. // Default node icons.
  11071. // (Use tree.options.icon callback to define custom icons based on node data)
  11072. doc: "icon-file-alt",
  11073. docOpen: "icon-file-alt",
  11074. folder: "icon-folder-close-alt",
  11075. folderOpen: "icon-folder-open-alt",
  11076. },
  11077. awesome4: {
  11078. _addClass: "fa",
  11079. checkbox: "fa-square-o",
  11080. checkboxSelected: "fa-check-square-o",
  11081. checkboxUnknown: "fa-square fancytree-helper-indeterminate-cb",
  11082. dragHelper: "fa-arrow-right",
  11083. dropMarker: "fa-long-arrow-right",
  11084. error: "fa-warning",
  11085. expanderClosed: "fa-caret-right",
  11086. expanderLazy: "fa-angle-right",
  11087. expanderOpen: "fa-caret-down",
  11088. // We may prevent wobbling rotations on FF by creating a separate sub element:
  11089. loading: { html: "<span class='fa fa-spinner fa-pulse' />" },
  11090. nodata: "fa-meh-o",
  11091. noExpander: "",
  11092. radio: "fa-circle-thin", // "fa-circle-o"
  11093. radioSelected: "fa-circle",
  11094. // radioUnknown: "fa-dot-circle-o",
  11095. // Default node icons.
  11096. // (Use tree.options.icon callback to define custom icons based on node data)
  11097. doc: "fa-file-o",
  11098. docOpen: "fa-file-o",
  11099. folder: "fa-folder-o",
  11100. folderOpen: "fa-folder-open-o",
  11101. },
  11102. awesome5: {
  11103. // fontawesome 5 have several different base classes
  11104. // "far, fas, fal and fab" The rendered svg puts that prefix
  11105. // in a different location so we have to keep them separate here
  11106. _addClass: "",
  11107. checkbox: "far fa-square",
  11108. checkboxSelected: "far fa-check-square",
  11109. // checkboxUnknown: "far fa-window-close",
  11110. checkboxUnknown:
  11111. "fas fa-square fancytree-helper-indeterminate-cb",
  11112. radio: "far fa-circle",
  11113. radioSelected: "fas fa-circle",
  11114. radioUnknown: "far fa-dot-circle",
  11115. dragHelper: "fas fa-arrow-right",
  11116. dropMarker: "fas fa-long-arrow-alt-right",
  11117. error: "fas fa-exclamation-triangle",
  11118. expanderClosed: "fas fa-caret-right",
  11119. expanderLazy: "fas fa-angle-right",
  11120. expanderOpen: "fas fa-caret-down",
  11121. loading: "fas fa-spinner fa-pulse",
  11122. nodata: "far fa-meh",
  11123. noExpander: "",
  11124. // Default node icons.
  11125. // (Use tree.options.icon callback to define custom icons based on node data)
  11126. doc: "far fa-file",
  11127. docOpen: "far fa-file",
  11128. folder: "far fa-folder",
  11129. folderOpen: "far fa-folder-open",
  11130. },
  11131. bootstrap3: {
  11132. _addClass: "glyphicon",
  11133. checkbox: "glyphicon-unchecked",
  11134. checkboxSelected: "glyphicon-check",
  11135. checkboxUnknown:
  11136. "glyphicon-expand fancytree-helper-indeterminate-cb", // "glyphicon-share",
  11137. dragHelper: "glyphicon-play",
  11138. dropMarker: "glyphicon-arrow-right",
  11139. error: "glyphicon-warning-sign",
  11140. expanderClosed: "glyphicon-menu-right", // glyphicon-plus-sign
  11141. expanderLazy: "glyphicon-menu-right", // glyphicon-plus-sign
  11142. expanderOpen: "glyphicon-menu-down", // glyphicon-minus-sign
  11143. loading: "glyphicon-refresh fancytree-helper-spin",
  11144. nodata: "glyphicon-info-sign",
  11145. noExpander: "",
  11146. radio: "glyphicon-remove-circle", // "glyphicon-unchecked",
  11147. radioSelected: "glyphicon-ok-circle", // "glyphicon-check",
  11148. // radioUnknown: "glyphicon-ban-circle",
  11149. // Default node icons.
  11150. // (Use tree.options.icon callback to define custom icons based on node data)
  11151. doc: "glyphicon-file",
  11152. docOpen: "glyphicon-file",
  11153. folder: "glyphicon-folder-close",
  11154. folderOpen: "glyphicon-folder-open",
  11155. },
  11156. material: {
  11157. _addClass: "material-icons",
  11158. checkbox: { text: "check_box_outline_blank" },
  11159. checkboxSelected: { text: "check_box" },
  11160. checkboxUnknown: { text: "indeterminate_check_box" },
  11161. dragHelper: { text: "play_arrow" },
  11162. dropMarker: { text: "arrow-forward" },
  11163. error: { text: "warning" },
  11164. expanderClosed: { text: "chevron_right" },
  11165. expanderLazy: { text: "last_page" },
  11166. expanderOpen: { text: "expand_more" },
  11167. loading: {
  11168. text: "autorenew",
  11169. addClass: "fancytree-helper-spin",
  11170. },
  11171. nodata: { text: "info" },
  11172. noExpander: { text: "" },
  11173. radio: { text: "radio_button_unchecked" },
  11174. radioSelected: { text: "radio_button_checked" },
  11175. // Default node icons.
  11176. // (Use tree.options.icon callback to define custom icons based on node data)
  11177. doc: { text: "insert_drive_file" },
  11178. docOpen: { text: "insert_drive_file" },
  11179. folder: { text: "folder" },
  11180. folderOpen: { text: "folder_open" },
  11181. },
  11182. };
  11183. function setIcon(node, span, baseClass, opts, type) {
  11184. var map = opts.map,
  11185. icon = map[type],
  11186. $span = $(span),
  11187. $counter = $span.find(".fancytree-childcounter"),
  11188. setClass = baseClass + " " + (map._addClass || "");
  11189. // #871 Allow a callback
  11190. if (typeof icon === "function") {
  11191. icon = icon.call(this, node, span, type);
  11192. }
  11193. // node.debug( "setIcon(" + baseClass + ", " + type + "): " + "oldIcon" + " -> " + icon );
  11194. // #871: propsed this, but I am not sure how robust this is, e.g.
  11195. // the prefix (fas, far) class changes are not considered?
  11196. // if (span.tagName === "svg" && opts.preset === "awesome5") {
  11197. // // fa5 script converts <i> to <svg> so call a specific handler.
  11198. // var oldIcon = "fa-" + $span.data("icon");
  11199. // // node.debug( "setIcon(" + baseClass + ", " + type + "): " + oldIcon + " -> " + icon );
  11200. // if (typeof oldIcon === "string") {
  11201. // $span.removeClass(oldIcon);
  11202. // }
  11203. // if (typeof icon === "string") {
  11204. // $span.addClass(icon);
  11205. // }
  11206. // return;
  11207. // }
  11208. if (typeof icon === "string") {
  11209. // #883: remove inner html that may be added by prev. mode
  11210. span.innerHTML = "";
  11211. $span.attr("class", setClass + " " + icon).append($counter);
  11212. } else if (icon) {
  11213. if (icon.text) {
  11214. span.textContent = "" + icon.text;
  11215. } else if (icon.html) {
  11216. span.innerHTML = icon.html;
  11217. } else {
  11218. span.innerHTML = "";
  11219. }
  11220. $span
  11221. .attr("class", setClass + " " + (icon.addClass || ""))
  11222. .append($counter);
  11223. }
  11224. }
  11225. $.ui.fancytree.registerExtension({
  11226. name: "glyph",
  11227. version: "2.38.3",
  11228. // Default options for this extension.
  11229. options: {
  11230. preset: null, // 'awesome3', 'awesome4', 'bootstrap3', 'material'
  11231. map: {},
  11232. },
  11233. treeInit: function (ctx) {
  11234. var tree = ctx.tree,
  11235. opts = ctx.options.glyph;
  11236. if (opts.preset) {
  11237. FT.assert(
  11238. !!PRESETS[opts.preset],
  11239. "Invalid value for `options.glyph.preset`: " + opts.preset
  11240. );
  11241. opts.map = $.extend({}, PRESETS[opts.preset], opts.map);
  11242. } else {
  11243. tree.warn("ext-glyph: missing `preset` option.");
  11244. }
  11245. this._superApply(arguments);
  11246. tree.$container.addClass("fancytree-ext-glyph");
  11247. },
  11248. nodeRenderStatus: function (ctx) {
  11249. var checkbox,
  11250. icon,
  11251. res,
  11252. span,
  11253. node = ctx.node,
  11254. $span = $(node.span),
  11255. opts = ctx.options.glyph;
  11256. res = this._super(ctx);
  11257. if (node.isRootNode()) {
  11258. return res;
  11259. }
  11260. span = $span.children(".fancytree-expander").get(0);
  11261. if (span) {
  11262. // if( node.isLoading() ){
  11263. // icon = "loading";
  11264. if (node.expanded && node.hasChildren()) {
  11265. icon = "expanderOpen";
  11266. } else if (node.isUndefined()) {
  11267. icon = "expanderLazy";
  11268. } else if (node.hasChildren()) {
  11269. icon = "expanderClosed";
  11270. } else {
  11271. icon = "noExpander";
  11272. }
  11273. // span.className = "fancytree-expander " + map[icon];
  11274. setIcon(node, span, "fancytree-expander", opts, icon);
  11275. }
  11276. if (node.tr) {
  11277. span = $("td", node.tr).find(".fancytree-checkbox").get(0);
  11278. } else {
  11279. span = $span.children(".fancytree-checkbox").get(0);
  11280. }
  11281. if (span) {
  11282. checkbox = FT.evalOption("checkbox", node, node, opts, false);
  11283. if (
  11284. (node.parent && node.parent.radiogroup) ||
  11285. checkbox === "radio"
  11286. ) {
  11287. icon = node.selected ? "radioSelected" : "radio";
  11288. setIcon(
  11289. node,
  11290. span,
  11291. "fancytree-checkbox fancytree-radio",
  11292. opts,
  11293. icon
  11294. );
  11295. } else {
  11296. // eslint-disable-next-line no-nested-ternary
  11297. icon = node.selected
  11298. ? "checkboxSelected"
  11299. : node.partsel
  11300. ? "checkboxUnknown"
  11301. : "checkbox";
  11302. // span.className = "fancytree-checkbox " + map[icon];
  11303. setIcon(node, span, "fancytree-checkbox", opts, icon);
  11304. }
  11305. }
  11306. // Standard icon (note that this does not match .fancytree-custom-icon,
  11307. // that might be set by opts.icon callbacks)
  11308. span = $span.children(".fancytree-icon").get(0);
  11309. if (span) {
  11310. if (node.statusNodeType) {
  11311. icon = node.statusNodeType; // loading, error
  11312. } else if (node.folder) {
  11313. icon =
  11314. node.expanded && node.hasChildren()
  11315. ? "folderOpen"
  11316. : "folder";
  11317. } else {
  11318. icon = node.expanded ? "docOpen" : "doc";
  11319. }
  11320. setIcon(node, span, "fancytree-icon", opts, icon);
  11321. }
  11322. return res;
  11323. },
  11324. nodeSetStatus: function (ctx, status, message, details) {
  11325. var res,
  11326. span,
  11327. opts = ctx.options.glyph,
  11328. node = ctx.node;
  11329. res = this._superApply(arguments);
  11330. if (
  11331. status === "error" ||
  11332. status === "loading" ||
  11333. status === "nodata"
  11334. ) {
  11335. if (node.parent) {
  11336. span = $(".fancytree-expander", node.span).get(0);
  11337. if (span) {
  11338. setIcon(node, span, "fancytree-expander", opts, status);
  11339. }
  11340. } else {
  11341. //
  11342. span = $(
  11343. ".fancytree-statusnode-" + status,
  11344. node[this.nodeContainerAttrName]
  11345. )
  11346. .find(".fancytree-icon")
  11347. .get(0);
  11348. if (span) {
  11349. setIcon(node, span, "fancytree-icon", opts, status);
  11350. }
  11351. }
  11352. }
  11353. return res;
  11354. },
  11355. });
  11356. // Value returned by `require('jquery.fancytree..')`
  11357. return $.ui.fancytree;
  11358. }); // End of closure
  11359. /*! Extension 'jquery.fancytree.gridnav.js' *//*!
  11360. * jquery.fancytree.gridnav.js
  11361. *
  11362. * Support keyboard navigation for trees with embedded input controls.
  11363. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  11364. *
  11365. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  11366. *
  11367. * Released under the MIT license
  11368. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  11369. *
  11370. * @version 2.38.3
  11371. * @date 2023-02-01T20:52:50Z
  11372. */
  11373. (function (factory) {
  11374. if (typeof define === "function" && define.amd) {
  11375. // AMD. Register as an anonymous module.
  11376. define([
  11377. "jquery",
  11378. "./jquery.fancytree",
  11379. "./jquery.fancytree.table",
  11380. ], factory);
  11381. } else if (typeof module === "object" && module.exports) {
  11382. // Node/CommonJS
  11383. require("./jquery.fancytree.table"); // core + table
  11384. module.exports = factory(require("jquery"));
  11385. } else {
  11386. // Browser globals
  11387. factory(jQuery);
  11388. }
  11389. })(function ($) {
  11390. "use strict";
  11391. /*******************************************************************************
  11392. * Private functions and variables
  11393. */
  11394. // Allow these navigation keys even when input controls are focused
  11395. var KC = $.ui.keyCode,
  11396. // which keys are *not* handled by embedded control, but passed to tree
  11397. // navigation handler:
  11398. NAV_KEYS = {
  11399. text: [KC.UP, KC.DOWN],
  11400. checkbox: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
  11401. link: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
  11402. radiobutton: [KC.UP, KC.DOWN, KC.LEFT, KC.RIGHT],
  11403. "select-one": [KC.LEFT, KC.RIGHT],
  11404. "select-multiple": [KC.LEFT, KC.RIGHT],
  11405. };
  11406. /* Calculate TD column index (considering colspans).*/
  11407. function getColIdx($tr, $td) {
  11408. var colspan,
  11409. td = $td.get(0),
  11410. idx = 0;
  11411. $tr.children().each(function () {
  11412. if (this === td) {
  11413. return false;
  11414. }
  11415. colspan = $(this).prop("colspan");
  11416. idx += colspan ? colspan : 1;
  11417. });
  11418. return idx;
  11419. }
  11420. /* Find TD at given column index (considering colspans).*/
  11421. function findTdAtColIdx($tr, colIdx) {
  11422. var colspan,
  11423. res = null,
  11424. idx = 0;
  11425. $tr.children().each(function () {
  11426. if (idx >= colIdx) {
  11427. res = $(this);
  11428. return false;
  11429. }
  11430. colspan = $(this).prop("colspan");
  11431. idx += colspan ? colspan : 1;
  11432. });
  11433. return res;
  11434. }
  11435. /* Find adjacent cell for a given direction. Skip empty cells and consider merged cells */
  11436. function findNeighbourTd($target, keyCode) {
  11437. var $tr,
  11438. colIdx,
  11439. $td = $target.closest("td"),
  11440. $tdNext = null;
  11441. switch (keyCode) {
  11442. case KC.LEFT:
  11443. $tdNext = $td.prev();
  11444. break;
  11445. case KC.RIGHT:
  11446. $tdNext = $td.next();
  11447. break;
  11448. case KC.UP:
  11449. case KC.DOWN:
  11450. $tr = $td.parent();
  11451. colIdx = getColIdx($tr, $td);
  11452. while (true) {
  11453. $tr = keyCode === KC.UP ? $tr.prev() : $tr.next();
  11454. if (!$tr.length) {
  11455. break;
  11456. }
  11457. // Skip hidden rows
  11458. if ($tr.is(":hidden")) {
  11459. continue;
  11460. }
  11461. // Find adjacent cell in the same column
  11462. $tdNext = findTdAtColIdx($tr, colIdx);
  11463. // Skip cells that don't conatain a focusable element
  11464. if ($tdNext && $tdNext.find(":input,a").length) {
  11465. break;
  11466. }
  11467. }
  11468. break;
  11469. }
  11470. return $tdNext;
  11471. }
  11472. /*******************************************************************************
  11473. * Extension code
  11474. */
  11475. $.ui.fancytree.registerExtension({
  11476. name: "gridnav",
  11477. version: "2.38.3",
  11478. // Default options for this extension.
  11479. options: {
  11480. autofocusInput: false, // Focus first embedded input if node gets activated
  11481. handleCursorKeys: true, // Allow UP/DOWN in inputs to move to prev/next node
  11482. },
  11483. treeInit: function (ctx) {
  11484. // gridnav requires the table extension to be loaded before itself
  11485. this._requireExtension("table", true, true);
  11486. this._superApply(arguments);
  11487. this.$container.addClass("fancytree-ext-gridnav");
  11488. // Activate node if embedded input gets focus (due to a click)
  11489. this.$container.on("focusin", function (event) {
  11490. var ctx2,
  11491. node = $.ui.fancytree.getNode(event.target);
  11492. if (node && !node.isActive()) {
  11493. // Call node.setActive(), but also pass the event
  11494. ctx2 = ctx.tree._makeHookContext(node, event);
  11495. ctx.tree._callHook("nodeSetActive", ctx2, true);
  11496. }
  11497. });
  11498. },
  11499. nodeSetActive: function (ctx, flag, callOpts) {
  11500. var $outer,
  11501. opts = ctx.options.gridnav,
  11502. node = ctx.node,
  11503. event = ctx.originalEvent || {},
  11504. triggeredByInput = $(event.target).is(":input");
  11505. flag = flag !== false;
  11506. this._superApply(arguments);
  11507. if (flag) {
  11508. if (ctx.options.titlesTabbable) {
  11509. if (!triggeredByInput) {
  11510. $(node.span).find("span.fancytree-title").focus();
  11511. node.setFocus();
  11512. }
  11513. // If one node is tabbable, the container no longer needs to be
  11514. ctx.tree.$container.attr("tabindex", "-1");
  11515. // ctx.tree.$container.removeAttr("tabindex");
  11516. } else if (opts.autofocusInput && !triggeredByInput) {
  11517. // Set focus to input sub input (if node was clicked, but not
  11518. // when TAB was pressed )
  11519. $outer = $(node.tr || node.span);
  11520. $outer.find(":input:enabled").first().focus();
  11521. }
  11522. }
  11523. },
  11524. nodeKeydown: function (ctx) {
  11525. var inputType,
  11526. handleKeys,
  11527. $td,
  11528. opts = ctx.options.gridnav,
  11529. event = ctx.originalEvent,
  11530. $target = $(event.target);
  11531. if ($target.is(":input:enabled")) {
  11532. inputType = $target.prop("type");
  11533. } else if ($target.is("a")) {
  11534. inputType = "link";
  11535. }
  11536. // ctx.tree.debug("ext-gridnav nodeKeydown", event, inputType);
  11537. if (inputType && opts.handleCursorKeys) {
  11538. handleKeys = NAV_KEYS[inputType];
  11539. if (handleKeys && $.inArray(event.which, handleKeys) >= 0) {
  11540. $td = findNeighbourTd($target, event.which);
  11541. if ($td && $td.length) {
  11542. // ctx.node.debug("ignore keydown in input", event.which, handleKeys);
  11543. $td.find(":input:enabled,a").focus();
  11544. // Prevent Fancytree default navigation
  11545. return false;
  11546. }
  11547. }
  11548. return true;
  11549. }
  11550. // ctx.tree.debug("ext-gridnav NOT HANDLED", event, inputType);
  11551. return this._superApply(arguments);
  11552. },
  11553. });
  11554. // Value returned by `require('jquery.fancytree..')`
  11555. return $.ui.fancytree;
  11556. }); // End of closure
  11557. /*! Extension 'jquery.fancytree.multi.js' *//*!
  11558. * jquery.fancytree.multi.js
  11559. *
  11560. * Allow multiple selection of nodes by mouse or keyboard.
  11561. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  11562. *
  11563. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  11564. *
  11565. * Released under the MIT license
  11566. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  11567. *
  11568. * @version 2.38.3
  11569. * @date 2023-02-01T20:52:50Z
  11570. */
  11571. (function (factory) {
  11572. if (typeof define === "function" && define.amd) {
  11573. // AMD. Register as an anonymous module.
  11574. define(["jquery", "./jquery.fancytree"], factory);
  11575. } else if (typeof module === "object" && module.exports) {
  11576. // Node/CommonJS
  11577. require("./jquery.fancytree");
  11578. module.exports = factory(require("jquery"));
  11579. } else {
  11580. // Browser globals
  11581. factory(jQuery);
  11582. }
  11583. })(function ($) {
  11584. "use strict";
  11585. /*******************************************************************************
  11586. * Private functions and variables
  11587. */
  11588. // var isMac = /Mac/.test(navigator.platform);
  11589. /*******************************************************************************
  11590. * Extension code
  11591. */
  11592. $.ui.fancytree.registerExtension({
  11593. name: "multi",
  11594. version: "2.38.3",
  11595. // Default options for this extension.
  11596. options: {
  11597. allowNoSelect: false, //
  11598. mode: "sameParent", //
  11599. // Events:
  11600. // beforeSelect: $.noop // Return false to prevent cancel/save (data.input is available)
  11601. },
  11602. treeInit: function (ctx) {
  11603. this._superApply(arguments);
  11604. this.$container.addClass("fancytree-ext-multi");
  11605. if (ctx.options.selectMode === 1) {
  11606. $.error(
  11607. "Fancytree ext-multi: selectMode: 1 (single) is not compatible."
  11608. );
  11609. }
  11610. },
  11611. nodeClick: function (ctx) {
  11612. var //pluginOpts = ctx.options.multi,
  11613. tree = ctx.tree,
  11614. node = ctx.node,
  11615. activeNode = tree.getActiveNode() || tree.getFirstChild(),
  11616. isCbClick = ctx.targetType === "checkbox",
  11617. isExpanderClick = ctx.targetType === "expander",
  11618. eventStr = $.ui.fancytree.eventToString(ctx.originalEvent);
  11619. switch (eventStr) {
  11620. case "click":
  11621. if (isExpanderClick) {
  11622. break;
  11623. } // Default handler will expand/collapse
  11624. if (!isCbClick) {
  11625. tree.selectAll(false);
  11626. // Select clicked node (radio-button mode)
  11627. node.setSelected();
  11628. }
  11629. // Default handler will toggle checkbox clicks and activate
  11630. break;
  11631. case "shift+click":
  11632. // node.debug("click")
  11633. tree.visitRows(
  11634. function (n) {
  11635. // n.debug("click2", n===node, node)
  11636. n.setSelected();
  11637. if (n === node) {
  11638. return false;
  11639. }
  11640. },
  11641. {
  11642. start: activeNode,
  11643. reverse: activeNode.isBelowOf(node),
  11644. }
  11645. );
  11646. break;
  11647. case "ctrl+click":
  11648. case "meta+click": // Mac: [Command]
  11649. node.toggleSelected();
  11650. return;
  11651. }
  11652. return this._superApply(arguments);
  11653. },
  11654. nodeKeydown: function (ctx) {
  11655. var tree = ctx.tree,
  11656. node = ctx.node,
  11657. event = ctx.originalEvent,
  11658. eventStr = $.ui.fancytree.eventToString(event);
  11659. switch (eventStr) {
  11660. case "up":
  11661. case "down":
  11662. tree.selectAll(false);
  11663. node.navigate(event.which, true);
  11664. tree.getActiveNode().setSelected();
  11665. break;
  11666. case "shift+up":
  11667. case "shift+down":
  11668. node.navigate(event.which, true);
  11669. tree.getActiveNode().setSelected();
  11670. break;
  11671. }
  11672. return this._superApply(arguments);
  11673. },
  11674. });
  11675. // Value returned by `require('jquery.fancytree..')`
  11676. return $.ui.fancytree;
  11677. }); // End of closure
  11678. /*! Extension 'jquery.fancytree.persist.js' *//*!
  11679. * jquery.fancytree.persist.js
  11680. *
  11681. * Persist tree status in cookiesRemove or highlight tree nodes, based on a filter.
  11682. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  11683. *
  11684. * @depends: js-cookie or jquery-cookie
  11685. *
  11686. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  11687. *
  11688. * Released under the MIT license
  11689. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  11690. *
  11691. * @version 2.38.3
  11692. * @date 2023-02-01T20:52:50Z
  11693. */
  11694. (function (factory) {
  11695. if (typeof define === "function" && define.amd) {
  11696. // AMD. Register as an anonymous module.
  11697. define(["jquery", "./jquery.fancytree"], factory);
  11698. } else if (typeof module === "object" && module.exports) {
  11699. // Node/CommonJS
  11700. require("./jquery.fancytree");
  11701. module.exports = factory(require("jquery"));
  11702. } else {
  11703. // Browser globals
  11704. factory(jQuery);
  11705. }
  11706. })(function ($) {
  11707. "use strict";
  11708. /* global Cookies:false */
  11709. /*******************************************************************************
  11710. * Private functions and variables
  11711. */
  11712. var cookieStore = null,
  11713. localStorageStore = null,
  11714. sessionStorageStore = null,
  11715. _assert = $.ui.fancytree.assert,
  11716. ACTIVE = "active",
  11717. EXPANDED = "expanded",
  11718. FOCUS = "focus",
  11719. SELECTED = "selected";
  11720. // Accessing window.xxxStorage may raise security exceptions (see #1022)
  11721. try {
  11722. _assert(window.localStorage && window.localStorage.getItem);
  11723. localStorageStore = {
  11724. get: function (key) {
  11725. return window.localStorage.getItem(key);
  11726. },
  11727. set: function (key, value) {
  11728. window.localStorage.setItem(key, value);
  11729. },
  11730. remove: function (key) {
  11731. window.localStorage.removeItem(key);
  11732. },
  11733. };
  11734. } catch (e) {
  11735. $.ui.fancytree.warn("Could not access window.localStorage", e);
  11736. }
  11737. try {
  11738. _assert(window.sessionStorage && window.sessionStorage.getItem);
  11739. sessionStorageStore = {
  11740. get: function (key) {
  11741. return window.sessionStorage.getItem(key);
  11742. },
  11743. set: function (key, value) {
  11744. window.sessionStorage.setItem(key, value);
  11745. },
  11746. remove: function (key) {
  11747. window.sessionStorage.removeItem(key);
  11748. },
  11749. };
  11750. } catch (e) {
  11751. $.ui.fancytree.warn("Could not access window.sessionStorage", e);
  11752. }
  11753. if (typeof Cookies === "function") {
  11754. // Assume https://github.com/js-cookie/js-cookie
  11755. cookieStore = {
  11756. get: Cookies.get,
  11757. set: function (key, value) {
  11758. Cookies.set(key, value, this.options.persist.cookie);
  11759. },
  11760. remove: Cookies.remove,
  11761. };
  11762. } else if ($ && typeof $.cookie === "function") {
  11763. // Fall back to https://github.com/carhartl/jquery-cookie
  11764. cookieStore = {
  11765. get: $.cookie,
  11766. set: function (key, value) {
  11767. $.cookie(key, value, this.options.persist.cookie);
  11768. },
  11769. remove: $.removeCookie,
  11770. };
  11771. }
  11772. /* Recursively load lazy nodes
  11773. * @param {string} mode 'load', 'expand', false
  11774. */
  11775. function _loadLazyNodes(tree, local, keyList, mode, dfd) {
  11776. var i,
  11777. key,
  11778. l,
  11779. node,
  11780. foundOne = false,
  11781. expandOpts = tree.options.persist.expandOpts,
  11782. deferredList = [],
  11783. missingKeyList = [];
  11784. keyList = keyList || [];
  11785. dfd = dfd || $.Deferred();
  11786. for (i = 0, l = keyList.length; i < l; i++) {
  11787. key = keyList[i];
  11788. node = tree.getNodeByKey(key);
  11789. if (node) {
  11790. if (mode && node.isUndefined()) {
  11791. foundOne = true;
  11792. tree.debug(
  11793. "_loadLazyNodes: " + node + " is lazy: loading..."
  11794. );
  11795. if (mode === "expand") {
  11796. deferredList.push(node.setExpanded(true, expandOpts));
  11797. } else {
  11798. deferredList.push(node.load());
  11799. }
  11800. } else {
  11801. tree.debug("_loadLazyNodes: " + node + " already loaded.");
  11802. node.setExpanded(true, expandOpts);
  11803. }
  11804. } else {
  11805. missingKeyList.push(key);
  11806. tree.debug("_loadLazyNodes: " + node + " was not yet found.");
  11807. }
  11808. }
  11809. $.when.apply($, deferredList).always(function () {
  11810. // All lazy-expands have finished
  11811. if (foundOne && missingKeyList.length > 0) {
  11812. // If we read new nodes from server, try to resolve yet-missing keys
  11813. _loadLazyNodes(tree, local, missingKeyList, mode, dfd);
  11814. } else {
  11815. if (missingKeyList.length) {
  11816. tree.warn(
  11817. "_loadLazyNodes: could not load those keys: ",
  11818. missingKeyList
  11819. );
  11820. for (i = 0, l = missingKeyList.length; i < l; i++) {
  11821. key = keyList[i];
  11822. local._appendKey(EXPANDED, keyList[i], false);
  11823. }
  11824. }
  11825. dfd.resolve();
  11826. }
  11827. });
  11828. return dfd;
  11829. }
  11830. /**
  11831. * [ext-persist] Remove persistence data of the given type(s).
  11832. * Called like
  11833. * $.ui.fancytree.getTree("#tree").clearCookies("active expanded focus selected");
  11834. *
  11835. * @alias Fancytree#clearPersistData
  11836. * @requires jquery.fancytree.persist.js
  11837. */
  11838. $.ui.fancytree._FancytreeClass.prototype.clearPersistData = function (
  11839. types
  11840. ) {
  11841. var local = this.ext.persist,
  11842. prefix = local.cookiePrefix;
  11843. types = types || "active expanded focus selected";
  11844. if (types.indexOf(ACTIVE) >= 0) {
  11845. local._data(prefix + ACTIVE, null);
  11846. }
  11847. if (types.indexOf(EXPANDED) >= 0) {
  11848. local._data(prefix + EXPANDED, null);
  11849. }
  11850. if (types.indexOf(FOCUS) >= 0) {
  11851. local._data(prefix + FOCUS, null);
  11852. }
  11853. if (types.indexOf(SELECTED) >= 0) {
  11854. local._data(prefix + SELECTED, null);
  11855. }
  11856. };
  11857. $.ui.fancytree._FancytreeClass.prototype.clearCookies = function (types) {
  11858. this.warn(
  11859. "'tree.clearCookies()' is deprecated since v2.27.0: use 'clearPersistData()' instead."
  11860. );
  11861. return this.clearPersistData(types);
  11862. };
  11863. /**
  11864. * [ext-persist] Return persistence information from cookies
  11865. *
  11866. * Called like
  11867. * $.ui.fancytree.getTree("#tree").getPersistData();
  11868. *
  11869. * @alias Fancytree#getPersistData
  11870. * @requires jquery.fancytree.persist.js
  11871. */
  11872. $.ui.fancytree._FancytreeClass.prototype.getPersistData = function () {
  11873. var local = this.ext.persist,
  11874. prefix = local.cookiePrefix,
  11875. delim = local.cookieDelimiter,
  11876. res = {};
  11877. res[ACTIVE] = local._data(prefix + ACTIVE);
  11878. res[EXPANDED] = (local._data(prefix + EXPANDED) || "").split(delim);
  11879. res[SELECTED] = (local._data(prefix + SELECTED) || "").split(delim);
  11880. res[FOCUS] = local._data(prefix + FOCUS);
  11881. return res;
  11882. };
  11883. /******************************************************************************
  11884. * Extension code
  11885. */
  11886. $.ui.fancytree.registerExtension({
  11887. name: "persist",
  11888. version: "2.38.3",
  11889. // Default options for this extension.
  11890. options: {
  11891. cookieDelimiter: "~",
  11892. cookiePrefix: undefined, // 'fancytree-<treeId>-' by default
  11893. cookie: {
  11894. raw: false,
  11895. expires: "",
  11896. path: "",
  11897. domain: "",
  11898. secure: false,
  11899. },
  11900. expandLazy: false, // true: recursively expand and load lazy nodes
  11901. expandOpts: undefined, // optional `opts` argument passed to setExpanded()
  11902. fireActivate: true, // false: suppress `activate` event after active node was restored
  11903. overrideSource: true, // true: cookie takes precedence over `source` data attributes.
  11904. store: "auto", // 'cookie': force cookie, 'local': force localStore, 'session': force sessionStore
  11905. types: "active expanded focus selected",
  11906. },
  11907. /* Generic read/write string data to cookie, sessionStorage or localStorage. */
  11908. _data: function (key, value) {
  11909. var store = this._local.store;
  11910. if (value === undefined) {
  11911. return store.get.call(this, key);
  11912. } else if (value === null) {
  11913. store.remove.call(this, key);
  11914. } else {
  11915. store.set.call(this, key, value);
  11916. }
  11917. },
  11918. /* Append `key` to a cookie. */
  11919. _appendKey: function (type, key, flag) {
  11920. key = "" + key; // #90
  11921. var local = this._local,
  11922. instOpts = this.options.persist,
  11923. delim = instOpts.cookieDelimiter,
  11924. cookieName = local.cookiePrefix + type,
  11925. data = local._data(cookieName),
  11926. keyList = data ? data.split(delim) : [],
  11927. idx = $.inArray(key, keyList);
  11928. // Remove, even if we add a key, so the key is always the last entry
  11929. if (idx >= 0) {
  11930. keyList.splice(idx, 1);
  11931. }
  11932. // Append key to cookie
  11933. if (flag) {
  11934. keyList.push(key);
  11935. }
  11936. local._data(cookieName, keyList.join(delim));
  11937. },
  11938. treeInit: function (ctx) {
  11939. var tree = ctx.tree,
  11940. opts = ctx.options,
  11941. local = this._local,
  11942. instOpts = this.options.persist;
  11943. // // For 'auto' or 'cookie' mode, the cookie plugin must be available
  11944. // _assert((instOpts.store !== "auto" && instOpts.store !== "cookie") || cookieStore,
  11945. // "Missing required plugin for 'persist' extension: js.cookie.js or jquery.cookie.js");
  11946. local.cookiePrefix =
  11947. instOpts.cookiePrefix || "fancytree-" + tree._id + "-";
  11948. local.storeActive = instOpts.types.indexOf(ACTIVE) >= 0;
  11949. local.storeExpanded = instOpts.types.indexOf(EXPANDED) >= 0;
  11950. local.storeSelected = instOpts.types.indexOf(SELECTED) >= 0;
  11951. local.storeFocus = instOpts.types.indexOf(FOCUS) >= 0;
  11952. local.store = null;
  11953. if (instOpts.store === "auto") {
  11954. instOpts.store = localStorageStore ? "local" : "cookie";
  11955. }
  11956. if ($.isPlainObject(instOpts.store)) {
  11957. local.store = instOpts.store;
  11958. } else if (instOpts.store === "cookie") {
  11959. local.store = cookieStore;
  11960. } else if (instOpts.store === "local") {
  11961. local.store =
  11962. instOpts.store === "local"
  11963. ? localStorageStore
  11964. : sessionStorageStore;
  11965. } else if (instOpts.store === "session") {
  11966. local.store =
  11967. instOpts.store === "local"
  11968. ? localStorageStore
  11969. : sessionStorageStore;
  11970. }
  11971. _assert(local.store, "Need a valid store.");
  11972. // Bind init-handler to apply cookie state
  11973. tree.$div.on("fancytreeinit", function (event) {
  11974. if (
  11975. tree._triggerTreeEvent("beforeRestore", null, {}) === false
  11976. ) {
  11977. return;
  11978. }
  11979. var cookie,
  11980. dfd,
  11981. i,
  11982. keyList,
  11983. node,
  11984. prevFocus = local._data(local.cookiePrefix + FOCUS), // record this before node.setActive() overrides it;
  11985. noEvents = instOpts.fireActivate === false;
  11986. // tree.debug("document.cookie:", document.cookie);
  11987. cookie = local._data(local.cookiePrefix + EXPANDED);
  11988. keyList = cookie && cookie.split(instOpts.cookieDelimiter);
  11989. if (local.storeExpanded) {
  11990. // Recursively load nested lazy nodes if expandLazy is 'expand' or 'load'
  11991. // Also remove expand-cookies for unmatched nodes
  11992. dfd = _loadLazyNodes(
  11993. tree,
  11994. local,
  11995. keyList,
  11996. instOpts.expandLazy ? "expand" : false,
  11997. null
  11998. );
  11999. } else {
  12000. // nothing to do
  12001. dfd = new $.Deferred().resolve();
  12002. }
  12003. dfd.done(function () {
  12004. if (local.storeSelected) {
  12005. cookie = local._data(local.cookiePrefix + SELECTED);
  12006. if (cookie) {
  12007. keyList = cookie.split(instOpts.cookieDelimiter);
  12008. for (i = 0; i < keyList.length; i++) {
  12009. node = tree.getNodeByKey(keyList[i]);
  12010. if (node) {
  12011. if (
  12012. node.selected === undefined ||
  12013. (instOpts.overrideSource &&
  12014. node.selected === false)
  12015. ) {
  12016. // node.setSelected();
  12017. node.selected = true;
  12018. node.renderStatus();
  12019. }
  12020. } else {
  12021. // node is no longer member of the tree: remove from cookie also
  12022. local._appendKey(
  12023. SELECTED,
  12024. keyList[i],
  12025. false
  12026. );
  12027. }
  12028. }
  12029. }
  12030. // In selectMode 3 we have to fix the child nodes, since we
  12031. // only stored the selected *top* nodes
  12032. if (tree.options.selectMode === 3) {
  12033. tree.visit(function (n) {
  12034. if (n.selected) {
  12035. n.fixSelection3AfterClick();
  12036. return "skip";
  12037. }
  12038. });
  12039. }
  12040. }
  12041. if (local.storeActive) {
  12042. cookie = local._data(local.cookiePrefix + ACTIVE);
  12043. if (
  12044. cookie &&
  12045. (opts.persist.overrideSource || !tree.activeNode)
  12046. ) {
  12047. node = tree.getNodeByKey(cookie);
  12048. if (node) {
  12049. node.debug("persist: set active", cookie);
  12050. // We only want to set the focus if the container
  12051. // had the keyboard focus before
  12052. node.setActive(true, {
  12053. noFocus: true,
  12054. noEvents: noEvents,
  12055. });
  12056. }
  12057. }
  12058. }
  12059. if (local.storeFocus && prevFocus) {
  12060. node = tree.getNodeByKey(prevFocus);
  12061. if (node) {
  12062. // node.debug("persist: set focus", cookie);
  12063. if (tree.options.titlesTabbable) {
  12064. $(node.span).find(".fancytree-title").focus();
  12065. } else {
  12066. $(tree.$container).focus();
  12067. }
  12068. // node.setFocus();
  12069. }
  12070. }
  12071. tree._triggerTreeEvent("restore", null, {});
  12072. });
  12073. });
  12074. // Init the tree
  12075. return this._superApply(arguments);
  12076. },
  12077. nodeSetActive: function (ctx, flag, callOpts) {
  12078. var res,
  12079. local = this._local;
  12080. flag = flag !== false;
  12081. res = this._superApply(arguments);
  12082. if (local.storeActive) {
  12083. local._data(
  12084. local.cookiePrefix + ACTIVE,
  12085. this.activeNode ? this.activeNode.key : null
  12086. );
  12087. }
  12088. return res;
  12089. },
  12090. nodeSetExpanded: function (ctx, flag, callOpts) {
  12091. var res,
  12092. node = ctx.node,
  12093. local = this._local;
  12094. flag = flag !== false;
  12095. res = this._superApply(arguments);
  12096. if (local.storeExpanded) {
  12097. local._appendKey(EXPANDED, node.key, flag);
  12098. }
  12099. return res;
  12100. },
  12101. nodeSetFocus: function (ctx, flag) {
  12102. var res,
  12103. local = this._local;
  12104. flag = flag !== false;
  12105. res = this._superApply(arguments);
  12106. if (local.storeFocus) {
  12107. local._data(
  12108. local.cookiePrefix + FOCUS,
  12109. this.focusNode ? this.focusNode.key : null
  12110. );
  12111. }
  12112. return res;
  12113. },
  12114. nodeSetSelected: function (ctx, flag, callOpts) {
  12115. var res,
  12116. selNodes,
  12117. tree = ctx.tree,
  12118. node = ctx.node,
  12119. local = this._local;
  12120. flag = flag !== false;
  12121. res = this._superApply(arguments);
  12122. if (local.storeSelected) {
  12123. if (tree.options.selectMode === 3) {
  12124. // In selectMode 3 we only store the the selected *top* nodes.
  12125. // De-selecting a node may also de-select some parents, so we
  12126. // calculate the current status again
  12127. selNodes = $.map(tree.getSelectedNodes(true), function (n) {
  12128. return n.key;
  12129. });
  12130. selNodes = selNodes.join(
  12131. ctx.options.persist.cookieDelimiter
  12132. );
  12133. local._data(local.cookiePrefix + SELECTED, selNodes);
  12134. } else {
  12135. // beforeSelect can prevent the change - flag doesn't reflect the node.selected state
  12136. local._appendKey(SELECTED, node.key, node.selected);
  12137. }
  12138. }
  12139. return res;
  12140. },
  12141. });
  12142. // Value returned by `require('jquery.fancytree..')`
  12143. return $.ui.fancytree;
  12144. }); // End of closure
  12145. /*! Extension 'jquery.fancytree.table.js' *//*!
  12146. * jquery.fancytree.table.js
  12147. *
  12148. * Render tree as table (aka 'tree grid', 'table tree').
  12149. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  12150. *
  12151. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  12152. *
  12153. * Released under the MIT license
  12154. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  12155. *
  12156. * @version 2.38.3
  12157. * @date 2023-02-01T20:52:50Z
  12158. */
  12159. (function (factory) {
  12160. if (typeof define === "function" && define.amd) {
  12161. // AMD. Register as an anonymous module.
  12162. define(["jquery", "./jquery.fancytree"], factory);
  12163. } else if (typeof module === "object" && module.exports) {
  12164. // Node/CommonJS
  12165. require("./jquery.fancytree");
  12166. module.exports = factory(require("jquery"));
  12167. } else {
  12168. // Browser globals
  12169. factory(jQuery);
  12170. }
  12171. })(function ($) {
  12172. "use strict";
  12173. /******************************************************************************
  12174. * Private functions and variables
  12175. */
  12176. var _assert = $.ui.fancytree.assert;
  12177. function insertFirstChild(referenceNode, newNode) {
  12178. referenceNode.insertBefore(newNode, referenceNode.firstChild);
  12179. }
  12180. function insertSiblingAfter(referenceNode, newNode) {
  12181. referenceNode.parentNode.insertBefore(
  12182. newNode,
  12183. referenceNode.nextSibling
  12184. );
  12185. }
  12186. /* Show/hide all rows that are structural descendants of `parent`. */
  12187. function setChildRowVisibility(parent, flag) {
  12188. parent.visit(function (node) {
  12189. var tr = node.tr;
  12190. // currentFlag = node.hide ? false : flag; // fix for ext-filter
  12191. if (tr) {
  12192. tr.style.display = node.hide || !flag ? "none" : "";
  12193. }
  12194. if (!node.expanded) {
  12195. return "skip";
  12196. }
  12197. });
  12198. }
  12199. /* Find node that is rendered in previous row. */
  12200. function findPrevRowNode(node) {
  12201. var i,
  12202. last,
  12203. prev,
  12204. parent = node.parent,
  12205. siblings = parent ? parent.children : null;
  12206. if (siblings && siblings.length > 1 && siblings[0] !== node) {
  12207. // use the lowest descendant of the preceeding sibling
  12208. i = $.inArray(node, siblings);
  12209. prev = siblings[i - 1];
  12210. _assert(prev.tr);
  12211. // descend to lowest child (with a <tr> tag)
  12212. while (prev.children && prev.children.length) {
  12213. last = prev.children[prev.children.length - 1];
  12214. if (!last.tr) {
  12215. break;
  12216. }
  12217. prev = last;
  12218. }
  12219. } else {
  12220. // if there is no preceding sibling, use the direct parent
  12221. prev = parent;
  12222. }
  12223. return prev;
  12224. }
  12225. $.ui.fancytree.registerExtension({
  12226. name: "table",
  12227. version: "2.38.3",
  12228. // Default options for this extension.
  12229. options: {
  12230. checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
  12231. indentation: 16, // indent every node level by 16px
  12232. mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR
  12233. nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0)
  12234. },
  12235. // Overide virtual methods for this extension.
  12236. // `this` : is this extension object
  12237. // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
  12238. treeInit: function (ctx) {
  12239. var i,
  12240. n,
  12241. $row,
  12242. $tbody,
  12243. tree = ctx.tree,
  12244. opts = ctx.options,
  12245. tableOpts = opts.table,
  12246. $table = tree.widget.element;
  12247. if (tableOpts.customStatus != null) {
  12248. if (opts.renderStatusColumns == null) {
  12249. tree.warn(
  12250. "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead."
  12251. );
  12252. opts.renderStatusColumns = tableOpts.customStatus;
  12253. } else {
  12254. $.error(
  12255. "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead."
  12256. );
  12257. }
  12258. }
  12259. if (opts.renderStatusColumns) {
  12260. if (opts.renderStatusColumns === true) {
  12261. opts.renderStatusColumns = opts.renderColumns;
  12262. // } else if( opts.renderStatusColumns === "wide" ) {
  12263. // opts.renderStatusColumns = _renderStatusNodeWide;
  12264. }
  12265. }
  12266. $table.addClass("fancytree-container fancytree-ext-table");
  12267. $tbody = $table.find(">tbody");
  12268. if (!$tbody.length) {
  12269. // TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s:
  12270. if ($table.find(">tr").length) {
  12271. $.error(
  12272. "Expected table > tbody > tr. If you see this please open an issue."
  12273. );
  12274. }
  12275. $tbody = $("<tbody>").appendTo($table);
  12276. }
  12277. tree.tbody = $tbody[0];
  12278. // Prepare row templates:
  12279. // Determine column count from table header if any
  12280. tree.columnCount = $("thead >tr", $table)
  12281. .last()
  12282. .find(">th", $table).length;
  12283. // Read TR templates from tbody if any
  12284. $row = $tbody.children("tr").first();
  12285. if ($row.length) {
  12286. n = $row.children("td").length;
  12287. if (tree.columnCount && n !== tree.columnCount) {
  12288. tree.warn(
  12289. "Column count mismatch between thead (" +
  12290. tree.columnCount +
  12291. ") and tbody (" +
  12292. n +
  12293. "): using tbody."
  12294. );
  12295. tree.columnCount = n;
  12296. }
  12297. $row = $row.clone();
  12298. } else {
  12299. // Only thead is defined: create default row markup
  12300. _assert(
  12301. tree.columnCount >= 1,
  12302. "Need either <thead> or <tbody> with <td> elements to determine column count."
  12303. );
  12304. $row = $("<tr />");
  12305. for (i = 0; i < tree.columnCount; i++) {
  12306. $row.append("<td />");
  12307. }
  12308. }
  12309. $row.find(">td")
  12310. .eq(tableOpts.nodeColumnIdx)
  12311. .html("<span class='fancytree-node' />");
  12312. if (opts.aria) {
  12313. $row.attr("role", "row");
  12314. $row.find("td").attr("role", "gridcell");
  12315. }
  12316. tree.rowFragment = document.createDocumentFragment();
  12317. tree.rowFragment.appendChild($row.get(0));
  12318. // // If tbody contains a second row, use this as status node template
  12319. // $row = $tbody.children("tr").eq(1);
  12320. // if( $row.length === 0 ) {
  12321. // tree.statusRowFragment = tree.rowFragment;
  12322. // } else {
  12323. // $row = $row.clone();
  12324. // tree.statusRowFragment = document.createDocumentFragment();
  12325. // tree.statusRowFragment.appendChild($row.get(0));
  12326. // }
  12327. //
  12328. $tbody.empty();
  12329. // Make sure that status classes are set on the node's <tr> elements
  12330. tree.statusClassPropName = "tr";
  12331. tree.ariaPropName = "tr";
  12332. this.nodeContainerAttrName = "tr";
  12333. // #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table
  12334. tree.$container = $table;
  12335. this._superApply(arguments);
  12336. // standard Fancytree created a root UL
  12337. $(tree.rootNode.ul).remove();
  12338. tree.rootNode.ul = null;
  12339. // Add container to the TAB chain
  12340. // #577: Allow to set tabindex to "0", "-1" and ""
  12341. this.$container.attr("tabindex", opts.tabindex);
  12342. // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1");
  12343. if (opts.aria) {
  12344. tree.$container
  12345. .attr("role", "treegrid")
  12346. .attr("aria-readonly", true);
  12347. }
  12348. },
  12349. nodeRemoveChildMarkup: function (ctx) {
  12350. var node = ctx.node;
  12351. // node.debug("nodeRemoveChildMarkup()");
  12352. node.visit(function (n) {
  12353. if (n.tr) {
  12354. $(n.tr).remove();
  12355. n.tr = null;
  12356. }
  12357. });
  12358. },
  12359. nodeRemoveMarkup: function (ctx) {
  12360. var node = ctx.node;
  12361. // node.debug("nodeRemoveMarkup()");
  12362. if (node.tr) {
  12363. $(node.tr).remove();
  12364. node.tr = null;
  12365. }
  12366. this.nodeRemoveChildMarkup(ctx);
  12367. },
  12368. /* Override standard render. */
  12369. nodeRender: function (ctx, force, deep, collapsed, _recursive) {
  12370. var children,
  12371. firstTr,
  12372. i,
  12373. l,
  12374. newRow,
  12375. prevNode,
  12376. prevTr,
  12377. subCtx,
  12378. tree = ctx.tree,
  12379. node = ctx.node,
  12380. opts = ctx.options,
  12381. isRootNode = !node.parent;
  12382. if (tree._enableUpdate === false) {
  12383. // $.ui.fancytree.debug("*** nodeRender _enableUpdate: false");
  12384. return;
  12385. }
  12386. if (!_recursive) {
  12387. ctx.hasCollapsedParents = node.parent && !node.parent.expanded;
  12388. }
  12389. // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr));
  12390. if (!isRootNode) {
  12391. if (node.tr && force) {
  12392. this.nodeRemoveMarkup(ctx);
  12393. }
  12394. if (node.tr) {
  12395. if (force) {
  12396. // Set icon, link, and title (normally this is only required on initial render)
  12397. this.nodeRenderTitle(ctx); // triggers renderColumns()
  12398. } else {
  12399. // Update element classes according to node state
  12400. this.nodeRenderStatus(ctx);
  12401. }
  12402. } else {
  12403. if (ctx.hasCollapsedParents && !deep) {
  12404. // #166: we assume that the parent will be (recursively) rendered
  12405. // later anyway.
  12406. // node.debug("nodeRender ignored due to unrendered parent");
  12407. return;
  12408. }
  12409. // Create new <tr> after previous row
  12410. // if( node.isStatusNode() ) {
  12411. // newRow = tree.statusRowFragment.firstChild.cloneNode(true);
  12412. // } else {
  12413. newRow = tree.rowFragment.firstChild.cloneNode(true);
  12414. // }
  12415. prevNode = findPrevRowNode(node);
  12416. // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key);
  12417. _assert(prevNode);
  12418. if (collapsed === true && _recursive) {
  12419. // hide all child rows, so we can use an animation to show it later
  12420. newRow.style.display = "none";
  12421. } else if (deep && ctx.hasCollapsedParents) {
  12422. // also hide this row if deep === true but any parent is collapsed
  12423. newRow.style.display = "none";
  12424. // newRow.style.color = "red";
  12425. }
  12426. if (prevNode.tr) {
  12427. insertSiblingAfter(prevNode.tr, newRow);
  12428. } else {
  12429. _assert(
  12430. !prevNode.parent,
  12431. "prev. row must have a tr, or be system root"
  12432. );
  12433. // tree.tbody.appendChild(newRow);
  12434. insertFirstChild(tree.tbody, newRow); // #675
  12435. }
  12436. node.tr = newRow;
  12437. if (node.key && opts.generateIds) {
  12438. node.tr.id = opts.idPrefix + node.key;
  12439. }
  12440. node.tr.ftnode = node;
  12441. // if(opts.aria){
  12442. // $(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key);
  12443. // }
  12444. node.span = $("span.fancytree-node", node.tr).get(0);
  12445. // Set icon, link, and title (normally this is only required on initial render)
  12446. this.nodeRenderTitle(ctx);
  12447. // Allow tweaking, binding, after node was created for the first time
  12448. // tree._triggerNodeEvent("createNode", ctx);
  12449. if (opts.createNode) {
  12450. opts.createNode.call(tree, { type: "createNode" }, ctx);
  12451. }
  12452. }
  12453. }
  12454. // Allow tweaking after node state was rendered
  12455. // tree._triggerNodeEvent("renderNode", ctx);
  12456. if (opts.renderNode) {
  12457. opts.renderNode.call(tree, { type: "renderNode" }, ctx);
  12458. }
  12459. // Visit child nodes
  12460. // Add child markup
  12461. children = node.children;
  12462. if (children && (isRootNode || deep || node.expanded)) {
  12463. for (i = 0, l = children.length; i < l; i++) {
  12464. subCtx = $.extend({}, ctx, { node: children[i] });
  12465. subCtx.hasCollapsedParents =
  12466. subCtx.hasCollapsedParents || !node.expanded;
  12467. this.nodeRender(subCtx, force, deep, collapsed, true);
  12468. }
  12469. }
  12470. // Make sure, that <tr> order matches node.children order.
  12471. if (children && !_recursive) {
  12472. // we only have to do it once, for the root branch
  12473. prevTr = node.tr || null;
  12474. firstTr = tree.tbody.firstChild;
  12475. // Iterate over all descendants
  12476. node.visit(function (n) {
  12477. if (n.tr) {
  12478. if (
  12479. !n.parent.expanded &&
  12480. n.tr.style.display !== "none"
  12481. ) {
  12482. // fix after a node was dropped over a collapsed
  12483. n.tr.style.display = "none";
  12484. setChildRowVisibility(n, false);
  12485. }
  12486. if (n.tr.previousSibling !== prevTr) {
  12487. node.debug("_fixOrder: mismatch at node: " + n);
  12488. var nextTr = prevTr ? prevTr.nextSibling : firstTr;
  12489. tree.tbody.insertBefore(n.tr, nextTr);
  12490. }
  12491. prevTr = n.tr;
  12492. }
  12493. });
  12494. }
  12495. // Update element classes according to node state
  12496. // if(!isRootNode){
  12497. // this.nodeRenderStatus(ctx);
  12498. // }
  12499. },
  12500. nodeRenderTitle: function (ctx, title) {
  12501. var $cb,
  12502. res,
  12503. tree = ctx.tree,
  12504. node = ctx.node,
  12505. opts = ctx.options,
  12506. isStatusNode = node.isStatusNode();
  12507. res = this._super(ctx, title);
  12508. if (node.isRootNode()) {
  12509. return res;
  12510. }
  12511. // Move checkbox to custom column
  12512. if (
  12513. opts.checkbox &&
  12514. !isStatusNode &&
  12515. opts.table.checkboxColumnIdx != null
  12516. ) {
  12517. $cb = $("span.fancytree-checkbox", node.span); //.detach();
  12518. $(node.tr)
  12519. .find("td")
  12520. .eq(+opts.table.checkboxColumnIdx)
  12521. .html($cb);
  12522. }
  12523. // Update element classes according to node state
  12524. this.nodeRenderStatus(ctx);
  12525. if (isStatusNode) {
  12526. if (opts.renderStatusColumns) {
  12527. // Let user code write column content
  12528. opts.renderStatusColumns.call(
  12529. tree,
  12530. { type: "renderStatusColumns" },
  12531. ctx
  12532. );
  12533. } else if (opts.table.mergeStatusColumns && node.isTopLevel()) {
  12534. $(node.tr)
  12535. .find(">td")
  12536. .eq(0)
  12537. .prop("colspan", tree.columnCount)
  12538. .text(node.title)
  12539. .addClass("fancytree-status-merged")
  12540. .nextAll()
  12541. .remove();
  12542. } // else: default rendering for status node: leave other cells empty
  12543. } else if (opts.renderColumns) {
  12544. opts.renderColumns.call(tree, { type: "renderColumns" }, ctx);
  12545. }
  12546. return res;
  12547. },
  12548. nodeRenderStatus: function (ctx) {
  12549. var indent,
  12550. node = ctx.node,
  12551. opts = ctx.options;
  12552. this._super(ctx);
  12553. $(node.tr).removeClass("fancytree-node");
  12554. // indent
  12555. indent = (node.getLevel() - 1) * opts.table.indentation;
  12556. if (opts.rtl) {
  12557. $(node.span).css({ paddingRight: indent + "px" });
  12558. } else {
  12559. $(node.span).css({ paddingLeft: indent + "px" });
  12560. }
  12561. },
  12562. /* Expand node, return Deferred.promise. */
  12563. nodeSetExpanded: function (ctx, flag, callOpts) {
  12564. // flag defaults to true
  12565. flag = flag !== false;
  12566. if ((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) {
  12567. // Expanded state isn't changed - just call base implementation
  12568. return this._superApply(arguments);
  12569. }
  12570. var dfd = new $.Deferred(),
  12571. subOpts = $.extend({}, callOpts, {
  12572. noEvents: true,
  12573. noAnimation: true,
  12574. });
  12575. callOpts = callOpts || {};
  12576. function _afterExpand(ok, args) {
  12577. // ctx.tree.info("ok:" + ok, args);
  12578. if (ok) {
  12579. // #1108 minExpandLevel: 2 together with table extension does not work
  12580. // don't call when 'ok' is false:
  12581. setChildRowVisibility(ctx.node, flag);
  12582. if (
  12583. flag &&
  12584. ctx.options.autoScroll &&
  12585. !callOpts.noAnimation &&
  12586. ctx.node.hasChildren()
  12587. ) {
  12588. // Scroll down to last child, but keep current node visible
  12589. ctx.node
  12590. .getLastChild()
  12591. .scrollIntoView(true, { topNode: ctx.node })
  12592. .always(function () {
  12593. if (!callOpts.noEvents) {
  12594. ctx.tree._triggerNodeEvent(
  12595. flag ? "expand" : "collapse",
  12596. ctx
  12597. );
  12598. }
  12599. dfd.resolveWith(ctx.node);
  12600. });
  12601. } else {
  12602. if (!callOpts.noEvents) {
  12603. ctx.tree._triggerNodeEvent(
  12604. flag ? "expand" : "collapse",
  12605. ctx
  12606. );
  12607. }
  12608. dfd.resolveWith(ctx.node);
  12609. }
  12610. } else {
  12611. if (!callOpts.noEvents) {
  12612. ctx.tree._triggerNodeEvent(
  12613. flag ? "expand" : "collapse",
  12614. ctx
  12615. );
  12616. }
  12617. dfd.rejectWith(ctx.node);
  12618. }
  12619. }
  12620. // Call base-expand with disabled events and animation
  12621. this._super(ctx, flag, subOpts)
  12622. .done(function () {
  12623. _afterExpand(true, arguments);
  12624. })
  12625. .fail(function () {
  12626. _afterExpand(false, arguments);
  12627. });
  12628. return dfd.promise();
  12629. },
  12630. nodeSetStatus: function (ctx, status, message, details) {
  12631. if (status === "ok") {
  12632. var node = ctx.node,
  12633. firstChild = node.children ? node.children[0] : null;
  12634. if (firstChild && firstChild.isStatusNode()) {
  12635. $(firstChild.tr).remove();
  12636. }
  12637. }
  12638. return this._superApply(arguments);
  12639. },
  12640. treeClear: function (ctx) {
  12641. this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode));
  12642. return this._superApply(arguments);
  12643. },
  12644. treeDestroy: function (ctx) {
  12645. this.$container.find("tbody").empty();
  12646. if (this.$source) {
  12647. this.$source.removeClass("fancytree-helper-hidden");
  12648. }
  12649. return this._superApply(arguments);
  12650. },
  12651. /*,
  12652. treeSetFocus: function(ctx, flag) {
  12653. // alert("treeSetFocus" + ctx.tree.$container);
  12654. ctx.tree.$container.focus();
  12655. $.ui.fancytree.focusTree = ctx.tree;
  12656. }*/
  12657. });
  12658. // Value returned by `require('jquery.fancytree..')`
  12659. return $.ui.fancytree;
  12660. }); // End of closure
  12661. /*! Extension 'jquery.fancytree.themeroller.js' *//*!
  12662. * jquery.fancytree.themeroller.js
  12663. *
  12664. * Enable jQuery UI ThemeRoller styles.
  12665. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  12666. *
  12667. * @see http://jqueryui.com/themeroller/
  12668. *
  12669. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  12670. *
  12671. * Released under the MIT license
  12672. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  12673. *
  12674. * @version 2.38.3
  12675. * @date 2023-02-01T20:52:50Z
  12676. */
  12677. (function (factory) {
  12678. if (typeof define === "function" && define.amd) {
  12679. // AMD. Register as an anonymous module.
  12680. define(["jquery", "./jquery.fancytree"], factory);
  12681. } else if (typeof module === "object" && module.exports) {
  12682. // Node/CommonJS
  12683. require("./jquery.fancytree");
  12684. module.exports = factory(require("jquery"));
  12685. } else {
  12686. // Browser globals
  12687. factory(jQuery);
  12688. }
  12689. })(function ($) {
  12690. "use strict";
  12691. /*******************************************************************************
  12692. * Extension code
  12693. */
  12694. $.ui.fancytree.registerExtension({
  12695. name: "themeroller",
  12696. version: "2.38.3",
  12697. // Default options for this extension.
  12698. options: {
  12699. activeClass: "ui-state-active", // Class added to active node
  12700. // activeClass: "ui-state-highlight",
  12701. addClass: "ui-corner-all", // Class added to all nodes
  12702. focusClass: "ui-state-focus", // Class added to focused node
  12703. hoverClass: "ui-state-hover", // Class added to hovered node
  12704. selectedClass: "ui-state-highlight", // Class added to selected nodes
  12705. // selectedClass: "ui-state-active"
  12706. },
  12707. treeInit: function (ctx) {
  12708. var $el = ctx.widget.element,
  12709. opts = ctx.options.themeroller;
  12710. this._superApply(arguments);
  12711. if ($el[0].nodeName === "TABLE") {
  12712. $el.addClass("ui-widget ui-corner-all");
  12713. $el.find(">thead tr").addClass("ui-widget-header");
  12714. $el.find(">tbody").addClass("ui-widget-conent");
  12715. } else {
  12716. $el.addClass("ui-widget ui-widget-content ui-corner-all");
  12717. }
  12718. $el.on(
  12719. "mouseenter mouseleave",
  12720. ".fancytree-node",
  12721. function (event) {
  12722. var node = $.ui.fancytree.getNode(event.target),
  12723. flag = event.type === "mouseenter";
  12724. $(node.tr ? node.tr : node.span).toggleClass(
  12725. opts.hoverClass + " " + opts.addClass,
  12726. flag
  12727. );
  12728. }
  12729. );
  12730. },
  12731. treeDestroy: function (ctx) {
  12732. this._superApply(arguments);
  12733. ctx.widget.element.removeClass(
  12734. "ui-widget ui-widget-content ui-corner-all"
  12735. );
  12736. },
  12737. nodeRenderStatus: function (ctx) {
  12738. var classes = {},
  12739. node = ctx.node,
  12740. $el = $(node.tr ? node.tr : node.span),
  12741. opts = ctx.options.themeroller;
  12742. this._super(ctx);
  12743. /*
  12744. .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.
  12745. .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.
  12746. .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.
  12747. .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.
  12748. .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.
  12749. .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.
  12750. .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.
  12751. */
  12752. // Set ui-state-* class (handle the case that the same class is assigned
  12753. // to different states)
  12754. classes[opts.activeClass] = false;
  12755. classes[opts.focusClass] = false;
  12756. classes[opts.selectedClass] = false;
  12757. if (node.isActive()) {
  12758. classes[opts.activeClass] = true;
  12759. }
  12760. if (node.hasFocus()) {
  12761. classes[opts.focusClass] = true;
  12762. }
  12763. // activeClass takes precedence before selectedClass:
  12764. if (node.isSelected() && !node.isActive()) {
  12765. classes[opts.selectedClass] = true;
  12766. }
  12767. $el.toggleClass(opts.activeClass, classes[opts.activeClass]);
  12768. $el.toggleClass(opts.focusClass, classes[opts.focusClass]);
  12769. $el.toggleClass(opts.selectedClass, classes[opts.selectedClass]);
  12770. // Additional classes (e.g. 'ui-corner-all')
  12771. $el.addClass(opts.addClass);
  12772. },
  12773. });
  12774. // Value returned by `require('jquery.fancytree..')`
  12775. return $.ui.fancytree;
  12776. }); // End of closure
  12777. /*! Extension 'jquery.fancytree.wide.js' *//*!
  12778. * jquery.fancytree.wide.js
  12779. * Support for 100% wide selection bars.
  12780. * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
  12781. *
  12782. * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
  12783. *
  12784. * Released under the MIT license
  12785. * https://github.com/mar10/fancytree/wiki/LicenseInfo
  12786. *
  12787. * @version 2.38.3
  12788. * @date 2023-02-01T20:52:50Z
  12789. */
  12790. (function (factory) {
  12791. if (typeof define === "function" && define.amd) {
  12792. // AMD. Register as an anonymous module.
  12793. define(["jquery", "./jquery.fancytree"], factory);
  12794. } else if (typeof module === "object" && module.exports) {
  12795. // Node/CommonJS
  12796. require("./jquery.fancytree");
  12797. module.exports = factory(require("jquery"));
  12798. } else {
  12799. // Browser globals
  12800. factory(jQuery);
  12801. }
  12802. })(function ($) {
  12803. "use strict";
  12804. var reNumUnit = /^([+-]?(?:\d+|\d*\.\d+))([a-z]*|%)$/; // split "1.5em" to ["1.5", "em"]
  12805. /*******************************************************************************
  12806. * Private functions and variables
  12807. */
  12808. // var _assert = $.ui.fancytree.assert;
  12809. /* Calculate inner width without scrollbar */
  12810. // function realInnerWidth($el) {
  12811. // // http://blog.jquery.com/2012/08/16/jquery-1-8-box-sizing-width-csswidth-and-outerwidth/
  12812. // // inst.contWidth = parseFloat(this.$container.css("width"), 10);
  12813. // // 'Client width without scrollbar' - 'padding'
  12814. // return $el[0].clientWidth - ($el.innerWidth() - parseFloat($el.css("width"), 10));
  12815. // }
  12816. /* Create a global embedded CSS style for the tree. */
  12817. function defineHeadStyleElement(id, cssText) {
  12818. id = "fancytree-style-" + id;
  12819. var $headStyle = $("#" + id);
  12820. if (!cssText) {
  12821. $headStyle.remove();
  12822. return null;
  12823. }
  12824. if (!$headStyle.length) {
  12825. $headStyle = $("<style />")
  12826. .attr("id", id)
  12827. .addClass("fancytree-style")
  12828. .prop("type", "text/css")
  12829. .appendTo("head");
  12830. }
  12831. try {
  12832. $headStyle.html(cssText);
  12833. } catch (e) {
  12834. // fix for IE 6-8
  12835. $headStyle[0].styleSheet.cssText = cssText;
  12836. }
  12837. return $headStyle;
  12838. }
  12839. /* Calculate the CSS rules that indent title spans. */
  12840. function renderLevelCss(
  12841. containerId,
  12842. depth,
  12843. levelOfs,
  12844. lineOfs,
  12845. labelOfs,
  12846. measureUnit
  12847. ) {
  12848. var i,
  12849. prefix = "#" + containerId + " span.fancytree-level-",
  12850. rules = [];
  12851. for (i = 0; i < depth; i++) {
  12852. rules.push(
  12853. prefix +
  12854. (i + 1) +
  12855. " span.fancytree-title { padding-left: " +
  12856. (i * levelOfs + lineOfs) +
  12857. measureUnit +
  12858. "; }"
  12859. );
  12860. }
  12861. // Some UI animations wrap the UL inside a DIV and set position:relative on both.
  12862. // This breaks the left:0 and padding-left:nn settings of the title
  12863. rules.push(
  12864. "#" +
  12865. containerId +
  12866. " div.ui-effects-wrapper ul li span.fancytree-title, " +
  12867. "#" +
  12868. containerId +
  12869. " li.fancytree-animating span.fancytree-title " + // #716
  12870. "{ padding-left: " +
  12871. labelOfs +
  12872. measureUnit +
  12873. "; position: static; width: auto; }"
  12874. );
  12875. return rules.join("\n");
  12876. }
  12877. // /**
  12878. // * [ext-wide] Recalculate the width of the selection bar after the tree container
  12879. // * was resized.<br>
  12880. // * May be called explicitly on container resize, since there is no resize event
  12881. // * for DIV tags.
  12882. // *
  12883. // * @alias Fancytree#wideUpdate
  12884. // * @requires jquery.fancytree.wide.js
  12885. // */
  12886. // $.ui.fancytree._FancytreeClass.prototype.wideUpdate = function(){
  12887. // var inst = this.ext.wide,
  12888. // prevCw = inst.contWidth,
  12889. // prevLo = inst.lineOfs;
  12890. // inst.contWidth = realInnerWidth(this.$container);
  12891. // // Each title is precceeded by 2 or 3 icons (16px + 3 margin)
  12892. // // + 1px title border and 3px title padding
  12893. // // TODO: use code from treeInit() below
  12894. // inst.lineOfs = (this.options.checkbox ? 3 : 2) * 19;
  12895. // if( prevCw !== inst.contWidth || prevLo !== inst.lineOfs ) {
  12896. // this.debug("wideUpdate: " + inst.contWidth);
  12897. // this.visit(function(node){
  12898. // node.tree._callHook("nodeRenderTitle", node);
  12899. // });
  12900. // }
  12901. // };
  12902. /*******************************************************************************
  12903. * Extension code
  12904. */
  12905. $.ui.fancytree.registerExtension({
  12906. name: "wide",
  12907. version: "2.38.3",
  12908. // Default options for this extension.
  12909. options: {
  12910. iconWidth: null, // Adjust this if @fancy-icon-width != "16px"
  12911. iconSpacing: null, // Adjust this if @fancy-icon-spacing != "3px"
  12912. labelSpacing: null, // Adjust this if padding between icon and label != "3px"
  12913. levelOfs: null, // Adjust this if ul padding != "16px"
  12914. },
  12915. treeCreate: function (ctx) {
  12916. this._superApply(arguments);
  12917. this.$container.addClass("fancytree-ext-wide");
  12918. var containerId,
  12919. cssText,
  12920. iconSpacingUnit,
  12921. labelSpacingUnit,
  12922. iconWidthUnit,
  12923. levelOfsUnit,
  12924. instOpts = ctx.options.wide,
  12925. // css sniffing
  12926. $dummyLI = $(
  12927. "<li id='fancytreeTemp'><span class='fancytree-node'><span class='fancytree-icon' /><span class='fancytree-title' /></span><ul />"
  12928. ).appendTo(ctx.tree.$container),
  12929. $dummyIcon = $dummyLI.find(".fancytree-icon"),
  12930. $dummyUL = $dummyLI.find("ul"),
  12931. // $dummyTitle = $dummyLI.find(".fancytree-title"),
  12932. iconSpacing =
  12933. instOpts.iconSpacing || $dummyIcon.css("margin-left"),
  12934. iconWidth = instOpts.iconWidth || $dummyIcon.css("width"),
  12935. labelSpacing = instOpts.labelSpacing || "3px",
  12936. levelOfs = instOpts.levelOfs || $dummyUL.css("padding-left");
  12937. $dummyLI.remove();
  12938. iconSpacingUnit = iconSpacing.match(reNumUnit)[2];
  12939. iconSpacing = parseFloat(iconSpacing, 10);
  12940. labelSpacingUnit = labelSpacing.match(reNumUnit)[2];
  12941. labelSpacing = parseFloat(labelSpacing, 10);
  12942. iconWidthUnit = iconWidth.match(reNumUnit)[2];
  12943. iconWidth = parseFloat(iconWidth, 10);
  12944. levelOfsUnit = levelOfs.match(reNumUnit)[2];
  12945. if (
  12946. iconSpacingUnit !== iconWidthUnit ||
  12947. levelOfsUnit !== iconWidthUnit ||
  12948. labelSpacingUnit !== iconWidthUnit
  12949. ) {
  12950. $.error(
  12951. "iconWidth, iconSpacing, and levelOfs must have the same css measure unit"
  12952. );
  12953. }
  12954. this._local.measureUnit = iconWidthUnit;
  12955. this._local.levelOfs = parseFloat(levelOfs);
  12956. this._local.lineOfs =
  12957. (1 +
  12958. (ctx.options.checkbox ? 1 : 0) +
  12959. (ctx.options.icon === false ? 0 : 1)) *
  12960. (iconWidth + iconSpacing) +
  12961. iconSpacing;
  12962. this._local.labelOfs = labelSpacing;
  12963. this._local.maxDepth = 10;
  12964. // Get/Set a unique Id on the container (if not already exists)
  12965. containerId = this.$container.uniqueId().attr("id");
  12966. // Generated css rules for some levels (extended on demand)
  12967. cssText = renderLevelCss(
  12968. containerId,
  12969. this._local.maxDepth,
  12970. this._local.levelOfs,
  12971. this._local.lineOfs,
  12972. this._local.labelOfs,
  12973. this._local.measureUnit
  12974. );
  12975. defineHeadStyleElement(containerId, cssText);
  12976. },
  12977. treeDestroy: function (ctx) {
  12978. // Remove generated css rules
  12979. defineHeadStyleElement(this.$container.attr("id"), null);
  12980. return this._superApply(arguments);
  12981. },
  12982. nodeRenderStatus: function (ctx) {
  12983. var containerId,
  12984. cssText,
  12985. res,
  12986. node = ctx.node,
  12987. level = node.getLevel();
  12988. res = this._super(ctx);
  12989. // Generate some more level-n rules if required
  12990. if (level > this._local.maxDepth) {
  12991. containerId = this.$container.attr("id");
  12992. this._local.maxDepth *= 2;
  12993. node.debug(
  12994. "Define global ext-wide css up to level " +
  12995. this._local.maxDepth
  12996. );
  12997. cssText = renderLevelCss(
  12998. containerId,
  12999. this._local.maxDepth,
  13000. this._local.levelOfs,
  13001. this._local.lineOfs,
  13002. this._local.labelSpacing,
  13003. this._local.measureUnit
  13004. );
  13005. defineHeadStyleElement(containerId, cssText);
  13006. }
  13007. // Add level-n class to apply indentation padding.
  13008. // (Setting element style would not work, since it cannot easily be
  13009. // overriden while animations run)
  13010. $(node.span).addClass("fancytree-level-" + level);
  13011. return res;
  13012. },
  13013. });
  13014. // Value returned by `require('jquery.fancytree..')`
  13015. return $.ui.fancytree;
  13016. }); // End of closure
  13017. // Value returned by `require('jquery.fancytree')`
  13018. return $.ui.fancytree;
  13019. })); // End of closure