1 /*globals jQuery, define, module, exports, require, window, document, postMessage */
4 if (typeof define
=== 'function' && define
.amd
) {
5 define(['jquery'], factory
);
7 else if(typeof module
!== 'undefined' && module
.exports
) {
8 module
.exports
= factory(require('jquery'));
13 }(function ($, undefined) {
19 * Copyright (c) 2014 Ivan Bozhanov (http://vakata.com)
21 * Licensed same as jquery - under the terms of the MIT License
22 * http://www.opensource.org/licenses/mit-license.php
25 * if using jslint please allow for the jQuery global and use following options:
26 * jslint: loopfunc: true, browser: true, ass: true, bitwise: true, continue: true, nomen: true, plusplus: true, regexp: true, unparam: true, todo: true, white: true
30 // prevent another load? maybe there is a better way?
36 * ### jsTree core functionality
40 var instance_counter
= 0,
45 src
= $('script:last').attr('src'),
46 document
= window
.document
; // local variable is always faster to access then a global
49 * holds all jstree related functions and variables, including the actual class and methods to create, access and manipulate instances.
54 * specifies the jstree version in use
55 * @name $.jstree.version
59 * holds all the default options used when creating new instances
60 * @name $.jstree.defaults
64 * configure which plugins will be active on an instance. Should be an array of strings, where each element is a plugin name. The default is `[]`
65 * @name $.jstree.defaults.plugins
70 * stores all loaded jstree plugins (used internally)
71 * @name $.jstree.plugins
74 path
: src
&& src
.indexOf('/') !== -1 ? src
.replace(/\/[^\/]+$/,'') : '',
75 idregex
: /[\\:&!^|()\[\]<>@*'+~#";.,=\- \/${}%?`]/g,
80 * creates a jstree instance
81 * @name $.jstree.create(el [, options])
82 * @param {DOMElement|jQuery|String} el the element to create the instance on, can be jQuery extended or a selector
83 * @param {Object} options options for this instance (extends `$.jstree.defaults`)
84 * @return {jsTree} the new instance
86 $.jstree
.create = function (el
, options
) {
87 var tmp
= new $.jstree
.core(++instance_counter
),
89 options
= $.extend(true, {}, $.jstree
.defaults
, options
);
90 if(opt
&& opt
.plugins
) {
91 options
.plugins
= opt
.plugins
;
93 $.each(options
.plugins
, function (i
, k
) {
95 tmp
= tmp
.plugin(k
, options
[k
]);
98 $(el
).data('jstree', tmp
);
99 tmp
.init(el
, options
);
103 * remove all traces of jstree from the DOM and destroy all instances
104 * @name $.jstree.destroy()
106 $.jstree
.destroy = function () {
107 $('.jstree:jstree').jstree('destroy');
108 $(document
).off('.jstree');
111 * the jstree class constructor, used only internally
113 * @name $.jstree.core(id)
114 * @param {Number} id this instance's index
116 $.jstree
.core = function (id
) {
137 * get a reference to an existing instance
141 * // provided a container with an ID of "tree", and a nested node with an ID of "branch"
142 * // all of there will return the same instance
143 * $.jstree.reference('tree');
144 * $.jstree.reference('#tree');
145 * $.jstree.reference($('#tree'));
146 * $.jstree.reference(document.getElementByID('tree'));
147 * $.jstree.reference('branch');
148 * $.jstree.reference('#branch');
149 * $.jstree.reference($('#branch'));
150 * $.jstree.reference(document.getElementByID('branch'));
152 * @name $.jstree.reference(needle)
153 * @param {DOMElement|jQuery|String} needle
154 * @return {jsTree|null} the instance or `null` if not found
156 $.jstree
.reference = function (needle
) {
159 if(needle
&& needle
.id
&& (!needle
.tagName
|| !needle
.nodeType
)) { needle
= needle
.id
; }
161 if(!obj
|| !obj
.length
) {
162 try { obj
= $(needle
); } catch (ignore
) { }
164 if(!obj
|| !obj
.length
) {
165 try { obj
= $('#' + needle
.replace($.jstree
.idregex
,'\\$&')); } catch (ignore
) { }
167 if(obj
&& obj
.length
&& (obj
= obj
.closest('.jstree')).length
&& (obj
= obj
.data('jstree'))) {
171 $('.jstree').each(function () {
172 var inst
= $(this).data('jstree');
173 if(inst
&& inst
._model
.data
[needle
]) {
182 * Create an instance, get an instance or invoke a command on a instance.
184 * If there is no instance associated with the current node a new one is created and `arg` is used to extend `$.jstree.defaults` for this new instance. There would be no return value (chaining is not broken).
186 * If there is an existing instance and `arg` is a string the command specified by `arg` is executed on the instance, with any additional arguments passed to the function. If the function returns a value it will be returned (chaining could break depending on function).
188 * If there is an existing instance and `arg` is not a string the instance itself is returned (similar to `$.jstree.reference`).
190 * In any other case - nothing is returned and chaining is not broken.
194 * $('#tree1').jstree(); // creates an instance
195 * $('#tree2').jstree({ plugins : [] }); // create an instance with some options
196 * $('#tree1').jstree('open_node', '#branch_1'); // call a method on an existing instance, passing additional arguments
197 * $('#tree2').jstree(); // get an existing instance (or create an instance)
198 * $('#tree2').jstree(true); // get an existing instance (will not create new instance)
199 * $('#branch_1').jstree().select_node('#branch_1'); // get an instance (using a nested element and call a method)
201 * @name $().jstree([arg])
202 * @param {String|Object} arg
205 $.fn
.jstree = function (arg
) {
206 // check for string argument
207 var is_method
= (typeof arg
=== 'string'),
208 args
= Array
.prototype.slice
.call(arguments
, 1),
210 if(arg
=== true && !this.length
) { return false; }
211 this.each(function () {
212 // get the instance (if there is one) and method (if it exists)
213 var instance
= $.jstree
.reference(this),
214 method
= is_method
&& instance
? instance
[arg
] : null;
215 // if calling a method, and method is available - execute on the instance
216 result
= is_method
&& method
?
217 method
.apply(instance
, args
) :
219 // if there is no instance and no method is being called - create one
220 if(!instance
&& !is_method
&& (arg
=== undefined || $.isPlainObject(arg
))) {
221 $.jstree
.create(this, arg
);
223 // if there is an instance and no method is called - return the instance
224 if( (instance
&& !is_method
) || arg
=== true ) {
225 result
= instance
|| false;
227 // if there was a method call which returned a result - break and return the value
228 if(result
!== null && result
!== undefined) {
232 // if there was a method call with a valid return value - return that, otherwise continue the chain
233 return result
!== null && result
!== undefined ?
237 * used to find elements containing an instance
241 * $('div:jstree').each(function () {
242 * $(this).jstree('destroy');
248 $.expr
.pseudos
.jstree
= $.expr
.createPseudo(function(search
) {
250 return $(a
).hasClass('jstree') &&
251 $(a
).data('jstree') !== undefined;
256 * stores all defaults for the core
257 * @name $.jstree.defaults.core
259 $.jstree
.defaults
.core
= {
263 * If left as `false` the HTML inside the jstree container element is used to populate the tree (that should be an unordered list with list items).
265 * You can also pass in a HTML string or a JSON array here.
267 * It is possible to pass in a standard jQuery-like AJAX config and jstree will automatically determine if the response is JSON or HTML and use that to populate the tree.
268 * In addition to the standard jQuery ajax options here you can suppy functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node is being loaded, the return value of those functions will be used.
270 * The last option is to specify a function, that function will receive the node being loaded as argument and a second param which is a function which should be called with the result.
275 * $('#tree').jstree({
278 * 'url' : '/get/children/',
279 * 'data' : function (node) {
280 * return { 'id' : node.id };
286 * $('#tree').jstree({
289 * 'Simple root node',
292 * 'text' : 'Root node with options',
293 * 'state' : { 'opened' : true, 'selected' : true },
294 * 'children' : [ { 'text' : 'Child 1' }, 'Child 2']
301 * $('#tree').jstree({
303 * 'data' : function (obj, callback) {
304 * callback.call(this, ['Root 1', 'Root 2']);
308 * @name $.jstree.defaults.core.data
312 * configure the various strings used throughout the tree
314 * You can use an object where the key is the string you need to replace and the value is your replacement.
315 * Another option is to specify a function which will be called with an argument of the needed string and should return the replacement.
316 * If left as `false` no replacement is made.
320 * $('#tree').jstree({
323 * 'Loading ...' : 'Please wait ...'
328 * @name $.jstree.defaults.core.strings
332 * determines what happens when a user tries to modify the structure of the tree
333 * If left as `false` all operations like create, rename, delete, move or copy are prevented.
334 * You can set this to `true` to allow all interactions or use a function to have better control.
338 * $('#tree').jstree({
340 * 'check_callback' : function (operation, node, node_parent, node_position, more) {
341 * // operation can be 'create_node', 'rename_node', 'delete_node', 'move_node', 'copy_node' or 'edit'
342 * // in case of 'rename_node' node_position is filled with the new node name
343 * return operation === 'rename_node' ? true : false;
348 * @name $.jstree.defaults.core.check_callback
350 check_callback
: false,
352 * a callback called with a single object parameter in the instance's scope when something goes wrong (operation prevented, ajax failed, etc)
353 * @name $.jstree.defaults.core.error
357 * the open / close animation duration in milliseconds - set this to `false` to disable the animation (default is `200`)
358 * @name $.jstree.defaults.core.animation
362 * a boolean indicating if multiple nodes can be selected
363 * @name $.jstree.defaults.core.multiple
367 * theme configuration object
368 * @name $.jstree.defaults.core.themes
372 * the name of the theme to use (if left as `false` the default theme is used)
373 * @name $.jstree.defaults.core.themes.name
377 * the URL of the theme's CSS file, leave this as `false` if you have manually included the theme CSS (recommended). You can set this to `true` too which will try to autoload the theme.
378 * @name $.jstree.defaults.core.themes.url
382 * the location of all jstree themes - only used if `url` is set to `true`
383 * @name $.jstree.defaults.core.themes.dir
387 * a boolean indicating if connecting dots are shown
388 * @name $.jstree.defaults.core.themes.dots
392 * a boolean indicating if node icons are shown
393 * @name $.jstree.defaults.core.themes.icons
397 * a boolean indicating if node ellipsis should be shown - this only works with a fixed with on the container
398 * @name $.jstree.defaults.core.themes.ellipsis
402 * a boolean indicating if the tree background is striped
403 * @name $.jstree.defaults.core.themes.stripes
407 * a string (or boolean `false`) specifying the theme variant to use (if the theme supports variants)
408 * @name $.jstree.defaults.core.themes.variant
412 * a boolean specifying if a reponsive version of the theme should kick in on smaller screens (if the theme supports it). Defaults to `false`.
413 * @name $.jstree.defaults.core.themes.responsive
418 * if left as `true` all parents of all selected nodes will be opened once the tree loads (so that all selected nodes are visible to the user)
419 * @name $.jstree.defaults.core.expand_selected_onload
421 expand_selected_onload
: true,
423 * if left as `true` web workers will be used to parse incoming JSON data where possible, so that the UI will not be blocked by large requests. Workers are however about 30% slower. Defaults to `true`
424 * @name $.jstree.defaults.core.worker
428 * Force node text to plain text (and escape HTML). Defaults to `false`
429 * @name $.jstree.defaults.core.force_text
433 * Should the node should be toggled if the text is double clicked . Defaults to `true`
434 * @name $.jstree.defaults.core.dblclick_toggle
436 dblclick_toggle
: true
438 $.jstree
.core
.prototype = {
440 * used to decorate an instance with a plugin. Used internally.
442 * @name plugin(deco [, opts])
443 * @param {String} deco the plugin to decorate with
444 * @param {Object} opts options for the plugin
447 plugin : function (deco
, opts
) {
448 var Child
= $.jstree
.plugins
[deco
];
450 this._data
[deco
] = {};
451 Child
.prototype = this;
452 return new Child(opts
, this);
457 * initialize the instance. Used internally.
459 * @name init(el, optons)
460 * @param {DOMElement|jQuery|String} el the element we are transforming
461 * @param {Object} options options for this instance
462 * @trigger init.jstree, loading.jstree, loaded.jstree, ready.jstree, changed.jstree
464 init : function (el
, options
) {
468 force_full_redraw
: false,
469 redraw_timeout
: false,
477 this._model
.data
[$.jstree
.root
] = {
483 state
: { loaded
: false }
486 this.element
= $(el
).addClass('jstree jstree-' + this._id
);
487 this.settings
= options
;
489 this._data
.core
.ready
= false;
490 this._data
.core
.loaded
= false;
491 this._data
.core
.rtl
= (this.element
.css("direction") === "rtl");
492 this.element
[this._data
.core
.rtl
? 'addClass' : 'removeClass']("jstree-rtl");
493 this.element
.attr('role','tree');
494 if(this.settings
.core
.multiple
) {
495 this.element
.attr('aria-multiselectable', true);
497 if(!this.element
.attr('tabindex')) {
498 this.element
.attr('tabindex','0');
503 * triggered after all events are bound
507 this.trigger("init");
509 this._data
.core
.original_container_html
= this.element
.find(" > ul > li").clone(true);
510 this._data
.core
.original_container_html
511 .find("li").addBack()
512 .contents().filter(function() {
513 return this.nodeType
=== 3 && (!this.nodeValue
|| /^\s+$/.test(this.nodeValue
));
516 this.element
.html("<"+"ul class='jstree-container-ul jstree-children' role='group'><"+"li id='j"+this._id
+"_loading' class='jstree-initial-node jstree-loading jstree-leaf jstree-last' role='tree-item'><i class='jstree-icon jstree-ocl'></i><"+"a class='jstree-anchor' href='#'><i class='jstree-icon jstree-themeicon-hidden'></i>" + this.get_string("Loading ...") + "</a></li></ul>");
517 this.element
.attr('aria-activedescendant','j' + this._id
+ '_loading');
518 this._data
.core
.li_height
= this.get_container_ul().children("li").first().outerHeight() || 24;
519 this._data
.core
.node
= this._create_prototype_node();
521 * triggered after the loading text is shown and before loading starts
523 * @name loading.jstree
525 this.trigger("loading");
526 this.load_node($.jstree
.root
);
529 * destroy an instance
531 * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact
533 destroy : function (keep_html
) {
535 * triggered before the tree is destroyed
537 * @name destroy.jstree
539 this.trigger("destroy");
542 window
.URL
.revokeObjectURL(this._wrk
);
547 if(!keep_html
) { this.element
.empty(); }
551 * Create prototype node
553 _create_prototype_node : function () {
554 var _node
= document
.createElement('LI'), _temp1
, _temp2
;
555 _node
.setAttribute('role', 'treeitem');
556 _temp1
= document
.createElement('I');
557 _temp1
.className
= 'jstree-icon jstree-ocl';
558 _temp1
.setAttribute('role', 'presentation');
559 _node
.appendChild(_temp1
);
560 _temp1
= document
.createElement('A');
561 _temp1
.className
= 'jstree-anchor';
562 _temp1
.setAttribute('href','#');
563 _temp1
.setAttribute('tabindex','-1');
564 _temp2
= document
.createElement('I');
565 _temp2
.className
= 'jstree-icon jstree-themeicon';
566 _temp2
.setAttribute('role', 'presentation');
567 _temp1
.appendChild(_temp2
);
568 _node
.appendChild(_temp1
);
569 _temp1
= _temp2
= null;
574 * part of the destroying of an instance. Used internally.
578 teardown : function () {
581 .removeClass('jstree')
582 .removeData('jstree')
583 .find("[class^='jstree']")
585 .attr("class", function () { return this.className
.replace(/jstree[^ ]*|$/ig,''); });
589 * bind all events. Used internally.
598 .on("dblclick.jstree", function (e
) {
599 if(e
.target
.tagName
&& e
.target
.tagName
.toLowerCase() === "input") { return true; }
600 if(document
.selection
&& document
.selection
.empty
) {
601 document
.selection
.empty();
604 if(window
.getSelection
) {
605 var sel
= window
.getSelection();
607 sel
.removeAllRanges();
613 .on("mousedown.jstree", $.proxy(function (e
) {
614 if(e
.target
=== this.element
[0]) {
615 e
.preventDefault(); // prevent losing focus when clicking scroll arrows (FF, Chrome)
616 was_click
= +(new Date()); // ie does not allow to prevent losing focus
619 .on("mousedown.jstree", ".jstree-ocl", function (e
) {
620 e
.preventDefault(); // prevent any node inside from losing focus when clicking the open/close icon
622 .on("click.jstree", ".jstree-ocl", $.proxy(function (e
) {
623 this.toggle_node(e
.target
);
625 .on("dblclick.jstree", ".jstree-anchor", $.proxy(function (e
) {
626 if(e
.target
.tagName
&& e
.target
.tagName
.toLowerCase() === "input") { return true; }
627 if(this.settings
.core
.dblclick_toggle
) {
628 this.toggle_node(e
.target
);
631 .on("click.jstree", ".jstree-anchor", $.proxy(function (e
) {
633 if(e
.currentTarget
!== document
.activeElement
) { $(e
.currentTarget
).focus(); }
634 this.activate_node(e
.currentTarget
, e
);
636 .on('keydown.jstree', '.jstree-anchor', $.proxy(function (e
) {
637 if(e
.target
.tagName
&& e
.target
.tagName
.toLowerCase() === "input") { return true; }
638 if(e
.which
!== 32 && e
.which
!== 13 && (e
.shiftKey
|| e
.ctrlKey
|| e
.altKey
|| e
.metaKey
)) { return true; }
640 if(this._data
.core
.rtl
) {
641 if(e
.which
=== 37) { e
.which
= 39; }
642 else if(e
.which
=== 39) { e
.which
= 37; }
645 case 32: // aria defines space only with Ctrl
648 $(e
.currentTarget
).trigger(e
);
653 $(e
.currentTarget
).trigger(e
);
657 if(this.is_open(e
.currentTarget
)) {
658 this.close_node(e
.currentTarget
);
661 o
= this.get_parent(e
.currentTarget
);
662 if(o
&& o
.id
!== $.jstree
.root
) { this.get_node(o
, true).children('.jstree-anchor').focus(); }
667 o
= this.get_prev_dom(e
.currentTarget
);
668 if(o
&& o
.length
) { o
.children('.jstree-anchor').focus(); }
672 if(this.is_closed(e
.currentTarget
)) {
673 this.open_node(e
.currentTarget
, function (o
) { this.get_node(o
, true).children('.jstree-anchor').focus(); });
675 else if (this.is_open(e
.currentTarget
)) {
676 o
= this.get_node(e
.currentTarget
, true).children('.jstree-children')[0];
677 if(o
) { $(this._firstChild(o
)).children('.jstree-anchor').focus(); }
682 o
= this.get_next_dom(e
.currentTarget
);
683 if(o
&& o
.length
) { o
.children('.jstree-anchor').focus(); }
685 case 106: // aria defines * on numpad as open_all - not very common
690 o
= this._firstChild(this.get_container_ul()[0]);
691 if(o
) { $(o
).children('.jstree-anchor').filter(':visible').focus(); }
695 this.element
.find('.jstree-anchor').filter(':visible').last().focus();
697 case 113: // f2 - safe to include - if check_callback is false it will fail
699 this.edit(e
.currentTarget
);
707 o = this.get_node(e.currentTarget);
708 if(o && o.id && o.id !== $.jstree.root) {
709 o = this.is_selected(o) ? this.get_selected() : o;
717 .on("load_node.jstree", $.proxy(function (e
, data
) {
719 if(data
.node
.id
=== $.jstree
.root
&& !this._data
.core
.loaded
) {
720 this._data
.core
.loaded
= true;
721 if(this._firstChild(this.get_container_ul()[0])) {
722 this.element
.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id
);
725 * triggered after the root node is loaded for the first time
727 * @name loaded.jstree
729 this.trigger("loaded");
731 if(!this._data
.core
.ready
) {
732 setTimeout($.proxy(function() {
733 if(this.element
&& !this.get_container_ul().find('.jstree-loading').length
) {
734 this._data
.core
.ready
= true;
735 if(this._data
.core
.selected
.length
) {
736 if(this.settings
.core
.expand_selected_onload
) {
738 for(i
= 0, j
= this._data
.core
.selected
.length
; i
< j
; i
++) {
739 tmp
= tmp
.concat(this._model
.data
[this._data
.core
.selected
[i
]].parents
);
741 tmp
= $.vakata
.array_unique(tmp
);
742 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
743 this.open_node(tmp
[i
], false, 0);
746 this.trigger('changed', { 'action' : 'ready', 'selected' : this._data
.core
.selected
});
749 * triggered after all nodes are finished loading
753 this.trigger("ready");
759 // quick searching when the tree is focused
760 .on('keypress.jstree', $.proxy(function (e
) {
761 if(e
.target
.tagName
&& e
.target
.tagName
.toLowerCase() === "input") { return true; }
762 if(tout
) { clearTimeout(tout
); }
763 tout
= setTimeout(function () {
767 var chr
= String
.fromCharCode(e
.which
).toLowerCase(),
768 col
= this.element
.find('.jstree-anchor').filter(':visible'),
769 ind
= col
.index(document
.activeElement
) || 0,
773 // match for whole word from current node down (including the current node)
774 if(word
.length
> 1) {
775 col
.slice(ind
).each($.proxy(function (i
, v
) {
776 if($(v
).text().toLowerCase().indexOf(word
) === 0) {
784 // match for whole word from the beginning of the tree
785 col
.slice(0, ind
).each($.proxy(function (i
, v
) {
786 if($(v
).text().toLowerCase().indexOf(word
) === 0) {
794 // list nodes that start with that letter (only if word consists of a single char)
795 if(new RegExp('^' + chr
.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '+$').test(word
)) {
796 // search for the next node starting with that letter
797 col
.slice(ind
+ 1).each($.proxy(function (i
, v
) {
798 if($(v
).text().toLowerCase().charAt(0) === chr
) {
806 // search from the beginning
807 col
.slice(0, ind
+ 1).each($.proxy(function (i
, v
) {
808 if($(v
).text().toLowerCase().charAt(0) === chr
) {
818 .on("init.jstree", $.proxy(function () {
819 var s
= this.settings
.core
.themes
;
820 this._data
.core
.themes
.dots
= s
.dots
;
821 this._data
.core
.themes
.stripes
= s
.stripes
;
822 this._data
.core
.themes
.icons
= s
.icons
;
823 this._data
.core
.themes
.ellipsis
= s
.ellipsis
;
824 this.set_theme(s
.name
|| "default", s
.url
);
825 this.set_theme_variant(s
.variant
);
827 .on("loading.jstree", $.proxy(function () {
828 this[ this._data
.core
.themes
.dots
? "show_dots" : "hide_dots" ]();
829 this[ this._data
.core
.themes
.icons
? "show_icons" : "hide_icons" ]();
830 this[ this._data
.core
.themes
.stripes
? "show_stripes" : "hide_stripes" ]();
831 this[ this._data
.core
.themes
.ellipsis
? "show_ellipsis" : "hide_ellipsis" ]();
833 .on('blur.jstree', '.jstree-anchor', $.proxy(function (e
) {
834 this._data
.core
.focused
= null;
835 $(e
.currentTarget
).filter('.jstree-hovered').mouseleave();
836 this.element
.attr('tabindex', '0');
838 .on('focus.jstree', '.jstree-anchor', $.proxy(function (e
) {
839 var tmp
= this.get_node(e
.currentTarget
);
841 this._data
.core
.focused
= tmp
.id
;
843 this.element
.find('.jstree-hovered').not(e
.currentTarget
).mouseleave();
844 $(e
.currentTarget
).mouseenter();
845 this.element
.attr('tabindex', '-1');
847 .on('focus.jstree', $.proxy(function () {
848 if(+(new Date()) - was_click
> 500 && !this._data
.core
.focused
) {
850 var act
= this.get_node(this.element
.attr('aria-activedescendant'), true);
852 act
.find('> .jstree-anchor').focus();
856 .on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e
) {
857 this.hover_node(e
.currentTarget
);
859 .on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e
) {
860 this.dehover_node(e
.currentTarget
);
864 * part of the destroying of an instance. Used internally.
868 unbind : function () {
869 this.element
.off('.jstree');
870 $(document
).off('.jstree-' + this._id
);
873 * trigger an event. Used internally.
875 * @name trigger(ev [, data])
876 * @param {String} ev the name of the event to trigger
877 * @param {Object} data additional data to pass with the event
879 trigger : function (ev
, data
) {
883 data
.instance
= this;
884 this.element
.triggerHandler(ev
.replace('.jstree','') + '.jstree', data
);
887 * returns the jQuery extended instance container
888 * @name get_container()
891 get_container : function () {
895 * returns the jQuery extended main UL node inside the instance container. Used internally.
897 * @name get_container_ul()
900 get_container_ul : function () {
901 return this.element
.children(".jstree-children").first();
904 * gets string replacements (localization). Used internally.
906 * @name get_string(key)
907 * @param {String} key
910 get_string : function (key
) {
911 var a
= this.settings
.core
.strings
;
912 if($.isFunction(a
)) { return a
.call(this, key
); }
913 if(a
&& a
[key
]) { return a
[key
]; }
917 * gets the first child of a DOM node. Used internally.
919 * @name _firstChild(dom)
920 * @param {DOMElement} dom
921 * @return {DOMElement}
923 _firstChild : function (dom
) {
924 dom
= dom
? dom
.firstChild
: null;
925 while(dom
!== null && dom
.nodeType
!== 1) {
926 dom
= dom
.nextSibling
;
931 * gets the next sibling of a DOM node. Used internally.
933 * @name _nextSibling(dom)
934 * @param {DOMElement} dom
935 * @return {DOMElement}
937 _nextSibling : function (dom
) {
938 dom
= dom
? dom
.nextSibling
: null;
939 while(dom
!== null && dom
.nodeType
!== 1) {
940 dom
= dom
.nextSibling
;
945 * gets the previous sibling of a DOM node. Used internally.
947 * @name _previousSibling(dom)
948 * @param {DOMElement} dom
949 * @return {DOMElement}
951 _previousSibling : function (dom
) {
952 dom
= dom
? dom
.previousSibling
: null;
953 while(dom
!== null && dom
.nodeType
!== 1) {
954 dom
= dom
.previousSibling
;
959 * get the JSON representation of a node (or the actual jQuery extended DOM node) by using any input (child DOM element, ID string, selector, etc)
960 * @name get_node(obj [, as_dom])
962 * @param {Boolean} as_dom
963 * @return {Object|jQuery}
965 get_node : function (obj
, as_dom
) {
971 if(this._model
.data
[obj
]) {
972 obj
= this._model
.data
[obj
];
974 else if(typeof obj
=== "string" && this._model
.data
[obj
.replace(/^#/, '')]) {
975 obj
= this._model
.data
[obj
.replace(/^#/, '')];
977 else if(typeof obj
=== "string" && (dom
= $('#' + obj
.replace($.jstree
.idregex
,'\\$&'), this.element
)).length
&& this._model
.data
[dom
.closest('.jstree-node').attr('id')]) {
978 obj
= this._model
.data
[dom
.closest('.jstree-node').attr('id')];
980 else if((dom
= $(obj
, this.element
)).length
&& this._model
.data
[dom
.closest('.jstree-node').attr('id')]) {
981 obj
= this._model
.data
[dom
.closest('.jstree-node').attr('id')];
983 else if((dom
= $(obj
, this.element
)).length
&& dom
.hasClass('jstree')) {
984 obj
= this._model
.data
[$.jstree
.root
];
991 obj
= obj
.id
=== $.jstree
.root
? this.element
: $('#' + obj
.id
.replace($.jstree
.idregex
,'\\$&'), this.element
);
994 } catch (ex
) { return false; }
997 * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array)
998 * @name get_path(obj [, glue, ids])
999 * @param {mixed} obj the node
1000 * @param {String} glue if you want the path as a string - pass the glue here (for example '/'), if a falsy value is supplied here, an array is returned
1001 * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used
1004 get_path : function (obj
, glue
, ids
) {
1005 obj
= obj
.parents
? obj
: this.get_node(obj
);
1006 if(!obj
|| obj
.id
=== $.jstree
.root
|| !obj
.parents
) {
1010 p
.push(ids
? obj
.id
: obj
.text
);
1011 for(i
= 0, j
= obj
.parents
.length
; i
< j
; i
++) {
1012 p
.push(ids
? obj
.parents
[i
] : this.get_text(obj
.parents
[i
]));
1014 p
= p
.reverse().slice(1);
1015 return glue
? p
.join(glue
) : p
;
1018 * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
1019 * @name get_next_dom(obj [, strict])
1020 * @param {mixed} obj
1021 * @param {Boolean} strict
1024 get_next_dom : function (obj
, strict
) {
1026 obj
= this.get_node(obj
, true);
1027 if(obj
[0] === this.element
[0]) {
1028 tmp
= this._firstChild(this.get_container_ul()[0]);
1029 while (tmp
&& tmp
.offsetHeight
=== 0) {
1030 tmp
= this._nextSibling(tmp
);
1032 return tmp
? $(tmp
) : false;
1034 if(!obj
|| !obj
.length
) {
1040 tmp
= this._nextSibling(tmp
);
1041 } while (tmp
&& tmp
.offsetHeight
=== 0);
1042 return tmp
? $(tmp
) : false;
1044 if(obj
.hasClass("jstree-open")) {
1045 tmp
= this._firstChild(obj
.children('.jstree-children')[0]);
1046 while (tmp
&& tmp
.offsetHeight
=== 0) {
1047 tmp
= this._nextSibling(tmp
);
1055 tmp
= this._nextSibling(tmp
);
1056 } while (tmp
&& tmp
.offsetHeight
=== 0);
1060 return obj
.parentsUntil(".jstree",".jstree-node").nextAll(".jstree-node:visible").first();
1063 * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
1064 * @name get_prev_dom(obj [, strict])
1065 * @param {mixed} obj
1066 * @param {Boolean} strict
1069 get_prev_dom : function (obj
, strict
) {
1071 obj
= this.get_node(obj
, true);
1072 if(obj
[0] === this.element
[0]) {
1073 tmp
= this.get_container_ul()[0].lastChild
;
1074 while (tmp
&& tmp
.offsetHeight
=== 0) {
1075 tmp
= this._previousSibling(tmp
);
1077 return tmp
? $(tmp
) : false;
1079 if(!obj
|| !obj
.length
) {
1085 tmp
= this._previousSibling(tmp
);
1086 } while (tmp
&& tmp
.offsetHeight
=== 0);
1087 return tmp
? $(tmp
) : false;
1091 tmp
= this._previousSibling(tmp
);
1092 } while (tmp
&& tmp
.offsetHeight
=== 0);
1095 while(obj
.hasClass("jstree-open")) {
1096 obj
= obj
.children(".jstree-children").first().children(".jstree-node:visible:last");
1100 tmp
= obj
[0].parentNode
.parentNode
;
1101 return tmp
&& tmp
.className
&& tmp
.className
.indexOf('jstree-node') !== -1 ? $(tmp
) : false;
1104 * get the parent ID of a node
1105 * @name get_parent(obj)
1106 * @param {mixed} obj
1109 get_parent : function (obj
) {
1110 obj
= this.get_node(obj
);
1111 if(!obj
|| obj
.id
=== $.jstree
.root
) {
1117 * get a jQuery collection of all the children of a node (node must be rendered)
1118 * @name get_children_dom(obj)
1119 * @param {mixed} obj
1122 get_children_dom : function (obj
) {
1123 obj
= this.get_node(obj
, true);
1124 if(obj
[0] === this.element
[0]) {
1125 return this.get_container_ul().children(".jstree-node");
1127 if(!obj
|| !obj
.length
) {
1130 return obj
.children(".jstree-children").children(".jstree-node");
1133 * checks if a node has children
1134 * @name is_parent(obj)
1135 * @param {mixed} obj
1138 is_parent : function (obj
) {
1139 obj
= this.get_node(obj
);
1140 return obj
&& (obj
.state
.loaded
=== false || obj
.children
.length
> 0);
1143 * checks if a node is loaded (its children are available)
1144 * @name is_loaded(obj)
1145 * @param {mixed} obj
1148 is_loaded : function (obj
) {
1149 obj
= this.get_node(obj
);
1150 return obj
&& obj
.state
.loaded
;
1153 * check if a node is currently loading (fetching children)
1154 * @name is_loading(obj)
1155 * @param {mixed} obj
1158 is_loading : function (obj
) {
1159 obj
= this.get_node(obj
);
1160 return obj
&& obj
.state
&& obj
.state
.loading
;
1163 * check if a node is opened
1164 * @name is_open(obj)
1165 * @param {mixed} obj
1168 is_open : function (obj
) {
1169 obj
= this.get_node(obj
);
1170 return obj
&& obj
.state
.opened
;
1173 * check if a node is in a closed state
1174 * @name is_closed(obj)
1175 * @param {mixed} obj
1178 is_closed : function (obj
) {
1179 obj
= this.get_node(obj
);
1180 return obj
&& this.is_parent(obj
) && !obj
.state
.opened
;
1183 * check if a node has no children
1184 * @name is_leaf(obj)
1185 * @param {mixed} obj
1188 is_leaf : function (obj
) {
1189 return !this.is_parent(obj
);
1192 * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array.
1193 * @name load_node(obj [, callback])
1194 * @param {mixed} obj
1195 * @param {function} callback a function to be executed once loading is complete, the function is executed in the instance's scope and receives two arguments - the node and a boolean status
1197 * @trigger load_node.jstree
1199 load_node : function (obj
, callback
) {
1201 if($.isArray(obj
)) {
1202 this._load_nodes(obj
.slice(), callback
);
1205 obj
= this.get_node(obj
);
1207 if(callback
) { callback
.call(this, obj
, false); }
1210 // if(obj.state.loading) { } // the node is already loading - just wait for it to load and invoke callback? but if called implicitly it should be loaded again?
1211 if(obj
.state
.loaded
) {
1212 obj
.state
.loaded
= false;
1213 for(i
= 0, j
= obj
.parents
.length
; i
< j
; i
++) {
1214 this._model
.data
[obj
.parents
[i
]].children_d
= $.vakata
.array_filter(this._model
.data
[obj
.parents
[i
]].children_d
, function (v
) {
1215 return $.inArray(v
, obj
.children_d
) === -1;
1218 for(k
= 0, l
= obj
.children_d
.length
; k
< l
; k
++) {
1219 if(this._model
.data
[obj
.children_d
[k
]].state
.selected
) {
1222 delete this._model
.data
[obj
.children_d
[k
]];
1225 this._data
.core
.selected
= $.vakata
.array_filter(this._data
.core
.selected
, function (v
) {
1226 return $.inArray(v
, obj
.children_d
) === -1;
1230 obj
.children_d
= [];
1232 this.trigger('changed', { 'action' : 'load_node', 'node' : obj
, 'selected' : this._data
.core
.selected
});
1235 obj
.state
.failed
= false;
1236 obj
.state
.loading
= true;
1237 this.get_node(obj
, true).addClass("jstree-loading").attr('aria-busy',true);
1238 this._load_node(obj
, $.proxy(function (status
) {
1239 obj
= this._model
.data
[obj
.id
];
1240 obj
.state
.loading
= false;
1241 obj
.state
.loaded
= status
;
1242 obj
.state
.failed
= !obj
.state
.loaded
;
1243 var dom
= this.get_node(obj
, true), i
= 0, j
= 0, m
= this._model
.data
, has_children
= false;
1244 for(i
= 0, j
= obj
.children
.length
; i
< j
; i
++) {
1245 if(m
[obj
.children
[i
]] && !m
[obj
.children
[i
]].state
.hidden
) {
1246 has_children
= true;
1250 if(obj
.state
.loaded
&& dom
&& dom
.length
) {
1251 dom
.removeClass('jstree-closed jstree-open jstree-leaf');
1252 if (!has_children
) {
1253 dom
.addClass('jstree-leaf');
1256 if (obj
.id
!== '#') {
1257 dom
.addClass(obj
.state
.opened
? 'jstree-open' : 'jstree-closed');
1261 dom
.removeClass("jstree-loading").attr('aria-busy',false);
1263 * triggered after a node is loaded
1265 * @name load_node.jstree
1266 * @param {Object} node the node that was loading
1267 * @param {Boolean} status was the node loaded successfully
1269 this.trigger('load_node', { "node" : obj
, "status" : status
});
1271 callback
.call(this, obj
, status
);
1277 * load an array of nodes (will also load unavailable nodes as soon as the appear in the structure). Used internally.
1279 * @name _load_nodes(nodes [, callback])
1280 * @param {array} nodes
1281 * @param {function} callback a function to be executed once loading is complete, the function is executed in the instance's scope and receives one argument - the array passed to _load_nodes
1283 _load_nodes : function (nodes
, callback
, is_callback
, force_reload
) {
1285 c = function () { this._load_nodes(nodes
, callback
, true); },
1286 m
= this._model
.data
, i
, j
, tmp
= [];
1287 for(i
= 0, j
= nodes
.length
; i
< j
; i
++) {
1288 if(m
[nodes
[i
]] && ( (!m
[nodes
[i
]].state
.loaded
&& !m
[nodes
[i
]].state
.failed
) || (!is_callback
&& force_reload
) )) {
1289 if(!this.is_loading(nodes
[i
])) {
1290 this.load_node(nodes
[i
], c
);
1296 for(i
= 0, j
= nodes
.length
; i
< j
; i
++) {
1297 if(m
[nodes
[i
]] && m
[nodes
[i
]].state
.loaded
) {
1301 if(callback
&& !callback
.done
) {
1302 callback
.call(this, tmp
);
1303 callback
.done
= true;
1308 * loads all unloaded nodes
1309 * @name load_all([obj, callback])
1310 * @param {mixed} obj the node to load recursively, omit to load all nodes in the tree
1311 * @param {function} callback a function to be executed once loading all the nodes is complete,
1312 * @trigger load_all.jstree
1314 load_all : function (obj
, callback
) {
1315 if(!obj
) { obj
= $.jstree
.root
; }
1316 obj
= this.get_node(obj
);
1317 if(!obj
) { return false; }
1319 m
= this._model
.data
,
1320 c
= m
[obj
.id
].children_d
,
1322 if(obj
.state
&& !obj
.state
.loaded
) {
1323 to_load
.push(obj
.id
);
1325 for(i
= 0, j
= c
.length
; i
< j
; i
++) {
1326 if(m
[c
[i
]] && m
[c
[i
]].state
&& !m
[c
[i
]].state
.loaded
) {
1330 if(to_load
.length
) {
1331 this._load_nodes(to_load
, function () {
1332 this.load_all(obj
, callback
);
1337 * triggered after a load_all call completes
1339 * @name load_all.jstree
1340 * @param {Object} node the recursively loaded node
1342 if(callback
) { callback
.call(this, obj
); }
1343 this.trigger('load_all', { "node" : obj
});
1347 * handles the actual loading of a node. Used only internally.
1349 * @name _load_node(obj [, callback])
1350 * @param {mixed} obj
1351 * @param {function} callback a function to be executed once loading is complete, the function is executed in the instance's scope and receives one argument - a boolean status
1354 _load_node : function (obj
, callback
) {
1355 var s
= this.settings
.core
.data
, t
;
1356 var notTextOrCommentNode
= function notTextOrCommentNode () {
1357 return this.nodeType
!== 3 && this.nodeType
!== 8;
1359 // use original HTML
1361 if(obj
.id
=== $.jstree
.root
) {
1362 return this._append_html_data(obj
, this._data
.core
.original_container_html
.clone(true), function (status
) {
1363 callback
.call(this, status
);
1367 return callback
.call(this, false);
1369 // return callback.call(this, obj.id === $.jstree.root ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false);
1371 if($.isFunction(s
)) {
1372 return s
.call(this, obj
, $.proxy(function (d
) {
1374 callback
.call(this, false);
1377 this[typeof d
=== 'string' ? '_append_html_data' : '_append_json_data'](obj
, typeof d
=== 'string' ? $($.parseHTML(d
)).filter(notTextOrCommentNode
) : d
, function (status
) {
1378 callback
.call(this, status
);
1381 // return d === false ? callback.call(this, false) : callback.call(this, this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $(d) : d));
1384 if(typeof s
=== 'object') {
1386 s
= $.extend(true, {}, s
);
1387 if($.isFunction(s
.url
)) {
1388 s
.url
= s
.url
.call(this, obj
);
1390 if($.isFunction(s
.data
)) {
1391 s
.data
= s
.data
.call(this, obj
);
1394 .done($.proxy(function (d
,t
,x
) {
1395 var type
= x
.getResponseHeader('Content-Type');
1396 if((type
&& type
.indexOf('json') !== -1) || typeof d
=== "object") {
1397 return this._append_json_data(obj
, d
, function (status
) { callback
.call(this, status
); });
1398 //return callback.call(this, this._append_json_data(obj, d));
1400 if((type
&& type
.indexOf('html') !== -1) || typeof d
=== "string") {
1401 return this._append_html_data(obj
, $($.parseHTML(d
)).filter(notTextOrCommentNode
), function (status
) { callback
.call(this, status
); });
1402 // return callback.call(this, this._append_html_data(obj, $(d)));
1404 this._data
.core
.last_error
= { 'error' : 'ajax', 'plugin' : 'core', 'id' : 'core_04', 'reason' : 'Could not load node', 'data' : JSON
.stringify({ 'id' : obj
.id
, 'xhr' : x
}) };
1405 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
1406 return callback
.call(this, false);
1408 .fail($.proxy(function (f
) {
1409 this._data
.core
.last_error
= { 'error' : 'ajax', 'plugin' : 'core', 'id' : 'core_04', 'reason' : 'Could not load node', 'data' : JSON
.stringify({ 'id' : obj
.id
, 'xhr' : f
}) };
1410 callback
.call(this, false);
1411 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
1415 t
= $.extend(true, [], s
);
1416 } else if ($.isPlainObject(s
)) {
1417 t
= $.extend(true, {}, s
);
1421 if(obj
.id
=== $.jstree
.root
) {
1422 return this._append_json_data(obj
, t
, function (status
) {
1423 callback
.call(this, status
);
1427 this._data
.core
.last_error
= { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON
.stringify({ 'id' : obj
.id
}) };
1428 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
1429 return callback
.call(this, false);
1431 //return callback.call(this, (obj.id === $.jstree.root ? this._append_json_data(obj, t) : false) );
1433 if(typeof s
=== 'string') {
1434 if(obj
.id
=== $.jstree
.root
) {
1435 return this._append_html_data(obj
, $($.parseHTML(s
)).filter(notTextOrCommentNode
), function (status
) {
1436 callback
.call(this, status
);
1440 this._data
.core
.last_error
= { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON
.stringify({ 'id' : obj
.id
}) };
1441 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
1442 return callback
.call(this, false);
1444 //return callback.call(this, (obj.id === $.jstree.root ? this._append_html_data(obj, $(s)) : false) );
1446 return callback
.call(this, false);
1449 * adds a node to the list of nodes to redraw. Used only internally.
1451 * @name _node_changed(obj [, callback])
1452 * @param {mixed} obj
1454 _node_changed : function (obj
) {
1455 obj
= this.get_node(obj
);
1457 this._model
.changed
.push(obj
.id
);
1461 * appends HTML content to the tree. Used internally.
1463 * @name _append_html_data(obj, data)
1464 * @param {mixed} obj the node to append to
1465 * @param {String} data the HTML string to parse and append
1466 * @trigger model.jstree, changed.jstree
1468 _append_html_data : function (dom
, data
, cb
) {
1469 dom
= this.get_node(dom
);
1471 dom
.children_d
= [];
1472 var dat
= data
.is('ul') ? data
.children() : data
,
1476 m
= this._model
.data
,
1478 s
= this._data
.core
.selected
.length
,
1480 dat
.each($.proxy(function (i
, v
) {
1481 tmp
= this._parse_model_from_html($(v
), par
, p
.parents
.concat());
1485 if(m
[tmp
].children_d
.length
) {
1486 dpc
= dpc
.concat(m
[tmp
].children_d
);
1492 for(i
= 0, j
= p
.parents
.length
; i
< j
; i
++) {
1493 m
[p
.parents
[i
]].children_d
= m
[p
.parents
[i
]].children_d
.concat(dpc
);
1496 * triggered when new data is inserted to the tree model
1498 * @name model.jstree
1499 * @param {Array} nodes an array of node IDs
1500 * @param {String} parent the parent ID of the nodes
1502 this.trigger('model', { "nodes" : dpc
, 'parent' : par
});
1503 if(par
!== $.jstree
.root
) {
1504 this._node_changed(par
);
1508 this.get_container_ul().children('.jstree-initial-node').remove();
1511 if(this._data
.core
.selected
.length
!== s
) {
1512 this.trigger('changed', { 'action' : 'model', 'selected' : this._data
.core
.selected
});
1514 cb
.call(this, true);
1517 * appends JSON content to the tree. Used internally.
1519 * @name _append_json_data(obj, data)
1520 * @param {mixed} obj the node to append to
1521 * @param {String} data the JSON object to parse and append
1522 * @param {Boolean} force_processing internal param - do not set
1523 * @trigger model.jstree, changed.jstree
1525 _append_json_data : function (dom
, data
, cb
, force_processing
) {
1526 if(this.element
=== null) { return; }
1527 dom
= this.get_node(dom
);
1529 dom
.children_d
= [];
1533 if(typeof data
=== "string") {
1534 data
= JSON
.parse(data
);
1537 if(!$.isArray(data
)) { data
= [data
]; }
1540 'df' : this._model
.default_state
,
1543 'm' : this._model
.data
,
1545 't_cnt' : this._cnt
,
1546 'sel' : this._data
.core
.selected
1548 func = function (data
, undefined) {
1549 if(data
.data
) { data
= data
.data
; }
1562 parse_flat = function (d
, p
, ps
) {
1563 if(!ps
) { ps
= []; }
1564 else { ps
= ps
.concat(); }
1565 if(p
) { ps
.unshift(p
); }
1566 var tid
= d
.id
.toString(),
1570 text
: d
.text
|| '',
1571 icon
: d
.icon
!== undefined ? d
.icon
: true,
1574 children
: d
.children
|| [],
1575 children_d
: d
.children_d
|| [],
1578 li_attr
: { id
: false },
1579 a_attr
: { href
: '#' },
1583 if(df
.hasOwnProperty(i
)) {
1584 tmp
.state
[i
] = df
[i
];
1587 if(d
&& d
.data
&& d
.data
.jstree
&& d
.data
.jstree
.icon
) {
1588 tmp
.icon
= d
.data
.jstree
.icon
;
1590 if(tmp
.icon
=== undefined || tmp
.icon
=== null || tmp
.icon
=== "") {
1596 for(i
in d
.data
.jstree
) {
1597 if(d
.data
.jstree
.hasOwnProperty(i
)) {
1598 tmp
.state
[i
] = d
.data
.jstree
[i
];
1603 if(d
&& typeof d
.state
=== 'object') {
1604 for (i
in d
.state
) {
1605 if(d
.state
.hasOwnProperty(i
)) {
1606 tmp
.state
[i
] = d
.state
[i
];
1610 if(d
&& typeof d
.li_attr
=== 'object') {
1611 for (i
in d
.li_attr
) {
1612 if(d
.li_attr
.hasOwnProperty(i
)) {
1613 tmp
.li_attr
[i
] = d
.li_attr
[i
];
1617 if(!tmp
.li_attr
.id
) {
1618 tmp
.li_attr
.id
= tid
;
1620 if(d
&& typeof d
.a_attr
=== 'object') {
1621 for (i
in d
.a_attr
) {
1622 if(d
.a_attr
.hasOwnProperty(i
)) {
1623 tmp
.a_attr
[i
] = d
.a_attr
[i
];
1627 if(d
&& d
.children
&& d
.children
=== true) {
1628 tmp
.state
.loaded
= false;
1630 tmp
.children_d
= [];
1633 for(i
= 0, j
= tmp
.children
.length
; i
< j
; i
++) {
1634 c
= parse_flat(m
[tmp
.children
[i
]], tmp
.id
, ps
);
1636 tmp
.children_d
.push(c
);
1637 if(e
.children_d
.length
) {
1638 tmp
.children_d
= tmp
.children_d
.concat(e
.children_d
);
1643 m
[tmp
.id
].original
= d
;
1644 if(tmp
.state
.selected
) {
1649 parse_nest = function (d
, p
, ps
) {
1650 if(!ps
) { ps
= []; }
1651 else { ps
= ps
.concat(); }
1652 if(p
) { ps
.unshift(p
); }
1653 var tid
= false, i
, j
, c
, e
, tmp
;
1655 tid
= 'j' + t_id
+ '_' + (++t_cnt
);
1660 text
: typeof d
=== 'string' ? d
: '',
1661 icon
: typeof d
=== 'object' && d
.icon
!== undefined ? d
.icon
: true,
1668 li_attr
: { id
: false },
1669 a_attr
: { href
: '#' },
1673 if(df
.hasOwnProperty(i
)) {
1674 tmp
.state
[i
] = df
[i
];
1677 if(d
&& d
.id
) { tmp
.id
= d
.id
.toString(); }
1678 if(d
&& d
.text
) { tmp
.text
= d
.text
; }
1679 if(d
&& d
.data
&& d
.data
.jstree
&& d
.data
.jstree
.icon
) {
1680 tmp
.icon
= d
.data
.jstree
.icon
;
1682 if(tmp
.icon
=== undefined || tmp
.icon
=== null || tmp
.icon
=== "") {
1688 for(i
in d
.data
.jstree
) {
1689 if(d
.data
.jstree
.hasOwnProperty(i
)) {
1690 tmp
.state
[i
] = d
.data
.jstree
[i
];
1695 if(d
&& typeof d
.state
=== 'object') {
1696 for (i
in d
.state
) {
1697 if(d
.state
.hasOwnProperty(i
)) {
1698 tmp
.state
[i
] = d
.state
[i
];
1702 if(d
&& typeof d
.li_attr
=== 'object') {
1703 for (i
in d
.li_attr
) {
1704 if(d
.li_attr
.hasOwnProperty(i
)) {
1705 tmp
.li_attr
[i
] = d
.li_attr
[i
];
1709 if(tmp
.li_attr
.id
&& !tmp
.id
) {
1710 tmp
.id
= tmp
.li_attr
.id
.toString();
1715 if(!tmp
.li_attr
.id
) {
1716 tmp
.li_attr
.id
= tmp
.id
;
1718 if(d
&& typeof d
.a_attr
=== 'object') {
1719 for (i
in d
.a_attr
) {
1720 if(d
.a_attr
.hasOwnProperty(i
)) {
1721 tmp
.a_attr
[i
] = d
.a_attr
[i
];
1725 if(d
&& d
.children
&& d
.children
.length
) {
1726 for(i
= 0, j
= d
.children
.length
; i
< j
; i
++) {
1727 c
= parse_nest(d
.children
[i
], tmp
.id
, ps
);
1729 tmp
.children
.push(c
);
1730 if(e
.children_d
.length
) {
1731 tmp
.children_d
= tmp
.children_d
.concat(e
.children_d
);
1734 tmp
.children_d
= tmp
.children_d
.concat(tmp
.children
);
1736 if(d
&& d
.children
&& d
.children
=== true) {
1737 tmp
.state
.loaded
= false;
1739 tmp
.children_d
= [];
1745 if(tmp
.state
.selected
) {
1751 if(dat
.length
&& dat
[0].id
!== undefined && dat
[0].parent
!== undefined) {
1752 // Flat JSON support (for easy import from DB):
1753 // 1) convert to object (foreach)
1754 for(i
= 0, j
= dat
.length
; i
< j
; i
++) {
1755 if(!dat
[i
].children
) {
1756 dat
[i
].children
= [];
1758 m
[dat
[i
].id
.toString()] = dat
[i
];
1760 // 2) populate children (foreach)
1761 for(i
= 0, j
= dat
.length
; i
< j
; i
++) {
1762 m
[dat
[i
].parent
.toString()].children
.push(dat
[i
].id
.toString());
1763 // populate parent.children_d
1764 p
.children_d
.push(dat
[i
].id
.toString());
1766 // 3) normalize && populate parents and children_d with recursion
1767 for(i
= 0, j
= p
.children
.length
; i
< j
; i
++) {
1768 tmp
= parse_flat(m
[p
.children
[i
]], par
, p
.parents
.concat());
1770 if(m
[tmp
].children_d
.length
) {
1771 dpc
= dpc
.concat(m
[tmp
].children_d
);
1774 for(i
= 0, j
= p
.parents
.length
; i
< j
; i
++) {
1775 m
[p
.parents
[i
]].children_d
= m
[p
.parents
[i
]].children_d
.concat(dpc
);
1777 // ?) three_state selection - p.state.selected && t - (if three_state foreach(dat => ch) -> foreach(parents) if(parent.selected) child.selected = true;
1788 for(i
= 0, j
= dat
.length
; i
< j
; i
++) {
1789 tmp
= parse_nest(dat
[i
], par
, p
.parents
.concat());
1793 if(m
[tmp
].children_d
.length
) {
1794 dpc
= dpc
.concat(m
[tmp
].children_d
);
1800 for(i
= 0, j
= p
.parents
.length
; i
< j
; i
++) {
1801 m
[p
.parents
[i
]].children_d
= m
[p
.parents
[i
]].children_d
.concat(dpc
);
1812 if(typeof window
=== 'undefined' || typeof window
.document
=== 'undefined') {
1819 rslt = function (rslt
, worker
) {
1820 if(this.element
=== null) { return; }
1821 this._cnt
= rslt
.cnt
;
1822 var i
, m
= this._model
.data
;
1824 if (m
.hasOwnProperty(i
) && m
[i
].state
&& m
[i
].state
.loading
&& rslt
.mod
[i
]) {
1825 rslt
.mod
[i
].state
.loading
= true;
1828 this._model
.data
= rslt
.mod
; // breaks the reference in load_node - careful
1831 var j
, a
= rslt
.add
, r
= rslt
.sel
, s
= this._data
.core
.selected
.slice();
1832 m
= this._model
.data
;
1833 // if selection was changed while calculating in worker
1834 if(r
.length
!== s
.length
|| $.vakata
.array_unique(r
.concat(s
)).length
!== r
.length
) {
1835 // deselect nodes that are no longer selected
1836 for(i
= 0, j
= r
.length
; i
< j
; i
++) {
1837 if($.inArray(r
[i
], a
) === -1 && $.inArray(r
[i
], s
) === -1) {
1838 m
[r
[i
]].state
.selected
= false;
1841 // select nodes that were selected in the mean time
1842 for(i
= 0, j
= s
.length
; i
< j
; i
++) {
1843 if($.inArray(s
[i
], r
) === -1) {
1844 m
[s
[i
]].state
.selected
= true;
1849 if(rslt
.add
.length
) {
1850 this._data
.core
.selected
= this._data
.core
.selected
.concat(rslt
.add
);
1853 this.trigger('model', { "nodes" : rslt
.dpc
, 'parent' : rslt
.par
});
1855 if(rslt
.par
!== $.jstree
.root
) {
1856 this._node_changed(rslt
.par
);
1860 // this.get_container_ul().children('.jstree-initial-node').remove();
1863 if(rslt
.add
.length
) {
1864 this.trigger('changed', { 'action' : 'model', 'selected' : this._data
.core
.selected
});
1866 cb
.call(this, true);
1868 if(this.settings
.core
.worker
&& window
.Blob
&& window
.URL
&& window
.Worker
) {
1870 if(this._wrk
=== null) {
1871 this._wrk
= window
.URL
.createObjectURL(
1873 ['self.onmessage = ' + func
.toString()],
1874 {type
:"text/javascript"}
1878 if(!this._data
.core
.working
|| force_processing
) {
1879 this._data
.core
.working
= true;
1880 w
= new window
.Worker(this._wrk
);
1881 w
.onmessage
= $.proxy(function (e
) {
1882 rslt
.call(this, e
.data
, true);
1883 try { w
.terminate(); w
= null; } catch(ignore
) { }
1884 if(this._data
.core
.worker_queue
.length
) {
1885 this._append_json_data
.apply(this, this._data
.core
.worker_queue
.shift());
1888 this._data
.core
.working
= false;
1892 if(this._data
.core
.worker_queue
.length
) {
1893 this._append_json_data
.apply(this, this._data
.core
.worker_queue
.shift());
1896 this._data
.core
.working
= false;
1900 w
.postMessage(args
);
1904 this._data
.core
.worker_queue
.push([dom
, data
, cb
, true]);
1908 rslt
.call(this, func(args
), false);
1909 if(this._data
.core
.worker_queue
.length
) {
1910 this._append_json_data
.apply(this, this._data
.core
.worker_queue
.shift());
1913 this._data
.core
.working
= false;
1918 rslt
.call(this, func(args
), false);
1922 * parses a node from a jQuery object and appends them to the in memory tree model. Used internally.
1924 * @name _parse_model_from_html(d [, p, ps])
1925 * @param {jQuery} d the jQuery object to parse
1926 * @param {String} p the parent ID
1927 * @param {Array} ps list of all parents
1928 * @return {String} the ID of the object added to the model
1930 _parse_model_from_html : function (d
, p
, ps
) {
1931 if(!ps
) { ps
= []; }
1932 else { ps
= [].concat(ps
); }
1933 if(p
) { ps
.unshift(p
); }
1934 var c
, e
, m
= this._model
.data
,
1945 li_attr
: { id
: false },
1946 a_attr
: { href
: '#' },
1949 for(i
in this._model
.default_state
) {
1950 if(this._model
.default_state
.hasOwnProperty(i
)) {
1951 data
.state
[i
] = this._model
.default_state
[i
];
1954 tmp
= $.vakata
.attributes(d
, true);
1955 $.each(tmp
, function (i
, v
) {
1957 if(!v
.length
) { return true; }
1958 data
.li_attr
[i
] = v
;
1960 data
.id
= v
.toString();
1963 tmp
= d
.children('a').first();
1965 tmp
= $.vakata
.attributes(tmp
, true);
1966 $.each(tmp
, function (i
, v
) {
1973 tmp
= d
.children("a").first().length
? d
.children("a").first().clone() : d
.clone();
1974 tmp
.children("ins, i, ul").remove();
1976 tmp
= $('<div />').html(tmp
);
1977 data
.text
= this.settings
.core
.force_text
? tmp
.text() : tmp
.html();
1979 data
.data
= tmp
? $.extend(true, {}, tmp
) : null;
1980 data
.state
.opened
= d
.hasClass('jstree-open');
1981 data
.state
.selected
= d
.children('a').hasClass('jstree-clicked');
1982 data
.state
.disabled
= d
.children('a').hasClass('jstree-disabled');
1983 if(data
.data
&& data
.data
.jstree
) {
1984 for(i
in data
.data
.jstree
) {
1985 if(data
.data
.jstree
.hasOwnProperty(i
)) {
1986 data
.state
[i
] = data
.data
.jstree
[i
];
1990 tmp
= d
.children("a").children(".jstree-themeicon");
1992 data
.icon
= tmp
.hasClass('jstree-themeicon-hidden') ? false : tmp
.attr('rel');
1994 if(data
.state
.icon
!== undefined) {
1995 data
.icon
= data
.state
.icon
;
1997 if(data
.icon
=== undefined || data
.icon
=== null || data
.icon
=== "") {
2000 tmp
= d
.children("ul").children("li");
2002 tid
= 'j' + this._id
+ '_' + (++this._cnt
);
2004 data
.id
= data
.li_attr
.id
? data
.li_attr
.id
.toString() : tid
;
2006 tmp
.each($.proxy(function (i
, v
) {
2007 c
= this._parse_model_from_html($(v
), data
.id
, ps
);
2008 e
= this._model
.data
[c
];
2009 data
.children
.push(c
);
2010 if(e
.children_d
.length
) {
2011 data
.children_d
= data
.children_d
.concat(e
.children_d
);
2014 data
.children_d
= data
.children_d
.concat(data
.children
);
2017 if(d
.hasClass('jstree-closed')) {
2018 data
.state
.loaded
= false;
2021 if(data
.li_attr
['class']) {
2022 data
.li_attr
['class'] = data
.li_attr
['class'].replace('jstree-closed','').replace('jstree-open','');
2024 if(data
.a_attr
['class']) {
2025 data
.a_attr
['class'] = data
.a_attr
['class'].replace('jstree-clicked','').replace('jstree-disabled','');
2028 if(data
.state
.selected
) {
2029 this._data
.core
.selected
.push(data
.id
);
2034 * parses a node from a JSON object (used when dealing with flat data, which has no nesting of children, but has id and parent properties) and appends it to the in memory tree model. Used internally.
2036 * @name _parse_model_from_flat_json(d [, p, ps])
2037 * @param {Object} d the JSON object to parse
2038 * @param {String} p the parent ID
2039 * @param {Array} ps list of all parents
2040 * @return {String} the ID of the object added to the model
2042 _parse_model_from_flat_json : function (d
, p
, ps
) {
2043 if(!ps
) { ps
= []; }
2044 else { ps
= ps
.concat(); }
2045 if(p
) { ps
.unshift(p
); }
2046 var tid
= d
.id
.toString(),
2047 m
= this._model
.data
,
2048 df
= this._model
.default_state
,
2052 text
: d
.text
|| '',
2053 icon
: d
.icon
!== undefined ? d
.icon
: true,
2056 children
: d
.children
|| [],
2057 children_d
: d
.children_d
|| [],
2060 li_attr
: { id
: false },
2061 a_attr
: { href
: '#' },
2065 if(df
.hasOwnProperty(i
)) {
2066 tmp
.state
[i
] = df
[i
];
2069 if(d
&& d
.data
&& d
.data
.jstree
&& d
.data
.jstree
.icon
) {
2070 tmp
.icon
= d
.data
.jstree
.icon
;
2072 if(tmp
.icon
=== undefined || tmp
.icon
=== null || tmp
.icon
=== "") {
2078 for(i
in d
.data
.jstree
) {
2079 if(d
.data
.jstree
.hasOwnProperty(i
)) {
2080 tmp
.state
[i
] = d
.data
.jstree
[i
];
2085 if(d
&& typeof d
.state
=== 'object') {
2086 for (i
in d
.state
) {
2087 if(d
.state
.hasOwnProperty(i
)) {
2088 tmp
.state
[i
] = d
.state
[i
];
2092 if(d
&& typeof d
.li_attr
=== 'object') {
2093 for (i
in d
.li_attr
) {
2094 if(d
.li_attr
.hasOwnProperty(i
)) {
2095 tmp
.li_attr
[i
] = d
.li_attr
[i
];
2099 if(!tmp
.li_attr
.id
) {
2100 tmp
.li_attr
.id
= tid
;
2102 if(d
&& typeof d
.a_attr
=== 'object') {
2103 for (i
in d
.a_attr
) {
2104 if(d
.a_attr
.hasOwnProperty(i
)) {
2105 tmp
.a_attr
[i
] = d
.a_attr
[i
];
2109 if(d
&& d
.children
&& d
.children
=== true) {
2110 tmp
.state
.loaded
= false;
2112 tmp
.children_d
= [];
2115 for(i
= 0, j
= tmp
.children
.length
; i
< j
; i
++) {
2116 c
= this._parse_model_from_flat_json(m
[tmp
.children
[i
]], tmp
.id
, ps
);
2118 tmp
.children_d
.push(c
);
2119 if(e
.children_d
.length
) {
2120 tmp
.children_d
= tmp
.children_d
.concat(e
.children_d
);
2125 m
[tmp
.id
].original
= d
;
2126 if(tmp
.state
.selected
) {
2127 this._data
.core
.selected
.push(tmp
.id
);
2132 * parses a node from a JSON object and appends it to the in memory tree model. Used internally.
2134 * @name _parse_model_from_json(d [, p, ps])
2135 * @param {Object} d the JSON object to parse
2136 * @param {String} p the parent ID
2137 * @param {Array} ps list of all parents
2138 * @return {String} the ID of the object added to the model
2140 _parse_model_from_json : function (d
, p
, ps
) {
2141 if(!ps
) { ps
= []; }
2142 else { ps
= ps
.concat(); }
2143 if(p
) { ps
.unshift(p
); }
2144 var tid
= false, i
, j
, c
, e
, m
= this._model
.data
, df
= this._model
.default_state
, tmp
;
2146 tid
= 'j' + this._id
+ '_' + (++this._cnt
);
2151 text
: typeof d
=== 'string' ? d
: '',
2152 icon
: typeof d
=== 'object' && d
.icon
!== undefined ? d
.icon
: true,
2159 li_attr
: { id
: false },
2160 a_attr
: { href
: '#' },
2164 if(df
.hasOwnProperty(i
)) {
2165 tmp
.state
[i
] = df
[i
];
2168 if(d
&& d
.id
) { tmp
.id
= d
.id
.toString(); }
2169 if(d
&& d
.text
) { tmp
.text
= d
.text
; }
2170 if(d
&& d
.data
&& d
.data
.jstree
&& d
.data
.jstree
.icon
) {
2171 tmp
.icon
= d
.data
.jstree
.icon
;
2173 if(tmp
.icon
=== undefined || tmp
.icon
=== null || tmp
.icon
=== "") {
2179 for(i
in d
.data
.jstree
) {
2180 if(d
.data
.jstree
.hasOwnProperty(i
)) {
2181 tmp
.state
[i
] = d
.data
.jstree
[i
];
2186 if(d
&& typeof d
.state
=== 'object') {
2187 for (i
in d
.state
) {
2188 if(d
.state
.hasOwnProperty(i
)) {
2189 tmp
.state
[i
] = d
.state
[i
];
2193 if(d
&& typeof d
.li_attr
=== 'object') {
2194 for (i
in d
.li_attr
) {
2195 if(d
.li_attr
.hasOwnProperty(i
)) {
2196 tmp
.li_attr
[i
] = d
.li_attr
[i
];
2200 if(tmp
.li_attr
.id
&& !tmp
.id
) {
2201 tmp
.id
= tmp
.li_attr
.id
.toString();
2206 if(!tmp
.li_attr
.id
) {
2207 tmp
.li_attr
.id
= tmp
.id
;
2209 if(d
&& typeof d
.a_attr
=== 'object') {
2210 for (i
in d
.a_attr
) {
2211 if(d
.a_attr
.hasOwnProperty(i
)) {
2212 tmp
.a_attr
[i
] = d
.a_attr
[i
];
2216 if(d
&& d
.children
&& d
.children
.length
) {
2217 for(i
= 0, j
= d
.children
.length
; i
< j
; i
++) {
2218 c
= this._parse_model_from_json(d
.children
[i
], tmp
.id
, ps
);
2220 tmp
.children
.push(c
);
2221 if(e
.children_d
.length
) {
2222 tmp
.children_d
= tmp
.children_d
.concat(e
.children_d
);
2225 tmp
.children_d
= tmp
.children_d
.concat(tmp
.children
);
2227 if(d
&& d
.children
&& d
.children
=== true) {
2228 tmp
.state
.loaded
= false;
2230 tmp
.children_d
= [];
2236 if(tmp
.state
.selected
) {
2237 this._data
.core
.selected
.push(tmp
.id
);
2242 * redraws all nodes that need to be redrawn. Used internally.
2245 * @trigger redraw.jstree
2247 _redraw : function () {
2248 var nodes
= this._model
.force_full_redraw
? this._model
.data
[$.jstree
.root
].children
.concat([]) : this._model
.changed
.concat([]),
2249 f
= document
.createElement('UL'), tmp
, i
, j
, fe
= this._data
.core
.focused
;
2250 for(i
= 0, j
= nodes
.length
; i
< j
; i
++) {
2251 tmp
= this.redraw_node(nodes
[i
], true, this._model
.force_full_redraw
);
2252 if(tmp
&& this._model
.force_full_redraw
) {
2256 if(this._model
.force_full_redraw
) {
2257 f
.className
= this.get_container_ul()[0].className
;
2258 f
.setAttribute('role','group');
2259 this.element
.empty().append(f
);
2260 //this.get_container_ul()[0].appendChild(f);
2263 tmp
= this.get_node(fe
, true);
2264 if(tmp
&& tmp
.length
&& tmp
.children('.jstree-anchor')[0] !== document
.activeElement
) {
2265 tmp
.children('.jstree-anchor').focus();
2268 this._data
.core
.focused
= null;
2271 this._model
.force_full_redraw
= false;
2272 this._model
.changed
= [];
2274 * triggered after nodes are redrawn
2276 * @name redraw.jstree
2277 * @param {array} nodes the redrawn nodes
2279 this.trigger('redraw', { "nodes" : nodes
});
2282 * redraws all nodes that need to be redrawn or optionally - the whole tree
2283 * @name redraw([full])
2284 * @param {Boolean} full if set to `true` all nodes are redrawn.
2286 redraw : function (full
) {
2288 this._model
.force_full_redraw
= true;
2290 //if(this._model.redraw_timeout) {
2291 // clearTimeout(this._model.redraw_timeout);
2293 //this._model.redraw_timeout = setTimeout($.proxy(this._redraw, this),0);
2297 * redraws a single node's children. Used internally.
2299 * @name draw_children(node)
2300 * @param {mixed} node the node whose children will be redrawn
2302 draw_children : function (node
) {
2303 var obj
= this.get_node(node
),
2308 if(!obj
) { return false; }
2309 if(obj
.id
=== $.jstree
.root
) { return this.redraw(true); }
2310 node
= this.get_node(node
, true);
2311 if(!node
|| !node
.length
) { return false; } // TODO: quick toggle
2313 node
.children('.jstree-children').remove();
2315 if(obj
.children
.length
&& obj
.state
.loaded
) {
2316 k
= d
.createElement('UL');
2317 k
.setAttribute('role', 'group');
2318 k
.className
= 'jstree-children';
2319 for(i
= 0, j
= obj
.children
.length
; i
< j
; i
++) {
2320 k
.appendChild(this.redraw_node(obj
.children
[i
], true, true));
2322 node
.appendChild(k
);
2326 * redraws a single node. Used internally.
2328 * @name redraw_node(node, deep, is_callback, force_render)
2329 * @param {mixed} node the node to redraw
2330 * @param {Boolean} deep should child nodes be redrawn too
2331 * @param {Boolean} is_callback is this a recursion call
2332 * @param {Boolean} force_render should children of closed parents be drawn anyway
2334 redraw_node : function (node
, deep
, is_callback
, force_render
) {
2335 var obj
= this.get_node(node
),
2344 m
= this._model
.data
,
2350 has_children
= false,
2351 last_sibling
= false;
2352 if(!obj
) { return false; }
2353 if(obj
.id
=== $.jstree
.root
) { return this.redraw(true); }
2354 deep
= deep
|| obj
.children
.length
=== 0;
2355 node
= !document
.querySelector
? document
.getElementById(obj
.id
) : this.element
[0].querySelector('#' + ("0123456789".indexOf(obj
.id
[0]) !== -1 ? '\\3' + obj
.id
[0] + ' ' + obj
.id
.substr(1).replace($.jstree
.idregex
,'\\$&') : obj
.id
.replace($.jstree
.idregex
,'\\$&')) ); //, this.element);
2358 //node = d.createElement('LI');
2360 par
= obj
.parent
!== $.jstree
.root
? $('#' + obj
.parent
.replace($.jstree
.idregex
,'\\$&'), this.element
)[0] : null;
2361 if(par
!== null && (!par
|| !m
[obj
.parent
].state
.opened
)) {
2364 ind
= $.inArray(obj
.id
, par
=== null ? m
[$.jstree
.root
].children
: m
[obj
.parent
].children
);
2370 par
= node
.parent().parent()[0];
2371 if(par
=== this.element
[0]) {
2376 // m[obj.id].data = node.data(); // use only node's data, no need to touch jquery storage
2377 if(!deep
&& obj
.children
.length
&& !node
.children('.jstree-children').length
) {
2381 old
= node
.children('.jstree-children')[0];
2383 f
= node
.children('.jstree-anchor')[0] === document
.activeElement
;
2385 //node = d.createElement('LI');
2388 node
= this._data
.core
.node
.cloneNode(true);
2389 // node is DOM, deep is boolean
2392 for(i
in obj
.li_attr
) {
2393 if(obj
.li_attr
.hasOwnProperty(i
)) {
2394 if(i
=== 'id') { continue; }
2396 node
.setAttribute(i
, obj
.li_attr
[i
]);
2399 c
+= obj
.li_attr
[i
];
2403 if(!obj
.a_attr
.id
) {
2404 obj
.a_attr
.id
= obj
.id
+ '_anchor';
2406 node
.setAttribute('aria-selected', !!obj
.state
.selected
);
2407 node
.setAttribute('aria-level', obj
.parents
.length
);
2408 node
.setAttribute('aria-labelledby', obj
.a_attr
.id
);
2409 if(obj
.state
.disabled
) {
2410 node
.setAttribute('aria-disabled', true);
2413 for(i
= 0, j
= obj
.children
.length
; i
< j
; i
++) {
2414 if(!m
[obj
.children
[i
]].state
.hidden
) {
2415 has_children
= true;
2419 if(obj
.parent
!== null && m
[obj
.parent
] && !obj
.state
.hidden
) {
2420 i
= $.inArray(obj
.id
, m
[obj
.parent
].children
);
2421 last_sibling
= obj
.id
;
2424 for(j
= m
[obj
.parent
].children
.length
; i
< j
; i
++) {
2425 if(!m
[m
[obj
.parent
].children
[i
]].state
.hidden
) {
2426 last_sibling
= m
[obj
.parent
].children
[i
];
2428 if(last_sibling
!== obj
.id
) {
2435 if(obj
.state
.hidden
) {
2436 c
+= ' jstree-hidden';
2438 if(obj
.state
.loaded
&& !has_children
) {
2439 c
+= ' jstree-leaf';
2442 c
+= obj
.state
.opened
&& obj
.state
.loaded
? ' jstree-open' : ' jstree-closed';
2443 node
.setAttribute('aria-expanded', (obj
.state
.opened
&& obj
.state
.loaded
) );
2445 if(last_sibling
=== obj
.id
) {
2446 c
+= ' jstree-last';
2450 c
= ( obj
.state
.selected
? ' jstree-clicked' : '') + ( obj
.state
.disabled
? ' jstree-disabled' : '');
2451 for(j
in obj
.a_attr
) {
2452 if(obj
.a_attr
.hasOwnProperty(j
)) {
2453 if(j
=== 'href' && obj
.a_attr
[j
] === '#') { continue; }
2455 node
.childNodes
[1].setAttribute(j
, obj
.a_attr
[j
]);
2458 c
+= ' ' + obj
.a_attr
[j
];
2463 node
.childNodes
[1].className
= 'jstree-anchor ' + c
;
2465 if((obj
.icon
&& obj
.icon
!== true) || obj
.icon
=== false) {
2466 if(obj
.icon
=== false) {
2467 node
.childNodes
[1].childNodes
[0].className
+= ' jstree-themeicon-hidden';
2469 else if(obj
.icon
.indexOf('/') === -1 && obj
.icon
.indexOf('.') === -1) {
2470 node
.childNodes
[1].childNodes
[0].className
+= ' ' + obj
.icon
+ ' jstree-themeicon-custom';
2473 node
.childNodes
[1].childNodes
[0].style
.backgroundImage
= 'url("'+obj
.icon
+'")';
2474 node
.childNodes
[1].childNodes
[0].style
.backgroundPosition
= 'center center';
2475 node
.childNodes
[1].childNodes
[0].style
.backgroundSize
= 'auto';
2476 node
.childNodes
[1].childNodes
[0].className
+= ' jstree-themeicon-custom';
2480 if(this.settings
.core
.force_text
) {
2481 node
.childNodes
[1].appendChild(d
.createTextNode(obj
.text
));
2484 node
.childNodes
[1].innerHTML
+= obj
.text
;
2488 if(deep
&& obj
.children
.length
&& (obj
.state
.opened
|| force_render
) && obj
.state
.loaded
) {
2489 k
= d
.createElement('UL');
2490 k
.setAttribute('role', 'group');
2491 k
.className
= 'jstree-children';
2492 for(i
= 0, j
= obj
.children
.length
; i
< j
; i
++) {
2493 k
.appendChild(this.redraw_node(obj
.children
[i
], deep
, true));
2495 node
.appendChild(k
);
2498 node
.appendChild(old
);
2501 // append back using par / ind
2503 par
= this.element
[0];
2505 for(i
= 0, j
= par
.childNodes
.length
; i
< j
; i
++) {
2506 if(par
.childNodes
[i
] && par
.childNodes
[i
].className
&& par
.childNodes
[i
].className
.indexOf('jstree-children') !== -1) {
2507 tmp
= par
.childNodes
[i
];
2512 tmp
= d
.createElement('UL');
2513 tmp
.setAttribute('role', 'group');
2514 tmp
.className
= 'jstree-children';
2515 par
.appendChild(tmp
);
2519 if(ind
< par
.childNodes
.length
) {
2520 par
.insertBefore(node
, par
.childNodes
[ind
]);
2523 par
.appendChild(node
);
2526 t
= this.element
[0].scrollTop
;
2527 l
= this.element
[0].scrollLeft
;
2528 node
.childNodes
[1].focus();
2529 this.element
[0].scrollTop
= t
;
2530 this.element
[0].scrollLeft
= l
;
2533 if(obj
.state
.opened
&& !obj
.state
.loaded
) {
2534 obj
.state
.opened
= false;
2535 setTimeout($.proxy(function () {
2536 this.open_node(obj
.id
, false, 0);
2542 * opens a node, revaling its children. If the node is not loaded it will be loaded and opened once ready.
2543 * @name open_node(obj [, callback, animation])
2544 * @param {mixed} obj the node to open
2545 * @param {Function} callback a function to execute once the node is opened
2546 * @param {Number} animation the animation duration in milliseconds when opening the node (overrides the `core.animation` setting). Use `false` for no animation.
2547 * @trigger open_node.jstree, after_open.jstree, before_open.jstree
2549 open_node : function (obj
, callback
, animation
) {
2551 if($.isArray(obj
)) {
2553 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2554 this.open_node(obj
[t1
], callback
, animation
);
2558 obj
= this.get_node(obj
);
2559 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2562 animation
= animation
=== undefined ? this.settings
.core
.animation
: animation
;
2563 if(!this.is_closed(obj
)) {
2565 callback
.call(this, obj
, false);
2569 if(!this.is_loaded(obj
)) {
2570 if(this.is_loading(obj
)) {
2571 return setTimeout($.proxy(function () {
2572 this.open_node(obj
, callback
, animation
);
2575 this.load_node(obj
, function (o
, ok
) {
2576 return ok
? this.open_node(o
, callback
, animation
) : (callback
? callback
.call(this, o
, false) : false);
2580 d
= this.get_node(obj
, true);
2583 if(animation
&& d
.children(".jstree-children").length
) {
2584 d
.children(".jstree-children").stop(true, true);
2586 if(obj
.children
.length
&& !this._firstChild(d
.children('.jstree-children')[0])) {
2587 this.draw_children(obj
);
2588 //d = this.get_node(obj, true);
2591 this.trigger('before_open', { "node" : obj
});
2592 d
[0].className
= d
[0].className
.replace('jstree-closed', 'jstree-open');
2593 d
[0].setAttribute("aria-expanded", true);
2596 this.trigger('before_open', { "node" : obj
});
2598 .children(".jstree-children").css("display","none").end()
2599 .removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true)
2600 .children(".jstree-children").stop(true, true)
2601 .slideDown(animation
, function () {
2602 this.style
.display
= "";
2604 t
.trigger("after_open", { "node" : obj
});
2609 obj
.state
.opened
= true;
2611 callback
.call(this, obj
, true);
2615 * triggered when a node is about to be opened (if the node is supposed to be in the DOM, it will be, but it won't be visible yet)
2617 * @name before_open.jstree
2618 * @param {Object} node the opened node
2620 this.trigger('before_open', { "node" : obj
});
2623 * triggered when a node is opened (if there is an animation it will not be completed yet)
2625 * @name open_node.jstree
2626 * @param {Object} node the opened node
2628 this.trigger('open_node', { "node" : obj
});
2629 if(!animation
|| !d
.length
) {
2631 * triggered when a node is opened and the animation is complete
2633 * @name after_open.jstree
2634 * @param {Object} node the opened node
2636 this.trigger("after_open", { "node" : obj
});
2642 * opens every parent of a node (node should be loaded)
2643 * @name _open_to(obj)
2644 * @param {mixed} obj the node to reveal
2647 _open_to : function (obj
) {
2648 obj
= this.get_node(obj
);
2649 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2652 var i
, j
, p
= obj
.parents
;
2653 for(i
= 0, j
= p
.length
; i
< j
; i
+=1) {
2654 if(i
!== $.jstree
.root
) {
2655 this.open_node(p
[i
], false, 0);
2658 return $('#' + obj
.id
.replace($.jstree
.idregex
,'\\$&'), this.element
);
2661 * closes a node, hiding its children
2662 * @name close_node(obj [, animation])
2663 * @param {mixed} obj the node to close
2664 * @param {Number} animation the animation duration in milliseconds when closing the node (overrides the `core.animation` setting). Use `false` for no animation.
2665 * @trigger close_node.jstree, after_close.jstree
2667 close_node : function (obj
, animation
) {
2669 if($.isArray(obj
)) {
2671 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2672 this.close_node(obj
[t1
], animation
);
2676 obj
= this.get_node(obj
);
2677 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2680 if(this.is_closed(obj
)) {
2683 animation
= animation
=== undefined ? this.settings
.core
.animation
: animation
;
2685 d
= this.get_node(obj
, true);
2687 obj
.state
.opened
= false;
2689 * triggered when a node is closed (if there is an animation it will not be complete yet)
2691 * @name close_node.jstree
2692 * @param {Object} node the closed node
2694 this.trigger('close_node',{ "node" : obj
});
2697 * triggered when a node is closed and the animation is complete
2699 * @name after_close.jstree
2700 * @param {Object} node the closed node
2702 this.trigger("after_close", { "node" : obj
});
2706 d
[0].className
= d
[0].className
.replace('jstree-open', 'jstree-closed');
2707 d
.attr("aria-expanded", false).children('.jstree-children').remove();
2708 this.trigger("after_close", { "node" : obj
});
2712 .children(".jstree-children").attr("style","display:block !important").end()
2713 .removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false)
2714 .children(".jstree-children").stop(true, true).slideUp(animation
, function () {
2715 this.style
.display
= "";
2716 d
.children('.jstree-children').remove();
2718 t
.trigger("after_close", { "node" : obj
});
2725 * toggles a node - closing it if it is open, opening it if it is closed
2726 * @name toggle_node(obj)
2727 * @param {mixed} obj the node to toggle
2729 toggle_node : function (obj
) {
2731 if($.isArray(obj
)) {
2733 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2734 this.toggle_node(obj
[t1
]);
2738 if(this.is_closed(obj
)) {
2739 return this.open_node(obj
);
2741 if(this.is_open(obj
)) {
2742 return this.close_node(obj
);
2746 * opens all nodes within a node (or the tree), revaling their children. If the node is not loaded it will be loaded and opened once ready.
2747 * @name open_all([obj, animation, original_obj])
2748 * @param {mixed} obj the node to open recursively, omit to open all nodes in the tree
2749 * @param {Number} animation the animation duration in milliseconds when opening the nodes, the default is no animation
2750 * @param {jQuery} reference to the node that started the process (internal use)
2751 * @trigger open_all.jstree
2753 open_all : function (obj
, animation
, original_obj
) {
2754 if(!obj
) { obj
= $.jstree
.root
; }
2755 obj
= this.get_node(obj
);
2756 if(!obj
) { return false; }
2757 var dom
= obj
.id
=== $.jstree
.root
? this.get_container_ul() : this.get_node(obj
, true), i
, j
, _this
;
2759 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
2760 if(this.is_closed(this._model
.data
[obj
.children_d
[i
]])) {
2761 this._model
.data
[obj
.children_d
[i
]].state
.opened
= true;
2764 return this.trigger('open_all', { "node" : obj
});
2766 original_obj
= original_obj
|| dom
;
2768 dom
= this.is_closed(obj
) ? dom
.find('.jstree-closed').addBack() : dom
.find('.jstree-closed');
2769 dom
.each(function () {
2772 function(node
, status
) { if(status
&& this.is_parent(node
)) { this.open_all(node
, animation
, original_obj
); } },
2776 if(original_obj
.find('.jstree-closed').length
=== 0) {
2778 * triggered when an `open_all` call completes
2780 * @name open_all.jstree
2781 * @param {Object} node the opened node
2783 this.trigger('open_all', { "node" : this.get_node(original_obj
) });
2787 * closes all nodes within a node (or the tree), revaling their children
2788 * @name close_all([obj, animation])
2789 * @param {mixed} obj the node to close recursively, omit to close all nodes in the tree
2790 * @param {Number} animation the animation duration in milliseconds when closing the nodes, the default is no animation
2791 * @trigger close_all.jstree
2793 close_all : function (obj
, animation
) {
2794 if(!obj
) { obj
= $.jstree
.root
; }
2795 obj
= this.get_node(obj
);
2796 if(!obj
) { return false; }
2797 var dom
= obj
.id
=== $.jstree
.root
? this.get_container_ul() : this.get_node(obj
, true),
2800 dom
= this.is_open(obj
) ? dom
.find('.jstree-open').addBack() : dom
.find('.jstree-open');
2801 $(dom
.get().reverse()).each(function () { _this
.close_node(this, animation
|| 0); });
2803 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
2804 this._model
.data
[obj
.children_d
[i
]].state
.opened
= false;
2807 * triggered when an `close_all` call completes
2809 * @name close_all.jstree
2810 * @param {Object} node the closed node
2812 this.trigger('close_all', { "node" : obj
});
2815 * checks if a node is disabled (not selectable)
2816 * @name is_disabled(obj)
2817 * @param {mixed} obj
2820 is_disabled : function (obj
) {
2821 obj
= this.get_node(obj
);
2822 return obj
&& obj
.state
&& obj
.state
.disabled
;
2825 * enables a node - so that it can be selected
2826 * @name enable_node(obj)
2827 * @param {mixed} obj the node to enable
2828 * @trigger enable_node.jstree
2830 enable_node : function (obj
) {
2832 if($.isArray(obj
)) {
2834 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2835 this.enable_node(obj
[t1
]);
2839 obj
= this.get_node(obj
);
2840 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2843 obj
.state
.disabled
= false;
2844 this.get_node(obj
,true).children('.jstree-anchor').removeClass('jstree-disabled').attr('aria-disabled', false);
2846 * triggered when an node is enabled
2848 * @name enable_node.jstree
2849 * @param {Object} node the enabled node
2851 this.trigger('enable_node', { 'node' : obj
});
2854 * disables a node - so that it can not be selected
2855 * @name disable_node(obj)
2856 * @param {mixed} obj the node to disable
2857 * @trigger disable_node.jstree
2859 disable_node : function (obj
) {
2861 if($.isArray(obj
)) {
2863 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2864 this.disable_node(obj
[t1
]);
2868 obj
= this.get_node(obj
);
2869 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2872 obj
.state
.disabled
= true;
2873 this.get_node(obj
,true).children('.jstree-anchor').addClass('jstree-disabled').attr('aria-disabled', true);
2875 * triggered when an node is disabled
2877 * @name disable_node.jstree
2878 * @param {Object} node the disabled node
2880 this.trigger('disable_node', { 'node' : obj
});
2883 * determines if a node is hidden
2884 * @name is_hidden(obj)
2885 * @param {mixed} obj the node
2887 is_hidden : function (obj
) {
2888 obj
= this.get_node(obj
);
2889 return obj
.state
.hidden
=== true;
2892 * hides a node - it is still in the structure but will not be visible
2893 * @name hide_node(obj)
2894 * @param {mixed} obj the node to hide
2895 * @param {Boolean} skip_redraw internal parameter controlling if redraw is called
2896 * @trigger hide_node.jstree
2898 hide_node : function (obj
, skip_redraw
) {
2900 if($.isArray(obj
)) {
2902 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2903 this.hide_node(obj
[t1
], true);
2910 obj
= this.get_node(obj
);
2911 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2914 if(!obj
.state
.hidden
) {
2915 obj
.state
.hidden
= true;
2916 this._node_changed(obj
.parent
);
2921 * triggered when an node is hidden
2923 * @name hide_node.jstree
2924 * @param {Object} node the hidden node
2926 this.trigger('hide_node', { 'node' : obj
});
2931 * @name show_node(obj)
2932 * @param {mixed} obj the node to show
2933 * @param {Boolean} skip_redraw internal parameter controlling if redraw is called
2934 * @trigger show_node.jstree
2936 show_node : function (obj
, skip_redraw
) {
2938 if($.isArray(obj
)) {
2940 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
2941 this.show_node(obj
[t1
], true);
2948 obj
= this.get_node(obj
);
2949 if(!obj
|| obj
.id
=== $.jstree
.root
) {
2952 if(obj
.state
.hidden
) {
2953 obj
.state
.hidden
= false;
2954 this._node_changed(obj
.parent
);
2959 * triggered when an node is shown
2961 * @name show_node.jstree
2962 * @param {Object} node the shown node
2964 this.trigger('show_node', { 'node' : obj
});
2970 * @trigger hide_all.jstree
2972 hide_all : function (skip_redraw
) {
2973 var i
, m
= this._model
.data
, ids
= [];
2975 if(m
.hasOwnProperty(i
) && i
!== $.jstree
.root
&& !m
[i
].state
.hidden
) {
2976 m
[i
].state
.hidden
= true;
2980 this._model
.force_full_redraw
= true;
2985 * triggered when all nodes are hidden
2987 * @name hide_all.jstree
2988 * @param {Array} nodes the IDs of all hidden nodes
2990 this.trigger('hide_all', { 'nodes' : ids
});
2996 * @trigger show_all.jstree
2998 show_all : function (skip_redraw
) {
2999 var i
, m
= this._model
.data
, ids
= [];
3001 if(m
.hasOwnProperty(i
) && i
!== $.jstree
.root
&& m
[i
].state
.hidden
) {
3002 m
[i
].state
.hidden
= false;
3006 this._model
.force_full_redraw
= true;
3011 * triggered when all nodes are shown
3013 * @name show_all.jstree
3014 * @param {Array} nodes the IDs of all shown nodes
3016 this.trigger('show_all', { 'nodes' : ids
});
3020 * called when a node is selected by the user. Used internally.
3022 * @name activate_node(obj, e)
3023 * @param {mixed} obj the node
3024 * @param {Object} e the related event
3025 * @trigger activate_node.jstree, changed.jstree
3027 activate_node : function (obj
, e
) {
3028 if(this.is_disabled(obj
)) {
3031 if(!e
|| typeof e
!== 'object') {
3035 // ensure last_clicked is still in the DOM, make it fresh (maybe it was moved?) and make sure it is still selected, if not - make last_clicked the last selected node
3036 this._data
.core
.last_clicked
= this._data
.core
.last_clicked
&& this._data
.core
.last_clicked
.id
!== undefined ? this.get_node(this._data
.core
.last_clicked
.id
) : null;
3037 if(this._data
.core
.last_clicked
&& !this._data
.core
.last_clicked
.state
.selected
) { this._data
.core
.last_clicked
= null; }
3038 if(!this._data
.core
.last_clicked
&& this._data
.core
.selected
.length
) { this._data
.core
.last_clicked
= this.get_node(this._data
.core
.selected
[this._data
.core
.selected
.length
- 1]); }
3040 if(!this.settings
.core
.multiple
|| (!e
.metaKey
&& !e
.ctrlKey
&& !e
.shiftKey
) || (e
.shiftKey
&& (!this._data
.core
.last_clicked
|| !this.get_parent(obj
) || this.get_parent(obj
) !== this._data
.core
.last_clicked
.parent
) )) {
3041 if(!this.settings
.core
.multiple
&& (e
.metaKey
|| e
.ctrlKey
|| e
.shiftKey
) && this.is_selected(obj
)) {
3042 this.deselect_node(obj
, false, e
);
3045 this.deselect_all(true);
3046 this.select_node(obj
, false, false, e
);
3047 this._data
.core
.last_clicked
= this.get_node(obj
);
3052 var o
= this.get_node(obj
).id
,
3053 l
= this._data
.core
.last_clicked
.id
,
3054 p
= this.get_node(this._data
.core
.last_clicked
.parent
).children
,
3057 for(i
= 0, j
= p
.length
; i
< j
; i
+= 1) {
3058 // separate IFs work whem o and l are the same
3065 if(!this.is_disabled(p
[i
]) && (c
|| p
[i
] === o
|| p
[i
] === l
)) {
3066 if (!this.is_hidden(p
[i
])) {
3067 this.select_node(p
[i
], true, false, e
);
3071 this.deselect_node(p
[i
], true, e
);
3074 this.trigger('changed', { 'action' : 'select_node', 'node' : this.get_node(obj
), 'selected' : this._data
.core
.selected
, 'event' : e
});
3077 if(!this.is_selected(obj
)) {
3078 this.select_node(obj
, false, false, e
);
3081 this.deselect_node(obj
, false, e
);
3086 * triggered when an node is clicked or intercated with by the user
3088 * @name activate_node.jstree
3089 * @param {Object} node
3090 * @param {Object} event the ooriginal event (if any) which triggered the call (may be an empty object)
3092 this.trigger('activate_node', { 'node' : this.get_node(obj
), 'event' : e
});
3095 * applies the hover state on a node, called when a node is hovered by the user. Used internally.
3097 * @name hover_node(obj)
3098 * @param {mixed} obj
3099 * @trigger hover_node.jstree
3101 hover_node : function (obj
) {
3102 obj
= this.get_node(obj
, true);
3103 if(!obj
|| !obj
.length
|| obj
.children('.jstree-hovered').length
) {
3106 var o
= this.element
.find('.jstree-hovered'), t
= this.element
;
3107 if(o
&& o
.length
) { this.dehover_node(o
); }
3109 obj
.children('.jstree-anchor').addClass('jstree-hovered');
3111 * triggered when an node is hovered
3113 * @name hover_node.jstree
3114 * @param {Object} node
3116 this.trigger('hover_node', { 'node' : this.get_node(obj
) });
3117 setTimeout(function () { t
.attr('aria-activedescendant', obj
[0].id
); }, 0);
3120 * removes the hover state from a nodecalled when a node is no longer hovered by the user. Used internally.
3122 * @name dehover_node(obj)
3123 * @param {mixed} obj
3124 * @trigger dehover_node.jstree
3126 dehover_node : function (obj
) {
3127 obj
= this.get_node(obj
, true);
3128 if(!obj
|| !obj
.length
|| !obj
.children('.jstree-hovered').length
) {
3131 obj
.children('.jstree-anchor').removeClass('jstree-hovered');
3133 * triggered when an node is no longer hovered
3135 * @name dehover_node.jstree
3136 * @param {Object} node
3138 this.trigger('dehover_node', { 'node' : this.get_node(obj
) });
3142 * @name select_node(obj [, supress_event, prevent_open])
3143 * @param {mixed} obj an array can be used to select multiple nodes
3144 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3145 * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened
3146 * @trigger select_node.jstree, changed.jstree
3148 select_node : function (obj
, supress_event
, prevent_open
, e
) {
3149 var dom
, t1
, t2
, th
;
3150 if($.isArray(obj
)) {
3152 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
3153 this.select_node(obj
[t1
], supress_event
, prevent_open
, e
);
3157 obj
= this.get_node(obj
);
3158 if(!obj
|| obj
.id
=== $.jstree
.root
) {
3161 dom
= this.get_node(obj
, true);
3162 if(!obj
.state
.selected
) {
3163 obj
.state
.selected
= true;
3164 this._data
.core
.selected
.push(obj
.id
);
3166 dom
= this._open_to(obj
);
3168 if(dom
&& dom
.length
) {
3169 dom
.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked');
3172 * triggered when an node is selected
3174 * @name select_node.jstree
3175 * @param {Object} node
3176 * @param {Array} selected the current selection
3177 * @param {Object} event the event (if any) that triggered this select_node
3179 this.trigger('select_node', { 'node' : obj
, 'selected' : this._data
.core
.selected
, 'event' : e
});
3180 if(!supress_event
) {
3182 * triggered when selection changes
3184 * @name changed.jstree
3185 * @param {Object} node
3186 * @param {Object} action the action that caused the selection to change
3187 * @param {Array} selected the current selection
3188 * @param {Object} event the event (if any) that triggered this changed event
3190 this.trigger('changed', { 'action' : 'select_node', 'node' : obj
, 'selected' : this._data
.core
.selected
, 'event' : e
});
3196 * @name deselect_node(obj [, supress_event])
3197 * @param {mixed} obj an array can be used to deselect multiple nodes
3198 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3199 * @trigger deselect_node.jstree, changed.jstree
3201 deselect_node : function (obj
, supress_event
, e
) {
3203 if($.isArray(obj
)) {
3205 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
3206 this.deselect_node(obj
[t1
], supress_event
, e
);
3210 obj
= this.get_node(obj
);
3211 if(!obj
|| obj
.id
=== $.jstree
.root
) {
3214 dom
= this.get_node(obj
, true);
3215 if(obj
.state
.selected
) {
3216 obj
.state
.selected
= false;
3217 this._data
.core
.selected
= $.vakata
.array_remove_item(this._data
.core
.selected
, obj
.id
);
3219 dom
.attr('aria-selected', false).children('.jstree-anchor').removeClass('jstree-clicked');
3222 * triggered when an node is deselected
3224 * @name deselect_node.jstree
3225 * @param {Object} node
3226 * @param {Array} selected the current selection
3227 * @param {Object} event the event (if any) that triggered this deselect_node
3229 this.trigger('deselect_node', { 'node' : obj
, 'selected' : this._data
.core
.selected
, 'event' : e
});
3230 if(!supress_event
) {
3231 this.trigger('changed', { 'action' : 'deselect_node', 'node' : obj
, 'selected' : this._data
.core
.selected
, 'event' : e
});
3236 * select all nodes in the tree
3237 * @name select_all([supress_event])
3238 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3239 * @trigger select_all.jstree, changed.jstree
3241 select_all : function (supress_event
) {
3242 var tmp
= this._data
.core
.selected
.concat([]), i
, j
;
3243 this._data
.core
.selected
= this._model
.data
[$.jstree
.root
].children_d
.concat();
3244 for(i
= 0, j
= this._data
.core
.selected
.length
; i
< j
; i
++) {
3245 if(this._model
.data
[this._data
.core
.selected
[i
]]) {
3246 this._model
.data
[this._data
.core
.selected
[i
]].state
.selected
= true;
3251 * triggered when all nodes are selected
3253 * @name select_all.jstree
3254 * @param {Array} selected the current selection
3256 this.trigger('select_all', { 'selected' : this._data
.core
.selected
});
3257 if(!supress_event
) {
3258 this.trigger('changed', { 'action' : 'select_all', 'selected' : this._data
.core
.selected
, 'old_selection' : tmp
});
3262 * deselect all selected nodes
3263 * @name deselect_all([supress_event])
3264 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3265 * @trigger deselect_all.jstree, changed.jstree
3267 deselect_all : function (supress_event
) {
3268 var tmp
= this._data
.core
.selected
.concat([]), i
, j
;
3269 for(i
= 0, j
= this._data
.core
.selected
.length
; i
< j
; i
++) {
3270 if(this._model
.data
[this._data
.core
.selected
[i
]]) {
3271 this._model
.data
[this._data
.core
.selected
[i
]].state
.selected
= false;
3274 this._data
.core
.selected
= [];
3275 this.element
.find('.jstree-clicked').removeClass('jstree-clicked').parent().attr('aria-selected', false);
3277 * triggered when all nodes are deselected
3279 * @name deselect_all.jstree
3280 * @param {Object} node the previous selection
3281 * @param {Array} selected the current selection
3283 this.trigger('deselect_all', { 'selected' : this._data
.core
.selected
, 'node' : tmp
});
3284 if(!supress_event
) {
3285 this.trigger('changed', { 'action' : 'deselect_all', 'selected' : this._data
.core
.selected
, 'old_selection' : tmp
});
3289 * checks if a node is selected
3290 * @name is_selected(obj)
3291 * @param {mixed} obj
3294 is_selected : function (obj
) {
3295 obj
= this.get_node(obj
);
3296 if(!obj
|| obj
.id
=== $.jstree
.root
) {
3299 return obj
.state
.selected
;
3302 * get an array of all selected nodes
3303 * @name get_selected([full])
3304 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3307 get_selected : function (full
) {
3308 return full
? $.map(this._data
.core
.selected
, $.proxy(function (i
) { return this.get_node(i
); }, this)) : this._data
.core
.selected
.slice();
3311 * get an array of all top level selected nodes (ignoring children of selected nodes)
3312 * @name get_top_selected([full])
3313 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3316 get_top_selected : function (full
) {
3317 var tmp
= this.get_selected(true),
3318 obj
= {}, i
, j
, k
, l
;
3319 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
3320 obj
[tmp
[i
].id
] = tmp
[i
];
3322 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
3323 for(k
= 0, l
= tmp
[i
].children_d
.length
; k
< l
; k
++) {
3324 if(obj
[tmp
[i
].children_d
[k
]]) {
3325 delete obj
[tmp
[i
].children_d
[k
]];
3331 if(obj
.hasOwnProperty(i
)) {
3335 return full
? $.map(tmp
, $.proxy(function (i
) { return this.get_node(i
); }, this)) : tmp
;
3338 * get an array of all bottom level selected nodes (ignoring selected parents)
3339 * @name get_bottom_selected([full])
3340 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3343 get_bottom_selected : function (full
) {
3344 var tmp
= this.get_selected(true),
3346 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
3347 if(!tmp
[i
].children
.length
) {
3348 obj
.push(tmp
[i
].id
);
3351 return full
? $.map(obj
, $.proxy(function (i
) { return this.get_node(i
); }, this)) : obj
;
3354 * gets the current state of the tree so that it can be restored later with `set_state(state)`. Used internally.
3359 get_state : function () {
3364 'left' : this.element
.scrollLeft(),
3365 'top' : this.element
.scrollTop()
3369 'name' : this.get_theme(),
3370 'icons' : this._data.core.themes.icons,
3371 'dots' : this._data.core.themes.dots
3377 for(i
in this._model
.data
) {
3378 if(this._model
.data
.hasOwnProperty(i
)) {
3379 if(i
!== $.jstree
.root
) {
3380 if(this._model
.data
[i
].state
.opened
) {
3381 state
.core
.open
.push(i
);
3383 if(this._model
.data
[i
].state
.selected
) {
3384 state
.core
.selected
.push(i
);
3392 * sets the state of the tree. Used internally.
3393 * @name set_state(state [, callback])
3395 * @param {Object} state the state to restore. Keep in mind this object is passed by reference and jstree will modify it.
3396 * @param {Function} callback an optional function to execute once the state is restored.
3397 * @trigger set_state.jstree
3399 set_state : function (state
, callback
) {
3401 if(state
.core
&& state
.core
.selected
&& state
.core
.initial_selection
=== undefined) {
3402 state
.core
.initial_selection
= this._data
.core
.selected
.concat([]).sort().join(',');
3405 var res
, n
, t
, _this
, i
;
3406 if(state
.core
.open
) {
3407 if(!$.isArray(state
.core
.open
) || !state
.core
.open
.length
) {
3408 delete state
.core
.open
;
3409 this.set_state(state
, callback
);
3412 this._load_nodes(state
.core
.open
, function (nodes
) {
3413 this.open_node(nodes
, false, 0);
3414 delete state
.core
.open
;
3415 this.set_state(state
, callback
);
3420 if(state
.core
.scroll
) {
3421 if(state
.core
.scroll
&& state
.core
.scroll
.left
!== undefined) {
3422 this.element
.scrollLeft(state
.core
.scroll
.left
);
3424 if(state
.core
.scroll
&& state
.core
.scroll
.top
!== undefined) {
3425 this.element
.scrollTop(state
.core
.scroll
.top
);
3427 delete state
.core
.scroll
;
3428 this.set_state(state
, callback
);
3431 if(state
.core
.selected
) {
3433 if (state
.core
.initial_selection
=== undefined ||
3434 state
.core
.initial_selection
=== this._data
.core
.selected
.concat([]).sort().join(',')
3436 this.deselect_all();
3437 $.each(state
.core
.selected
, function (i
, v
) {
3438 _this
.select_node(v
, false, true);
3441 delete state
.core
.initial_selection
;
3442 delete state
.core
.selected
;
3443 this.set_state(state
, callback
);
3447 if(state
.hasOwnProperty(i
) && i
!== "core" && $.inArray(i
, this.settings
.plugins
) === -1) {
3451 if($.isEmptyObject(state
.core
)) {
3453 this.set_state(state
, callback
);
3457 if($.isEmptyObject(state
)) {
3459 if(callback
) { callback
.call(this); }
3461 * triggered when a `set_state` call completes
3463 * @name set_state.jstree
3465 this.trigger('set_state');
3473 * refreshes the tree - all nodes are reloaded with calls to `load_node`.
3475 * @param {Boolean} skip_loading an option to skip showing the loading indicator
3476 * @param {Mixed} forget_state if set to `true` state will not be reapplied, if set to a function (receiving the current state as argument) the result of that function will be used as state
3477 * @trigger refresh.jstree
3479 refresh : function (skip_loading
, forget_state
) {
3480 this._data
.core
.state
= forget_state
=== true ? {} : this.get_state();
3481 if(forget_state
&& $.isFunction(forget_state
)) { this._data
.core
.state
= forget_state
.call(this, this._data
.core
.state
); }
3483 this._model
.data
= {};
3484 this._model
.data
[$.jstree
.root
] = {
3490 state
: { loaded
: false }
3492 this._data
.core
.selected
= [];
3493 this._data
.core
.last_clicked
= null;
3494 this._data
.core
.focused
= null;
3496 var c
= this.get_container_ul()[0].className
;
3498 this.element
.html("<"+"ul class='"+c
+"' role='group'><"+"li class='jstree-initial-node jstree-loading jstree-leaf jstree-last' role='treeitem' id='j"+this._id
+"_loading'><i class='jstree-icon jstree-ocl'></i><"+"a class='jstree-anchor' href='#'><i class='jstree-icon jstree-themeicon-hidden'></i>" + this.get_string("Loading ...") + "</a></li></ul>");
3499 this.element
.attr('aria-activedescendant','j'+this._id
+'_loading');
3501 this.load_node($.jstree
.root
, function (o
, s
) {
3503 this.get_container_ul()[0].className
= c
;
3504 if(this._firstChild(this.get_container_ul()[0])) {
3505 this.element
.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id
);
3507 this.set_state($.extend(true, {}, this._data
.core
.state
), function () {
3509 * triggered when a `refresh` call completes
3511 * @name refresh.jstree
3513 this.trigger('refresh');
3516 this._data
.core
.state
= null;
3520 * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`.
3521 * @name refresh_node(obj)
3522 * @param {mixed} obj the node
3523 * @trigger refresh_node.jstree
3525 refresh_node : function (obj
) {
3526 obj
= this.get_node(obj
);
3527 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
3528 var opened
= [], to_load
= [], s
= this._data
.core
.selected
.concat([]);
3529 to_load
.push(obj
.id
);
3530 if(obj
.state
.opened
=== true) { opened
.push(obj
.id
); }
3531 this.get_node(obj
, true).find('.jstree-open').each(function() { to_load
.push(this.id
); opened
.push(this.id
); });
3532 this._load_nodes(to_load
, $.proxy(function (nodes
) {
3533 this.open_node(opened
, false, 0);
3534 this.select_node(s
);
3536 * triggered when a node is refreshed
3538 * @name refresh_node.jstree
3539 * @param {Object} node - the refreshed node
3540 * @param {Array} nodes - an array of the IDs of the nodes that were reloaded
3542 this.trigger('refresh_node', { 'node' : obj
, 'nodes' : nodes
});
3543 }, this), false, true);
3546 * set (change) the ID of a node
3547 * @name set_id(obj, id)
3548 * @param {mixed} obj the node
3549 * @param {String} id the new ID
3551 * @trigger set_id.jstree
3553 set_id : function (obj
, id
) {
3554 obj
= this.get_node(obj
);
3555 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
3556 var i
, j
, m
= this._model
.data
, old
= obj
.id
;
3558 // update parents (replace current ID with new one in children and children_d)
3559 m
[obj
.parent
].children
[$.inArray(obj
.id
, m
[obj
.parent
].children
)] = id
;
3560 for(i
= 0, j
= obj
.parents
.length
; i
< j
; i
++) {
3561 m
[obj
.parents
[i
]].children_d
[$.inArray(obj
.id
, m
[obj
.parents
[i
]].children_d
)] = id
;
3563 // update children (replace current ID with new one in parent and parents)
3564 for(i
= 0, j
= obj
.children
.length
; i
< j
; i
++) {
3565 m
[obj
.children
[i
]].parent
= id
;
3567 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
3568 m
[obj
.children_d
[i
]].parents
[$.inArray(obj
.id
, m
[obj
.children_d
[i
]].parents
)] = id
;
3570 i
= $.inArray(obj
.id
, this._data
.core
.selected
);
3571 if(i
!== -1) { this._data
.core
.selected
[i
] = id
; }
3572 // update model and obj itself (obj.id, this._model.data[KEY])
3573 i
= this.get_node(obj
.id
, true);
3575 i
.attr('id', id
); //.children('.jstree-anchor').attr('id', id + '_anchor').end().attr('aria-labelledby', id + '_anchor');
3576 if(this.element
.attr('aria-activedescendant') === obj
.id
) {
3577 this.element
.attr('aria-activedescendant', id
);
3582 obj
.li_attr
.id
= id
;
3585 * triggered when a node id value is changed
3587 * @name set_id.jstree
3588 * @param {Object} node
3589 * @param {String} old the old id
3591 this.trigger('set_id',{ "node" : obj
, "new" : obj
.id
, "old" : old
});
3595 * get the text value of a node
3596 * @name get_text(obj)
3597 * @param {mixed} obj the node
3600 get_text : function (obj
) {
3601 obj
= this.get_node(obj
);
3602 return (!obj
|| obj
.id
=== $.jstree
.root
) ? false : obj
.text
;
3605 * set the text value of a node. Used internally, please use `rename_node(obj, val)`.
3607 * @name set_text(obj, val)
3608 * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes
3609 * @param {String} val the new text value
3611 * @trigger set_text.jstree
3613 set_text : function (obj
, val
) {
3615 if($.isArray(obj
)) {
3617 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
3618 this.set_text(obj
[t1
], val
);
3622 obj
= this.get_node(obj
);
3623 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
3625 if(this.get_node(obj
, true).length
) {
3626 this.redraw_node(obj
.id
);
3629 * triggered when a node text value is changed
3631 * @name set_text.jstree
3632 * @param {Object} obj
3633 * @param {String} text the new value
3635 this.trigger('set_text',{ "obj" : obj
, "text" : val
});
3639 * gets a JSON representation of a node (or the whole tree)
3640 * @name get_json([obj, options])
3641 * @param {mixed} obj
3642 * @param {Object} options
3643 * @param {Boolean} options.no_state do not return state information
3644 * @param {Boolean} options.no_id do not return ID
3645 * @param {Boolean} options.no_children do not include children
3646 * @param {Boolean} options.no_data do not include node data
3647 * @param {Boolean} options.no_li_attr do not include LI attributes
3648 * @param {Boolean} options.no_a_attr do not include A attributes
3649 * @param {Boolean} options.flat return flat JSON instead of nested
3652 get_json : function (obj
, options
, flat
) {
3653 obj
= this.get_node(obj
|| $.jstree
.root
);
3654 if(!obj
) { return false; }
3655 if(options
&& options
.flat
&& !flat
) { flat
= []; }
3659 'icon' : this.get_icon(obj
),
3660 'li_attr' : $.extend(true, {}, obj
.li_attr
),
3661 'a_attr' : $.extend(true, {}, obj
.a_attr
),
3663 'data' : options
&& options
.no_data
? false : $.extend(true, $.isArray(obj
.data
)?[]:{}, obj
.data
)
3664 //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ),
3666 if(options
&& options
.flat
) {
3667 tmp
.parent
= obj
.parent
;
3672 if(!options
|| !options
.no_state
) {
3673 for(i
in obj
.state
) {
3674 if(obj
.state
.hasOwnProperty(i
)) {
3675 tmp
.state
[i
] = obj
.state
[i
];
3681 if(options
&& options
.no_li_attr
) {
3684 if(options
&& options
.no_a_attr
) {
3687 if(options
&& options
.no_id
) {
3689 if(tmp
.li_attr
&& tmp
.li_attr
.id
) {
3690 delete tmp
.li_attr
.id
;
3692 if(tmp
.a_attr
&& tmp
.a_attr
.id
) {
3693 delete tmp
.a_attr
.id
;
3696 if(options
&& options
.flat
&& obj
.id
!== $.jstree
.root
) {
3699 if(!options
|| !options
.no_children
) {
3700 for(i
= 0, j
= obj
.children
.length
; i
< j
; i
++) {
3701 if(options
&& options
.flat
) {
3702 this.get_json(obj
.children
[i
], options
, flat
);
3705 tmp
.children
.push(this.get_json(obj
.children
[i
], options
));
3709 return options
&& options
.flat
? flat
: (obj
.id
=== $.jstree
.root
? tmp
.children
: tmp
);
3712 * create a new node (do not confuse with load_node)
3713 * @name create_node([par, node, pos, callback, is_loaded])
3714 * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`)
3715 * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name)
3716 * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last"
3717 * @param {Function} callback a function to be called once the node is created
3718 * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded
3719 * @return {String} the ID of the newly create node
3720 * @trigger model.jstree, create_node.jstree
3722 create_node : function (par
, node
, pos
, callback
, is_loaded
) {
3723 if(par
=== null) { par
= $.jstree
.root
; }
3724 par
= this.get_node(par
);
3725 if(!par
) { return false; }
3726 pos
= pos
=== undefined ? "last" : pos
;
3727 if(!pos
.toString().match(/^(before|after)$/) && !is_loaded
&& !this.is_loaded(par
)) {
3728 return this.load_node(par
, function () { this.create_node(par
, node
, pos
, callback
, true); });
3730 if(!node
) { node
= { "text" : this.get_string('New node') }; }
3731 if(typeof node
=== "string") {
3732 node
= { "text" : node
};
3734 node
= $.extend(true, {}, node
);
3736 if(node
.text
=== undefined) { node
.text
= this.get_string('New node'); }
3739 if(par
.id
=== $.jstree
.root
) {
3740 if(pos
=== "before") { pos
= "first"; }
3741 if(pos
=== "after") { pos
= "last"; }
3745 tmp
= this.get_node(par
.parent
);
3746 pos
= $.inArray(par
.id
, tmp
.children
);
3750 tmp
= this.get_node(par
.parent
);
3751 pos
= $.inArray(par
.id
, tmp
.children
) + 1;
3759 pos
= par
.children
.length
;
3762 if(!pos
) { pos
= 0; }
3765 if(pos
> par
.children
.length
) { pos
= par
.children
.length
; }
3766 if(!node
.id
) { node
.id
= true; }
3767 if(!this.check("create_node", node
, par
, pos
)) {
3768 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
3771 if(node
.id
=== true) { delete node
.id
; }
3772 node
= this._parse_model_from_json(node
, par
.id
, par
.parents
.concat());
3773 if(!node
) { return false; }
3774 tmp
= this.get_node(node
);
3777 dpc
= dpc
.concat(tmp
.children_d
);
3778 this.trigger('model', { "nodes" : dpc
, "parent" : par
.id
});
3780 par
.children_d
= par
.children_d
.concat(dpc
);
3781 for(i
= 0, j
= par
.parents
.length
; i
< j
; i
++) {
3782 this._model
.data
[par
.parents
[i
]].children_d
= this._model
.data
[par
.parents
[i
]].children_d
.concat(dpc
);
3786 for(i
= 0, j
= par
.children
.length
; i
< j
; i
++) {
3787 tmp
[i
>= pos
? i
+1 : i
] = par
.children
[i
];
3792 this.redraw_node(par
, true);
3794 * triggered when a node is created
3796 * @name create_node.jstree
3797 * @param {Object} node
3798 * @param {String} parent the parent's ID
3799 * @param {Number} position the position of the new node among the parent's children
3801 this.trigger('create_node', { "node" : this.get_node(node
), "parent" : par
.id
, "position" : pos
});
3802 if(callback
) { callback
.call(this, this.get_node(node
)); }
3806 * set the text value of a node
3807 * @name rename_node(obj, val)
3808 * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name
3809 * @param {String} val the new text value
3811 * @trigger rename_node.jstree
3813 rename_node : function (obj
, val
) {
3815 if($.isArray(obj
)) {
3817 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
3818 this.rename_node(obj
[t1
], val
);
3822 obj
= this.get_node(obj
);
3823 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
3825 if(!this.check("rename_node", obj
, this.get_parent(obj
), val
)) {
3826 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
3829 this.set_text(obj
, val
); // .apply(this, Array.prototype.slice.call(arguments))
3831 * triggered when a node is renamed
3833 * @name rename_node.jstree
3834 * @param {Object} node
3835 * @param {String} text the new value
3836 * @param {String} old the old value
3838 this.trigger('rename_node', { "node" : obj
, "text" : val
, "old" : old
});
3843 * @name delete_node(obj)
3844 * @param {mixed} obj the node, you can pass an array to delete multiple nodes
3846 * @trigger delete_node.jstree, changed.jstree
3848 delete_node : function (obj
) {
3849 var t1
, t2
, par
, pos
, tmp
, i
, j
, k
, l
, c
, top
, lft
;
3850 if($.isArray(obj
)) {
3852 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
3853 this.delete_node(obj
[t1
]);
3857 obj
= this.get_node(obj
);
3858 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
3859 par
= this.get_node(obj
.parent
);
3860 pos
= $.inArray(obj
.id
, par
.children
);
3862 if(!this.check("delete_node", obj
, par
, pos
)) {
3863 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
3867 par
.children
= $.vakata
.array_remove(par
.children
, pos
);
3869 tmp
= obj
.children_d
.concat([]);
3871 for(i
= 0, j
= obj
.parents
.length
; i
< j
; i
++) {
3872 this._model
.data
[obj
.parents
[i
]].children_d
= $.vakata
.array_filter(this._model
.data
[obj
.parents
[i
]].children_d
, function (v
) {
3873 return $.inArray(v
, tmp
) === -1;
3876 for(k
= 0, l
= tmp
.length
; k
< l
; k
++) {
3877 if(this._model
.data
[tmp
[k
]].state
.selected
) {
3883 this._data
.core
.selected
= $.vakata
.array_filter(this._data
.core
.selected
, function (v
) {
3884 return $.inArray(v
, tmp
) === -1;
3888 * triggered when a node is deleted
3890 * @name delete_node.jstree
3891 * @param {Object} node
3892 * @param {String} parent the parent's ID
3894 this.trigger('delete_node', { "node" : obj
, "parent" : par
.id
});
3896 this.trigger('changed', { 'action' : 'delete_node', 'node' : obj
, 'selected' : this._data
.core
.selected
, 'parent' : par
.id
});
3898 for(k
= 0, l
= tmp
.length
; k
< l
; k
++) {
3899 delete this._model
.data
[tmp
[k
]];
3901 if($.inArray(this._data
.core
.focused
, tmp
) !== -1) {
3902 this._data
.core
.focused
= null;
3903 top
= this.element
[0].scrollTop
;
3904 lft
= this.element
[0].scrollLeft
;
3905 if(par
.id
=== $.jstree
.root
) {
3906 if (this._model
.data
[$.jstree
.root
].children
[0]) {
3907 this.get_node(this._model
.data
[$.jstree
.root
].children
[0], true).children('.jstree-anchor').focus();
3911 this.get_node(par
, true).children('.jstree-anchor').focus();
3913 this.element
[0].scrollTop
= top
;
3914 this.element
[0].scrollLeft
= lft
;
3916 this.redraw_node(par
, true);
3920 * check if an operation is premitted on the tree. Used internally.
3922 * @name check(chk, obj, par, pos)
3923 * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node"
3924 * @param {mixed} obj the node
3925 * @param {mixed} par the parent
3926 * @param {mixed} pos the position to insert at, or if "rename_node" - the new name
3927 * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node
3930 check : function (chk
, obj
, par
, pos
, more
) {
3931 obj
= obj
&& obj
.id
? obj
: this.get_node(obj
);
3932 par
= par
&& par
.id
? par
: this.get_node(par
);
3933 var tmp
= chk
.match(/^move_node|copy_node|create_node$/i) ? par
: obj
,
3934 chc
= this.settings
.core
.check_callback
;
3935 if(chk
=== "move_node" || chk
=== "copy_node") {
3936 if((!more
|| !more
.is_multi
) && (obj
.id
=== par
.id
|| (chk
=== "move_node" && $.inArray(obj
.id
, par
.children
) === pos
) || $.inArray(par
.id
, obj
.children_d
) !== -1)) {
3937 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_01', 'reason' : 'Moving parent inside child', 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
3941 if(tmp
&& tmp
.data
) { tmp
= tmp
.data
; }
3942 if(tmp
&& tmp
.functions
&& (tmp
.functions
[chk
] === false || tmp
.functions
[chk
] === true)) {
3943 if(tmp
.functions
[chk
] === false) {
3944 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_02', 'reason' : 'Node data prevents function: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
3946 return tmp
.functions
[chk
];
3948 if(chc
=== false || ($.isFunction(chc
) && chc
.call(this, chk
, obj
, par
, pos
, more
) === false) || (chc
&& chc
[chk
] === false)) {
3949 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_03', 'reason' : 'User config for core.check_callback prevents function: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
3955 * get the last error
3956 * @name last_error()
3959 last_error : function () {
3960 return this._data
.core
.last_error
;
3963 * move a node to a new parent
3964 * @name move_node(obj, par [, pos, callback, is_loaded])
3965 * @param {mixed} obj the node to move, pass an array to move multiple nodes
3966 * @param {mixed} par the new parent
3967 * @param {mixed} pos the position to insert at (besides integer values, "first" and "last" are supported, as well as "before" and "after"), defaults to integer `0`
3968 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
3969 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
3970 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
3971 * @param {Boolean} instance internal parameter indicating if the node comes from another instance
3972 * @trigger move_node.jstree
3974 move_node : function (obj
, par
, pos
, callback
, is_loaded
, skip_redraw
, origin
) {
3975 var t1
, t2
, old_par
, old_pos
, new_par
, old_ins
, is_multi
, dpc
, tmp
, i
, j
, k
, l
, p
;
3977 par
= this.get_node(par
);
3978 pos
= pos
=== undefined ? 0 : pos
;
3979 if(!par
) { return false; }
3980 if(!pos
.toString().match(/^(before|after)$/) && !is_loaded
&& !this.is_loaded(par
)) {
3981 return this.load_node(par
, function () { this.move_node(obj
, par
, pos
, callback
, true, false, origin
); });
3984 if($.isArray(obj
)) {
3985 if(obj
.length
=== 1) {
3989 //obj = obj.slice();
3990 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
3991 if((tmp
= this.move_node(obj
[t1
], par
, pos
, callback
, is_loaded
, false, origin
))) {
4000 obj
= obj
&& obj
.id
? obj
: this.get_node(obj
);
4002 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
4004 old_par
= (obj
.parent
|| $.jstree
.root
).toString();
4005 new_par
= (!pos
.toString().match(/^(before|after)$/) || par
.id
=== $.jstree
.root
) ? par
: this.get_node(par
.parent
);
4006 old_ins
= origin
? origin
: (this._model
.data
[obj
.id
] ? this : $.jstree
.reference(obj
.id
));
4007 is_multi
= !old_ins
|| !old_ins
._id
|| (this._id
!== old_ins
._id
);
4008 old_pos
= old_ins
&& old_ins
._id
&& old_par
&& old_ins
._model
.data
[old_par
] && old_ins
._model
.data
[old_par
].children
? $.inArray(obj
.id
, old_ins
._model
.data
[old_par
].children
) : -1;
4009 if(old_ins
&& old_ins
._id
) {
4010 obj
= old_ins
._model
.data
[obj
.id
];
4014 if((tmp
= this.copy_node(obj
, par
, pos
, callback
, is_loaded
, false, origin
))) {
4015 if(old_ins
) { old_ins
.delete_node(obj
); }
4020 //var m = this._model.data;
4021 if(par
.id
=== $.jstree
.root
) {
4022 if(pos
=== "before") { pos
= "first"; }
4023 if(pos
=== "after") { pos
= "last"; }
4027 pos
= $.inArray(par
.id
, new_par
.children
);
4030 pos
= $.inArray(par
.id
, new_par
.children
) + 1;
4037 pos
= new_par
.children
.length
;
4040 if(!pos
) { pos
= 0; }
4043 if(pos
> new_par
.children
.length
) { pos
= new_par
.children
.length
; }
4044 if(!this.check("move_node", obj
, new_par
, pos
, { 'core' : true, 'origin' : origin
, 'is_multi' : (old_ins
&& old_ins
._id
&& old_ins
._id
!== this._id
), 'is_foreign' : (!old_ins
|| !old_ins
._id
) })) {
4045 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
4048 if(obj
.parent
=== new_par
.id
) {
4049 dpc
= new_par
.children
.concat();
4050 tmp
= $.inArray(obj
.id
, dpc
);
4052 dpc
= $.vakata
.array_remove(dpc
, tmp
);
4053 if(pos
> tmp
) { pos
--; }
4056 for(i
= 0, j
= dpc
.length
; i
< j
; i
++) {
4057 tmp
[i
>= pos
? i
+1 : i
] = dpc
[i
];
4060 new_par
.children
= tmp
;
4061 this._node_changed(new_par
.id
);
4062 this.redraw(new_par
.id
=== $.jstree
.root
);
4065 // clean old parent and up
4066 tmp
= obj
.children_d
.concat();
4068 for(i
= 0, j
= obj
.parents
.length
; i
< j
; i
++) {
4070 p
= old_ins
._model
.data
[obj
.parents
[i
]].children_d
;
4071 for(k
= 0, l
= p
.length
; k
< l
; k
++) {
4072 if($.inArray(p
[k
], tmp
) === -1) {
4076 old_ins
._model
.data
[obj
.parents
[i
]].children_d
= dpc
;
4078 old_ins
._model
.data
[old_par
].children
= $.vakata
.array_remove_item(old_ins
._model
.data
[old_par
].children
, obj
.id
);
4080 // insert into new parent and up
4081 for(i
= 0, j
= new_par
.parents
.length
; i
< j
; i
++) {
4082 this._model
.data
[new_par
.parents
[i
]].children_d
= this._model
.data
[new_par
.parents
[i
]].children_d
.concat(tmp
);
4085 for(i
= 0, j
= new_par
.children
.length
; i
< j
; i
++) {
4086 dpc
[i
>= pos
? i
+1 : i
] = new_par
.children
[i
];
4089 new_par
.children
= dpc
;
4090 new_par
.children_d
.push(obj
.id
);
4091 new_par
.children_d
= new_par
.children_d
.concat(obj
.children_d
);
4094 obj
.parent
= new_par
.id
;
4095 tmp
= new_par
.parents
.concat();
4096 tmp
.unshift(new_par
.id
);
4097 p
= obj
.parents
.length
;
4100 // update object children
4102 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
4103 this._model
.data
[obj
.children_d
[i
]].parents
= this._model
.data
[obj
.children_d
[i
]].parents
.slice(0,p
*-1);
4104 Array
.prototype.push
.apply(this._model
.data
[obj
.children_d
[i
]].parents
, tmp
);
4107 if(old_par
=== $.jstree
.root
|| new_par
.id
=== $.jstree
.root
) {
4108 this._model
.force_full_redraw
= true;
4110 if(!this._model
.force_full_redraw
) {
4111 this._node_changed(old_par
);
4112 this._node_changed(new_par
.id
);
4118 if(callback
) { callback
.call(this, obj
, new_par
, pos
); }
4120 * triggered when a node is moved
4122 * @name move_node.jstree
4123 * @param {Object} node
4124 * @param {String} parent the parent's ID
4125 * @param {Number} position the position of the node among the parent's children
4126 * @param {String} old_parent the old parent of the node
4127 * @param {Number} old_position the old position of the node
4128 * @param {Boolean} is_multi do the node and new parent belong to different instances
4129 * @param {jsTree} old_instance the instance the node came from
4130 * @param {jsTree} new_instance the instance of the new parent
4132 this.trigger('move_node', { "node" : obj
, "parent" : new_par
.id
, "position" : pos
, "old_parent" : old_par
, "old_position" : old_pos
, 'is_multi' : (old_ins
&& old_ins
._id
&& old_ins
._id
!== this._id
), 'is_foreign' : (!old_ins
|| !old_ins
._id
), 'old_instance' : old_ins
, 'new_instance' : this });
4136 * copy a node to a new parent
4137 * @name copy_node(obj, par [, pos, callback, is_loaded])
4138 * @param {mixed} obj the node to copy, pass an array to copy multiple nodes
4139 * @param {mixed} par the new parent
4140 * @param {mixed} pos the position to insert at (besides integer values, "first" and "last" are supported, as well as "before" and "after"), defaults to integer `0`
4141 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
4142 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
4143 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
4144 * @param {Boolean} instance internal parameter indicating if the node comes from another instance
4145 * @trigger model.jstree copy_node.jstree
4147 copy_node : function (obj
, par
, pos
, callback
, is_loaded
, skip_redraw
, origin
) {
4148 var t1
, t2
, dpc
, tmp
, i
, j
, node
, old_par
, new_par
, old_ins
, is_multi
;
4150 par
= this.get_node(par
);
4151 pos
= pos
=== undefined ? 0 : pos
;
4152 if(!par
) { return false; }
4153 if(!pos
.toString().match(/^(before|after)$/) && !is_loaded
&& !this.is_loaded(par
)) {
4154 return this.load_node(par
, function () { this.copy_node(obj
, par
, pos
, callback
, true, false, origin
); });
4157 if($.isArray(obj
)) {
4158 if(obj
.length
=== 1) {
4162 //obj = obj.slice();
4163 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
4164 if((tmp
= this.copy_node(obj
[t1
], par
, pos
, callback
, is_loaded
, true, origin
))) {
4173 obj
= obj
&& obj
.id
? obj
: this.get_node(obj
);
4174 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
4176 old_par
= (obj
.parent
|| $.jstree
.root
).toString();
4177 new_par
= (!pos
.toString().match(/^(before|after)$/) || par
.id
=== $.jstree
.root
) ? par
: this.get_node(par
.parent
);
4178 old_ins
= origin
? origin
: (this._model
.data
[obj
.id
] ? this : $.jstree
.reference(obj
.id
));
4179 is_multi
= !old_ins
|| !old_ins
._id
|| (this._id
!== old_ins
._id
);
4181 if(old_ins
&& old_ins
._id
) {
4182 obj
= old_ins
._model
.data
[obj
.id
];
4185 if(par
.id
=== $.jstree
.root
) {
4186 if(pos
=== "before") { pos
= "first"; }
4187 if(pos
=== "after") { pos
= "last"; }
4191 pos
= $.inArray(par
.id
, new_par
.children
);
4194 pos
= $.inArray(par
.id
, new_par
.children
) + 1;
4201 pos
= new_par
.children
.length
;
4204 if(!pos
) { pos
= 0; }
4207 if(pos
> new_par
.children
.length
) { pos
= new_par
.children
.length
; }
4208 if(!this.check("copy_node", obj
, new_par
, pos
, { 'core' : true, 'origin' : origin
, 'is_multi' : (old_ins
&& old_ins
._id
&& old_ins
._id
!== this._id
), 'is_foreign' : (!old_ins
|| !old_ins
._id
) })) {
4209 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
4212 node
= old_ins
? old_ins
.get_json(obj
, { no_id
: true, no_data
: true, no_state
: true }) : obj
;
4213 if(!node
) { return false; }
4214 if(node
.id
=== true) { delete node
.id
; }
4215 node
= this._parse_model_from_json(node
, new_par
.id
, new_par
.parents
.concat());
4216 if(!node
) { return false; }
4217 tmp
= this.get_node(node
);
4218 if(obj
&& obj
.state
&& obj
.state
.loaded
=== false) { tmp
.state
.loaded
= false; }
4221 dpc
= dpc
.concat(tmp
.children_d
);
4222 this.trigger('model', { "nodes" : dpc
, "parent" : new_par
.id
});
4224 // insert into new parent and up
4225 for(i
= 0, j
= new_par
.parents
.length
; i
< j
; i
++) {
4226 this._model
.data
[new_par
.parents
[i
]].children_d
= this._model
.data
[new_par
.parents
[i
]].children_d
.concat(dpc
);
4229 for(i
= 0, j
= new_par
.children
.length
; i
< j
; i
++) {
4230 dpc
[i
>= pos
? i
+1 : i
] = new_par
.children
[i
];
4233 new_par
.children
= dpc
;
4234 new_par
.children_d
.push(tmp
.id
);
4235 new_par
.children_d
= new_par
.children_d
.concat(tmp
.children_d
);
4237 if(new_par
.id
=== $.jstree
.root
) {
4238 this._model
.force_full_redraw
= true;
4240 if(!this._model
.force_full_redraw
) {
4241 this._node_changed(new_par
.id
);
4244 this.redraw(new_par
.id
=== $.jstree
.root
);
4246 if(callback
) { callback
.call(this, tmp
, new_par
, pos
); }
4248 * triggered when a node is copied
4250 * @name copy_node.jstree
4251 * @param {Object} node the copied node
4252 * @param {Object} original the original node
4253 * @param {String} parent the parent's ID
4254 * @param {Number} position the position of the node among the parent's children
4255 * @param {String} old_parent the old parent of the node
4256 * @param {Number} old_position the position of the original node
4257 * @param {Boolean} is_multi do the node and new parent belong to different instances
4258 * @param {jsTree} old_instance the instance the node came from
4259 * @param {jsTree} new_instance the instance of the new parent
4261 this.trigger('copy_node', { "node" : tmp
, "original" : obj
, "parent" : new_par
.id
, "position" : pos
, "old_parent" : old_par
, "old_position" : old_ins
&& old_ins
._id
&& old_par
&& old_ins
._model
.data
[old_par
] && old_ins
._model
.data
[old_par
].children
? $.inArray(obj
.id
, old_ins
._model
.data
[old_par
].children
) : -1,'is_multi' : (old_ins
&& old_ins
._id
&& old_ins
._id
!== this._id
), 'is_foreign' : (!old_ins
|| !old_ins
._id
), 'old_instance' : old_ins
, 'new_instance' : this });
4265 * cut a node (a later call to `paste(obj)` would move the node)
4267 * @param {mixed} obj multiple objects can be passed using an array
4268 * @trigger cut.jstree
4270 cut : function (obj
) {
4271 if(!obj
) { obj
= this._data
.core
.selected
.concat(); }
4272 if(!$.isArray(obj
)) { obj
= [obj
]; }
4273 if(!obj
.length
) { return false; }
4274 var tmp
= [], o
, t1
, t2
;
4275 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
4276 o
= this.get_node(obj
[t1
]);
4277 if(o
&& o
.id
&& o
.id
!== $.jstree
.root
) { tmp
.push(o
); }
4279 if(!tmp
.length
) { return false; }
4282 ccp_mode
= 'move_node';
4284 * triggered when nodes are added to the buffer for moving
4287 * @param {Array} node
4289 this.trigger('cut', { "node" : obj
});
4292 * copy a node (a later call to `paste(obj)` would copy the node)
4294 * @param {mixed} obj multiple objects can be passed using an array
4295 * @trigger copy.jstree
4297 copy : function (obj
) {
4298 if(!obj
) { obj
= this._data
.core
.selected
.concat(); }
4299 if(!$.isArray(obj
)) { obj
= [obj
]; }
4300 if(!obj
.length
) { return false; }
4301 var tmp
= [], o
, t1
, t2
;
4302 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
4303 o
= this.get_node(obj
[t1
]);
4304 if(o
&& o
.id
&& o
.id
!== $.jstree
.root
) { tmp
.push(o
); }
4306 if(!tmp
.length
) { return false; }
4309 ccp_mode
= 'copy_node';
4311 * triggered when nodes are added to the buffer for copying
4314 * @param {Array} node
4316 this.trigger('copy', { "node" : obj
});
4319 * get the current buffer (any nodes that are waiting for a paste operation)
4320 * @name get_buffer()
4321 * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance)
4323 get_buffer : function () {
4324 return { 'mode' : ccp_mode
, 'node' : ccp_node
, 'inst' : ccp_inst
};
4327 * check if there is something in the buffer to paste
4331 can_paste : function () {
4332 return ccp_mode
!== false && ccp_node
!== false; // && ccp_inst._model.data[ccp_node];
4335 * copy or move the previously cut or copied nodes to a new parent
4336 * @name paste(obj [, pos])
4337 * @param {mixed} obj the new parent
4338 * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0`
4339 * @trigger paste.jstree
4341 paste : function (obj
, pos
) {
4342 obj
= this.get_node(obj
);
4343 if(!obj
|| !ccp_mode
|| !ccp_mode
.match(/^(copy_node|move_node)$/) || !ccp_node
) { return false; }
4344 if(this[ccp_mode
](ccp_node
, obj
, pos
, false, false, false, ccp_inst
)) {
4346 * triggered when paste is invoked
4348 * @name paste.jstree
4349 * @param {String} parent the ID of the receiving node
4350 * @param {Array} node the nodes in the buffer
4351 * @param {String} mode the performed operation - "copy_node" or "move_node"
4353 this.trigger('paste', { "parent" : obj
.id
, "node" : ccp_node
, "mode" : ccp_mode
});
4360 * clear the buffer of previously copied or cut nodes
4361 * @name clear_buffer()
4362 * @trigger clear_buffer.jstree
4364 clear_buffer : function () {
4369 * triggered when the copy / cut buffer is cleared
4371 * @name clear_buffer.jstree
4373 this.trigger('clear_buffer');
4376 * put a node in edit mode (input field to rename the node)
4377 * @name edit(obj [, default_text, callback])
4378 * @param {mixed} obj
4379 * @param {String} default_text the text to populate the input with (if omitted or set to a non-string value the node's text value is used)
4380 * @param {Function} callback a function to be called once the text box is blurred, it is called in the instance's scope and receives the node, a status parameter (true if the rename is successful, false otherwise) and a boolean indicating if the user cancelled the edit. You can access the node's title using .text
4382 edit : function (obj
, default_text
, callback
) {
4383 var rtl
, w
, a
, s
, t
, h1
, h2
, fn
, tmp
, cancel
= false;
4384 obj
= this.get_node(obj
);
4385 if(!obj
) { return false; }
4386 if(!this.check("edit", obj
, this.get_parent(obj
))) {
4387 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
4391 default_text
= typeof default_text
=== 'string' ? default_text
: obj
.text
;
4392 this.set_text(obj
, "");
4393 obj
= this._open_to(obj
);
4394 tmp
.text
= default_text
;
4396 rtl
= this._data
.core
.rtl
;
4397 w
= this.element
.width();
4398 this._data
.core
.focused
= tmp
.id
;
4399 a
= obj
.children('.jstree-anchor').focus();
4402 oi = obj.children("i:visible"),
4403 ai = a.children("i:visible"),
4404 w1 = oi.width() * oi.length,
4405 w2 = ai.width() * ai.length,
4408 h1
= $("<"+"div />", { css
: { "position" : "absolute", "top" : "-200px", "left" : (rtl
? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body");
4409 h2
= $("<"+"input />", {
4411 "class" : "jstree-rename-input",
4412 // "size" : t.length,
4415 "border" : "1px solid silver",
4416 "box-sizing" : "border-box",
4417 "display" : "inline-block",
4418 "height" : (this._data
.core
.li_height
) + "px",
4419 "lineHeight" : (this._data
.core
.li_height
) + "px",
4420 "width" : "150px" // will be set a bit further down
4422 "blur" : $.proxy(function (e
) {
4423 e
.stopImmediatePropagation();
4425 var i
= s
.children(".jstree-rename-input"),
4427 f
= this.settings
.core
.force_text
,
4429 if(v
=== "") { v
= t
; }
4433 t
= f
? t
: $('<div></div>').append($.parseHTML(t
)).html();
4434 this.set_text(obj
, t
);
4435 nv
= !!this.rename_node(obj
, f
? $('<div></div>').text(v
).text() : $('<div></div>').append($.parseHTML(v
)).html());
4437 this.set_text(obj
, t
); // move this up? and fix #483
4439 this._data
.core
.focused
= tmp
.id
;
4440 setTimeout($.proxy(function () {
4441 var node
= this.get_node(tmp
.id
, true);
4443 this._data
.core
.focused
= tmp
.id
;
4444 node
.children('.jstree-anchor').focus();
4448 callback
.call(this, tmp
, nv
, cancel
);
4452 "keydown" : function (e
) {
4458 if(key
=== 27 || key
=== 13 || key
=== 37 || key
=== 38 || key
=== 39 || key
=== 40 || key
=== 32) {
4459 e
.stopImmediatePropagation();
4461 if(key
=== 27 || key
=== 13) {
4466 "click" : function (e
) { e
.stopImmediatePropagation(); },
4467 "mousedown" : function (e
) { e
.stopImmediatePropagation(); },
4468 "keyup" : function (e
) {
4469 h2
.width(Math
.min(h1
.text("pW" + this.value
).width(),w
));
4471 "keypress" : function(e
) {
4472 if(e
.which
=== 13) { return false; }
4476 fontFamily
: a
.css('fontFamily') || '',
4477 fontSize
: a
.css('fontSize') || '',
4478 fontWeight
: a
.css('fontWeight') || '',
4479 fontStyle
: a
.css('fontStyle') || '',
4480 fontStretch
: a
.css('fontStretch') || '',
4481 fontVariant
: a
.css('fontVariant') || '',
4482 letterSpacing
: a
.css('letterSpacing') || '',
4483 wordSpacing
: a
.css('wordSpacing') || ''
4485 s
.attr('class', a
.attr('class')).append(a
.contents().clone()).append(h2
);
4488 h2
.css(fn
).width(Math
.min(h1
.text("pW" + h2
[0].value
).width(),w
))[0].select();
4489 $(document
).one('mousedown.jstree touchstart.jstree dnd_start.vakata', function (e
) {
4490 if (h2
&& e
.target
!== h2
) {
4499 * @name set_theme(theme_name [, theme_url])
4500 * @param {String} theme_name the name of the new theme to apply
4501 * @param {mixed} theme_url the location of the CSS file for this theme. Omit or set to `false` if you manually included the file. Set to `true` to autoload from the `core.themes.dir` directory.
4502 * @trigger set_theme.jstree
4504 set_theme : function (theme_name
, theme_url
) {
4505 if(!theme_name
) { return false; }
4506 if(theme_url
=== true) {
4507 var dir
= this.settings
.core
.themes
.dir
;
4508 if(!dir
) { dir
= $.jstree
.path
+ '/themes'; }
4509 theme_url
= dir
+ '/' + theme_name
+ '/style.css';
4511 if(theme_url
&& $.inArray(theme_url
, themes_loaded
) === -1) {
4512 $('head').append('<'+'link rel="stylesheet" href="' + theme_url
+ '" type="text/css" />');
4513 themes_loaded
.push(theme_url
);
4515 if(this._data
.core
.themes
.name
) {
4516 this.element
.removeClass('jstree-' + this._data
.core
.themes
.name
);
4518 this._data
.core
.themes
.name
= theme_name
;
4519 this.element
.addClass('jstree-' + theme_name
);
4520 this.element
[this.settings
.core
.themes
.responsive
? 'addClass' : 'removeClass' ]('jstree-' + theme_name
+ '-responsive');
4522 * triggered when a theme is set
4524 * @name set_theme.jstree
4525 * @param {String} theme the new theme
4527 this.trigger('set_theme', { 'theme' : theme_name
});
4530 * gets the name of the currently applied theme name
4534 get_theme : function () { return this._data
.core
.themes
.name
; },
4536 * changes the theme variant (if the theme has variants)
4537 * @name set_theme_variant(variant_name)
4538 * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed)
4540 set_theme_variant : function (variant_name
) {
4541 if(this._data
.core
.themes
.variant
) {
4542 this.element
.removeClass('jstree-' + this._data
.core
.themes
.name
+ '-' + this._data
.core
.themes
.variant
);
4544 this._data
.core
.themes
.variant
= variant_name
;
4546 this.element
.addClass('jstree-' + this._data
.core
.themes
.name
+ '-' + this._data
.core
.themes
.variant
);
4550 * gets the name of the currently applied theme variant
4554 get_theme_variant : function () { return this._data
.core
.themes
.variant
; },
4556 * shows a striped background on the container (if the theme supports it)
4557 * @name show_stripes()
4559 show_stripes : function () {
4560 this._data
.core
.themes
.stripes
= true;
4561 this.get_container_ul().addClass("jstree-striped");
4563 * triggered when stripes are shown
4565 * @name show_stripes.jstree
4567 this.trigger('show_stripes');
4570 * hides the striped background on the container
4571 * @name hide_stripes()
4573 hide_stripes : function () {
4574 this._data
.core
.themes
.stripes
= false;
4575 this.get_container_ul().removeClass("jstree-striped");
4577 * triggered when stripes are hidden
4579 * @name hide_stripes.jstree
4581 this.trigger('hide_stripes');
4584 * toggles the striped background on the container
4585 * @name toggle_stripes()
4587 toggle_stripes : function () { if(this._data
.core
.themes
.stripes
) { this.hide_stripes(); } else { this.show_stripes(); } },
4589 * shows the connecting dots (if the theme supports it)
4592 show_dots : function () {
4593 this._data
.core
.themes
.dots
= true;
4594 this.get_container_ul().removeClass("jstree-no-dots");
4596 * triggered when dots are shown
4598 * @name show_dots.jstree
4600 this.trigger('show_dots');
4603 * hides the connecting dots
4606 hide_dots : function () {
4607 this._data
.core
.themes
.dots
= false;
4608 this.get_container_ul().addClass("jstree-no-dots");
4610 * triggered when dots are hidden
4612 * @name hide_dots.jstree
4614 this.trigger('hide_dots');
4617 * toggles the connecting dots
4618 * @name toggle_dots()
4620 toggle_dots : function () { if(this._data
.core
.themes
.dots
) { this.hide_dots(); } else { this.show_dots(); } },
4622 * show the node icons
4623 * @name show_icons()
4625 show_icons : function () {
4626 this._data
.core
.themes
.icons
= true;
4627 this.get_container_ul().removeClass("jstree-no-icons");
4629 * triggered when icons are shown
4631 * @name show_icons.jstree
4633 this.trigger('show_icons');
4636 * hide the node icons
4637 * @name hide_icons()
4639 hide_icons : function () {
4640 this._data
.core
.themes
.icons
= false;
4641 this.get_container_ul().addClass("jstree-no-icons");
4643 * triggered when icons are hidden
4645 * @name hide_icons.jstree
4647 this.trigger('hide_icons');
4650 * toggle the node icons
4651 * @name toggle_icons()
4653 toggle_icons : function () { if(this._data
.core
.themes
.icons
) { this.hide_icons(); } else { this.show_icons(); } },
4655 * show the node ellipsis
4656 * @name show_icons()
4658 show_ellipsis : function () {
4659 this._data
.core
.themes
.ellipsis
= true;
4660 this.get_container_ul().addClass("jstree-ellipsis");
4662 * triggered when ellisis is shown
4664 * @name show_ellipsis.jstree
4666 this.trigger('show_ellipsis');
4669 * hide the node ellipsis
4670 * @name hide_ellipsis()
4672 hide_ellipsis : function () {
4673 this._data
.core
.themes
.ellipsis
= false;
4674 this.get_container_ul().removeClass("jstree-ellipsis");
4676 * triggered when ellisis is hidden
4678 * @name hide_ellipsis.jstree
4680 this.trigger('hide_ellipsis');
4683 * toggle the node ellipsis
4684 * @name toggle_icons()
4686 toggle_ellipsis : function () { if(this._data
.core
.themes
.ellipsis
) { this.hide_ellipsis(); } else { this.show_ellipsis(); } },
4688 * set the node icon for a node
4689 * @name set_icon(obj, icon)
4690 * @param {mixed} obj
4691 * @param {String} icon the new icon - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class
4693 set_icon : function (obj
, icon
) {
4694 var t1
, t2
, dom
, old
;
4695 if($.isArray(obj
)) {
4697 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
4698 this.set_icon(obj
[t1
], icon
);
4702 obj
= this.get_node(obj
);
4703 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
4705 obj
.icon
= icon
=== true || icon
=== null || icon
=== undefined || icon
=== '' ? true : icon
;
4706 dom
= this.get_node(obj
, true).children(".jstree-anchor").children(".jstree-themeicon");
4707 if(icon
=== false) {
4708 this.hide_icon(obj
);
4710 else if(icon
=== true || icon
=== null || icon
=== undefined || icon
=== '') {
4711 dom
.removeClass('jstree-themeicon-custom ' + old
).css("background","").removeAttr("rel");
4712 if(old
=== false) { this.show_icon(obj
); }
4714 else if(icon
.indexOf("/") === -1 && icon
.indexOf(".") === -1) {
4715 dom
.removeClass(old
).css("background","");
4716 dom
.addClass(icon
+ ' jstree-themeicon-custom').attr("rel",icon
);
4717 if(old
=== false) { this.show_icon(obj
); }
4720 dom
.removeClass(old
).css("background","");
4721 dom
.addClass('jstree-themeicon-custom').css("background", "url('" + icon
+ "') center center no-repeat").attr("rel",icon
);
4722 if(old
=== false) { this.show_icon(obj
); }
4727 * get the node icon for a node
4728 * @name get_icon(obj)
4729 * @param {mixed} obj
4732 get_icon : function (obj
) {
4733 obj
= this.get_node(obj
);
4734 return (!obj
|| obj
.id
=== $.jstree
.root
) ? false : obj
.icon
;
4737 * hide the icon on an individual node
4738 * @name hide_icon(obj)
4739 * @param {mixed} obj
4741 hide_icon : function (obj
) {
4743 if($.isArray(obj
)) {
4745 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
4746 this.hide_icon(obj
[t1
]);
4750 obj
= this.get_node(obj
);
4751 if(!obj
|| obj
=== $.jstree
.root
) { return false; }
4753 this.get_node(obj
, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden');
4757 * show the icon on an individual node
4758 * @name show_icon(obj)
4759 * @param {mixed} obj
4761 show_icon : function (obj
) {
4763 if($.isArray(obj
)) {
4765 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
4766 this.show_icon(obj
[t1
]);
4770 obj
= this.get_node(obj
);
4771 if(!obj
|| obj
=== $.jstree
.root
) { return false; }
4772 dom
= this.get_node(obj
, true);
4773 obj
.icon
= dom
.length
? dom
.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true;
4774 if(!obj
.icon
) { obj
.icon
= true; }
4775 dom
.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden');
4782 // collect attributes
4783 $.vakata
.attributes = function(node
, with_values
) {
4785 var attr
= with_values
? {} : [];
4786 if(node
&& node
.attributes
) {
4787 $.each(node
.attributes
, function (i
, v
) {
4788 if($.inArray(v
.name
.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; }
4789 if(v
.value
!== null && $.trim(v
.value
) !== '') {
4790 if(with_values
) { attr
[v
.name
] = v
.value
; }
4791 else { attr
.push(v
.name
); }
4797 $.vakata
.array_unique = function(array
) {
4798 var a
= [], i
, j
, l
, o
= {};
4799 for(i
= 0, l
= array
.length
; i
< l
; i
++) {
4800 if(o
[array
[i
]] === undefined) {
4807 // remove item from array
4808 $.vakata
.array_remove = function(array
, from) {
4809 array
.splice(from, 1);
4811 //var rest = array.slice((to || from) + 1 || array.length);
4812 //array.length = from < 0 ? array.length + from : from;
4813 //array.push.apply(array, rest);
4816 // remove item from array
4817 $.vakata
.array_remove_item = function(array
, item
) {
4818 var tmp
= $.inArray(item
, array
);
4819 return tmp
!== -1 ? $.vakata
.array_remove(array
, tmp
) : array
;
4821 $.vakata
.array_filter = function(c
,a
,b
,d
,e
) {
4823 return c
.filter(a
, b
);
4827 if (~~e
+''===e
+'' && e
>=0 && a
.call(b
,c
[e
],+e
,c
)) {
4836 * ### Changed plugin
4838 * This plugin adds more information to the `changed.jstree` event. The new data is contained in the `changed` event data property, and contains a lists of `selected` and `deselected` nodes.
4841 $.jstree
.plugins
.changed = function (options
, parent
) {
4843 this.trigger = function (ev
, data
) {
4848 if(ev
.replace('.jstree','') === 'changed') {
4849 data
.changed
= { selected
: [], deselected
: [] };
4851 for(i
= 0, j
= last
.length
; i
< j
; i
++) {
4854 for(i
= 0, j
= data
.selected
.length
; i
< j
; i
++) {
4855 if(!tmp
[data
.selected
[i
]]) {
4856 data
.changed
.selected
.push(data
.selected
[i
]);
4859 tmp
[data
.selected
[i
]] = 2;
4862 for(i
= 0, j
= last
.length
; i
< j
; i
++) {
4863 if(tmp
[last
[i
]] === 1) {
4864 data
.changed
.deselected
.push(last
[i
]);
4867 last
= data
.selected
.slice();
4870 * triggered when selection changes (the "changed" plugin enhances the original event with more data)
4872 * @name changed.jstree
4873 * @param {Object} node
4874 * @param {Object} action the action that caused the selection to change
4875 * @param {Array} selected the current selection
4876 * @param {Object} changed an object containing two properties `selected` and `deselected` - both arrays of node IDs, which were selected or deselected since the last changed event
4877 * @param {Object} event the event (if any) that triggered this changed event
4880 parent
.trigger
.call(this, ev
, data
);
4882 this.refresh = function (skip_loading
, forget_state
) {
4884 return parent
.refresh
.apply(this, arguments
);
4889 * ### Checkbox plugin
4891 * This plugin renders checkbox icons in front of each node, making multiple selection much easier.
4892 * It also supports tri-state behavior, meaning that if a node has a few of its children checked it will be rendered as undetermined, and state will be propagated up.
4895 var _i
= document
.createElement('I');
4896 _i
.className
= 'jstree-icon jstree-checkbox';
4897 _i
.setAttribute('role', 'presentation');
4899 * stores all defaults for the checkbox plugin
4900 * @name $.jstree.defaults.checkbox
4903 $.jstree
.defaults
.checkbox
= {
4905 * a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`.
4906 * @name $.jstree.defaults.checkbox.visible
4911 * a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`.
4912 * @name $.jstree.defaults.checkbox.three_state
4917 * a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`.
4918 * @name $.jstree.defaults.checkbox.whole_node
4923 * a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`.
4924 * @name $.jstree.defaults.checkbox.keep_selected_style
4927 keep_selected_style
: true,
4929 * This setting controls how cascading and undetermined nodes are applied.
4930 * If 'up' is in the string - cascading up is enabled, if 'down' is in the string - cascading down is enabled, if 'undetermined' is in the string - undetermined nodes will be used.
4931 * If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''.
4932 * @name $.jstree.defaults.checkbox.cascade
4937 * This setting controls if checkbox are bound to the general tree selection or to an internal array maintained by the checkbox plugin. Defaults to `true`, only set to `false` if you know exactly what you are doing.
4938 * @name $.jstree.defaults.checkbox.tie_selection
4941 tie_selection
: true,
4944 * This setting controls if cascading down affects disabled checkboxes
4945 * @name $.jstree.defaults.checkbox.cascade_to_disabled
4948 cascade_to_disabled
: true,
4951 * This setting controls if cascading down affects hidden checkboxes
4952 * @name $.jstree.defaults.checkbox.cascade_to_hidden
4955 cascade_to_hidden
: true
4957 $.jstree
.plugins
.checkbox = function (options
, parent
) {
4958 this.bind = function () {
4959 parent
.bind
.call(this);
4960 this._data
.checkbox
.uto
= false;
4961 this._data
.checkbox
.selected
= [];
4962 if(this.settings
.checkbox
.three_state
) {
4963 this.settings
.checkbox
.cascade
= 'up+down+undetermined';
4966 .on("init.jstree", $.proxy(function () {
4967 this._data
.checkbox
.visible
= this.settings
.checkbox
.visible
;
4968 if(!this.settings
.checkbox
.keep_selected_style
) {
4969 this.element
.addClass('jstree-checkbox-no-clicked');
4971 if(this.settings
.checkbox
.tie_selection
) {
4972 this.element
.addClass('jstree-checkbox-selection');
4975 .on("loading.jstree", $.proxy(function () {
4976 this[ this._data
.checkbox
.visible
? 'show_checkboxes' : 'hide_checkboxes' ]();
4978 if(this.settings
.checkbox
.cascade
.indexOf('undetermined') !== -1) {
4980 .on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree', $.proxy(function () {
4981 // only if undetermined is in setting
4982 if(this._data
.checkbox
.uto
) { clearTimeout(this._data
.checkbox
.uto
); }
4983 this._data
.checkbox
.uto
= setTimeout($.proxy(this._undetermined
, this), 50);
4986 if(!this.settings
.checkbox
.tie_selection
) {
4988 .on('model.jstree', $.proxy(function (e
, data
) {
4989 var m
= this._model
.data
,
4993 for(i
= 0, j
= dpc
.length
; i
< j
; i
++) {
4994 m
[dpc
[i
]].state
.checked
= m
[dpc
[i
]].state
.checked
|| (m
[dpc
[i
]].original
&& m
[dpc
[i
]].original
.state
&& m
[dpc
[i
]].original
.state
.checked
);
4995 if(m
[dpc
[i
]].state
.checked
) {
4996 this._data
.checkbox
.selected
.push(dpc
[i
]);
5001 if(this.settings
.checkbox
.cascade
.indexOf('up') !== -1 || this.settings
.checkbox
.cascade
.indexOf('down') !== -1) {
5003 .on('model.jstree', $.proxy(function (e
, data
) {
5004 var m
= this._model
.data
,
5008 c
, i
, j
, k
, l
, tmp
, s
= this.settings
.checkbox
.cascade
, t
= this.settings
.checkbox
.tie_selection
;
5010 if(s
.indexOf('down') !== -1) {
5012 if(p
.state
[ t
? 'selected' : 'checked' ]) {
5013 for(i
= 0, j
= dpc
.length
; i
< j
; i
++) {
5014 m
[dpc
[i
]].state
[ t
? 'selected' : 'checked' ] = true;
5017 this._data
[ t
? 'core' : 'checkbox' ].selected
= this._data
[ t
? 'core' : 'checkbox' ].selected
.concat(dpc
);
5020 for(i
= 0, j
= dpc
.length
; i
< j
; i
++) {
5021 if(m
[dpc
[i
]].state
[ t
? 'selected' : 'checked' ]) {
5022 for(k
= 0, l
= m
[dpc
[i
]].children_d
.length
; k
< l
; k
++) {
5023 m
[m
[dpc
[i
]].children_d
[k
]].state
[ t
? 'selected' : 'checked' ] = true;
5025 this._data
[ t
? 'core' : 'checkbox' ].selected
= this._data
[ t
? 'core' : 'checkbox' ].selected
.concat(m
[dpc
[i
]].children_d
);
5031 if(s
.indexOf('up') !== -1) {
5033 for(i
= 0, j
= p
.children_d
.length
; i
< j
; i
++) {
5034 if(!m
[p
.children_d
[i
]].children
.length
) {
5035 chd
.push(m
[p
.children_d
[i
]].parent
);
5038 chd
= $.vakata
.array_unique(chd
);
5039 for(k
= 0, l
= chd
.length
; k
< l
; k
++) {
5041 while(p
&& p
.id
!== $.jstree
.root
) {
5043 for(i
= 0, j
= p
.children
.length
; i
< j
; i
++) {
5044 c
+= m
[p
.children
[i
]].state
[ t
? 'selected' : 'checked' ];
5047 p
.state
[ t
? 'selected' : 'checked' ] = true;
5048 this._data
[ t
? 'core' : 'checkbox' ].selected
.push(p
.id
);
5049 tmp
= this.get_node(p
, true);
5050 if(tmp
&& tmp
.length
) {
5051 tmp
.attr('aria-selected', true).children('.jstree-anchor').addClass( t
? 'jstree-clicked' : 'jstree-checked');
5057 p
= this.get_node(p
.parent
);
5062 this._data
[ t
? 'core' : 'checkbox' ].selected
= $.vakata
.array_unique(this._data
[ t
? 'core' : 'checkbox' ].selected
);
5064 .on(this.settings
.checkbox
.tie_selection
? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e
, data
) {
5067 m
= this._model
.data
,
5068 par
= this.get_node(obj
.parent
),
5069 i
, j
, c
, tmp
, s
= this.settings
.checkbox
.cascade
, t
= this.settings
.checkbox
.tie_selection
,
5070 sel
= {}, cur
= this._data
[ t
? 'core' : 'checkbox' ].selected
;
5072 for (i
= 0, j
= cur
.length
; i
< j
; i
++) {
5077 if(s
.indexOf('down') !== -1) {
5078 //this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d));
5079 var selectedIds
= this._cascade_new_checked_state(obj
.id
, true);
5080 obj
.children_d
.concat(obj
.id
).forEach(function(id
) {
5081 if (selectedIds
.indexOf(id
) > -1) {
5091 if(s
.indexOf('up') !== -1) {
5092 while(par
&& par
.id
!== $.jstree
.root
) {
5094 for(i
= 0, j
= par
.children
.length
; i
< j
; i
++) {
5095 c
+= m
[par
.children
[i
]].state
[ t
? 'selected' : 'checked' ];
5098 par
.state
[ t
? 'selected' : 'checked' ] = true;
5100 //this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id);
5101 tmp
= this.get_node(par
, true);
5102 if(tmp
&& tmp
.length
) {
5103 tmp
.attr('aria-selected', true).children('.jstree-anchor').addClass(t
? 'jstree-clicked' : 'jstree-checked');
5109 par
= this.get_node(par
.parent
);
5115 if (sel
.hasOwnProperty(i
)) {
5119 this._data
[ t
? 'core' : 'checkbox' ].selected
= cur
;
5121 .on(this.settings
.checkbox
.tie_selection
? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e
, data
) {
5122 var obj
= this.get_node($.jstree
.root
),
5123 m
= this._model
.data
,
5125 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
5126 tmp
= m
[obj
.children_d
[i
]];
5127 if(tmp
&& tmp
.original
&& tmp
.original
.state
&& tmp
.original
.state
.undetermined
) {
5128 tmp
.original
.state
.undetermined
= false;
5132 .on(this.settings
.checkbox
.tie_selection
? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e
, data
) {
5135 dom
= this.get_node(obj
, true),
5136 i
, j
, tmp
, s
= this.settings
.checkbox
.cascade
, t
= this.settings
.checkbox
.tie_selection
,
5137 cur
= this._data
[ t
? 'core' : 'checkbox' ].selected
, sel
= {},
5138 stillSelectedIds
= [],
5139 allIds
= obj
.children_d
.concat(obj
.id
);
5142 if(s
.indexOf('down') !== -1) {
5143 var selectedIds
= this._cascade_new_checked_state(obj
.id
, false);
5145 cur
= cur
.filter(function(id
) {
5146 return allIds
.indexOf(id
) === -1 || selectedIds
.indexOf(id
) > -1;
5150 // only apply up if cascade up is enabled and if this node is not selected
5151 // (if all child nodes are disabled and cascade_to_disabled === false then this node will till be selected).
5152 if(s
.indexOf('up') !== -1 && cur
.indexOf(obj
.id
) === -1) {
5153 for(i
= 0, j
= obj
.parents
.length
; i
< j
; i
++) {
5154 tmp
= this._model
.data
[obj
.parents
[i
]];
5155 tmp
.state
[ t
? 'selected' : 'checked' ] = false;
5156 if(tmp
&& tmp
.original
&& tmp
.original
.state
&& tmp
.original
.state
.undetermined
) {
5157 tmp
.original
.state
.undetermined
= false;
5159 tmp
= this.get_node(obj
.parents
[i
], true);
5160 if(tmp
&& tmp
.length
) {
5161 tmp
.attr('aria-selected', false).children('.jstree-anchor').removeClass(t
? 'jstree-clicked' : 'jstree-checked');
5165 cur
= cur
.filter(function(id
) {
5166 return obj
.parents
.indexOf(id
) === -1;
5170 this._data
[ t
? 'core' : 'checkbox' ].selected
= cur
;
5173 if(this.settings
.checkbox
.cascade
.indexOf('up') !== -1) {
5175 .on('delete_node.jstree', $.proxy(function (e
, data
) {
5176 // apply up (whole handler)
5177 var p
= this.get_node(data
.parent
),
5178 m
= this._model
.data
,
5179 i
, j
, c
, tmp
, t
= this.settings
.checkbox
.tie_selection
;
5180 while(p
&& p
.id
!== $.jstree
.root
&& !p
.state
[ t
? 'selected' : 'checked' ]) {
5182 for(i
= 0, j
= p
.children
.length
; i
< j
; i
++) {
5183 c
+= m
[p
.children
[i
]].state
[ t
? 'selected' : 'checked' ];
5185 if(j
> 0 && c
=== j
) {
5186 p
.state
[ t
? 'selected' : 'checked' ] = true;
5187 this._data
[ t
? 'core' : 'checkbox' ].selected
.push(p
.id
);
5188 tmp
= this.get_node(p
, true);
5189 if(tmp
&& tmp
.length
) {
5190 tmp
.attr('aria-selected', true).children('.jstree-anchor').addClass(t
? 'jstree-clicked' : 'jstree-checked');
5196 p
= this.get_node(p
.parent
);
5199 .on('move_node.jstree', $.proxy(function (e
, data
) {
5200 // apply up (whole handler)
5201 var is_multi
= data
.is_multi
,
5202 old_par
= data
.old_parent
,
5203 new_par
= this.get_node(data
.parent
),
5204 m
= this._model
.data
,
5205 p
, c
, i
, j
, tmp
, t
= this.settings
.checkbox
.tie_selection
;
5207 p
= this.get_node(old_par
);
5208 while(p
&& p
.id
!== $.jstree
.root
&& !p
.state
[ t
? 'selected' : 'checked' ]) {
5210 for(i
= 0, j
= p
.children
.length
; i
< j
; i
++) {
5211 c
+= m
[p
.children
[i
]].state
[ t
? 'selected' : 'checked' ];
5213 if(j
> 0 && c
=== j
) {
5214 p
.state
[ t
? 'selected' : 'checked' ] = true;
5215 this._data
[ t
? 'core' : 'checkbox' ].selected
.push(p
.id
);
5216 tmp
= this.get_node(p
, true);
5217 if(tmp
&& tmp
.length
) {
5218 tmp
.attr('aria-selected', true).children('.jstree-anchor').addClass(t
? 'jstree-clicked' : 'jstree-checked');
5224 p
= this.get_node(p
.parent
);
5228 while(p
&& p
.id
!== $.jstree
.root
) {
5230 for(i
= 0, j
= p
.children
.length
; i
< j
; i
++) {
5231 c
+= m
[p
.children
[i
]].state
[ t
? 'selected' : 'checked' ];
5234 if(!p
.state
[ t
? 'selected' : 'checked' ]) {
5235 p
.state
[ t
? 'selected' : 'checked' ] = true;
5236 this._data
[ t
? 'core' : 'checkbox' ].selected
.push(p
.id
);
5237 tmp
= this.get_node(p
, true);
5238 if(tmp
&& tmp
.length
) {
5239 tmp
.attr('aria-selected', true).children('.jstree-anchor').addClass(t
? 'jstree-clicked' : 'jstree-checked');
5244 if(p
.state
[ t
? 'selected' : 'checked' ]) {
5245 p
.state
[ t
? 'selected' : 'checked' ] = false;
5246 this._data
[ t
? 'core' : 'checkbox' ].selected
= $.vakata
.array_remove_item(this._data
[ t
? 'core' : 'checkbox' ].selected
, p
.id
);
5247 tmp
= this.get_node(p
, true);
5248 if(tmp
&& tmp
.length
) {
5249 tmp
.attr('aria-selected', false).children('.jstree-anchor').removeClass(t
? 'jstree-clicked' : 'jstree-checked');
5256 p
= this.get_node(p
.parent
);
5263 * set the undetermined state where and if necessary. Used internally.
5265 * @name _undetermined()
5268 this._undetermined = function () {
5269 if(this.element
=== null) { return; }
5270 var i
, j
, k
, l
, o
= {}, m
= this._model
.data
, t
= this.settings
.checkbox
.tie_selection
, s
= this._data
[ t
? 'core' : 'checkbox' ].selected
, p
= [], tt
= this;
5271 for(i
= 0, j
= s
.length
; i
< j
; i
++) {
5272 if(m
[s
[i
]] && m
[s
[i
]].parents
) {
5273 for(k
= 0, l
= m
[s
[i
]].parents
.length
; k
< l
; k
++) {
5274 if(o
[m
[s
[i
]].parents
[k
]] !== undefined) {
5277 if(m
[s
[i
]].parents
[k
] !== $.jstree
.root
) {
5278 o
[m
[s
[i
]].parents
[k
]] = true;
5279 p
.push(m
[s
[i
]].parents
[k
]);
5284 // attempt for server side undetermined state
5285 this.element
.find('.jstree-closed').not(':has(.jstree-children)')
5287 var tmp
= tt
.get_node(this), tmp2
;
5289 if(!tmp
) { return; }
5291 if(!tmp
.state
.loaded
) {
5292 if(tmp
.original
&& tmp
.original
.state
&& tmp
.original
.state
.undetermined
&& tmp
.original
.state
.undetermined
=== true) {
5293 if(o
[tmp
.id
] === undefined && tmp
.id
!== $.jstree
.root
) {
5297 for(k
= 0, l
= tmp
.parents
.length
; k
< l
; k
++) {
5298 if(o
[tmp
.parents
[k
]] === undefined && tmp
.parents
[k
] !== $.jstree
.root
) {
5299 o
[tmp
.parents
[k
]] = true;
5300 p
.push(tmp
.parents
[k
]);
5306 for(i
= 0, j
= tmp
.children_d
.length
; i
< j
; i
++) {
5307 tmp2
= m
[tmp
.children_d
[i
]];
5308 if(!tmp2
.state
.loaded
&& tmp2
.original
&& tmp2
.original
.state
&& tmp2
.original
.state
.undetermined
&& tmp2
.original
.state
.undetermined
=== true) {
5309 if(o
[tmp2
.id
] === undefined && tmp2
.id
!== $.jstree
.root
) {
5313 for(k
= 0, l
= tmp2
.parents
.length
; k
< l
; k
++) {
5314 if(o
[tmp2
.parents
[k
]] === undefined && tmp2
.parents
[k
] !== $.jstree
.root
) {
5315 o
[tmp2
.parents
[k
]] = true;
5316 p
.push(tmp2
.parents
[k
]);
5324 this.element
.find('.jstree-undetermined').removeClass('jstree-undetermined');
5325 for(i
= 0, j
= p
.length
; i
< j
; i
++) {
5326 if(!m
[p
[i
]].state
[ t
? 'selected' : 'checked' ]) {
5327 s
= this.get_node(p
[i
], true);
5329 s
.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined');
5334 this.redraw_node = function(obj
, deep
, is_callback
, force_render
) {
5335 obj
= parent
.redraw_node
.apply(this, arguments
);
5337 var i
, j
, tmp
= null, icon
= null;
5338 for(i
= 0, j
= obj
.childNodes
.length
; i
< j
; i
++) {
5339 if(obj
.childNodes
[i
] && obj
.childNodes
[i
].className
&& obj
.childNodes
[i
].className
.indexOf("jstree-anchor") !== -1) {
5340 tmp
= obj
.childNodes
[i
];
5345 if(!this.settings
.checkbox
.tie_selection
&& this._model
.data
[obj
.id
].state
.checked
) { tmp
.className
+= ' jstree-checked'; }
5346 icon
= _i
.cloneNode(false);
5347 if(this._model
.data
[obj
.id
].state
.checkbox_disabled
) { icon
.className
+= ' jstree-checkbox-disabled'; }
5348 tmp
.insertBefore(icon
, tmp
.childNodes
[0]);
5351 if(!is_callback
&& this.settings
.checkbox
.cascade
.indexOf('undetermined') !== -1) {
5352 if(this._data
.checkbox
.uto
) { clearTimeout(this._data
.checkbox
.uto
); }
5353 this._data
.checkbox
.uto
= setTimeout($.proxy(this._undetermined
, this), 50);
5358 * show the node checkbox icons
5359 * @name show_checkboxes()
5362 this.show_checkboxes = function () { this._data
.core
.themes
.checkboxes
= true; this.get_container_ul().removeClass("jstree-no-checkboxes"); };
5364 * hide the node checkbox icons
5365 * @name hide_checkboxes()
5368 this.hide_checkboxes = function () { this._data
.core
.themes
.checkboxes
= false; this.get_container_ul().addClass("jstree-no-checkboxes"); };
5370 * toggle the node icons
5371 * @name toggle_checkboxes()
5374 this.toggle_checkboxes = function () { if(this._data
.core
.themes
.checkboxes
) { this.hide_checkboxes(); } else { this.show_checkboxes(); } };
5376 * checks if a node is in an undetermined state
5377 * @name is_undetermined(obj)
5378 * @param {mixed} obj
5381 this.is_undetermined = function (obj
) {
5382 obj
= this.get_node(obj
);
5383 var s
= this.settings
.checkbox
.cascade
, i
, j
, t
= this.settings
.checkbox
.tie_selection
, d
= this._data
[ t
? 'core' : 'checkbox' ].selected
, m
= this._model
.data
;
5384 if(!obj
|| obj
.state
[ t
? 'selected' : 'checked' ] === true || s
.indexOf('undetermined') === -1 || (s
.indexOf('down') === -1 && s
.indexOf('up') === -1)) {
5387 if(!obj
.state
.loaded
&& obj
.original
.state
.undetermined
=== true) {
5390 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
5391 if($.inArray(obj
.children_d
[i
], d
) !== -1 || (!m
[obj
.children_d
[i
]].state
.loaded
&& m
[obj
.children_d
[i
]].original
.state
.undetermined
)) {
5398 * disable a node's checkbox
5399 * @name disable_checkbox(obj)
5400 * @param {mixed} obj an array can be used too
5401 * @trigger disable_checkbox.jstree
5404 this.disable_checkbox = function (obj
) {
5406 if($.isArray(obj
)) {
5408 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
5409 this.disable_checkbox(obj
[t1
]);
5413 obj
= this.get_node(obj
);
5414 if(!obj
|| obj
.id
=== $.jstree
.root
) {
5417 dom
= this.get_node(obj
, true);
5418 if(!obj
.state
.checkbox_disabled
) {
5419 obj
.state
.checkbox_disabled
= true;
5420 if(dom
&& dom
.length
) {
5421 dom
.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-checkbox-disabled');
5424 * triggered when an node's checkbox is disabled
5426 * @name disable_checkbox.jstree
5427 * @param {Object} node
5430 this.trigger('disable_checkbox', { 'node' : obj
});
5434 * enable a node's checkbox
5435 * @name disable_checkbox(obj)
5436 * @param {mixed} obj an array can be used too
5437 * @trigger enable_checkbox.jstree
5440 this.enable_checkbox = function (obj
) {
5442 if($.isArray(obj
)) {
5444 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
5445 this.enable_checkbox(obj
[t1
]);
5449 obj
= this.get_node(obj
);
5450 if(!obj
|| obj
.id
=== $.jstree
.root
) {
5453 dom
= this.get_node(obj
, true);
5454 if(obj
.state
.checkbox_disabled
) {
5455 obj
.state
.checkbox_disabled
= false;
5456 if(dom
&& dom
.length
) {
5457 dom
.children('.jstree-anchor').children('.jstree-checkbox').removeClass('jstree-checkbox-disabled');
5460 * triggered when an node's checkbox is enabled
5462 * @name enable_checkbox.jstree
5463 * @param {Object} node
5466 this.trigger('enable_checkbox', { 'node' : obj
});
5470 this.activate_node = function (obj
, e
) {
5471 if($(e
.target
).hasClass('jstree-checkbox-disabled')) {
5474 if(this.settings
.checkbox
.tie_selection
&& (this.settings
.checkbox
.whole_node
|| $(e
.target
).hasClass('jstree-checkbox'))) {
5477 if(this.settings
.checkbox
.tie_selection
|| (!this.settings
.checkbox
.whole_node
&& !$(e
.target
).hasClass('jstree-checkbox'))) {
5478 return parent
.activate_node
.call(this, obj
, e
);
5480 if(this.is_disabled(obj
)) {
5483 if(this.is_checked(obj
)) {
5484 this.uncheck_node(obj
, e
);
5487 this.check_node(obj
, e
);
5489 this.trigger('activate_node', { 'node' : this.get_node(obj
) });
5493 * Unchecks a node and all its descendants. This function does NOT affect hidden and disabled nodes (or their descendants).
5494 * However if these unaffected nodes are already selected their ids will be included in the returned array.
5496 * @param checkedState
5497 * @returns {Array} Array of all node id's (in this tree branch) that are checked.
5499 this._cascade_new_checked_state = function(id
, checkedState
) {
5501 var t
= this.settings
.checkbox
.tie_selection
;
5502 var node
= this._model
.data
[id
];
5503 var selectedNodeIds
= [];
5504 var selectedChildrenIds
= [];
5507 (this.settings
.checkbox
.cascade_to_disabled
|| !node
.state
.disabled
) &&
5508 (this.settings
.checkbox
.cascade_to_hidden
|| !node
.state
.hidden
)
5510 //First try and check/uncheck the children
5511 if (node
.children
) {
5512 node
.children
.forEach(function(childId
) {
5513 var selectedChildIds
= self
._cascade_new_checked_state(childId
, checkedState
);
5514 selectedNodeIds
= selectedNodeIds
.concat(selectedChildIds
);
5515 if (selectedChildIds
.indexOf(childId
) > -1) {
5516 selectedChildrenIds
.push(childId
);
5521 var dom
= self
.get_node(node
, true);
5523 //A node's state is undetermined if some but not all of it's children are checked/selected .
5524 var undetermined
= selectedChildrenIds
.length
> 0 && selectedChildrenIds
.length
< node
.children
.length
;
5526 if(node
.original
&& node
.original
.state
&& node
.original
.state
.undetermined
) {
5527 node
.original
.state
.undetermined
= undetermined
;
5530 //If a node is undetermined then remove selected class
5532 node
.state
[ t
? 'selected' : 'checked' ] = false;
5533 dom
.attr('aria-selected', false).children('.jstree-anchor').removeClass(t
? 'jstree-clicked' : 'jstree-checked');
5535 //Otherwise, if the checkedState === true (i.e. the node is being checked now) and all of the node's children are checked (if it has any children),
5536 //check the node and style it correctly.
5537 else if (checkedState
&& selectedChildrenIds
.length
=== node
.children
.length
) {
5538 node
.state
[ t
? 'selected' : 'checked' ] = checkedState
;
5539 selectedNodeIds
.push(node
.id
);
5541 dom
.attr('aria-selected', true).children('.jstree-anchor').addClass(t
? 'jstree-clicked' : 'jstree-checked');
5544 node
.state
[ t
? 'selected' : 'checked' ] = false;
5545 dom
.attr('aria-selected', false).children('.jstree-anchor').removeClass(t
? 'jstree-clicked' : 'jstree-checked');
5549 var selectedChildIds
= this.get_checked_descendants(id
);
5551 if (node
.state
[ t
? 'selected' : 'checked' ]) {
5552 selectedChildIds
.push(node
.id
);
5555 selectedNodeIds
= selectedNodeIds
.concat(selectedChildIds
);
5558 return selectedNodeIds
;
5562 * Gets ids of nodes selected in branch (of tree) specified by id (does not include the node specified by id)
5565 this.get_checked_descendants = function(id
) {
5567 var t
= self
.settings
.checkbox
.tie_selection
;
5568 var node
= self
._model
.data
[id
];
5570 return node
.children_d
.filter(function(_id
) {
5571 return self
._model
.data
[_id
].state
[ t
? 'selected' : 'checked' ];
5576 * check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally)
5577 * @name check_node(obj)
5578 * @param {mixed} obj an array can be used to check multiple nodes
5579 * @trigger check_node.jstree
5582 this.check_node = function (obj
, e
) {
5583 if(this.settings
.checkbox
.tie_selection
) { return this.select_node(obj
, false, true, e
); }
5584 var dom
, t1
, t2
, th
;
5585 if($.isArray(obj
)) {
5587 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
5588 this.check_node(obj
[t1
], e
);
5592 obj
= this.get_node(obj
);
5593 if(!obj
|| obj
.id
=== $.jstree
.root
) {
5596 dom
= this.get_node(obj
, true);
5597 if(!obj
.state
.checked
) {
5598 obj
.state
.checked
= true;
5599 this._data
.checkbox
.selected
.push(obj
.id
);
5600 if(dom
&& dom
.length
) {
5601 dom
.children('.jstree-anchor').addClass('jstree-checked');
5604 * triggered when an node is checked (only if tie_selection in checkbox settings is false)
5606 * @name check_node.jstree
5607 * @param {Object} node
5608 * @param {Array} selected the current selection
5609 * @param {Object} event the event (if any) that triggered this check_node
5612 this.trigger('check_node', { 'node' : obj
, 'selected' : this._data
.checkbox
.selected
, 'event' : e
});
5616 * uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally)
5617 * @name uncheck_node(obj)
5618 * @param {mixed} obj an array can be used to uncheck multiple nodes
5619 * @trigger uncheck_node.jstree
5622 this.uncheck_node = function (obj
, e
) {
5623 if(this.settings
.checkbox
.tie_selection
) { return this.deselect_node(obj
, false, e
); }
5625 if($.isArray(obj
)) {
5627 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
5628 this.uncheck_node(obj
[t1
], e
);
5632 obj
= this.get_node(obj
);
5633 if(!obj
|| obj
.id
=== $.jstree
.root
) {
5636 dom
= this.get_node(obj
, true);
5637 if(obj
.state
.checked
) {
5638 obj
.state
.checked
= false;
5639 this._data
.checkbox
.selected
= $.vakata
.array_remove_item(this._data
.checkbox
.selected
, obj
.id
);
5641 dom
.children('.jstree-anchor').removeClass('jstree-checked');
5644 * triggered when an node is unchecked (only if tie_selection in checkbox settings is false)
5646 * @name uncheck_node.jstree
5647 * @param {Object} node
5648 * @param {Array} selected the current selection
5649 * @param {Object} event the event (if any) that triggered this uncheck_node
5652 this.trigger('uncheck_node', { 'node' : obj
, 'selected' : this._data
.checkbox
.selected
, 'event' : e
});
5657 * checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally)
5659 * @trigger check_all.jstree, changed.jstree
5662 this.check_all = function () {
5663 if(this.settings
.checkbox
.tie_selection
) { return this.select_all(); }
5664 var tmp
= this._data
.checkbox
.selected
.concat([]), i
, j
;
5665 this._data
.checkbox
.selected
= this._model
.data
[$.jstree
.root
].children_d
.concat();
5666 for(i
= 0, j
= this._data
.checkbox
.selected
.length
; i
< j
; i
++) {
5667 if(this._model
.data
[this._data
.checkbox
.selected
[i
]]) {
5668 this._model
.data
[this._data
.checkbox
.selected
[i
]].state
.checked
= true;
5673 * triggered when all nodes are checked (only if tie_selection in checkbox settings is false)
5675 * @name check_all.jstree
5676 * @param {Array} selected the current selection
5679 this.trigger('check_all', { 'selected' : this._data
.checkbox
.selected
});
5682 * uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally)
5683 * @name uncheck_all()
5684 * @trigger uncheck_all.jstree
5687 this.uncheck_all = function () {
5688 if(this.settings
.checkbox
.tie_selection
) { return this.deselect_all(); }
5689 var tmp
= this._data
.checkbox
.selected
.concat([]), i
, j
;
5690 for(i
= 0, j
= this._data
.checkbox
.selected
.length
; i
< j
; i
++) {
5691 if(this._model
.data
[this._data
.checkbox
.selected
[i
]]) {
5692 this._model
.data
[this._data
.checkbox
.selected
[i
]].state
.checked
= false;
5695 this._data
.checkbox
.selected
= [];
5696 this.element
.find('.jstree-checked').removeClass('jstree-checked');
5698 * triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false)
5700 * @name uncheck_all.jstree
5701 * @param {Object} node the previous selection
5702 * @param {Array} selected the current selection
5705 this.trigger('uncheck_all', { 'selected' : this._data
.checkbox
.selected
, 'node' : tmp
});
5708 * checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected)
5709 * @name is_checked(obj)
5710 * @param {mixed} obj
5714 this.is_checked = function (obj
) {
5715 if(this.settings
.checkbox
.tie_selection
) { return this.is_selected(obj
); }
5716 obj
= this.get_node(obj
);
5717 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
5718 return obj
.state
.checked
;
5721 * get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected)
5722 * @name get_checked([full])
5723 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
5727 this.get_checked = function (full
) {
5728 if(this.settings
.checkbox
.tie_selection
) { return this.get_selected(full
); }
5729 return full
? $.map(this._data
.checkbox
.selected
, $.proxy(function (i
) { return this.get_node(i
); }, this)) : this._data
.checkbox
.selected
;
5732 * get an array of all top level checked nodes (ignoring children of checked nodes) (if tie_selection is on in the settings this function will return the same as get_top_selected)
5733 * @name get_top_checked([full])
5734 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
5738 this.get_top_checked = function (full
) {
5739 if(this.settings
.checkbox
.tie_selection
) { return this.get_top_selected(full
); }
5740 var tmp
= this.get_checked(true),
5741 obj
= {}, i
, j
, k
, l
;
5742 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
5743 obj
[tmp
[i
].id
] = tmp
[i
];
5745 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
5746 for(k
= 0, l
= tmp
[i
].children_d
.length
; k
< l
; k
++) {
5747 if(obj
[tmp
[i
].children_d
[k
]]) {
5748 delete obj
[tmp
[i
].children_d
[k
]];
5754 if(obj
.hasOwnProperty(i
)) {
5758 return full
? $.map(tmp
, $.proxy(function (i
) { return this.get_node(i
); }, this)) : tmp
;
5761 * get an array of all bottom level checked nodes (ignoring selected parents) (if tie_selection is on in the settings this function will return the same as get_bottom_selected)
5762 * @name get_bottom_checked([full])
5763 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
5767 this.get_bottom_checked = function (full
) {
5768 if(this.settings
.checkbox
.tie_selection
) { return this.get_bottom_selected(full
); }
5769 var tmp
= this.get_checked(true),
5771 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
5772 if(!tmp
[i
].children
.length
) {
5773 obj
.push(tmp
[i
].id
);
5776 return full
? $.map(obj
, $.proxy(function (i
) { return this.get_node(i
); }, this)) : obj
;
5778 this.load_node = function (obj
, callback
) {
5779 var k
, l
, i
, j
, c
, tmp
;
5780 if(!$.isArray(obj
) && !this.settings
.checkbox
.tie_selection
) {
5781 tmp
= this.get_node(obj
);
5782 if(tmp
&& tmp
.state
.loaded
) {
5783 for(k
= 0, l
= tmp
.children_d
.length
; k
< l
; k
++) {
5784 if(this._model
.data
[tmp
.children_d
[k
]].state
.checked
) {
5786 this._data
.checkbox
.selected
= $.vakata
.array_remove_item(this._data
.checkbox
.selected
, tmp
.children_d
[k
]);
5791 return parent
.load_node
.apply(this, arguments
);
5793 this.get_state = function () {
5794 var state
= parent
.get_state
.apply(this, arguments
);
5795 if(this.settings
.checkbox
.tie_selection
) { return state
; }
5796 state
.checkbox
= this._data
.checkbox
.selected
.slice();
5799 this.set_state = function (state
, callback
) {
5800 var res
= parent
.set_state
.apply(this, arguments
);
5801 if(res
&& state
.checkbox
) {
5802 if(!this.settings
.checkbox
.tie_selection
) {
5805 $.each(state
.checkbox
, function (i
, v
) {
5806 _this
.check_node(v
);
5809 delete state
.checkbox
;
5810 this.set_state(state
, callback
);
5815 this.refresh = function (skip_loading
, forget_state
) {
5816 if(!this.settings
.checkbox
.tie_selection
) {
5817 this._data
.checkbox
.selected
= [];
5819 return parent
.refresh
.apply(this, arguments
);
5823 // include the checkbox plugin by default
5824 // $.jstree.defaults.plugins.push("checkbox");
5828 * ### Conditionalselect plugin
5830 * This plugin allows defining a callback to allow or deny node selection by user input (activate node method).
5834 * a callback (function) which is invoked in the instance's scope and receives two arguments - the node and the event that triggered the `activate_node` call. Returning false prevents working with the node, returning true allows invoking activate_node. Defaults to returning `true`.
5835 * @name $.jstree.defaults.checkbox.visible
5838 $.jstree
.defaults
.conditionalselect = function () { return true; };
5839 $.jstree
.plugins
.conditionalselect = function (options
, parent
) {
5841 this.activate_node = function (obj
, e
) {
5842 if(this.settings
.conditionalselect
.call(this, this.get_node(obj
), e
)) {
5843 parent
.activate_node
.call(this, obj
, e
);
5850 * ### Contextmenu plugin
5852 * Shows a context menu when a node is right-clicked.
5856 * stores all defaults for the contextmenu plugin
5857 * @name $.jstree.defaults.contextmenu
5858 * @plugin contextmenu
5860 $.jstree
.defaults
.contextmenu
= {
5862 * a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`.
5863 * @name $.jstree.defaults.contextmenu.select_node
5864 * @plugin contextmenu
5868 * a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used.
5869 * @name $.jstree.defaults.contextmenu.show_at_node
5870 * @plugin contextmenu
5872 show_at_node
: true,
5874 * an object of actions, or a function that accepts a node and a callback function and calls the callback function with an object of actions available for that node (you can also return the items too).
5876 * Each action consists of a key (a unique name) and a value which is an object with the following properties (only label and action are required). Once a menu item is activated the `action` function will be invoked with an object containing the following keys: item - the contextmenu item definition as seen below, reference - the DOM node that was used (the tree node), element - the contextmenu DOM element, position - an object with x/y properties indicating the position of the menu.
5878 * * `separator_before` - a boolean indicating if there should be a separator before this item
5879 * * `separator_after` - a boolean indicating if there should be a separator after this item
5880 * * `_disabled` - a boolean indicating if this action should be disabled
5881 * * `label` - a string - the name of the action (could be a function returning a string)
5882 * * `title` - a string - an optional tooltip for the item
5883 * * `action` - a function to be executed if this item is chosen, the function will receive
5884 * * `icon` - a string, can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class
5885 * * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2)
5886 * * `shortcut_label` - shortcut label (like for example `F2` for rename)
5887 * * `submenu` - an object with the same structure as $.jstree.defaults.contextmenu.items which can be used to create a submenu - each key will be rendered as a separate option in a submenu that will appear once the current item is hovered
5889 * @name $.jstree.defaults.contextmenu.items
5890 * @plugin contextmenu
5892 items : function (o
, cb
) { // Could be an object directly
5895 "separator_before" : false,
5896 "separator_after" : true,
5897 "_disabled" : false, //(this.check("create_node", data.reference, {}, "last")),
5899 "action" : function (data
) {
5900 var inst
= $.jstree
.reference(data
.reference
),
5901 obj
= inst
.get_node(data
.reference
);
5902 inst
.create_node(obj
, {}, "last", function (new_node
) {
5904 inst
.edit(new_node
);
5906 setTimeout(function () { inst
.edit(new_node
); },0);
5912 "separator_before" : false,
5913 "separator_after" : false,
5914 "_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")),
5918 "shortcut_label" : 'F2',
5919 "icon" : "glyphicon glyphicon-leaf",
5921 "action" : function (data
) {
5922 var inst
= $.jstree
.reference(data
.reference
),
5923 obj
= inst
.get_node(data
.reference
);
5928 "separator_before" : false,
5930 "separator_after" : false,
5931 "_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")),
5933 "action" : function (data
) {
5934 var inst
= $.jstree
.reference(data
.reference
),
5935 obj
= inst
.get_node(data
.reference
);
5936 if(inst
.is_selected(obj
)) {
5937 inst
.delete_node(inst
.get_selected());
5940 inst
.delete_node(obj
);
5945 "separator_before" : true,
5947 "separator_after" : false,
5952 "separator_before" : false,
5953 "separator_after" : false,
5955 "action" : function (data
) {
5956 var inst
= $.jstree
.reference(data
.reference
),
5957 obj
= inst
.get_node(data
.reference
);
5958 if(inst
.is_selected(obj
)) {
5959 inst
.cut(inst
.get_top_selected());
5967 "separator_before" : false,
5969 "separator_after" : false,
5971 "action" : function (data
) {
5972 var inst
= $.jstree
.reference(data
.reference
),
5973 obj
= inst
.get_node(data
.reference
);
5974 if(inst
.is_selected(obj
)) {
5975 inst
.copy(inst
.get_top_selected());
5983 "separator_before" : false,
5985 "_disabled" : function (data
) {
5986 return !$.jstree
.reference(data
.reference
).can_paste();
5988 "separator_after" : false,
5990 "action" : function (data
) {
5991 var inst
= $.jstree
.reference(data
.reference
),
5992 obj
= inst
.get_node(data
.reference
);
6002 $.jstree
.plugins
.contextmenu = function (options
, parent
) {
6003 this.bind = function () {
6004 parent
.bind
.call(this);
6006 var last_ts
= 0, cto
= null, ex
, ey
;
6008 .on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
6009 this.get_container_ul().addClass('jstree-contextmenu');
6011 .on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e
, data
) {
6012 if (e
.target
.tagName
.toLowerCase() === 'input') {
6016 last_ts
= e
.ctrlKey
? +new Date() : 0;
6018 last_ts
= (+new Date()) + 10000;
6023 if(!this.is_loading(e
.currentTarget
)) {
6024 this.show_contextmenu(e
.currentTarget
, e
.pageX
, e
.pageY
, e
);
6027 .on("click.jstree", ".jstree-anchor", $.proxy(function (e
) {
6028 if(this._data
.contextmenu
.visible
&& (!last_ts
|| (+new Date()) - last_ts
> 250)) { // work around safari & macOS ctrl+click
6029 $.vakata
.context
.hide();
6033 .on("touchstart.jstree", ".jstree-anchor", function (e
) {
6034 if(!e
.originalEvent
|| !e
.originalEvent
.changedTouches
|| !e
.originalEvent
.changedTouches
[0]) {
6037 ex
= e
.originalEvent
.changedTouches
[0].clientX
;
6038 ey
= e
.originalEvent
.changedTouches
[0].clientY
;
6039 cto
= setTimeout(function () {
6040 $(e
.currentTarget
).trigger('contextmenu', true);
6043 .on('touchmove.vakata.jstree', function (e
) {
6044 if(cto
&& e
.originalEvent
&& e
.originalEvent
.changedTouches
&& e
.originalEvent
.changedTouches
[0] && (Math
.abs(ex
- e
.originalEvent
.changedTouches
[0].clientX
) > 50 || Math
.abs(ey
- e
.originalEvent
.changedTouches
[0].clientY
) > 50)) {
6048 .on('touchend.vakata.jstree', function (e
) {
6055 if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) {
6056 var el = null, tm = null;
6058 .on("touchstart", ".jstree-anchor", function (e) {
6059 el = e.currentTarget;
6061 $(document).one("touchend", function (e) {
6062 e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset);
6063 e.currentTarget = e.target;
6064 tm = ((+(new Date())) - tm);
6065 if(e.target === el && tm > 600 && tm < 1000) {
6067 $(el).trigger('contextmenu', e);
6075 $(document
).on("context_hide.vakata.jstree", $.proxy(function (e
, data
) {
6076 this._data
.contextmenu
.visible
= false;
6077 $(data
.reference
).removeClass('jstree-context');
6080 this.teardown = function () {
6081 if(this._data
.contextmenu
.visible
) {
6082 $.vakata
.context
.hide();
6084 parent
.teardown
.call(this);
6088 * prepare and show the context menu for a node
6089 * @name show_contextmenu(obj [, x, y])
6090 * @param {mixed} obj the node
6091 * @param {Number} x the x-coordinate relative to the document to show the menu at
6092 * @param {Number} y the y-coordinate relative to the document to show the menu at
6093 * @param {Object} e the event if available that triggered the contextmenu
6094 * @plugin contextmenu
6095 * @trigger show_contextmenu.jstree
6097 this.show_contextmenu = function (obj
, x
, y
, e
) {
6098 obj
= this.get_node(obj
);
6099 if(!obj
|| obj
.id
=== $.jstree
.root
) { return false; }
6100 var s
= this.settings
.contextmenu
,
6101 d
= this.get_node(obj
, true),
6102 a
= d
.children(".jstree-anchor"),
6105 if(s
.show_at_node
|| x
=== undefined || y
=== undefined) {
6108 y
= o
.top
+ this._data
.core
.li_height
;
6110 if(this.settings
.contextmenu
.select_node
&& !this.is_selected(obj
)) {
6111 this.activate_node(obj
, e
);
6115 if($.isFunction(i
)) {
6116 i
= i
.call(this, obj
, $.proxy(function (i
) {
6117 this._show_contextmenu(obj
, x
, y
, i
);
6120 if($.isPlainObject(i
)) {
6121 this._show_contextmenu(obj
, x
, y
, i
);
6125 * show the prepared context menu for a node
6126 * @name _show_contextmenu(obj, x, y, i)
6127 * @param {mixed} obj the node
6128 * @param {Number} x the x-coordinate relative to the document to show the menu at
6129 * @param {Number} y the y-coordinate relative to the document to show the menu at
6130 * @param {Number} i the object of items to show
6131 * @plugin contextmenu
6132 * @trigger show_contextmenu.jstree
6135 this._show_contextmenu = function (obj
, x
, y
, i
) {
6136 var d
= this.get_node(obj
, true),
6137 a
= d
.children(".jstree-anchor");
6138 $(document
).one("context_show.vakata.jstree", $.proxy(function (e
, data
) {
6139 var cls
= 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu';
6140 $(data
.element
).addClass(cls
);
6141 a
.addClass('jstree-context');
6143 this._data
.contextmenu
.visible
= true;
6144 $.vakata
.context
.show(a
, { 'x' : x
, 'y' : y
}, i
);
6146 * triggered when the contextmenu is shown for a node
6148 * @name show_contextmenu.jstree
6149 * @param {Object} node the node
6150 * @param {Number} x the x-coordinate of the menu relative to the document
6151 * @param {Number} y the y-coordinate of the menu relative to the document
6152 * @plugin contextmenu
6154 this.trigger('show_contextmenu', { "node" : obj
, "x" : x
, "y" : y
});
6158 // contextmenu helper
6160 var right_to_left
= false,
6171 $.vakata
.context
= {
6173 hide_onmouseleave
: 0,
6176 _trigger : function (event_name
) {
6177 $(document
).triggerHandler("context_" + event_name
+ ".vakata", {
6178 "reference" : vakata_context
.reference
,
6179 "element" : vakata_context
.element
,
6181 "x" : vakata_context
.position_x
,
6182 "y" : vakata_context
.position_y
6186 _execute : function (i
) {
6187 i
= vakata_context
.items
[i
];
6188 return i
&& (!i
._disabled
|| ($.isFunction(i
._disabled
) && !i
._disabled({ "item" : i
, "reference" : vakata_context
.reference
, "element" : vakata_context
.element
}))) && i
.action
? i
.action
.call(null, {
6190 "reference" : vakata_context
.reference
,
6191 "element" : vakata_context
.element
,
6193 "x" : vakata_context
.position_x
,
6194 "y" : vakata_context
.position_y
6198 _parse : function (o
, is_callback
) {
6199 if(!o
) { return false; }
6201 vakata_context
.html
= "";
6202 vakata_context
.items
= [];
6208 if(is_callback
) { str
+= "<"+"ul>"; }
6209 $.each(o
, function (i
, val
) {
6210 if(!val
) { return true; }
6211 vakata_context
.items
.push(val
);
6212 if(!sep
&& val
.separator_before
) {
6213 str
+= "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata
.context
.settings
.icons
? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
6216 str
+= "<"+"li class='" + (val
._class
|| "") + (val
._disabled
=== true || ($.isFunction(val
._disabled
) && val
._disabled({ "item" : val
, "reference" : vakata_context
.reference
, "element" : vakata_context
.element
})) ? " vakata-contextmenu-disabled " : "") + "' "+(val
.shortcut
?" data-shortcut='"+val
.shortcut
+"' ":'')+">";
6217 str
+= "<"+"a href='#' rel='" + (vakata_context
.items
.length
- 1) + "' " + (val
.title
? "title='" + val
.title
+ "'" : "") + ">";
6218 if($.vakata
.context
.settings
.icons
) {
6221 if(val
.icon
.indexOf("/") !== -1 || val
.icon
.indexOf(".") !== -1) { str
+= " style='background:url(\"" + val
.icon
+ "\") center center no-repeat' "; }
6222 else { str
+= " class='" + val
.icon
+ "' "; }
6224 str
+= "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>";
6226 str
+= ($.isFunction(val
.label
) ? val
.label({ "item" : i
, "reference" : vakata_context
.reference
, "element" : vakata_context
.element
}) : val
.label
) + (val
.shortcut
?' <span class="vakata-contextmenu-shortcut vakata-contextmenu-shortcut-'+val
.shortcut
+'">'+ (val
.shortcut_label
|| '') +'</span>':'') + "<"+"/a>";
6228 tmp
= $.vakata
.context
._parse(val
.submenu
, true);
6229 if(tmp
) { str
+= tmp
; }
6232 if(val
.separator_after
) {
6233 str
+= "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata
.context
.settings
.icons
? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
6237 str
= str
.replace(/<li class\='vakata-context-separator'\><\/li\>$/,"");
6238 if(is_callback
) { str
+= "</ul>"; }
6240 * triggered on the document when the contextmenu is parsed (HTML is built)
6242 * @plugin contextmenu
6243 * @name context_parse.vakata
6244 * @param {jQuery} reference the element that was right clicked
6245 * @param {jQuery} element the DOM element of the menu itself
6246 * @param {Object} position the x & y coordinates of the menu
6248 if(!is_callback
) { vakata_context
.html
= str
; $.vakata
.context
._trigger("parse"); }
6249 return str
.length
> 10 ? str
: false;
6251 _show_submenu : function (o
) {
6253 if(!o
.length
|| !o
.children("ul").length
) { return; }
6254 var e
= o
.children("ul"),
6255 xl
= o
.offset().left
,
6256 x
= xl
+ o
.outerWidth(),
6260 dw
= $(window
).width() + $(window
).scrollLeft(),
6261 dh
= $(window
).height() + $(window
).scrollTop();
6262 // може да се спести е една проверка - дали няма някой от класовете вече нагоре
6264 o
[x
- (w
+ 10 + o
.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left");
6267 o
[x
+ w
> dw
&& xl
> dw
- x
? "addClass" : "removeClass"]("vakata-context-right");
6269 if(y
+ h
+ 10 > dh
) {
6270 e
.css("bottom","-1px");
6273 //if does not fit - stick it to the side
6274 if (o
.hasClass('vakata-context-right')) {
6276 e
.css("margin-right", xl
- w
);
6280 e
.css("margin-left", dw
- x
- w
);
6286 show : function (reference
, position
, data
) {
6287 var o
, e
, x
, y
, w
, h
, dw
, dh
, cond
= true;
6288 if(vakata_context
.element
&& vakata_context
.element
.length
) {
6289 vakata_context
.element
.width('');
6292 case (!position
&& !reference
):
6294 case (!!position
&& !!reference
):
6295 vakata_context
.reference
= reference
;
6296 vakata_context
.position_x
= position
.x
;
6297 vakata_context
.position_y
= position
.y
;
6299 case (!position
&& !!reference
):
6300 vakata_context
.reference
= reference
;
6301 o
= reference
.offset();
6302 vakata_context
.position_x
= o
.left
+ reference
.outerHeight();
6303 vakata_context
.position_y
= o
.top
;
6305 case (!!position
&& !reference
):
6306 vakata_context
.position_x
= position
.x
;
6307 vakata_context
.position_y
= position
.y
;
6310 if(!!reference
&& !data
&& $(reference
).data('vakata_contextmenu')) {
6311 data
= $(reference
).data('vakata_contextmenu');
6313 if($.vakata
.context
._parse(data
)) {
6314 vakata_context
.element
.html(vakata_context
.html
);
6316 if(vakata_context
.items
.length
) {
6317 vakata_context
.element
.appendTo("body");
6318 e
= vakata_context
.element
;
6319 x
= vakata_context
.position_x
;
6320 y
= vakata_context
.position_y
;
6323 dw
= $(window
).width() + $(window
).scrollLeft();
6324 dh
= $(window
).height() + $(window
).scrollTop();
6326 x
-= (e
.outerWidth() - $(reference
).outerWidth());
6327 if(x
< $(window
).scrollLeft() + 20) {
6328 x
= $(window
).scrollLeft() + 20;
6331 if(x
+ w
+ 20 > dw
) {
6334 if(y
+ h
+ 20 > dh
) {
6338 vakata_context
.element
6339 .css({ "left" : x
, "top" : y
})
6341 .find('a').first().focus().parent().addClass("vakata-context-hover");
6342 vakata_context
.is_visible
= true;
6344 * triggered on the document when the contextmenu is shown
6346 * @plugin contextmenu
6347 * @name context_show.vakata
6348 * @param {jQuery} reference the element that was right clicked
6349 * @param {jQuery} element the DOM element of the menu itself
6350 * @param {Object} position the x & y coordinates of the menu
6352 $.vakata
.context
._trigger("show");
6355 hide : function () {
6356 if(vakata_context
.is_visible
) {
6357 vakata_context
.element
.hide().find("ul").hide().end().find(':focus').blur().end().detach();
6358 vakata_context
.is_visible
= false;
6360 * triggered on the document when the contextmenu is hidden
6362 * @plugin contextmenu
6363 * @name context_hide.vakata
6364 * @param {jQuery} reference the element that was right clicked
6365 * @param {jQuery} element the DOM element of the menu itself
6366 * @param {Object} position the x & y coordinates of the menu
6368 $.vakata
.context
._trigger("hide");
6373 right_to_left
= $("body").css("direction") === "rtl";
6376 vakata_context
.element
= $("<ul class='vakata-context'></ul>");
6377 vakata_context
.element
6378 .on("mouseenter", "li", function (e
) {
6379 e
.stopImmediatePropagation();
6381 if($.contains(this, e
.relatedTarget
)) {
6382 // премахнато заради delegate mouseleave по-долу
6383 // $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
6387 if(to
) { clearTimeout(to
); }
6388 vakata_context
.element
.find(".vakata-context-hover").removeClass("vakata-context-hover").end();
6391 .siblings().find("ul").hide().end().end()
6392 .parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover");
6393 $.vakata
.context
._show_submenu(this);
6395 // тестово - дали не натоварва?
6396 .on("mouseleave", "li", function (e
) {
6397 if($.contains(this, e
.relatedTarget
)) { return; }
6398 $(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover");
6400 .on("mouseleave", function (e
) {
6401 $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
6402 if($.vakata
.context
.settings
.hide_onmouseleave
) {
6405 return function () { $.vakata
.context
.hide(); };
6406 }(this)), $.vakata
.context
.settings
.hide_onmouseleave
);
6409 .on("click", "a", function (e
) {
6412 //.on("mouseup", "a", function (e) {
6413 if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata
.context
._execute($(this).attr("rel")) !== false) {
6414 $.vakata
.context
.hide();
6417 .on('keydown', 'a', function (e
) {
6424 $(e
.currentTarget
).trigger(e
);
6427 if(vakata_context
.is_visible
) {
6428 vakata_context
.element
.find(".vakata-context-hover").last().closest("li").first().find("ul").hide().find(".vakata-context-hover").removeClass("vakata-context-hover").end().end().children('a').focus();
6429 e
.stopImmediatePropagation();
6434 if(vakata_context
.is_visible
) {
6435 o
= vakata_context
.element
.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first();
6436 if(!o
.length
) { o
= vakata_context
.element
.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); }
6437 o
.addClass("vakata-context-hover").children('a').focus();
6438 e
.stopImmediatePropagation();
6443 if(vakata_context
.is_visible
) {
6444 vakata_context
.element
.find(".vakata-context-hover").last().children("ul").show().children("li:not(.vakata-context-separator)").removeClass("vakata-context-hover").first().addClass("vakata-context-hover").children('a').focus();
6445 e
.stopImmediatePropagation();
6450 if(vakata_context
.is_visible
) {
6451 o
= vakata_context
.element
.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first();
6452 if(!o
.length
) { o
= vakata_context
.element
.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); }
6453 o
.addClass("vakata-context-hover").children('a').focus();
6454 e
.stopImmediatePropagation();
6459 $.vakata
.context
.hide();
6463 //console.log(e.which);
6467 .on('keydown', function (e
) {
6469 var a
= vakata_context
.element
.find('.vakata-contextmenu-shortcut-' + e
.which
).parent();
6470 if(a
.parent().not('.vakata-context-disabled')) {
6476 .on("mousedown.vakata.jstree", function (e
) {
6477 if(vakata_context
.is_visible
&& vakata_context
.element
[0] !== e
.target
&& !$.contains(vakata_context
.element
[0], e
.target
)) {
6478 $.vakata
.context
.hide();
6481 .on("context_show.vakata.jstree", function (e
, data
) {
6482 vakata_context
.element
.find("li:has(ul)").children("a").addClass("vakata-context-parent");
6484 vakata_context
.element
.addClass("vakata-context-rtl").css("direction", "rtl");
6486 // also apply a RTL class?
6487 vakata_context
.element
.find("ul").hide().end();
6491 // $.jstree.defaults.plugins.push("contextmenu");
6495 * ### Drag'n'drop plugin
6497 * Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations.
6501 * stores all defaults for the drag'n'drop plugin
6502 * @name $.jstree.defaults.dnd
6505 $.jstree
.defaults
.dnd
= {
6507 * a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`.
6508 * @name $.jstree.defaults.dnd.copy
6513 * a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`.
6514 * @name $.jstree.defaults.dnd.open_timeout
6519 * a function invoked each time a node is about to be dragged, invoked in the tree's scope and receives the nodes about to be dragged as an argument (array) and the event that started the drag - return `false` to prevent dragging
6520 * @name $.jstree.defaults.dnd.is_draggable
6523 is_draggable
: true,
6525 * a boolean indicating if checks should constantly be made while the user is dragging the node (as opposed to checking only on drop), default is `true`
6526 * @name $.jstree.defaults.dnd.check_while_dragging
6529 check_while_dragging
: true,
6531 * a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false`
6532 * @name $.jstree.defaults.dnd.always_copy
6535 always_copy
: false,
6537 * when dropping a node "inside", this setting indicates the position the node should go to - it can be an integer or a string: "first" (same as 0) or "last", default is `0`
6538 * @name $.jstree.defaults.dnd.inside_pos
6543 * when starting the drag on a node that is selected this setting controls if all selected nodes are dragged or only the single node, default is `true`, which means all selected nodes are dragged when the drag is started on a selected node
6544 * @name $.jstree.defaults.dnd.drag_selection
6547 drag_selection
: true,
6549 * controls whether dnd works on touch devices. If left as boolean true dnd will work the same as in desktop browsers, which in some cases may impair scrolling. If set to boolean false dnd will not work on touch devices. There is a special third option - string "selected" which means only selected nodes can be dragged on touch devices.
6550 * @name $.jstree.defaults.dnd.touch
6555 * controls whether items can be dropped anywhere on the node, not just on the anchor, by default only the node anchor is a valid drop target. Works best with the wholerow plugin. If enabled on mobile depending on the interface it might be hard for the user to cancel the drop, since the whole tree container will be a valid drop target.
6556 * @name $.jstree.defaults.dnd.large_drop_target
6559 large_drop_target
: false,
6561 * controls whether a drag can be initiated from any part of the node and not just the text/icon part, works best with the wholerow plugin. Keep in mind it can cause problems with tree scrolling on mobile depending on the interface - in that case set the touch option to "selected".
6562 * @name $.jstree.defaults.dnd.large_drag_target
6565 large_drag_target
: false,
6567 * controls whether use HTML5 dnd api instead of classical. That will allow better integration of dnd events with other HTML5 controls.
6568 * @reference http://caniuse.com/#feat=dragndrop
6569 * @name $.jstree.defaults.dnd.use_html5
6575 // TODO: now check works by checking for each node individually, how about max_children, unique, etc?
6576 $.jstree
.plugins
.dnd = function (options
, parent
) {
6577 this.init = function (el
, options
) {
6578 parent
.init
.call(this, el
, options
);
6579 this.settings
.dnd
.use_html5
= this.settings
.dnd
.use_html5
&& ('draggable' in document
.createElement('span'));
6581 this.bind = function () {
6582 parent
.bind
.call(this);
6585 .on(this.settings
.dnd
.use_html5
? 'dragstart.jstree' : 'mousedown.jstree touchstart.jstree', this.settings
.dnd
.large_drag_target
? '.jstree-node' : '.jstree-anchor', $.proxy(function (e
) {
6586 if(this.settings
.dnd
.large_drag_target
&& $(e
.target
).closest('.jstree-node')[0] !== e
.currentTarget
) {
6589 if(e
.type
=== "touchstart" && (!this.settings
.dnd
.touch
|| (this.settings
.dnd
.touch
=== 'selected' && !$(e
.currentTarget
).closest('.jstree-node').children('.jstree-anchor').hasClass('jstree-clicked')))) {
6592 var obj
= this.get_node(e
.target
),
6593 mlt
= this.is_selected(obj
) && this.settings
.dnd
.drag_selection
? this.get_top_selected().length
: 1,
6594 txt
= (mlt
> 1 ? mlt
+ ' ' + this.get_string('nodes') : this.get_text(e
.currentTarget
));
6595 if(this.settings
.core
.force_text
) {
6596 txt
= $.vakata
.html
.escape(txt
);
6598 if(obj
&& obj
.id
&& obj
.id
!== $.jstree
.root
&& (e
.which
=== 1 || e
.type
=== "touchstart" || e
.type
=== "dragstart") &&
6599 (this.settings
.dnd
.is_draggable
=== true || ($.isFunction(this.settings
.dnd
.is_draggable
) && this.settings
.dnd
.is_draggable
.call(this, (mlt
> 1 ? this.get_top_selected(true) : [obj
]), e
)))
6601 drg
= { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj
,true), 'nodes' : mlt
> 1 ? this.get_top_selected() : [obj
.id
] };
6602 elm
= e
.currentTarget
;
6603 if (this.settings
.dnd
.use_html5
) {
6604 $.vakata
.dnd
._trigger('start', e
, { 'helper': $(), 'element': elm
, 'data': drg
});
6606 this.element
.trigger('mousedown.jstree');
6607 return $.vakata
.dnd
.start(e
, drg
, '<div id="jstree-dnd" class="jstree-' + this.get_theme() + ' jstree-' + this.get_theme() + '-' + this.get_theme_variant() + ' ' + ( this.settings
.core
.themes
.responsive
? ' jstree-dnd-responsive' : '' ) + '"><i class="jstree-icon jstree-er"></i>' + txt
+ '<ins class="jstree-copy" style="display:none;">+</ins></div>');
6611 if (this.settings
.dnd
.use_html5
) {
6613 .on('dragover.jstree', function (e
) {
6615 $.vakata
.dnd
._trigger('move', e
, { 'helper': $(), 'element': elm
, 'data': drg
});
6618 //.on('dragenter.jstree', this.settings.dnd.large_drop_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) {
6619 // e.preventDefault();
6620 // $.vakata.dnd._trigger('move', e, { 'helper': $(), 'element': elm, 'data': drg });
6623 .on('drop.jstree', $.proxy(function (e
) {
6625 $.vakata
.dnd
._trigger('stop', e
, { 'helper': $(), 'element': elm
, 'data': drg
});
6630 this.redraw_node = function(obj
, deep
, callback
, force_render
) {
6631 obj
= parent
.redraw_node
.apply(this, arguments
);
6632 if (obj
&& this.settings
.dnd
.use_html5
) {
6633 if (this.settings
.dnd
.large_drag_target
) {
6634 obj
.setAttribute('draggable', true);
6636 var i
, j
, tmp
= null;
6637 for(i
= 0, j
= obj
.childNodes
.length
; i
< j
; i
++) {
6638 if(obj
.childNodes
[i
] && obj
.childNodes
[i
].className
&& obj
.childNodes
[i
].className
.indexOf("jstree-anchor") !== -1) {
6639 tmp
= obj
.childNodes
[i
];
6644 tmp
.setAttribute('draggable', true);
6653 // bind only once for all instances
6658 marker
= $('<div id="jstree-marker"> </div>').hide(); //.appendTo('body');
6661 .on('dnd_start.vakata.jstree', function (e
, data
) {
6664 if(!data
|| !data
.data
|| !data
.data
.jstree
) { return; }
6665 marker
.appendTo('body'); //.show();
6667 .on('dnd_move.vakata.jstree', function (e
, data
) {
6668 var isDifferentNode
= data
.event
.target
!== lastev
.target
;
6670 if (!data
.event
|| data
.event
.type
!== 'dragover' || isDifferentNode
) {
6671 clearTimeout(opento
);
6674 if(!data
|| !data
.data
|| !data
.data
.jstree
) { return; }
6676 // if we are hovering the marker image do nothing (can happen on "inside" drags)
6677 if(data
.event
.target
.id
&& data
.event
.target
.id
=== 'jstree-marker') {
6680 lastev
= data
.event
;
6682 var ins
= $.jstree
.reference(data
.event
.target
),
6686 tmp
, l
, t
, h
, p
, i
, o
, ok
, t1
, t2
, op
, ps
, pr
, ip
, tm
, is_copy
, pn
;
6687 // if we are over an instance
6688 if(ins
&& ins
._data
&& ins
._data
.dnd
) {
6689 marker
.attr('class', 'jstree-' + ins
.get_theme() + ( ins
.settings
.core
.themes
.responsive
? ' jstree-dnd-responsive' : '' ));
6690 is_copy
= data
.data
.origin
&& (data
.data
.origin
.settings
.dnd
.always_copy
|| (data
.data
.origin
.settings
.dnd
.copy
&& (data
.event
.metaKey
|| data
.event
.ctrlKey
)));
6692 .children().attr('class', 'jstree-' + ins
.get_theme() + ' jstree-' + ins
.get_theme() + '-' + ins
.get_theme_variant() + ' ' + ( ins
.settings
.core
.themes
.responsive
? ' jstree-dnd-responsive' : '' ))
6693 .find('.jstree-copy').first()[ is_copy
? 'show' : 'hide' ]();
6695 // if are hovering the container itself add a new root node
6696 //console.log(data.event);
6697 if( (data
.event
.target
=== ins
.element
[0] || data
.event
.target
=== ins
.get_container_ul()[0]) && ins
.get_container_ul().children().length
=== 0) {
6699 for(t1
= 0, t2
= data
.data
.nodes
.length
; t1
< t2
; t1
++) {
6700 ok
= ok
&& ins
.check( (data
.data
.origin
&& (data
.data
.origin
.settings
.dnd
.always_copy
|| (data
.data
.origin
.settings
.dnd
.copy
&& (data
.event
.metaKey
|| data
.event
.ctrlKey
)) ) ? "copy_node" : "move_node"), (data
.data
.origin
&& data
.data
.origin
!== ins
? data
.data
.origin
.get_node(data
.data
.nodes
[t1
]) : data
.data
.nodes
[t1
]), $.jstree
.root
, 'last', { 'dnd' : true, 'ref' : ins
.get_node($.jstree
.root
), 'pos' : 'i', 'origin' : data
.data
.origin
, 'is_multi' : (data
.data
.origin
&& data
.data
.origin
!== ins
), 'is_foreign' : (!data
.data
.origin
) });
6704 lastmv
= { 'ins' : ins
, 'par' : $.jstree
.root
, 'pos' : 'last' };
6706 data
.helper
.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
6707 if (data
.event
.originalEvent
&& data
.event
.originalEvent
.dataTransfer
) {
6708 data
.event
.originalEvent
.dataTransfer
.dropEffect
= is_copy
? 'copy' : 'move';
6714 // if we are hovering a tree node
6715 ref
= ins
.settings
.dnd
.large_drop_target
? $(data
.event
.target
).closest('.jstree-node').children('.jstree-anchor') : $(data
.event
.target
).closest('.jstree-anchor');
6716 if(ref
&& ref
.length
&& ref
.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) {
6718 rel
= (data
.event
.pageY
!== undefined ? data
.event
.pageY
: data
.event
.originalEvent
.pageY
) - off
.top
;
6719 h
= ref
.outerHeight();
6721 o
= ['b', 'i', 'a'];
6723 else if(rel
> h
- h
/ 3) {
6724 o
= ['a', 'i', 'b'];
6727 o
= rel
> h
/ 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a'];
6729 $.each(o
, function (j
, v
) {
6734 p
= ins
.get_parent(ref
);
6735 i
= ref
.parent().index();
6738 ip
= ins
.settings
.dnd
.inside_pos
;
6739 tm
= ins
.get_node(ref
.parent());
6741 t
= off
.top
+ h
/ 2 + 1;
6743 i
= ip
=== 'first' ? 0 : (ip
=== 'last' ? tm
.children
.length
: Math
.min(ip
, tm
.children
.length
));
6748 p
= ins
.get_parent(ref
);
6749 i
= ref
.parent().index() + 1;
6753 for(t1
= 0, t2
= data
.data
.nodes
.length
; t1
< t2
; t1
++) {
6754 op
= data
.data
.origin
&& (data
.data
.origin
.settings
.dnd
.always_copy
|| (data
.data
.origin
.settings
.dnd
.copy
&& (data
.event
.metaKey
|| data
.event
.ctrlKey
))) ? "copy_node" : "move_node";
6756 if(op
=== "move_node" && v
=== 'a' && (data
.data
.origin
&& data
.data
.origin
=== ins
) && p
=== ins
.get_parent(data
.data
.nodes
[t1
])) {
6757 pr
= ins
.get_node(p
);
6758 if(ps
> $.inArray(data
.data
.nodes
[t1
], pr
.children
)) {
6762 ok
= ok
&& ( (ins
&& ins
.settings
&& ins
.settings
.dnd
&& ins
.settings
.dnd
.check_while_dragging
=== false) || ins
.check(op
, (data
.data
.origin
&& data
.data
.origin
!== ins
? data
.data
.origin
.get_node(data
.data
.nodes
[t1
]) : data
.data
.nodes
[t1
]), p
, ps
, { 'dnd' : true, 'ref' : ins
.get_node(ref
.parent()), 'pos' : v
, 'origin' : data
.data
.origin
, 'is_multi' : (data
.data
.origin
&& data
.data
.origin
!== ins
), 'is_foreign' : (!data
.data
.origin
) }) );
6764 if(ins
&& ins
.last_error
) { laster
= ins
.last_error(); }
6768 if(v
=== 'i' && ref
.parent().is('.jstree-closed') && ins
.settings
.dnd
.open_timeout
) {
6769 if (!data
.event
|| data
.event
.type
!== 'dragover' || isDifferentNode
) {
6770 if (opento
) { clearTimeout(opento
); }
6771 opento
= setTimeout((function (x
, z
) { return function () { x
.open_node(z
); }; }(ins
, ref
)), ins
.settings
.dnd
.open_timeout
);
6775 pn
= ins
.get_node(p
, true);
6776 if (!pn
.hasClass('.jstree-dnd-parent')) {
6777 $('.jstree-dnd-parent').removeClass('jstree-dnd-parent');
6778 pn
.addClass('jstree-dnd-parent');
6780 lastmv
= { 'ins' : ins
, 'par' : p
, 'pos' : v
=== 'i' && ip
=== 'last' && i
=== 0 && !ins
.is_loaded(tm
) ? 'last' : i
};
6781 marker
.css({ 'left' : l
+ 'px', 'top' : t
+ 'px' }).show();
6782 data
.helper
.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
6783 if (data
.event
.originalEvent
&& data
.event
.originalEvent
.dataTransfer
) {
6784 data
.event
.originalEvent
.dataTransfer
.dropEffect
= is_copy
? 'copy' : 'move';
6791 if(o
=== true) { return; }
6795 $('.jstree-dnd-parent').removeClass('jstree-dnd-parent');
6797 data
.helper
.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er');
6798 if (data
.event
.originalEvent
&& data
.event
.originalEvent
.dataTransfer
) {
6799 data
.event
.originalEvent
.dataTransfer
.dropEffect
= 'none';
6803 .on('dnd_scroll.vakata.jstree', function (e
, data
) {
6804 if(!data
|| !data
.data
|| !data
.data
.jstree
) { return; }
6808 data
.helper
.find('.jstree-icon').first().removeClass('jstree-ok').addClass('jstree-er');
6810 .on('dnd_stop.vakata.jstree', function (e
, data
) {
6811 $('.jstree-dnd-parent').removeClass('jstree-dnd-parent');
6812 if(opento
) { clearTimeout(opento
); }
6813 if(!data
|| !data
.data
|| !data
.data
.jstree
) { return; }
6814 marker
.hide().detach();
6815 var i
, j
, nodes
= [];
6817 for(i
= 0, j
= data
.data
.nodes
.length
; i
< j
; i
++) {
6818 nodes
[i
] = data
.data
.origin
? data
.data
.origin
.get_node(data
.data
.nodes
[i
]) : data
.data
.nodes
[i
];
6820 lastmv
.ins
[ data
.data
.origin
&& (data
.data
.origin
.settings
.dnd
.always_copy
|| (data
.data
.origin
.settings
.dnd
.copy
&& (data
.event
.metaKey
|| data
.event
.ctrlKey
))) ? 'copy_node' : 'move_node' ](nodes
, lastmv
.par
, lastmv
.pos
, false, false, false, data
.data
.origin
);
6823 i
= $(data
.event
.target
).closest('.jstree');
6824 if(i
.length
&& laster
&& laster
.error
&& laster
.error
=== 'check') {
6827 i
.settings
.core
.error
.call(this, laster
);
6834 .on('keyup.jstree keydown.jstree', function (e
, data
) {
6835 data
= $.vakata
.dnd
._get();
6836 if(data
&& data
.data
&& data
.data
.jstree
) {
6837 if (e
.type
=== "keyup" && e
.which
=== 27) {
6838 if (opento
) { clearTimeout(opento
); }
6843 marker
.hide().detach();
6844 $.vakata
.dnd
._clean();
6846 data
.helper
.find('.jstree-copy').first()[ data
.data
.origin
&& (data
.data
.origin
.settings
.dnd
.always_copy
|| (data
.data
.origin
.settings
.dnd
.copy
&& (e
.metaKey
|| e
.ctrlKey
))) ? 'show' : 'hide' ]();
6848 lastev
.metaKey
= e
.metaKey
;
6849 lastev
.ctrlKey
= e
.ctrlKey
;
6850 $.vakata
.dnd
._trigger('move', lastev
);
6861 escape : function (str
) {
6862 return $.vakata
.html
.div
.text(str
).html();
6864 strip : function (str
) {
6865 return $.vakata
.html
.div
.empty().append($.parseHTML(str
)).text();
6888 scroll_proximity
: 20,
6892 threshold_touch
: 50
6894 _trigger : function (event_name
, e
, data
) {
6895 if (data
=== undefined) {
6896 data
= $.vakata
.dnd
._get();
6899 $(document
).triggerHandler("dnd_" + event_name
+ ".vakata", data
);
6901 _get : function () {
6903 "data" : vakata_dnd
.data
,
6904 "element" : vakata_dnd
.element
,
6905 "helper" : vakata_dnd
.helper
6908 _clean : function () {
6909 if(vakata_dnd
.helper
) { vakata_dnd
.helper
.remove(); }
6910 if(vakata_dnd
.scroll_i
) { clearInterval(vakata_dnd
.scroll_i
); vakata_dnd
.scroll_i
= false; }
6927 $(document
).off("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata
.dnd
.drag
);
6928 $(document
).off("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata
.dnd
.stop
);
6930 _scroll : function (init_only
) {
6931 if(!vakata_dnd
.scroll_e
|| (!vakata_dnd
.scroll_l
&& !vakata_dnd
.scroll_t
)) {
6932 if(vakata_dnd
.scroll_i
) { clearInterval(vakata_dnd
.scroll_i
); vakata_dnd
.scroll_i
= false; }
6935 if(!vakata_dnd
.scroll_i
) {
6936 vakata_dnd
.scroll_i
= setInterval($.vakata
.dnd
._scroll
, 100);
6939 if(init_only
=== true) { return false; }
6941 var i
= vakata_dnd
.scroll_e
.scrollTop(),
6942 j
= vakata_dnd
.scroll_e
.scrollLeft();
6943 vakata_dnd
.scroll_e
.scrollTop(i
+ vakata_dnd
.scroll_t
* $.vakata
.dnd
.settings
.scroll_speed
);
6944 vakata_dnd
.scroll_e
.scrollLeft(j
+ vakata_dnd
.scroll_l
* $.vakata
.dnd
.settings
.scroll_speed
);
6945 if(i
!== vakata_dnd
.scroll_e
.scrollTop() || j
!== vakata_dnd
.scroll_e
.scrollLeft()) {
6947 * triggered on the document when a drag causes an element to scroll
6950 * @name dnd_scroll.vakata
6951 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
6952 * @param {DOM} element the DOM element being dragged
6953 * @param {jQuery} helper the helper shown next to the mouse
6954 * @param {jQuery} event the element that is scrolling
6956 $.vakata
.dnd
._trigger("scroll", vakata_dnd
.scroll_e
);
6959 start : function (e
, data
, html
) {
6960 if(e
.type
=== "touchstart" && e
.originalEvent
&& e
.originalEvent
.changedTouches
&& e
.originalEvent
.changedTouches
[0]) {
6961 e
.pageX
= e
.originalEvent
.changedTouches
[0].pageX
;
6962 e
.pageY
= e
.originalEvent
.changedTouches
[0].pageY
;
6963 e
.target
= document
.elementFromPoint(e
.originalEvent
.changedTouches
[0].pageX
- window
.pageXOffset
, e
.originalEvent
.changedTouches
[0].pageY
- window
.pageYOffset
);
6965 if(vakata_dnd
.is_drag
) { $.vakata
.dnd
.stop({}); }
6967 e
.currentTarget
.unselectable
= "on";
6968 e
.currentTarget
.onselectstart = function() { return false; };
6969 if(e
.currentTarget
.style
) {
6970 e
.currentTarget
.style
.touchAction
= "none";
6971 e
.currentTarget
.style
.msTouchAction
= "none";
6972 e
.currentTarget
.style
.MozUserSelect
= "none";
6975 vakata_dnd
.init_x
= e
.pageX
;
6976 vakata_dnd
.init_y
= e
.pageY
;
6977 vakata_dnd
.data
= data
;
6978 vakata_dnd
.is_down
= true;
6979 vakata_dnd
.element
= e
.currentTarget
;
6980 vakata_dnd
.target
= e
.target
;
6981 vakata_dnd
.is_touch
= e
.type
=== "touchstart";
6982 if(html
!== false) {
6983 vakata_dnd
.helper
= $("<div id='vakata-dnd'></div>").html(html
).css({
6984 "display" : "block",
6987 "position" : "absolute",
6989 "lineHeight" : "16px",
6993 $(document
).on("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata
.dnd
.drag
);
6994 $(document
).on("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata
.dnd
.stop
);
6997 drag : function (e
) {
6998 if(e
.type
=== "touchmove" && e
.originalEvent
&& e
.originalEvent
.changedTouches
&& e
.originalEvent
.changedTouches
[0]) {
6999 e
.pageX
= e
.originalEvent
.changedTouches
[0].pageX
;
7000 e
.pageY
= e
.originalEvent
.changedTouches
[0].pageY
;
7001 e
.target
= document
.elementFromPoint(e
.originalEvent
.changedTouches
[0].pageX
- window
.pageXOffset
, e
.originalEvent
.changedTouches
[0].pageY
- window
.pageYOffset
);
7003 if(!vakata_dnd
.is_down
) { return; }
7004 if(!vakata_dnd
.is_drag
) {
7006 Math
.abs(e
.pageX
- vakata_dnd
.init_x
) > (vakata_dnd
.is_touch
? $.vakata
.dnd
.settings
.threshold_touch
: $.vakata
.dnd
.settings
.threshold
) ||
7007 Math
.abs(e
.pageY
- vakata_dnd
.init_y
) > (vakata_dnd
.is_touch
? $.vakata
.dnd
.settings
.threshold_touch
: $.vakata
.dnd
.settings
.threshold
)
7009 if(vakata_dnd
.helper
) {
7010 vakata_dnd
.helper
.appendTo("body");
7011 vakata_dnd
.helper_w
= vakata_dnd
.helper
.outerWidth();
7013 vakata_dnd
.is_drag
= true;
7014 $(vakata_dnd
.target
).one('click.vakata', false);
7016 * triggered on the document when a drag starts
7019 * @name dnd_start.vakata
7020 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
7021 * @param {DOM} element the DOM element being dragged
7022 * @param {jQuery} helper the helper shown next to the mouse
7023 * @param {Object} event the event that caused the start (probably mousemove)
7025 $.vakata
.dnd
._trigger("start", e
);
7030 var d
= false, w
= false,
7031 dh
= false, wh
= false,
7032 dw
= false, ww
= false,
7033 dt
= false, dl
= false,
7034 ht
= false, hl
= false;
7036 vakata_dnd
.scroll_t
= 0;
7037 vakata_dnd
.scroll_l
= 0;
7038 vakata_dnd
.scroll_e
= false;
7039 $($(e
.target
).parentsUntil("body").addBack().get().reverse())
7040 .filter(function () {
7041 return (/^auto|scroll$/).test($(this).css("overflow")) &&
7042 (this.scrollHeight
> this.offsetHeight
|| this.scrollWidth
> this.offsetWidth
);
7045 var t
= $(this), o
= t
.offset();
7046 if(this.scrollHeight
> this.offsetHeight
) {
7047 if(o
.top
+ t
.height() - e
.pageY
< $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_t
= 1; }
7048 if(e
.pageY
- o
.top
< $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_t
= -1; }
7050 if(this.scrollWidth
> this.offsetWidth
) {
7051 if(o
.left
+ t
.width() - e
.pageX
< $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_l
= 1; }
7052 if(e
.pageX
- o
.left
< $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_l
= -1; }
7054 if(vakata_dnd
.scroll_t
|| vakata_dnd
.scroll_l
) {
7055 vakata_dnd
.scroll_e
= $(this);
7060 if(!vakata_dnd
.scroll_e
) {
7061 d
= $(document
); w
= $(window
);
7062 dh
= d
.height(); wh
= w
.height();
7063 dw
= d
.width(); ww
= w
.width();
7064 dt
= d
.scrollTop(); dl
= d
.scrollLeft();
7065 if(dh
> wh
&& e
.pageY
- dt
< $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_t
= -1; }
7066 if(dh
> wh
&& wh
- (e
.pageY
- dt
) < $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_t
= 1; }
7067 if(dw
> ww
&& e
.pageX
- dl
< $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_l
= -1; }
7068 if(dw
> ww
&& ww
- (e
.pageX
- dl
) < $.vakata
.dnd
.settings
.scroll_proximity
) { vakata_dnd
.scroll_l
= 1; }
7069 if(vakata_dnd
.scroll_t
|| vakata_dnd
.scroll_l
) {
7070 vakata_dnd
.scroll_e
= d
;
7073 if(vakata_dnd
.scroll_e
) { $.vakata
.dnd
._scroll(true); }
7075 if(vakata_dnd
.helper
) {
7076 ht
= parseInt(e
.pageY
+ $.vakata
.dnd
.settings
.helper_top
, 10);
7077 hl
= parseInt(e
.pageX
+ $.vakata
.dnd
.settings
.helper_left
, 10);
7078 if(dh
&& ht
+ 25 > dh
) { ht
= dh
- 50; }
7079 if(dw
&& hl
+ vakata_dnd
.helper_w
> dw
) { hl
= dw
- (vakata_dnd
.helper_w
+ 2); }
7080 vakata_dnd
.helper
.css({
7086 * triggered on the document when a drag is in progress
7089 * @name dnd_move.vakata
7090 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
7091 * @param {DOM} element the DOM element being dragged
7092 * @param {jQuery} helper the helper shown next to the mouse
7093 * @param {Object} event the event that caused this to trigger (most likely mousemove)
7095 $.vakata
.dnd
._trigger("move", e
);
7098 stop : function (e
) {
7099 if(e
.type
=== "touchend" && e
.originalEvent
&& e
.originalEvent
.changedTouches
&& e
.originalEvent
.changedTouches
[0]) {
7100 e
.pageX
= e
.originalEvent
.changedTouches
[0].pageX
;
7101 e
.pageY
= e
.originalEvent
.changedTouches
[0].pageY
;
7102 e
.target
= document
.elementFromPoint(e
.originalEvent
.changedTouches
[0].pageX
- window
.pageXOffset
, e
.originalEvent
.changedTouches
[0].pageY
- window
.pageYOffset
);
7104 if(vakata_dnd
.is_drag
) {
7106 * triggered on the document when a drag stops (the dragged element is dropped)
7109 * @name dnd_stop.vakata
7110 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
7111 * @param {DOM} element the DOM element being dragged
7112 * @param {jQuery} helper the helper shown next to the mouse
7113 * @param {Object} event the event that caused the stop
7115 if (e
.target
!== vakata_dnd
.target
) {
7116 $(vakata_dnd
.target
).off('click.vakata');
7118 $.vakata
.dnd
._trigger("stop", e
);
7121 if(e
.type
=== "touchend" && e
.target
=== vakata_dnd
.target
) {
7122 var to
= setTimeout(function () { $(e
.target
).click(); }, 100);
7123 $(e
.target
).one('click', function() { if(to
) { clearTimeout(to
); } });
7126 $.vakata
.dnd
._clean();
7132 // include the dnd plugin by default
7133 // $.jstree.defaults.plugins.push("dnd");
7137 * ### Massload plugin
7139 * Adds massload functionality to jsTree, so that multiple nodes can be loaded in a single request (only useful with lazy loading).
7143 * massload configuration
7145 * It is possible to set this to a standard jQuery-like AJAX config.
7146 * In addition to the standard jQuery ajax options here you can supply functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node IDs need to be loaded, the return value of those functions will be used.
7148 * You can also set this to a function, that function will receive the node IDs being loaded as argument and a second param which is a function (callback) which should be called with the result.
7150 * Both the AJAX and the function approach rely on the same return value - an object where the keys are the node IDs, and the value is the children of that node as an array.
7153 * "id1" : [{ "text" : "Child of ID1", "id" : "c1" }, { "text" : "Another child of ID1", "id" : "c2" }],
7154 * "id2" : [{ "text" : "Child of ID2", "id" : "c3" }]
7157 * @name $.jstree.defaults.massload
7160 $.jstree
.defaults
.massload
= null;
7161 $.jstree
.plugins
.massload = function (options
, parent
) {
7162 this.init = function (el
, options
) {
7163 this._data
.massload
= {};
7164 parent
.init
.call(this, el
, options
);
7166 this._load_nodes = function (nodes
, callback
, is_callback
, force_reload
) {
7167 var s
= this.settings
.massload
,
7168 nodesString
= JSON
.stringify(nodes
),
7170 m
= this._model
.data
,
7173 for(i
= 0, j
= nodes
.length
; i
< j
; i
++) {
7174 if(!m
[nodes
[i
]] || ( (!m
[nodes
[i
]].state
.loaded
&& !m
[nodes
[i
]].state
.failed
) || force_reload
) ) {
7175 toLoad
.push(nodes
[i
]);
7176 dom
= this.get_node(nodes
[i
], true);
7177 if (dom
&& dom
.length
) {
7178 dom
.addClass("jstree-loading").attr('aria-busy',true);
7182 this._data
.massload
= {};
7183 if (toLoad
.length
) {
7184 if($.isFunction(s
)) {
7185 return s
.call(this, toLoad
, $.proxy(function (data
) {
7189 if(data
.hasOwnProperty(i
)) {
7190 this._data
.massload
[i
] = data
[i
];
7194 for(i
= 0, j
= nodes
.length
; i
< j
; i
++) {
7195 dom
= this.get_node(nodes
[i
], true);
7196 if (dom
&& dom
.length
) {
7197 dom
.removeClass("jstree-loading").attr('aria-busy',false);
7200 parent
._load_nodes
.call(this, nodes
, callback
, is_callback
, force_reload
);
7203 if(typeof s
=== 'object' && s
&& s
.url
) {
7204 s
= $.extend(true, {}, s
);
7205 if($.isFunction(s
.url
)) {
7206 s
.url
= s
.url
.call(this, toLoad
);
7208 if($.isFunction(s
.data
)) {
7209 s
.data
= s
.data
.call(this, toLoad
);
7212 .done($.proxy(function (data
,t
,x
) {
7216 if(data
.hasOwnProperty(i
)) {
7217 this._data
.massload
[i
] = data
[i
];
7221 for(i
= 0, j
= nodes
.length
; i
< j
; i
++) {
7222 dom
= this.get_node(nodes
[i
], true);
7223 if (dom
&& dom
.length
) {
7224 dom
.removeClass("jstree-loading").attr('aria-busy',false);
7227 parent
._load_nodes
.call(this, nodes
, callback
, is_callback
, force_reload
);
7229 .fail($.proxy(function (f
) {
7230 parent
._load_nodes
.call(this, nodes
, callback
, is_callback
, force_reload
);
7235 return parent
._load_nodes
.call(this, nodes
, callback
, is_callback
, force_reload
);
7237 this._load_node = function (obj
, callback
) {
7238 var data
= this._data
.massload
[obj
.id
],
7241 rslt
= this[typeof data
=== 'string' ? '_append_html_data' : '_append_json_data'](
7243 typeof data
=== 'string' ? $($.parseHTML(data
)).filter(function () { return this.nodeType
!== 3; }) : data
,
7244 function (status
) { callback
.call(this, status
); }
7246 dom
= this.get_node(obj
.id
, true);
7247 if (dom
&& dom
.length
) {
7248 dom
.removeClass("jstree-loading").attr('aria-busy',false);
7250 delete this._data
.massload
[obj
.id
];
7253 return parent
._load_node
.call(this, obj
, callback
);
7260 * Adds search functionality to jsTree.
7264 * stores all defaults for the search plugin
7265 * @name $.jstree.defaults.search
7268 $.jstree
.defaults
.search
= {
7270 * a jQuery-like AJAX config, which jstree uses if a server should be queried for results.
7272 * A `str` (which is the search string) parameter will be added with the request, an optional `inside` parameter will be added if the search is limited to a node id. The expected result is a JSON array with nodes that need to be opened so that matching nodes will be revealed.
7273 * Leave this setting as `false` to not query the server. You can also set this to a function, which will be invoked in the instance's scope and receive 3 parameters - the search string, the callback to call with the array of nodes to load, and the optional node ID to limit the search to
7274 * @name $.jstree.defaults.search.ajax
7279 * Indicates if the search should be fuzzy or not (should `chnd3` match `child node 3`). Default is `false`.
7280 * @name $.jstree.defaults.search.fuzzy
7285 * Indicates if the search should be case sensitive. Default is `false`.
7286 * @name $.jstree.defaults.search.case_sensitive
7289 case_sensitive
: false,
7291 * Indicates if the tree should be filtered (by default) to show only matching nodes (keep in mind this can be a heavy on large trees in old browsers).
7292 * This setting can be changed at runtime when calling the search method. Default is `false`.
7293 * @name $.jstree.defaults.search.show_only_matches
7296 show_only_matches
: false,
7298 * Indicates if the children of matched element are shown (when show_only_matches is true)
7299 * This setting can be changed at runtime when calling the search method. Default is `false`.
7300 * @name $.jstree.defaults.search.show_only_matches_children
7303 show_only_matches_children
: false,
7305 * Indicates if all nodes opened to reveal the search result, should be closed when the search is cleared or a new search is performed. Default is `true`.
7306 * @name $.jstree.defaults.search.close_opened_onclear
7309 close_opened_onclear
: true,
7311 * Indicates if only leaf nodes should be included in search results. Default is `false`.
7312 * @name $.jstree.defaults.search.search_leaves_only
7315 search_leaves_only
: false,
7317 * If set to a function it wil be called in the instance's scope with two arguments - search string and node (where node will be every node in the structure, so use with caution).
7318 * If the function returns a truthy value the node will be considered a match (it might not be displayed if search_only_leaves is set to true and the node is not a leaf). Default is `false`.
7319 * @name $.jstree.defaults.search.search_callback
7322 search_callback
: false
7325 $.jstree
.plugins
.search = function (options
, parent
) {
7326 this.bind = function () {
7327 parent
.bind
.call(this);
7329 this._data
.search
.str
= "";
7330 this._data
.search
.dom
= $();
7331 this._data
.search
.res
= [];
7332 this._data
.search
.opn
= [];
7333 this._data
.search
.som
= false;
7334 this._data
.search
.smc
= false;
7335 this._data
.search
.hdn
= [];
7338 .on("search.jstree", $.proxy(function (e
, data
) {
7339 if(this._data
.search
.som
&& data
.res
.length
) {
7340 var m
= this._model
.data
, i
, j
, p
= [], k
, l
;
7341 for(i
= 0, j
= data
.res
.length
; i
< j
; i
++) {
7342 if(m
[data
.res
[i
]] && !m
[data
.res
[i
]].state
.hidden
) {
7343 p
.push(data
.res
[i
]);
7344 p
= p
.concat(m
[data
.res
[i
]].parents
);
7345 if(this._data
.search
.smc
) {
7346 for (k
= 0, l
= m
[data
.res
[i
]].children_d
.length
; k
< l
; k
++) {
7347 if (m
[m
[data
.res
[i
]].children_d
[k
]] && !m
[m
[data
.res
[i
]].children_d
[k
]].state
.hidden
) {
7348 p
.push(m
[data
.res
[i
]].children_d
[k
]);
7354 p
= $.vakata
.array_remove_item($.vakata
.array_unique(p
), $.jstree
.root
);
7355 this._data
.search
.hdn
= this.hide_all(true);
7356 this.show_node(p
, true);
7360 .on("clear_search.jstree", $.proxy(function (e
, data
) {
7361 if(this._data
.search
.som
&& data
.res
.length
) {
7362 this.show_node(this._data
.search
.hdn
, true);
7368 * used to search the tree nodes for a given string
7369 * @name search(str [, skip_async])
7370 * @param {String} str the search string
7371 * @param {Boolean} skip_async if set to true server will not be queried even if configured
7372 * @param {Boolean} show_only_matches if set to true only matching nodes will be shown (keep in mind this can be very slow on large trees or old browsers)
7373 * @param {mixed} inside an optional node to whose children to limit the search
7374 * @param {Boolean} append if set to true the results of this search are appended to the previous search
7376 * @trigger search.jstree
7378 this.search = function (str
, skip_async
, show_only_matches
, inside
, append
, show_only_matches_children
) {
7379 if(str
=== false || $.trim(str
.toString()) === "") {
7380 return this.clear_search();
7382 inside
= this.get_node(inside
);
7383 inside
= inside
&& inside
.id
? inside
.id
: null;
7384 str
= str
.toString();
7385 var s
= this.settings
.search
,
7386 a
= s
.ajax
? s
.ajax
: false,
7387 m
= this._model
.data
,
7391 if(this._data
.search
.res
.length
&& !append
) {
7392 this.clear_search();
7394 if(show_only_matches
=== undefined) {
7395 show_only_matches
= s
.show_only_matches
;
7397 if(show_only_matches_children
=== undefined) {
7398 show_only_matches_children
= s
.show_only_matches_children
;
7400 if(!skip_async
&& a
!== false) {
7401 if($.isFunction(a
)) {
7402 return a
.call(this, str
, $.proxy(function (d
) {
7403 if(d
&& d
.d
) { d
= d
.d
; }
7404 this._load_nodes(!$.isArray(d
) ? [] : $.vakata
.array_unique(d
), function () {
7405 this.search(str
, true, show_only_matches
, inside
, append
, show_only_matches_children
);
7410 a
= $.extend({}, a
);
7411 if(!a
.data
) { a
.data
= {}; }
7414 a
.data
.inside
= inside
;
7416 if (this._data
.search
.lastRequest
) {
7417 this._data
.search
.lastRequest
.abort();
7419 this._data
.search
.lastRequest
= $.ajax(a
)
7420 .fail($.proxy(function () {
7421 this._data
.core
.last_error
= { 'error' : 'ajax', 'plugin' : 'search', 'id' : 'search_01', 'reason' : 'Could not load search parents', 'data' : JSON
.stringify(a
) };
7422 this.settings
.core
.error
.call(this, this._data
.core
.last_error
);
7424 .done($.proxy(function (d
) {
7425 if(d
&& d
.d
) { d
= d
.d
; }
7426 this._load_nodes(!$.isArray(d
) ? [] : $.vakata
.array_unique(d
), function () {
7427 this.search(str
, true, show_only_matches
, inside
, append
, show_only_matches_children
);
7430 return this._data
.search
.lastRequest
;
7434 this._data
.search
.str
= str
;
7435 this._data
.search
.dom
= $();
7436 this._data
.search
.res
= [];
7437 this._data
.search
.opn
= [];
7438 this._data
.search
.som
= show_only_matches
;
7439 this._data
.search
.smc
= show_only_matches_children
;
7442 f
= new $.vakata
.search(str
, true, { caseSensitive
: s
.case_sensitive
, fuzzy
: s
.fuzzy
});
7443 $.each(m
[inside
? inside
: $.jstree
.root
].children_d
, function (ii
, i
) {
7445 if(v
.text
&& !v
.state
.hidden
&& (!s
.search_leaves_only
|| (v
.state
.loaded
&& v
.children
.length
=== 0)) && ( (s
.search_callback
&& s
.search_callback
.call(this, str
, v
)) || (!s
.search_callback
&& f
.search(v
.text
).isMatch
) ) ) {
7447 p
= p
.concat(v
.parents
);
7451 p
= $.vakata
.array_unique(p
);
7452 for(i
= 0, j
= p
.length
; i
< j
; i
++) {
7453 if(p
[i
] !== $.jstree
.root
&& m
[p
[i
]] && this.open_node(p
[i
], null, 0) === true) {
7454 this._data
.search
.opn
.push(p
[i
]);
7458 this._data
.search
.dom
= $(this.element
[0].querySelectorAll('#' + $.map(r
, function (v
) { return "0123456789".indexOf(v
[0]) !== -1 ? '\\3' + v
[0] + ' ' + v
.substr(1).replace($.jstree
.idregex
,'\\$&') : v
.replace($.jstree
.idregex
,'\\$&'); }).join(', #')));
7459 this._data
.search
.res
= r
;
7462 this._data
.search
.dom
= this._data
.search
.dom
.add($(this.element
[0].querySelectorAll('#' + $.map(r
, function (v
) { return "0123456789".indexOf(v
[0]) !== -1 ? '\\3' + v
[0] + ' ' + v
.substr(1).replace($.jstree
.idregex
,'\\$&') : v
.replace($.jstree
.idregex
,'\\$&'); }).join(', #'))));
7463 this._data
.search
.res
= $.vakata
.array_unique(this._data
.search
.res
.concat(r
));
7465 this._data
.search
.dom
.children(".jstree-anchor").addClass('jstree-search');
7468 * triggered after search is complete
7470 * @name search.jstree
7471 * @param {jQuery} nodes a jQuery collection of matching nodes
7472 * @param {String} str the search string
7473 * @param {Array} res a collection of objects represeing the matching nodes
7476 this.trigger('search', { nodes
: this._data
.search
.dom
, str
: str
, res
: this._data
.search
.res
, show_only_matches
: show_only_matches
});
7479 * used to clear the last search (removes classes and shows all nodes if filtering is on)
7480 * @name clear_search()
7482 * @trigger clear_search.jstree
7484 this.clear_search = function () {
7485 if(this.settings
.search
.close_opened_onclear
) {
7486 this.close_node(this._data
.search
.opn
, 0);
7489 * triggered after search is complete
7491 * @name clear_search.jstree
7492 * @param {jQuery} nodes a jQuery collection of matching nodes (the result from the last search)
7493 * @param {String} str the search string (the last search string)
7494 * @param {Array} res a collection of objects represeing the matching nodes (the result from the last search)
7497 this.trigger('clear_search', { 'nodes' : this._data
.search
.dom
, str
: this._data
.search
.str
, res
: this._data
.search
.res
});
7498 if(this._data
.search
.res
.length
) {
7499 this._data
.search
.dom
= $(this.element
[0].querySelectorAll('#' + $.map(this._data
.search
.res
, function (v
) {
7500 return "0123456789".indexOf(v
[0]) !== -1 ? '\\3' + v
[0] + ' ' + v
.substr(1).replace($.jstree
.idregex
,'\\$&') : v
.replace($.jstree
.idregex
,'\\$&');
7502 this._data
.search
.dom
.children(".jstree-anchor").removeClass("jstree-search");
7504 this._data
.search
.str
= "";
7505 this._data
.search
.res
= [];
7506 this._data
.search
.opn
= [];
7507 this._data
.search
.dom
= $();
7510 this.redraw_node = function(obj
, deep
, callback
, force_render
) {
7511 obj
= parent
.redraw_node
.apply(this, arguments
);
7513 if($.inArray(obj
.id
, this._data
.search
.res
) !== -1) {
7514 var i
, j
, tmp
= null;
7515 for(i
= 0, j
= obj
.childNodes
.length
; i
< j
; i
++) {
7516 if(obj
.childNodes
[i
] && obj
.childNodes
[i
].className
&& obj
.childNodes
[i
].className
.indexOf("jstree-anchor") !== -1) {
7517 tmp
= obj
.childNodes
[i
];
7522 tmp
.className
+= ' jstree-search';
7532 // from http://kiro.me/projects/fuse.html
7533 $.vakata
.search = function(pattern
, txt
, options
) {
7534 options
= options
|| {};
7535 options
= $.extend({}, $.vakata
.search
.defaults
, options
);
7536 if(options
.fuzzy
!== false) {
7537 options
.fuzzy
= true;
7539 pattern
= options
.caseSensitive
? pattern
: pattern
.toLowerCase();
7540 var MATCH_LOCATION
= options
.location
,
7541 MATCH_DISTANCE
= options
.distance
,
7542 MATCH_THRESHOLD
= options
.threshold
,
7543 patternLen
= pattern
.length
,
7544 matchmask
, pattern_alphabet
, match_bitapScore
, search
;
7545 if(patternLen
> 32) {
7546 options
.fuzzy
= false;
7549 matchmask
= 1 << (patternLen
- 1);
7550 pattern_alphabet
= (function () {
7553 for (i
= 0; i
< patternLen
; i
++) {
7554 mask
[pattern
.charAt(i
)] = 0;
7556 for (i
= 0; i
< patternLen
; i
++) {
7557 mask
[pattern
.charAt(i
)] |= 1 << (patternLen
- i
- 1);
7561 match_bitapScore = function (e
, x
) {
7562 var accuracy
= e
/ patternLen
,
7563 proximity
= Math
.abs(MATCH_LOCATION
- x
);
7564 if(!MATCH_DISTANCE
) {
7565 return proximity
? 1.0 : accuracy
;
7567 return accuracy
+ (proximity
/ MATCH_DISTANCE
);
7570 search = function (text
) {
7571 text
= options
.caseSensitive
? text
: text
.toLowerCase();
7572 if(pattern
=== text
|| text
.indexOf(pattern
) !== -1) {
7578 if(!options
.fuzzy
) {
7585 textLen
= text
.length
,
7586 scoreThreshold
= MATCH_THRESHOLD
,
7587 bestLoc
= text
.indexOf(pattern
, MATCH_LOCATION
),
7589 binMax
= patternLen
+ textLen
,
7590 lastRd
, start
, finish
, rd
, charMatch
,
7593 if (bestLoc
!== -1) {
7594 scoreThreshold
= Math
.min(match_bitapScore(0, bestLoc
), scoreThreshold
);
7595 bestLoc
= text
.lastIndexOf(pattern
, MATCH_LOCATION
+ patternLen
);
7596 if (bestLoc
!== -1) {
7597 scoreThreshold
= Math
.min(match_bitapScore(0, bestLoc
), scoreThreshold
);
7601 for (i
= 0; i
< patternLen
; i
++) {
7604 while (binMin
< binMid
) {
7605 if (match_bitapScore(i
, MATCH_LOCATION
+ binMid
) <= scoreThreshold
) {
7610 binMid
= Math
.floor((binMax
- binMin
) / 2 + binMin
);
7613 start
= Math
.max(1, MATCH_LOCATION
- binMid
+ 1);
7614 finish
= Math
.min(MATCH_LOCATION
+ binMid
, textLen
) + patternLen
;
7615 rd
= new Array(finish
+ 2);
7616 rd
[finish
+ 1] = (1 << i
) - 1;
7617 for (j
= finish
; j
>= start
; j
--) {
7618 charMatch
= pattern_alphabet
[text
.charAt(j
- 1)];
7620 rd
[j
] = ((rd
[j
+ 1] << 1) | 1) & charMatch
;
7622 rd
[j
] = ((rd
[j
+ 1] << 1) | 1) & charMatch
| (((lastRd
[j
+ 1] | lastRd
[j
]) << 1) | 1) | lastRd
[j
+ 1];
7624 if (rd
[j
] & matchmask
) {
7625 score
= match_bitapScore(i
, j
- 1);
7626 if (score
<= scoreThreshold
) {
7627 scoreThreshold
= score
;
7629 locations
.push(bestLoc
);
7630 if (bestLoc
> MATCH_LOCATION
) {
7631 start
= Math
.max(1, 2 * MATCH_LOCATION
- bestLoc
);
7638 if (match_bitapScore(i
+ 1, MATCH_LOCATION
) > scoreThreshold
) {
7644 isMatch
: bestLoc
>= 0,
7648 return txt
=== true ? { 'search' : search
} : search(txt
);
7650 $.vakata
.search
.defaults
= {
7655 caseSensitive
: false
7659 // include the search plugin by default
7660 // $.jstree.defaults.plugins.push("search");
7666 * Automatically sorts all siblings in the tree according to a sorting function.
7670 * the settings function used to sort the nodes.
7671 * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`.
7672 * @name $.jstree.defaults.sort
7675 $.jstree
.defaults
.sort = function (a
, b
) {
7676 //return this.get_type(a) === this.get_type(b) ? (this.get_text(a) > this.get_text(b) ? 1 : -1) : this.get_type(a) >= this.get_type(b);
7677 return this.get_text(a
) > this.get_text(b
) ? 1 : -1;
7679 $.jstree
.plugins
.sort = function (options
, parent
) {
7680 this.bind = function () {
7681 parent
.bind
.call(this);
7683 .on("model.jstree", $.proxy(function (e
, data
) {
7684 this.sort(data
.parent
, true);
7686 .on("rename_node.jstree create_node.jstree", $.proxy(function (e
, data
) {
7687 this.sort(data
.parent
|| data
.node
.parent
, false);
7688 this.redraw_node(data
.parent
|| data
.node
.parent
, true);
7690 .on("move_node.jstree copy_node.jstree", $.proxy(function (e
, data
) {
7691 this.sort(data
.parent
, false);
7692 this.redraw_node(data
.parent
, true);
7696 * used to sort a node's children
7698 * @name sort(obj [, deep])
7699 * @param {mixed} obj the node
7700 * @param {Boolean} deep if set to `true` nodes are sorted recursively.
7702 * @trigger search.jstree
7704 this.sort = function (obj
, deep
) {
7706 obj
= this.get_node(obj
);
7707 if(obj
&& obj
.children
&& obj
.children
.length
) {
7708 obj
.children
.sort($.proxy(this.settings
.sort
, this));
7710 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
7711 this.sort(obj
.children_d
[i
], false);
7718 // include the sort plugin by default
7719 // $.jstree.defaults.plugins.push("sort");
7724 * Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc)
7729 * stores all defaults for the state plugin
7730 * @name $.jstree.defaults.state
7733 $.jstree
.defaults
.state
= {
7735 * A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`.
7736 * @name $.jstree.defaults.state.key
7741 * A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`.
7742 * @name $.jstree.defaults.state.events
7745 events
: 'changed.jstree open_node.jstree close_node.jstree check_node.jstree uncheck_node.jstree',
7747 * Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire.
7748 * @name $.jstree.defaults.state.ttl
7753 * A function that will be executed prior to restoring state with one argument - the state object. Can be used to clear unwanted parts of the state.
7754 * @name $.jstree.defaults.state.filter
7759 $.jstree
.plugins
.state = function (options
, parent
) {
7760 this.bind = function () {
7761 parent
.bind
.call(this);
7762 var bind
= $.proxy(function () {
7763 this.element
.on(this.settings
.state
.events
, $.proxy(function () {
7764 if(to
) { clearTimeout(to
); }
7765 to
= setTimeout($.proxy(function () { this.save_state(); }, this), 100);
7768 * triggered when the state plugin is finished restoring the state (and immediately after ready if there is no state to restore).
7770 * @name state_ready.jstree
7773 this.trigger('state_ready');
7776 .on("ready.jstree", $.proxy(function (e
, data
) {
7777 this.element
.one("restore_state.jstree", bind
);
7778 if(!this.restore_state()) { bind(); }
7783 * @name save_state()
7786 this.save_state = function () {
7787 var st
= { 'state' : this.get_state(), 'ttl' : this.settings
.state
.ttl
, 'sec' : +(new Date()) };
7788 $.vakata
.storage
.set(this.settings
.state
.key
, JSON
.stringify(st
));
7791 * restore the state from the user's computer
7792 * @name restore_state()
7795 this.restore_state = function () {
7796 var k
= $.vakata
.storage
.get(this.settings
.state
.key
);
7797 if(!!k
) { try { k
= JSON
.parse(k
); } catch(ex
) { return false; } }
7798 if(!!k
&& k
.ttl
&& k
.sec
&& +(new Date()) - k
.sec
> k
.ttl
) { return false; }
7799 if(!!k
&& k
.state
) { k
= k
.state
; }
7800 if(!!k
&& $.isFunction(this.settings
.state
.filter
)) { k
= this.settings
.state
.filter
.call(this, k
); }
7802 this.element
.one("set_state.jstree", function (e
, data
) { data
.instance
.trigger('restore_state', { 'state' : $.extend(true, {}, k
) }); });
7809 * clear the state on the user's computer
7810 * @name clear_state()
7813 this.clear_state = function () {
7814 return $.vakata
.storage
.del(this.settings
.state
.key
);
7818 (function ($, undefined) {
7819 $.vakata
.storage
= {
7820 // simply specifying the functions in FF throws an error
7821 set : function (key
, val
) { return window
.localStorage
.setItem(key
, val
); },
7822 get : function (key
) { return window
.localStorage
.getItem(key
); },
7823 del : function (key
) { return window
.localStorage
.removeItem(key
); }
7827 // include the state plugin by default
7828 // $.jstree.defaults.plugins.push("state");
7833 * Makes it possible to add predefined types for groups of nodes, which make it possible to easily control nesting rules and icon for each group.
7837 * An object storing all types as key value pairs, where the key is the type name and the value is an object that could contain following keys (all optional).
7839 * * `max_children` the maximum number of immediate children this node type can have. Do not specify or set to `-1` for unlimited.
7840 * * `max_depth` the maximum number of nesting this node type can have. A value of `1` would mean that the node can have children, but no grandchildren. Do not specify or set to `-1` for unlimited.
7841 * * `valid_children` an array of node type strings, that nodes of this type can have as children. Do not specify or set to `-1` for no limits.
7842 * * `icon` a string - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class. Omit to use the default icon from your theme.
7843 * * `li_attr` an object of values which will be used to add HTML attributes on the resulting LI DOM node (merged with the node's own data)
7844 * * `a_attr` an object of values which will be used to add HTML attributes on the resulting A DOM node (merged with the node's own data)
7846 * There are two predefined types:
7848 * * `#` represents the root of the tree, for example `max_children` would control the maximum number of root nodes.
7849 * * `default` represents the default node - any settings here will be applied to all nodes that do not have a type specified.
7851 * @name $.jstree.defaults.types
7854 $.jstree
.defaults
.types
= {
7857 $.jstree
.defaults
.types
[$.jstree
.root
] = {};
7859 $.jstree
.plugins
.types = function (options
, parent
) {
7860 this.init = function (el
, options
) {
7862 if(options
&& options
.types
&& options
.types
['default']) {
7863 for(i
in options
.types
) {
7864 if(i
!== "default" && i
!== $.jstree
.root
&& options
.types
.hasOwnProperty(i
)) {
7865 for(j
in options
.types
['default']) {
7866 if(options
.types
['default'].hasOwnProperty(j
) && options
.types
[i
][j
] === undefined) {
7867 options
.types
[i
][j
] = options
.types
['default'][j
];
7873 parent
.init
.call(this, el
, options
);
7874 this._model
.data
[$.jstree
.root
].type
= $.jstree
.root
;
7876 this.refresh = function (skip_loading
, forget_state
) {
7877 parent
.refresh
.call(this, skip_loading
, forget_state
);
7878 this._model
.data
[$.jstree
.root
].type
= $.jstree
.root
;
7880 this.bind = function () {
7882 .on('model.jstree', $.proxy(function (e
, data
) {
7883 var m
= this._model
.data
,
7885 t
= this.settings
.types
,
7886 i
, j
, c
= 'default', k
;
7887 for(i
= 0, j
= dpc
.length
; i
< j
; i
++) {
7889 if(m
[dpc
[i
]].original
&& m
[dpc
[i
]].original
.type
&& t
[m
[dpc
[i
]].original
.type
]) {
7890 c
= m
[dpc
[i
]].original
.type
;
7892 if(m
[dpc
[i
]].data
&& m
[dpc
[i
]].data
.jstree
&& m
[dpc
[i
]].data
.jstree
.type
&& t
[m
[dpc
[i
]].data
.jstree
.type
]) {
7893 c
= m
[dpc
[i
]].data
.jstree
.type
;
7896 if(m
[dpc
[i
]].icon
=== true && t
[c
].icon
!== undefined) {
7897 m
[dpc
[i
]].icon
= t
[c
].icon
;
7899 if(t
[c
].li_attr
!== undefined && typeof t
[c
].li_attr
=== 'object') {
7900 for (k
in t
[c
].li_attr
) {
7901 if (t
[c
].li_attr
.hasOwnProperty(k
)) {
7905 else if (m
[dpc
[i
]].li_attr
[k
] === undefined) {
7906 m
[dpc
[i
]].li_attr
[k
] = t
[c
].li_attr
[k
];
7908 else if (k
=== 'class') {
7909 m
[dpc
[i
]].li_attr
['class'] = t
[c
].li_attr
['class'] + ' ' + m
[dpc
[i
]].li_attr
['class'];
7914 if(t
[c
].a_attr
!== undefined && typeof t
[c
].a_attr
=== 'object') {
7915 for (k
in t
[c
].a_attr
) {
7916 if (t
[c
].a_attr
.hasOwnProperty(k
)) {
7920 else if (m
[dpc
[i
]].a_attr
[k
] === undefined) {
7921 m
[dpc
[i
]].a_attr
[k
] = t
[c
].a_attr
[k
];
7923 else if (k
=== 'href' && m
[dpc
[i
]].a_attr
[k
] === '#') {
7924 m
[dpc
[i
]].a_attr
['href'] = t
[c
].a_attr
['href'];
7926 else if (k
=== 'class') {
7927 m
[dpc
[i
]].a_attr
['class'] = t
[c
].a_attr
['class'] + ' ' + m
[dpc
[i
]].a_attr
['class'];
7933 m
[$.jstree
.root
].type
= $.jstree
.root
;
7935 parent
.bind
.call(this);
7937 this.get_json = function (obj
, options
, flat
) {
7939 m
= this._model
.data
,
7940 opt
= options
? $.extend(true, {}, options
, {no_id
:false}) : {},
7941 tmp
= parent
.get_json
.call(this, obj
, opt
, flat
);
7942 if(tmp
=== false) { return false; }
7943 if($.isArray(tmp
)) {
7944 for(i
= 0, j
= tmp
.length
; i
< j
; i
++) {
7945 tmp
[i
].type
= tmp
[i
].id
&& m
[tmp
[i
].id
] && m
[tmp
[i
].id
].type
? m
[tmp
[i
].id
].type
: "default";
7946 if(options
&& options
.no_id
) {
7948 if(tmp
[i
].li_attr
&& tmp
[i
].li_attr
.id
) {
7949 delete tmp
[i
].li_attr
.id
;
7951 if(tmp
[i
].a_attr
&& tmp
[i
].a_attr
.id
) {
7952 delete tmp
[i
].a_attr
.id
;
7958 tmp
.type
= tmp
.id
&& m
[tmp
.id
] && m
[tmp
.id
].type
? m
[tmp
.id
].type
: "default";
7959 if(options
&& options
.no_id
) {
7960 tmp
= this._delete_ids(tmp
);
7965 this._delete_ids = function (tmp
) {
7966 if($.isArray(tmp
)) {
7967 for(var i
= 0, j
= tmp
.length
; i
< j
; i
++) {
7968 tmp
[i
] = this._delete_ids(tmp
[i
]);
7973 if(tmp
.li_attr
&& tmp
.li_attr
.id
) {
7974 delete tmp
.li_attr
.id
;
7976 if(tmp
.a_attr
&& tmp
.a_attr
.id
) {
7977 delete tmp
.a_attr
.id
;
7979 if(tmp
.children
&& $.isArray(tmp
.children
)) {
7980 tmp
.children
= this._delete_ids(tmp
.children
);
7984 this.check = function (chk
, obj
, par
, pos
, more
) {
7985 if(parent
.check
.call(this, chk
, obj
, par
, pos
, more
) === false) { return false; }
7986 obj
= obj
&& obj
.id
? obj
: this.get_node(obj
);
7987 par
= par
&& par
.id
? par
: this.get_node(par
);
7988 var m
= obj
&& obj
.id
? (more
&& more
.origin
? more
.origin
: $.jstree
.reference(obj
.id
)) : null, tmp
, d
, i
, j
;
7989 m
= m
&& m
._model
&& m
._model
.data
? m
._model
.data
: null;
7994 if(chk
!== 'move_node' || $.inArray(obj
.id
, par
.children
) === -1) {
7995 tmp
= this.get_rules(par
);
7996 if(tmp
.max_children
!== undefined && tmp
.max_children
!== -1 && tmp
.max_children
=== par
.children
.length
) {
7997 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_01', 'reason' : 'max_children prevents function: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8000 if(tmp
.valid_children
!== undefined && tmp
.valid_children
!== -1 && $.inArray((obj
.type
|| 'default'), tmp
.valid_children
) === -1) {
8001 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_02', 'reason' : 'valid_children prevents function: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8004 if(m
&& obj
.children_d
&& obj
.parents
) {
8006 for(i
= 0, j
= obj
.children_d
.length
; i
< j
; i
++) {
8007 d
= Math
.max(d
, m
[obj
.children_d
[i
]].parents
.length
);
8009 d
= d
- obj
.parents
.length
+ 1;
8011 if(d
<= 0 || d
=== undefined) { d
= 1; }
8013 if(tmp
.max_depth
!== undefined && tmp
.max_depth
!== -1 && tmp
.max_depth
< d
) {
8014 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_03', 'reason' : 'max_depth prevents function: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8017 par
= this.get_node(par
.parent
);
8018 tmp
= this.get_rules(par
);
8027 * used to retrieve the type settings object for a node
8028 * @name get_rules(obj)
8029 * @param {mixed} obj the node to find the rules for
8033 this.get_rules = function (obj
) {
8034 obj
= this.get_node(obj
);
8035 if(!obj
) { return false; }
8036 var tmp
= this.get_type(obj
, true);
8037 if(tmp
.max_depth
=== undefined) { tmp
.max_depth
= -1; }
8038 if(tmp
.max_children
=== undefined) { tmp
.max_children
= -1; }
8039 if(tmp
.valid_children
=== undefined) { tmp
.valid_children
= -1; }
8043 * used to retrieve the type string or settings object for a node
8044 * @name get_type(obj [, rules])
8045 * @param {mixed} obj the node to find the rules for
8046 * @param {Boolean} rules if set to `true` instead of a string the settings object will be returned
8047 * @return {String|Object}
8050 this.get_type = function (obj
, rules
) {
8051 obj
= this.get_node(obj
);
8052 return (!obj
) ? false : ( rules
? $.extend({ 'type' : obj
.type
}, this.settings
.types
[obj
.type
]) : obj
.type
);
8055 * used to change a node's type
8056 * @name set_type(obj, type)
8057 * @param {mixed} obj the node to change
8058 * @param {String} type the new type
8061 this.set_type = function (obj
, type
) {
8062 var m
= this._model
.data
, t
, t1
, t2
, old_type
, old_icon
, k
, d
, a
;
8063 if($.isArray(obj
)) {
8065 for(t1
= 0, t2
= obj
.length
; t1
< t2
; t1
++) {
8066 this.set_type(obj
[t1
], type
);
8070 t
= this.settings
.types
;
8071 obj
= this.get_node(obj
);
8072 if(!t
[type
] || !obj
) { return false; }
8073 d
= this.get_node(obj
, true);
8074 if (d
&& d
.length
) {
8075 a
= d
.children('.jstree-anchor');
8077 old_type
= obj
.type
;
8078 old_icon
= this.get_icon(obj
);
8080 if(old_icon
=== true || !t
[old_type
] || (t
[old_type
].icon
!== undefined && old_icon
=== t
[old_type
].icon
)) {
8081 this.set_icon(obj
, t
[type
].icon
!== undefined ? t
[type
].icon
: true);
8084 // remove old type props
8085 if(t
[old_type
] && t
[old_type
].li_attr
!== undefined && typeof t
[old_type
].li_attr
=== 'object') {
8086 for (k
in t
[old_type
].li_attr
) {
8087 if (t
[old_type
].li_attr
.hasOwnProperty(k
)) {
8091 else if (k
=== 'class') {
8092 m
[obj
.id
].li_attr
['class'] = (m
[obj
.id
].li_attr
['class'] || '').replace(t
[old_type
].li_attr
[k
], '');
8093 if (d
) { d
.removeClass(t
[old_type
].li_attr
[k
]); }
8095 else if (m
[obj
.id
].li_attr
[k
] === t
[old_type
].li_attr
[k
]) {
8096 m
[obj
.id
].li_attr
[k
] = null;
8097 if (d
) { d
.removeAttr(k
); }
8102 if(t
[old_type
] && t
[old_type
].a_attr
!== undefined && typeof t
[old_type
].a_attr
=== 'object') {
8103 for (k
in t
[old_type
].a_attr
) {
8104 if (t
[old_type
].a_attr
.hasOwnProperty(k
)) {
8108 else if (k
=== 'class') {
8109 m
[obj
.id
].a_attr
['class'] = (m
[obj
.id
].a_attr
['class'] || '').replace(t
[old_type
].a_attr
[k
], '');
8110 if (a
) { a
.removeClass(t
[old_type
].a_attr
[k
]); }
8112 else if (m
[obj
.id
].a_attr
[k
] === t
[old_type
].a_attr
[k
]) {
8114 m
[obj
.id
].a_attr
[k
] = '#';
8115 if (a
) { a
.attr('href', '#'); }
8118 delete m
[obj
.id
].a_attr
[k
];
8119 if (a
) { a
.removeAttr(k
); }
8127 if(t
[type
].li_attr
!== undefined && typeof t
[type
].li_attr
=== 'object') {
8128 for (k
in t
[type
].li_attr
) {
8129 if (t
[type
].li_attr
.hasOwnProperty(k
)) {
8133 else if (m
[obj
.id
].li_attr
[k
] === undefined) {
8134 m
[obj
.id
].li_attr
[k
] = t
[type
].li_attr
[k
];
8136 if (k
=== 'class') {
8137 d
.addClass(t
[type
].li_attr
[k
]);
8140 d
.attr(k
, t
[type
].li_attr
[k
]);
8144 else if (k
=== 'class') {
8145 m
[obj
.id
].li_attr
['class'] = t
[type
].li_attr
[k
] + ' ' + m
[obj
.id
].li_attr
['class'];
8146 if (d
) { d
.addClass(t
[type
].li_attr
[k
]); }
8151 if(t
[type
].a_attr
!== undefined && typeof t
[type
].a_attr
=== 'object') {
8152 for (k
in t
[type
].a_attr
) {
8153 if (t
[type
].a_attr
.hasOwnProperty(k
)) {
8157 else if (m
[obj
.id
].a_attr
[k
] === undefined) {
8158 m
[obj
.id
].a_attr
[k
] = t
[type
].a_attr
[k
];
8160 if (k
=== 'class') {
8161 a
.addClass(t
[type
].a_attr
[k
]);
8164 a
.attr(k
, t
[type
].a_attr
[k
]);
8168 else if (k
=== 'href' && m
[obj
.id
].a_attr
[k
] === '#') {
8169 m
[obj
.id
].a_attr
['href'] = t
[type
].a_attr
['href'];
8170 if (a
) { a
.attr('href', t
[type
].a_attr
['href']); }
8172 else if (k
=== 'class') {
8173 m
[obj
.id
].a_attr
['class'] = t
[type
].a_attr
['class'] + ' ' + m
[obj
.id
].a_attr
['class'];
8174 if (a
) { a
.addClass(t
[type
].a_attr
[k
]); }
8183 // include the types plugin by default
8184 // $.jstree.defaults.plugins.push("types");
8190 * Enforces that no nodes with the same name can coexist as siblings.
8194 * stores all defaults for the unique plugin
8195 * @name $.jstree.defaults.unique
8198 $.jstree
.defaults
.unique
= {
8200 * Indicates if the comparison should be case sensitive. Default is `false`.
8201 * @name $.jstree.defaults.unique.case_sensitive
8204 case_sensitive
: false,
8206 * A callback executed in the instance's scope when a new node is created and the name is already taken, the two arguments are the conflicting name and the counter. The default will produce results like `New node (2)`.
8207 * @name $.jstree.defaults.unique.duplicate
8210 duplicate : function (name
, counter
) {
8211 return name
+ ' (' + counter
+ ')';
8215 $.jstree
.plugins
.unique = function (options
, parent
) {
8216 this.check = function (chk
, obj
, par
, pos
, more
) {
8217 if(parent
.check
.call(this, chk
, obj
, par
, pos
, more
) === false) { return false; }
8218 obj
= obj
&& obj
.id
? obj
: this.get_node(obj
);
8219 par
= par
&& par
.id
? par
: this.get_node(par
);
8220 if(!par
|| !par
.children
) { return true; }
8221 var n
= chk
=== "rename_node" ? pos
: obj
.text
,
8223 s
= this.settings
.unique
.case_sensitive
,
8224 m
= this._model
.data
, i
, j
;
8225 for(i
= 0, j
= par
.children
.length
; i
< j
; i
++) {
8226 c
.push(s
? m
[par
.children
[i
]].text
: m
[par
.children
[i
]].text
.toLowerCase());
8228 if(!s
) { n
= n
.toLowerCase(); }
8233 i
= ($.inArray(n
, c
) === -1 || (obj
.text
&& obj
.text
[ s
? 'toString' : 'toLowerCase']() === n
));
8235 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_01', 'reason' : 'Child with name ' + n
+ ' already exists. Preventing: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8239 i
= ($.inArray(n
, c
) === -1);
8241 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_04', 'reason' : 'Child with name ' + n
+ ' already exists. Preventing: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8245 i
= ($.inArray(n
, c
) === -1);
8247 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_02', 'reason' : 'Child with name ' + n
+ ' already exists. Preventing: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8251 i
= ( (obj
.parent
=== par
.id
&& (!more
|| !more
.is_multi
)) || $.inArray(n
, c
) === -1);
8253 this._data
.core
.last_error
= { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_03', 'reason' : 'Child with name ' + n
+ ' already exists. Preventing: ' + chk
, 'data' : JSON
.stringify({ 'chk' : chk
, 'pos' : pos
, 'obj' : obj
&& obj
.id
? obj
.id
: false, 'par' : par
&& par
.id
? par
.id
: false }) };
8259 this.create_node = function (par
, node
, pos
, callback
, is_loaded
) {
8260 if(!node
|| node
.text
=== undefined) {
8262 par
= $.jstree
.root
;
8264 par
= this.get_node(par
);
8266 return parent
.create_node
.call(this, par
, node
, pos
, callback
, is_loaded
);
8268 pos
= pos
=== undefined ? "last" : pos
;
8269 if(!pos
.toString().match(/^(before|after)$/) && !is_loaded
&& !this.is_loaded(par
)) {
8270 return parent
.create_node
.call(this, par
, node
, pos
, callback
, is_loaded
);
8272 if(!node
) { node
= {}; }
8273 var tmp
, n
, dpc
, i
, j
, m
= this._model
.data
, s
= this.settings
.unique
.case_sensitive
, cb
= this.settings
.unique
.duplicate
;
8274 n
= tmp
= this.get_string('New node');
8276 for(i
= 0, j
= par
.children
.length
; i
< j
; i
++) {
8277 dpc
.push(s
? m
[par
.children
[i
]].text
: m
[par
.children
[i
]].text
.toLowerCase());
8280 while($.inArray(s
? n
: n
.toLowerCase(), dpc
) !== -1) {
8281 n
= cb
.call(this, tmp
, (++i
)).toString();
8285 return parent
.create_node
.call(this, par
, node
, pos
, callback
, is_loaded
);
8289 // include the unique plugin by default
8290 // $.jstree.defaults.plugins.push("unique");
8294 * ### Wholerow plugin
8296 * Makes each node appear block level. Making selection easier. May cause slow down for large trees in old browsers.
8299 var div
= document
.createElement('DIV');
8300 div
.setAttribute('unselectable','on');
8301 div
.setAttribute('role','presentation');
8302 div
.className
= 'jstree-wholerow';
8303 div
.innerHTML
= ' ';
8304 $.jstree
.plugins
.wholerow = function (options
, parent
) {
8305 this.bind = function () {
8306 parent
.bind
.call(this);
8309 .on('ready.jstree set_state.jstree', $.proxy(function () {
8312 .on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
8313 //div.style.height = this._data.core.li_height + 'px';
8314 this.get_container_ul().addClass('jstree-wholerow-ul');
8316 .on("deselect_all.jstree", $.proxy(function (e
, data
) {
8317 this.element
.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
8319 .on("changed.jstree", $.proxy(function (e
, data
) {
8320 this.element
.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
8321 var tmp
= false, i
, j
;
8322 for(i
= 0, j
= data
.selected
.length
; i
< j
; i
++) {
8323 tmp
= this.get_node(data
.selected
[i
], true);
8324 if(tmp
&& tmp
.length
) {
8325 tmp
.children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
8329 .on("open_node.jstree", $.proxy(function (e
, data
) {
8330 this.get_node(data
.node
, true).find('.jstree-clicked').parent().children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
8332 .on("hover_node.jstree dehover_node.jstree", $.proxy(function (e
, data
) {
8333 if(e
.type
=== "hover_node" && this.is_disabled(data
.node
)) { return; }
8334 this.get_node(data
.node
, true).children('.jstree-wholerow')[e
.type
=== "hover_node"?"addClass":"removeClass"]('jstree-wholerow-hovered');
8336 .on("contextmenu.jstree", ".jstree-wholerow", $.proxy(function (e
) {
8337 if (this._data
.contextmenu
) {
8339 var tmp
= $.Event('contextmenu', { metaKey
: e
.metaKey
, ctrlKey
: e
.ctrlKey
, altKey
: e
.altKey
, shiftKey
: e
.shiftKey
, pageX
: e
.pageX
, pageY
: e
.pageY
});
8340 $(e
.currentTarget
).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp
);
8344 .on("mousedown.jstree touchstart.jstree", ".jstree-wholerow", function (e) {
8345 if(e.target === e.currentTarget) {
8346 var a = $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor");
8352 .on("click.jstree", ".jstree-wholerow", function (e
) {
8353 e
.stopImmediatePropagation();
8354 var tmp
= $.Event('click', { metaKey
: e
.metaKey
, ctrlKey
: e
.ctrlKey
, altKey
: e
.altKey
, shiftKey
: e
.shiftKey
});
8355 $(e
.currentTarget
).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp
).focus();
8357 .on("dblclick.jstree", ".jstree-wholerow", function (e
) {
8358 e
.stopImmediatePropagation();
8359 var tmp
= $.Event('dblclick', { metaKey
: e
.metaKey
, ctrlKey
: e
.ctrlKey
, altKey
: e
.altKey
, shiftKey
: e
.shiftKey
});
8360 $(e
.currentTarget
).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp
).focus();
8362 .on("click.jstree", ".jstree-leaf > .jstree-ocl", $.proxy(function (e
) {
8363 e
.stopImmediatePropagation();
8364 var tmp
= $.Event('click', { metaKey
: e
.metaKey
, ctrlKey
: e
.ctrlKey
, altKey
: e
.altKey
, shiftKey
: e
.shiftKey
});
8365 $(e
.currentTarget
).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp
).focus();
8367 .on("mouseover.jstree", ".jstree-wholerow, .jstree-icon", $.proxy(function (e
) {
8368 e
.stopImmediatePropagation();
8369 if(!this.is_disabled(e
.currentTarget
)) {
8370 this.hover_node(e
.currentTarget
);
8374 .on("mouseleave.jstree", ".jstree-node", $.proxy(function (e
) {
8375 this.dehover_node(e
.currentTarget
);
8378 this.teardown = function () {
8379 if(this.settings
.wholerow
) {
8380 this.element
.find(".jstree-wholerow").remove();
8382 parent
.teardown
.call(this);
8384 this.redraw_node = function(obj
, deep
, callback
, force_render
) {
8385 obj
= parent
.redraw_node
.apply(this, arguments
);
8387 var tmp
= div
.cloneNode(true);
8388 //tmp.style.height = this._data.core.li_height + 'px';
8389 if($.inArray(obj
.id
, this._data
.core
.selected
) !== -1) { tmp
.className
+= ' jstree-wholerow-clicked'; }
8390 if(this._data
.core
.focused
&& this._data
.core
.focused
=== obj
.id
) { tmp
.className
+= ' jstree-wholerow-hovered'; }
8391 obj
.insertBefore(tmp
, obj
.childNodes
[0]);
8396 // include the wholerow plugin by default
8397 // $.jstree.defaults.plugins.push("wholerow");
8398 if(document
.registerElement
&& Object
&& Object
.create
) {
8399 var proto
= Object
.create(HTMLElement
.prototype);
8400 proto
.createdCallback = function () {
8401 var c
= { core
: {}, plugins
: [] }, i
;
8402 for(i
in $.jstree
.plugins
) {
8403 if($.jstree
.plugins
.hasOwnProperty(i
) && this.attributes
[i
]) {
8405 if(this.getAttribute(i
) && JSON
.parse(this.getAttribute(i
))) {
8406 c
[i
] = JSON
.parse(this.getAttribute(i
));
8410 for(i
in $.jstree
.defaults
.core
) {
8411 if($.jstree
.defaults
.core
.hasOwnProperty(i
) && this.attributes
[i
]) {
8412 c
.core
[i
] = JSON
.parse(this.getAttribute(i
)) || this.getAttribute(i
);
8417 // proto.attributeChangedCallback = function (name, previous, value) { };
8419 document
.registerElement("vakata-jstree", { prototype: proto
});