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: browser: true, ass: true, bitwise: true, continue: true, nomen: true, plusplus: true, regexp: true, unparam: true, todo: true, white: true
29 // prevent another load? maybe there is a better way?
35 * ### jsTree core functionality
39 var instance_counter = 0,
44 src = $('script:last').attr('src'),
45 document = window.document, // local variable is always faster to access then a global
46 _node = document.createElement('LI'), _temp1, _temp2;
48 _node.setAttribute('role', 'treeitem');
49 _temp1 = document.createElement('I');
50 _temp1.className = 'jstree-icon jstree-ocl';
51 _temp1.setAttribute('role', 'presentation');
52 _node.appendChild(_temp1);
53 _temp1 = document.createElement('A');
54 _temp1.className = 'jstree-anchor';
55 _temp1.setAttribute('href','#');
56 _temp1.setAttribute('tabindex','-1');
57 _temp2 = document.createElement('I');
58 _temp2.className = 'jstree-icon jstree-themeicon';
59 _temp2.setAttribute('role', 'presentation');
60 _temp1.appendChild(_temp2);
61 _node.appendChild(_temp1);
62 _temp1 = _temp2 = null;
66 * holds all jstree related functions and variables, including the actual class and methods to create, access and manipulate instances.
71 * specifies the jstree version in use
72 * @name $.jstree.version
76 * holds all the default options used when creating new instances
77 * @name $.jstree.defaults
81 * 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 `[]`
82 * @name $.jstree.defaults.plugins
87 * stores all loaded jstree plugins (used internally)
88 * @name $.jstree.plugins
91 path : src && src.indexOf('/') !== -1 ? src.replace(/\/[^\/]+$/,'') : '',
92 idregex : /[\\:&!^|()\[\]<>@*'+~#";.,=\- \/${}%?`]/g,
96 * creates a jstree instance
97 * @name $.jstree.create(el [, options])
98 * @param {DOMElement|jQuery|String} el the element to create the instance on, can be jQuery extended or a selector
99 * @param {Object} options options for this instance (extends `$.jstree.defaults`)
100 * @return {jsTree} the new instance
102 $.jstree.create = function (el, options) {
103 var tmp = new $.jstree.core(++instance_counter),
105 options = $.extend(true, {}, $.jstree.defaults, options);
106 if(opt && opt.plugins) {
107 options.plugins = opt.plugins;
109 $.each(options.plugins, function (i, k) {
111 tmp = tmp.plugin(k, options[k]);
114 $(el).data('jstree', tmp);
115 tmp.init(el, options);
119 * remove all traces of jstree from the DOM and destroy all instances
120 * @name $.jstree.destroy()
122 $.jstree.destroy = function () {
123 $('.jstree:jstree').jstree('destroy');
124 $(document).off('.jstree');
127 * the jstree class constructor, used only internally
129 * @name $.jstree.core(id)
130 * @param {Number} id this instance's index
132 $.jstree.core = function (id) {
152 * get a reference to an existing instance
156 * // provided a container with an ID of "tree", and a nested node with an ID of "branch"
157 * // all of there will return the same instance
158 * $.jstree.reference('tree');
159 * $.jstree.reference('#tree');
160 * $.jstree.reference($('#tree'));
161 * $.jstree.reference(document.getElementByID('tree'));
162 * $.jstree.reference('branch');
163 * $.jstree.reference('#branch');
164 * $.jstree.reference($('#branch'));
165 * $.jstree.reference(document.getElementByID('branch'));
167 * @name $.jstree.reference(needle)
168 * @param {DOMElement|jQuery|String} needle
169 * @return {jsTree|null} the instance or `null` if not found
171 $.jstree.reference = function (needle) {
174 if(needle && needle.id && (!needle.tagName || !needle.nodeType)) { needle = needle.id; }
176 if(!obj || !obj.length) {
177 try { obj = $(needle); } catch (ignore) { }
179 if(!obj || !obj.length) {
180 try { obj = $('#' + needle.replace($.jstree.idregex,'\\$&')); } catch (ignore) { }
182 if(obj && obj.length && (obj = obj.closest('.jstree')).length && (obj = obj.data('jstree'))) {
186 $('.jstree').each(function () {
187 var inst = $(this).data('jstree');
188 if(inst && inst._model.data[needle]) {
197 * Create an instance, get an instance or invoke a command on a instance.
199 * 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).
201 * 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).
203 * If there is an existing instance and `arg` is not a string the instance itself is returned (similar to `$.jstree.reference`).
205 * In any other case - nothing is returned and chaining is not broken.
209 * $('#tree1').jstree(); // creates an instance
210 * $('#tree2').jstree({ plugins : [] }); // create an instance with some options
211 * $('#tree1').jstree('open_node', '#branch_1'); // call a method on an existing instance, passing additional arguments
212 * $('#tree2').jstree(); // get an existing instance (or create an instance)
213 * $('#tree2').jstree(true); // get an existing instance (will not create new instance)
214 * $('#branch_1').jstree().select_node('#branch_1'); // get an instance (using a nested element and call a method)
216 * @name $().jstree([arg])
217 * @param {String|Object} arg
220 $.fn.jstree = function (arg) {
221 // check for string argument
222 var is_method = (typeof arg === 'string'),
223 args = Array.prototype.slice.call(arguments, 1),
225 if(arg === true && !this.length) { return false; }
226 this.each(function () {
227 // get the instance (if there is one) and method (if it exists)
228 var instance = $.jstree.reference(this),
229 method = is_method && instance ? instance[arg] : null;
230 // if calling a method, and method is available - execute on the instance
231 result = is_method && method ?
232 method.apply(instance, args) :
234 // if there is no instance and no method is being called - create one
235 if(!instance && !is_method && (arg === undefined || $.isPlainObject(arg))) {
236 $.jstree.create(this, arg);
238 // if there is an instance and no method is called - return the instance
239 if( (instance && !is_method) || arg === true ) {
240 result = instance || false;
242 // if there was a method call which returned a result - break and return the value
243 if(result !== null && result !== undefined) {
247 // if there was a method call with a valid return value - return that, otherwise continue the chain
248 return result !== null && result !== undefined ?
252 * used to find elements containing an instance
256 * $('div:jstree').each(function () {
257 * $(this).jstree('destroy');
263 $.expr[':'].jstree = $.expr.createPseudo(function(search) {
265 return $(a).hasClass('jstree') &&
266 $(a).data('jstree') !== undefined;
271 * stores all defaults for the core
272 * @name $.jstree.defaults.core
274 $.jstree.defaults.core = {
278 * 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).
280 * You can also pass in a HTML string or a JSON array here.
282 * 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.
283 * 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.
285 * 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.
290 * $('#tree').jstree({
293 * 'url' : '/get/children/',
294 * 'data' : function (node) {
295 * return { 'id' : node.id };
301 * $('#tree').jstree({
304 * 'Simple root node',
307 * 'text' : 'Root node with options',
308 * 'state' : { 'opened' : true, 'selected' : true },
309 * 'children' : [ { 'text' : 'Child 1' }, 'Child 2']
315 * $('#tree').jstree({
317 * 'data' : function (obj, callback) {
318 * callback.call(this, ['Root 1', 'Root 2']);
322 * @name $.jstree.defaults.core.data
326 * configure the various strings used throughout the tree
328 * You can use an object where the key is the string you need to replace and the value is your replacement.
329 * Another option is to specify a function which will be called with an argument of the needed string and should return the replacement.
330 * If left as `false` no replacement is made.
334 * $('#tree').jstree({
337 * 'Loading ...' : 'Please wait ...'
342 * @name $.jstree.defaults.core.strings
346 * determines what happens when a user tries to modify the structure of the tree
347 * If left as `false` all operations like create, rename, delete, move or copy are prevented.
348 * You can set this to `true` to allow all interactions or use a function to have better control.
352 * $('#tree').jstree({
354 * 'check_callback' : function (operation, node, node_parent, node_position, more) {
355 * // operation can be 'create_node', 'rename_node', 'delete_node', 'move_node' or 'copy_node'
356 * // in case of 'rename_node' node_position is filled with the new node name
357 * return operation === 'rename_node' ? true : false;
362 * @name $.jstree.defaults.core.check_callback
364 check_callback : false,
366 * a callback called with a single object parameter in the instance's scope when something goes wrong (operation prevented, ajax failed, etc)
367 * @name $.jstree.defaults.core.error
371 * the open / close animation duration in milliseconds - set this to `false` to disable the animation (default is `200`)
372 * @name $.jstree.defaults.core.animation
376 * a boolean indicating if multiple nodes can be selected
377 * @name $.jstree.defaults.core.multiple
381 * theme configuration object
382 * @name $.jstree.defaults.core.themes
386 * the name of the theme to use (if left as `false` the default theme is used)
387 * @name $.jstree.defaults.core.themes.name
391 * 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.
392 * @name $.jstree.defaults.core.themes.url
396 * the location of all jstree themes - only used if `url` is set to `true`
397 * @name $.jstree.defaults.core.themes.dir
401 * a boolean indicating if connecting dots are shown
402 * @name $.jstree.defaults.core.themes.dots
406 * a boolean indicating if node icons are shown
407 * @name $.jstree.defaults.core.themes.icons
411 * a boolean indicating if the tree background is striped
412 * @name $.jstree.defaults.core.themes.stripes
416 * a string (or boolean `false`) specifying the theme variant to use (if the theme supports variants)
417 * @name $.jstree.defaults.core.themes.variant
421 * a boolean specifying if a reponsive version of the theme should kick in on smaller screens (if the theme supports it). Defaults to `false`.
422 * @name $.jstree.defaults.core.themes.responsive
427 * 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)
428 * @name $.jstree.defaults.core.expand_selected_onload
430 expand_selected_onload : true,
432 * 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`
433 * @name $.jstree.defaults.core.worker
437 * Force node text to plain text (and escape HTML). Defaults to `false`
438 * @name $.jstree.defaults.core.force_text
442 * Should the node should be toggled if the text is double clicked . Defaults to `true`
443 * @name $.jstree.defaults.core.dblclick_toggle
445 dblclick_toggle : true
447 $.jstree.core.prototype = {
449 * used to decorate an instance with a plugin. Used internally.
451 * @name plugin(deco [, opts])
452 * @param {String} deco the plugin to decorate with
453 * @param {Object} opts options for the plugin
456 plugin : function (deco, opts) {
457 var Child = $.jstree.plugins[deco];
459 this._data[deco] = {};
460 Child.prototype = this;
461 return new Child(opts, this);
466 * initialize the instance. Used internally.
468 * @name init(el, optons)
469 * @param {DOMElement|jQuery|String} el the element we are transforming
470 * @param {Object} options options for this instance
471 * @trigger init.jstree, loading.jstree, loaded.jstree, ready.jstree, changed.jstree
473 init : function (el, options) {
477 force_full_redraw : false,
478 redraw_timeout : false,
486 this._model.data[$.jstree.root] = {
492 state : { loaded : false }
495 this.element = $(el).addClass('jstree jstree-' + this._id);
496 this.settings = options;
498 this._data.core.ready = false;
499 this._data.core.loaded = false;
500 this._data.core.rtl = (this.element.css("direction") === "rtl");
501 this.element[this._data.core.rtl ? 'addClass' : 'removeClass']("jstree-rtl");
502 this.element.attr('role','tree');
503 if(this.settings.core.multiple) {
504 this.element.attr('aria-multiselectable', true);
506 if(!this.element.attr('tabindex')) {
507 this.element.attr('tabindex','0');
512 * triggered after all events are bound
516 this.trigger("init");
518 this._data.core.original_container_html = this.element.find(" > ul > li").clone(true);
519 this._data.core.original_container_html
520 .find("li").addBack()
521 .contents().filter(function() {
522 return this.nodeType === 3 && (!this.nodeValue || /^\s+$/.test(this.nodeValue));
525 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>");
526 this.element.attr('aria-activedescendant','j' + this._id + '_loading');
527 this._data.core.li_height = this.get_container_ul().children("li").first().height() || 24;
529 * triggered after the loading text is shown and before loading starts
531 * @name loading.jstree
533 this.trigger("loading");
534 this.load_node($.jstree.root);
537 * destroy an instance
539 * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact
541 destroy : function (keep_html) {
544 window.URL.revokeObjectURL(this._wrk);
549 if(!keep_html) { this.element.empty(); }
553 * part of the destroying of an instance. Used internally.
557 teardown : function () {
560 .removeClass('jstree')
561 .removeData('jstree')
562 .find("[class^='jstree']")
564 .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); });
568 * bind all events. Used internally.
577 .on("dblclick.jstree", function (e) {
578 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
579 if(document.selection && document.selection.empty) {
580 document.selection.empty();
583 if(window.getSelection) {
584 var sel = window.getSelection();
586 sel.removeAllRanges();
592 .on("mousedown.jstree", $.proxy(function (e) {
593 if(e.target === this.element[0]) {
594 e.preventDefault(); // prevent losing focus when clicking scroll arrows (FF, Chrome)
595 was_click = +(new Date()); // ie does not allow to prevent losing focus
598 .on("mousedown.jstree", ".jstree-ocl", function (e) {
599 e.preventDefault(); // prevent any node inside from losing focus when clicking the open/close icon
601 .on("click.jstree", ".jstree-ocl", $.proxy(function (e) {
602 this.toggle_node(e.target);
604 .on("dblclick.jstree", ".jstree-anchor", $.proxy(function (e) {
605 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
606 if(this.settings.core.dblclick_toggle) {
607 this.toggle_node(e.target);
610 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
612 if(e.currentTarget !== document.activeElement) { $(e.currentTarget).focus(); }
613 this.activate_node(e.currentTarget, e);
615 .on('keydown.jstree', '.jstree-anchor', $.proxy(function (e) {
616 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
617 if(e.which !== 32 && e.which !== 13 && (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) { return true; }
619 if(this._data.core.rtl) {
620 if(e.which === 37) { e.which = 39; }
621 else if(e.which === 39) { e.which = 37; }
624 case 32: // aria defines space only with Ctrl
627 $(e.currentTarget).trigger(e);
632 $(e.currentTarget).trigger(e);
636 if(this.is_open(e.currentTarget)) {
637 this.close_node(e.currentTarget);
640 o = this.get_parent(e.currentTarget);
641 if(o && o.id !== $.jstree.root) { this.get_node(o, true).children('.jstree-anchor').focus(); }
646 o = this.get_prev_dom(e.currentTarget);
647 if(o && o.length) { o.children('.jstree-anchor').focus(); }
651 if(this.is_closed(e.currentTarget)) {
652 this.open_node(e.currentTarget, function (o) { this.get_node(o, true).children('.jstree-anchor').focus(); });
654 else if (this.is_open(e.currentTarget)) {
655 o = this.get_node(e.currentTarget, true).children('.jstree-children')[0];
656 if(o) { $(this._firstChild(o)).children('.jstree-anchor').focus(); }
661 o = this.get_next_dom(e.currentTarget);
662 if(o && o.length) { o.children('.jstree-anchor').focus(); }
664 case 106: // aria defines * on numpad as open_all - not very common
669 o = this._firstChild(this.get_container_ul()[0]);
670 if(o) { $(o).children('.jstree-anchor').filter(':visible').focus(); }
674 this.element.find('.jstree-anchor').filter(':visible').last().focus();
680 o = this.get_node(e.currentTarget);
681 if(o && o.id && o.id !== $.jstree.root) {
682 o = this.is_selected(o) ? this.get_selected() : o;
689 o = this.get_node(e.currentTarget);
690 if(o && o.id && o.id !== $.jstree.root) {
695 // console.log(e.which);
700 .on("load_node.jstree", $.proxy(function (e, data) {
702 if(data.node.id === $.jstree.root && !this._data.core.loaded) {
703 this._data.core.loaded = true;
704 if(this._firstChild(this.get_container_ul()[0])) {
705 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
708 * triggered after the root node is loaded for the first time
710 * @name loaded.jstree
712 this.trigger("loaded");
714 if(!this._data.core.ready) {
715 setTimeout($.proxy(function() {
716 if(this.element && !this.get_container_ul().find('.jstree-loading').length) {
717 this._data.core.ready = true;
718 if(this._data.core.selected.length) {
719 if(this.settings.core.expand_selected_onload) {
721 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
722 tmp = tmp.concat(this._model.data[this._data.core.selected[i]].parents);
724 tmp = $.vakata.array_unique(tmp);
725 for(i = 0, j = tmp.length; i < j; i++) {
726 this.open_node(tmp[i], false, 0);
729 this.trigger('changed', { 'action' : 'ready', 'selected' : this._data.core.selected });
732 * triggered after all nodes are finished loading
736 this.trigger("ready");
742 // quick searching when the tree is focused
743 .on('keypress.jstree', $.proxy(function (e) {
744 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
745 if(tout) { clearTimeout(tout); }
746 tout = setTimeout(function () {
750 var chr = String.fromCharCode(e.which).toLowerCase(),
751 col = this.element.find('.jstree-anchor').filter(':visible'),
752 ind = col.index(document.activeElement) || 0,
756 // match for whole word from current node down (including the current node)
757 if(word.length > 1) {
758 col.slice(ind).each($.proxy(function (i, v) {
759 if($(v).text().toLowerCase().indexOf(word) === 0) {
767 // match for whole word from the beginning of the tree
768 col.slice(0, ind).each($.proxy(function (i, v) {
769 if($(v).text().toLowerCase().indexOf(word) === 0) {
777 // list nodes that start with that letter (only if word consists of a single char)
778 if(new RegExp('^' + chr.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '+$').test(word)) {
779 // search for the next node starting with that letter
780 col.slice(ind + 1).each($.proxy(function (i, v) {
781 if($(v).text().toLowerCase().charAt(0) === chr) {
789 // search from the beginning
790 col.slice(0, ind + 1).each($.proxy(function (i, v) {
791 if($(v).text().toLowerCase().charAt(0) === chr) {
801 .on("init.jstree", $.proxy(function () {
802 var s = this.settings.core.themes;
803 this._data.core.themes.dots = s.dots;
804 this._data.core.themes.stripes = s.stripes;
805 this._data.core.themes.icons = s.icons;
806 this.set_theme(s.name || "default", s.url);
807 this.set_theme_variant(s.variant);
809 .on("loading.jstree", $.proxy(function () {
810 this[ this._data.core.themes.dots ? "show_dots" : "hide_dots" ]();
811 this[ this._data.core.themes.icons ? "show_icons" : "hide_icons" ]();
812 this[ this._data.core.themes.stripes ? "show_stripes" : "hide_stripes" ]();
814 .on('blur.jstree', '.jstree-anchor', $.proxy(function (e) {
815 this._data.core.focused = null;
816 $(e.currentTarget).filter('.jstree-hovered').mouseleave();
817 this.element.attr('tabindex', '0');
819 .on('focus.jstree', '.jstree-anchor', $.proxy(function (e) {
820 var tmp = this.get_node(e.currentTarget);
822 this._data.core.focused = tmp.id;
824 this.element.find('.jstree-hovered').not(e.currentTarget).mouseleave();
825 $(e.currentTarget).mouseenter();
826 this.element.attr('tabindex', '-1');
828 .on('focus.jstree', $.proxy(function () {
829 if(+(new Date()) - was_click > 500 && !this._data.core.focused) {
831 var act = this.get_node(this.element.attr('aria-activedescendant'), true);
833 act.find('> .jstree-anchor').focus();
837 .on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e) {
838 this.hover_node(e.currentTarget);
840 .on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e) {
841 this.dehover_node(e.currentTarget);
845 * part of the destroying of an instance. Used internally.
849 unbind : function () {
850 this.element.off('.jstree');
851 $(document).off('.jstree-' + this._id);
854 * trigger an event. Used internally.
856 * @name trigger(ev [, data])
857 * @param {String} ev the name of the event to trigger
858 * @param {Object} data additional data to pass with the event
860 trigger : function (ev, data) {
864 data.instance = this;
865 this.element.triggerHandler(ev.replace('.jstree','') + '.jstree', data);
868 * returns the jQuery extended instance container
869 * @name get_container()
872 get_container : function () {
876 * returns the jQuery extended main UL node inside the instance container. Used internally.
878 * @name get_container_ul()
881 get_container_ul : function () {
882 return this.element.children(".jstree-children").first();
885 * gets string replacements (localization). Used internally.
887 * @name get_string(key)
888 * @param {String} key
891 get_string : function (key) {
892 var a = this.settings.core.strings;
893 if($.isFunction(a)) { return a.call(this, key); }
894 if(a && a[key]) { return a[key]; }
898 * gets the first child of a DOM node. Used internally.
900 * @name _firstChild(dom)
901 * @param {DOMElement} dom
902 * @return {DOMElement}
904 _firstChild : function (dom) {
905 dom = dom ? dom.firstChild : null;
906 while(dom !== null && dom.nodeType !== 1) {
907 dom = dom.nextSibling;
912 * gets the next sibling of a DOM node. Used internally.
914 * @name _nextSibling(dom)
915 * @param {DOMElement} dom
916 * @return {DOMElement}
918 _nextSibling : function (dom) {
919 dom = dom ? dom.nextSibling : null;
920 while(dom !== null && dom.nodeType !== 1) {
921 dom = dom.nextSibling;
926 * gets the previous sibling of a DOM node. Used internally.
928 * @name _previousSibling(dom)
929 * @param {DOMElement} dom
930 * @return {DOMElement}
932 _previousSibling : function (dom) {
933 dom = dom ? dom.previousSibling : null;
934 while(dom !== null && dom.nodeType !== 1) {
935 dom = dom.previousSibling;
940 * 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)
941 * @name get_node(obj [, as_dom])
943 * @param {Boolean} as_dom
944 * @return {Object|jQuery}
946 get_node : function (obj, as_dom) {
952 if(this._model.data[obj]) {
953 obj = this._model.data[obj];
955 else if(typeof obj === "string" && this._model.data[obj.replace(/^#/, '')]) {
956 obj = this._model.data[obj.replace(/^#/, '')];
958 else if(typeof obj === "string" && (dom = $('#' + obj.replace($.jstree.idregex,'\\$&'), this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) {
959 obj = this._model.data[dom.closest('.jstree-node').attr('id')];
961 else if((dom = $(obj, this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) {
962 obj = this._model.data[dom.closest('.jstree-node').attr('id')];
964 else if((dom = $(obj, this.element)).length && dom.hasClass('jstree')) {
965 obj = this._model.data[$.jstree.root];
972 obj = obj.id === $.jstree.root ? this.element : $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element);
975 } catch (ex) { return false; }
978 * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array)
979 * @name get_path(obj [, glue, ids])
980 * @param {mixed} obj the node
981 * @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
982 * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used
985 get_path : function (obj, glue, ids) {
986 obj = obj.parents ? obj : this.get_node(obj);
987 if(!obj || obj.id === $.jstree.root || !obj.parents) {
991 p.push(ids ? obj.id : obj.text);
992 for(i = 0, j = obj.parents.length; i < j; i++) {
993 p.push(ids ? obj.parents[i] : this.get_text(obj.parents[i]));
995 p = p.reverse().slice(1);
996 return glue ? p.join(glue) : p;
999 * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
1000 * @name get_next_dom(obj [, strict])
1001 * @param {mixed} obj
1002 * @param {Boolean} strict
1005 get_next_dom : function (obj, strict) {
1007 obj = this.get_node(obj, true);
1008 if(obj[0] === this.element[0]) {
1009 tmp = this._firstChild(this.get_container_ul()[0]);
1010 while (tmp && tmp.offsetHeight === 0) {
1011 tmp = this._nextSibling(tmp);
1013 return tmp ? $(tmp) : false;
1015 if(!obj || !obj.length) {
1021 tmp = this._nextSibling(tmp);
1022 } while (tmp && tmp.offsetHeight === 0);
1023 return tmp ? $(tmp) : false;
1025 if(obj.hasClass("jstree-open")) {
1026 tmp = this._firstChild(obj.children('.jstree-children')[0]);
1027 while (tmp && tmp.offsetHeight === 0) {
1028 tmp = this._nextSibling(tmp);
1036 tmp = this._nextSibling(tmp);
1037 } while (tmp && tmp.offsetHeight === 0);
1041 return obj.parentsUntil(".jstree",".jstree-node").nextAll(".jstree-node:visible").first();
1044 * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
1045 * @name get_prev_dom(obj [, strict])
1046 * @param {mixed} obj
1047 * @param {Boolean} strict
1050 get_prev_dom : function (obj, strict) {
1052 obj = this.get_node(obj, true);
1053 if(obj[0] === this.element[0]) {
1054 tmp = this.get_container_ul()[0].lastChild;
1055 while (tmp && tmp.offsetHeight === 0) {
1056 tmp = this._previousSibling(tmp);
1058 return tmp ? $(tmp) : false;
1060 if(!obj || !obj.length) {
1066 tmp = this._previousSibling(tmp);
1067 } while (tmp && tmp.offsetHeight === 0);
1068 return tmp ? $(tmp) : false;
1072 tmp = this._previousSibling(tmp);
1073 } while (tmp && tmp.offsetHeight === 0);
1076 while(obj.hasClass("jstree-open")) {
1077 obj = obj.children(".jstree-children").first().children(".jstree-node:visible:last");
1081 tmp = obj[0].parentNode.parentNode;
1082 return tmp && tmp.className && tmp.className.indexOf('jstree-node') !== -1 ? $(tmp) : false;
1085 * get the parent ID of a node
1086 * @name get_parent(obj)
1087 * @param {mixed} obj
1090 get_parent : function (obj) {
1091 obj = this.get_node(obj);
1092 if(!obj || obj.id === $.jstree.root) {
1098 * get a jQuery collection of all the children of a node (node must be rendered)
1099 * @name get_children_dom(obj)
1100 * @param {mixed} obj
1103 get_children_dom : function (obj) {
1104 obj = this.get_node(obj, true);
1105 if(obj[0] === this.element[0]) {
1106 return this.get_container_ul().children(".jstree-node");
1108 if(!obj || !obj.length) {
1111 return obj.children(".jstree-children").children(".jstree-node");
1114 * checks if a node has children
1115 * @name is_parent(obj)
1116 * @param {mixed} obj
1119 is_parent : function (obj) {
1120 obj = this.get_node(obj);
1121 return obj && (obj.state.loaded === false || obj.children.length > 0);
1124 * checks if a node is loaded (its children are available)
1125 * @name is_loaded(obj)
1126 * @param {mixed} obj
1129 is_loaded : function (obj) {
1130 obj = this.get_node(obj);
1131 return obj && obj.state.loaded;
1134 * check if a node is currently loading (fetching children)
1135 * @name is_loading(obj)
1136 * @param {mixed} obj
1139 is_loading : function (obj) {
1140 obj = this.get_node(obj);
1141 return obj && obj.state && obj.state.loading;
1144 * check if a node is opened
1145 * @name is_open(obj)
1146 * @param {mixed} obj
1149 is_open : function (obj) {
1150 obj = this.get_node(obj);
1151 return obj && obj.state.opened;
1154 * check if a node is in a closed state
1155 * @name is_closed(obj)
1156 * @param {mixed} obj
1159 is_closed : function (obj) {
1160 obj = this.get_node(obj);
1161 return obj && this.is_parent(obj) && !obj.state.opened;
1164 * check if a node has no children
1165 * @name is_leaf(obj)
1166 * @param {mixed} obj
1169 is_leaf : function (obj) {
1170 return !this.is_parent(obj);
1173 * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array.
1174 * @name load_node(obj [, callback])
1175 * @param {mixed} obj
1176 * @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
1178 * @trigger load_node.jstree
1180 load_node : function (obj, callback) {
1182 if($.isArray(obj)) {
1183 this._load_nodes(obj.slice(), callback);
1186 obj = this.get_node(obj);
1188 if(callback) { callback.call(this, obj, false); }
1191 // 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?
1192 if(obj.state.loaded) {
1193 obj.state.loaded = false;
1194 for(k = 0, l = obj.children_d.length; k < l; k++) {
1195 for(i = 0, j = obj.parents.length; i < j; i++) {
1196 this._model.data[obj.parents[i]].children_d = $.vakata.array_remove_item(this._model.data[obj.parents[i]].children_d, obj.children_d[k]);
1198 if(this._model.data[obj.children_d[k]].state.selected) {
1200 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.children_d[k]);
1202 delete this._model.data[obj.children_d[k]];
1205 obj.children_d = [];
1207 this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected });
1210 obj.state.failed = false;
1211 obj.state.loading = true;
1212 this.get_node(obj, true).addClass("jstree-loading").attr('aria-busy',true);
1213 this._load_node(obj, $.proxy(function (status) {
1214 obj = this._model.data[obj.id];
1215 obj.state.loading = false;
1216 obj.state.loaded = status;
1217 obj.state.failed = !obj.state.loaded;
1218 var dom = this.get_node(obj, true), i = 0, j = 0, m = this._model.data, has_children = false;
1219 for(i = 0, j = obj.children.length; i < j; i++) {
1220 if(m[obj.children[i]] && !m[obj.children[i]].state.hidden) {
1221 has_children = true;
1225 if(obj.state.loaded && !has_children && dom && dom.length && !dom.hasClass('jstree-leaf')) {
1226 dom.removeClass('jstree-closed jstree-open').addClass('jstree-leaf');
1228 dom.removeClass("jstree-loading").attr('aria-busy',false);
1230 * triggered after a node is loaded
1232 * @name load_node.jstree
1233 * @param {Object} node the node that was loading
1234 * @param {Boolean} status was the node loaded successfully
1236 this.trigger('load_node', { "node" : obj, "status" : status });
1238 callback.call(this, obj, status);
1244 * load an array of nodes (will also load unavailable nodes as soon as the appear in the structure). Used internally.
1246 * @name _load_nodes(nodes [, callback])
1247 * @param {array} nodes
1248 * @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
1250 _load_nodes : function (nodes, callback, is_callback) {
1252 c = function () { this._load_nodes(nodes, callback, true); },
1253 m = this._model.data, i, j, tmp = [];
1254 for(i = 0, j = nodes.length; i < j; i++) {
1255 if(m[nodes[i]] && ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || !is_callback)) {
1256 if(!this.is_loading(nodes[i])) {
1257 this.load_node(nodes[i], c);
1263 for(i = 0, j = nodes.length; i < j; i++) {
1264 if(m[nodes[i]] && m[nodes[i]].state.loaded) {
1268 if(callback && !callback.done) {
1269 callback.call(this, tmp);
1270 callback.done = true;
1275 * loads all unloaded nodes
1276 * @name load_all([obj, callback])
1277 * @param {mixed} obj the node to load recursively, omit to load all nodes in the tree
1278 * @param {function} callback a function to be executed once loading all the nodes is complete,
1279 * @trigger load_all.jstree
1281 load_all : function (obj, callback) {
1282 if(!obj) { obj = $.jstree.root; }
1283 obj = this.get_node(obj);
1284 if(!obj) { return false; }
1286 m = this._model.data,
1287 c = m[obj.id].children_d,
1289 if(obj.state && !obj.state.loaded) {
1290 to_load.push(obj.id);
1292 for(i = 0, j = c.length; i < j; i++) {
1293 if(m[c[i]] && m[c[i]].state && !m[c[i]].state.loaded) {
1297 if(to_load.length) {
1298 this._load_nodes(to_load, function () {
1299 this.load_all(obj, callback);
1304 * triggered after a load_all call completes
1306 * @name load_all.jstree
1307 * @param {Object} node the recursively loaded node
1309 if(callback) { callback.call(this, obj); }
1310 this.trigger('load_all', { "node" : obj });
1314 * handles the actual loading of a node. Used only internally.
1316 * @name _load_node(obj [, callback])
1317 * @param {mixed} obj
1318 * @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
1321 _load_node : function (obj, callback) {
1322 var s = this.settings.core.data, t;
1323 // use original HTML
1325 if(obj.id === $.jstree.root) {
1326 return this._append_html_data(obj, this._data.core.original_container_html.clone(true), function (status) {
1327 callback.call(this, status);
1331 return callback.call(this, false);
1333 // return callback.call(this, obj.id === $.jstree.root ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false);
1335 if($.isFunction(s)) {
1336 return s.call(this, obj, $.proxy(function (d) {
1338 callback.call(this, false);
1340 this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }) : d, function (status) {
1341 callback.call(this, status);
1343 // 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));
1346 if(typeof s === 'object') {
1348 s = $.extend(true, {}, s);
1349 if($.isFunction(s.url)) {
1350 s.url = s.url.call(this, obj);
1352 if($.isFunction(s.data)) {
1353 s.data = s.data.call(this, obj);
1356 .done($.proxy(function (d,t,x) {
1357 var type = x.getResponseHeader('Content-Type');
1358 if((type && type.indexOf('json') !== -1) || typeof d === "object") {
1359 return this._append_json_data(obj, d, function (status) { callback.call(this, status); });
1360 //return callback.call(this, this._append_json_data(obj, d));
1362 if((type && type.indexOf('html') !== -1) || typeof d === "string") {
1363 return this._append_html_data(obj, $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }), function (status) { callback.call(this, status); });
1364 // return callback.call(this, this._append_html_data(obj, $(d)));
1366 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 }) };
1367 this.settings.core.error.call(this, this._data.core.last_error);
1368 return callback.call(this, false);
1370 .fail($.proxy(function (f) {
1371 callback.call(this, false);
1372 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 }) };
1373 this.settings.core.error.call(this, this._data.core.last_error);
1376 t = ($.isArray(s) || $.isPlainObject(s)) ? JSON.parse(JSON.stringify(s)) : s;
1377 if(obj.id === $.jstree.root) {
1378 return this._append_json_data(obj, t, function (status) {
1379 callback.call(this, status);
1383 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
1384 this.settings.core.error.call(this, this._data.core.last_error);
1385 return callback.call(this, false);
1387 //return callback.call(this, (obj.id === $.jstree.root ? this._append_json_data(obj, t) : false) );
1389 if(typeof s === 'string') {
1390 if(obj.id === $.jstree.root) {
1391 return this._append_html_data(obj, $($.parseHTML(s)).filter(function () { return this.nodeType !== 3; }), function (status) {
1392 callback.call(this, status);
1396 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
1397 this.settings.core.error.call(this, this._data.core.last_error);
1398 return callback.call(this, false);
1400 //return callback.call(this, (obj.id === $.jstree.root ? this._append_html_data(obj, $(s)) : false) );
1402 return callback.call(this, false);
1405 * adds a node to the list of nodes to redraw. Used only internally.
1407 * @name _node_changed(obj [, callback])
1408 * @param {mixed} obj
1410 _node_changed : function (obj) {
1411 obj = this.get_node(obj);
1413 this._model.changed.push(obj.id);
1417 * appends HTML content to the tree. Used internally.
1419 * @name _append_html_data(obj, data)
1420 * @param {mixed} obj the node to append to
1421 * @param {String} data the HTML string to parse and append
1422 * @trigger model.jstree, changed.jstree
1424 _append_html_data : function (dom, data, cb) {
1425 dom = this.get_node(dom);
1427 dom.children_d = [];
1428 var dat = data.is('ul') ? data.children() : data,
1432 m = this._model.data,
1434 s = this._data.core.selected.length,
1436 dat.each($.proxy(function (i, v) {
1437 tmp = this._parse_model_from_html($(v), par, p.parents.concat());
1441 if(m[tmp].children_d.length) {
1442 dpc = dpc.concat(m[tmp].children_d);
1448 for(i = 0, j = p.parents.length; i < j; i++) {
1449 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1452 * triggered when new data is inserted to the tree model
1454 * @name model.jstree
1455 * @param {Array} nodes an array of node IDs
1456 * @param {String} parent the parent ID of the nodes
1458 this.trigger('model', { "nodes" : dpc, 'parent' : par });
1459 if(par !== $.jstree.root) {
1460 this._node_changed(par);
1464 this.get_container_ul().children('.jstree-initial-node').remove();
1467 if(this._data.core.selected.length !== s) {
1468 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
1470 cb.call(this, true);
1473 * appends JSON content to the tree. Used internally.
1475 * @name _append_json_data(obj, data)
1476 * @param {mixed} obj the node to append to
1477 * @param {String} data the JSON object to parse and append
1478 * @param {Boolean} force_processing internal param - do not set
1479 * @trigger model.jstree, changed.jstree
1481 _append_json_data : function (dom, data, cb, force_processing) {
1482 if(this.element === null) { return; }
1483 dom = this.get_node(dom);
1485 dom.children_d = [];
1489 if(typeof data === "string") {
1490 data = JSON.parse(data);
1493 if(!$.isArray(data)) { data = [data]; }
1496 'df' : this._model.default_state,
1499 'm' : this._model.data,
1501 't_cnt' : this._cnt,
1502 'sel' : this._data.core.selected
1504 func = function (data, undefined) {
1505 if(data.data) { data = data.data; }
1518 parse_flat = function (d, p, ps) {
1519 if(!ps) { ps = []; }
1520 else { ps = ps.concat(); }
1521 if(p) { ps.unshift(p); }
1522 var tid = d.id.toString(),
1526 text : d.text || '',
1527 icon : d.icon !== undefined ? d.icon : true,
1530 children : d.children || [],
1531 children_d : d.children_d || [],
1534 li_attr : { id : false },
1535 a_attr : { href : '#' },
1539 if(df.hasOwnProperty(i)) {
1540 tmp.state[i] = df[i];
1543 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1544 tmp.icon = d.data.jstree.icon;
1546 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
1552 for(i in d.data.jstree) {
1553 if(d.data.jstree.hasOwnProperty(i)) {
1554 tmp.state[i] = d.data.jstree[i];
1559 if(d && typeof d.state === 'object') {
1560 for (i in d.state) {
1561 if(d.state.hasOwnProperty(i)) {
1562 tmp.state[i] = d.state[i];
1566 if(d && typeof d.li_attr === 'object') {
1567 for (i in d.li_attr) {
1568 if(d.li_attr.hasOwnProperty(i)) {
1569 tmp.li_attr[i] = d.li_attr[i];
1573 if(!tmp.li_attr.id) {
1574 tmp.li_attr.id = tid;
1576 if(d && typeof d.a_attr === 'object') {
1577 for (i in d.a_attr) {
1578 if(d.a_attr.hasOwnProperty(i)) {
1579 tmp.a_attr[i] = d.a_attr[i];
1583 if(d && d.children && d.children === true) {
1584 tmp.state.loaded = false;
1586 tmp.children_d = [];
1589 for(i = 0, j = tmp.children.length; i < j; i++) {
1590 c = parse_flat(m[tmp.children[i]], tmp.id, ps);
1592 tmp.children_d.push(c);
1593 if(e.children_d.length) {
1594 tmp.children_d = tmp.children_d.concat(e.children_d);
1599 m[tmp.id].original = d;
1600 if(tmp.state.selected) {
1605 parse_nest = function (d, p, ps) {
1606 if(!ps) { ps = []; }
1607 else { ps = ps.concat(); }
1608 if(p) { ps.unshift(p); }
1609 var tid = false, i, j, c, e, tmp;
1611 tid = 'j' + t_id + '_' + (++t_cnt);
1616 text : typeof d === 'string' ? d : '',
1617 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true,
1624 li_attr : { id : false },
1625 a_attr : { href : '#' },
1629 if(df.hasOwnProperty(i)) {
1630 tmp.state[i] = df[i];
1633 if(d && d.id) { tmp.id = d.id.toString(); }
1634 if(d && d.text) { tmp.text = d.text; }
1635 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1636 tmp.icon = d.data.jstree.icon;
1638 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
1644 for(i in d.data.jstree) {
1645 if(d.data.jstree.hasOwnProperty(i)) {
1646 tmp.state[i] = d.data.jstree[i];
1651 if(d && typeof d.state === 'object') {
1652 for (i in d.state) {
1653 if(d.state.hasOwnProperty(i)) {
1654 tmp.state[i] = d.state[i];
1658 if(d && typeof d.li_attr === 'object') {
1659 for (i in d.li_attr) {
1660 if(d.li_attr.hasOwnProperty(i)) {
1661 tmp.li_attr[i] = d.li_attr[i];
1665 if(tmp.li_attr.id && !tmp.id) {
1666 tmp.id = tmp.li_attr.id.toString();
1671 if(!tmp.li_attr.id) {
1672 tmp.li_attr.id = tmp.id;
1674 if(d && typeof d.a_attr === 'object') {
1675 for (i in d.a_attr) {
1676 if(d.a_attr.hasOwnProperty(i)) {
1677 tmp.a_attr[i] = d.a_attr[i];
1681 if(d && d.children && d.children.length) {
1682 for(i = 0, j = d.children.length; i < j; i++) {
1683 c = parse_nest(d.children[i], tmp.id, ps);
1685 tmp.children.push(c);
1686 if(e.children_d.length) {
1687 tmp.children_d = tmp.children_d.concat(e.children_d);
1690 tmp.children_d = tmp.children_d.concat(tmp.children);
1692 if(d && d.children && d.children === true) {
1693 tmp.state.loaded = false;
1695 tmp.children_d = [];
1701 if(tmp.state.selected) {
1707 if(dat.length && dat[0].id !== undefined && dat[0].parent !== undefined) {
1708 // Flat JSON support (for easy import from DB):
1709 // 1) convert to object (foreach)
1710 for(i = 0, j = dat.length; i < j; i++) {
1711 if(!dat[i].children) {
1712 dat[i].children = [];
1714 m[dat[i].id.toString()] = dat[i];
1716 // 2) populate children (foreach)
1717 for(i = 0, j = dat.length; i < j; i++) {
1718 m[dat[i].parent.toString()].children.push(dat[i].id.toString());
1719 // populate parent.children_d
1720 p.children_d.push(dat[i].id.toString());
1722 // 3) normalize && populate parents and children_d with recursion
1723 for(i = 0, j = p.children.length; i < j; i++) {
1724 tmp = parse_flat(m[p.children[i]], par, p.parents.concat());
1726 if(m[tmp].children_d.length) {
1727 dpc = dpc.concat(m[tmp].children_d);
1730 for(i = 0, j = p.parents.length; i < j; i++) {
1731 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1733 // ?) three_state selection - p.state.selected && t - (if three_state foreach(dat => ch) -> foreach(parents) if(parent.selected) child.selected = true;
1744 for(i = 0, j = dat.length; i < j; i++) {
1745 tmp = parse_nest(dat[i], par, p.parents.concat());
1749 if(m[tmp].children_d.length) {
1750 dpc = dpc.concat(m[tmp].children_d);
1756 for(i = 0, j = p.parents.length; i < j; i++) {
1757 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1768 if(typeof window === 'undefined' || typeof window.document === 'undefined') {
1775 rslt = function (rslt, worker) {
1776 if(this.element === null) { return; }
1777 this._cnt = rslt.cnt;
1778 this._model.data = rslt.mod; // breaks the reference in load_node - careful
1781 var i, j, a = rslt.add, r = rslt.sel, s = this._data.core.selected.slice(), m = this._model.data;
1782 // if selection was changed while calculating in worker
1783 if(r.length !== s.length || $.vakata.array_unique(r.concat(s)).length !== r.length) {
1784 // deselect nodes that are no longer selected
1785 for(i = 0, j = r.length; i < j; i++) {
1786 if($.inArray(r[i], a) === -1 && $.inArray(r[i], s) === -1) {
1787 m[r[i]].state.selected = false;
1790 // select nodes that were selected in the mean time
1791 for(i = 0, j = s.length; i < j; i++) {
1792 if($.inArray(s[i], r) === -1) {
1793 m[s[i]].state.selected = true;
1798 if(rslt.add.length) {
1799 this._data.core.selected = this._data.core.selected.concat(rslt.add);
1802 this.trigger('model', { "nodes" : rslt.dpc, 'parent' : rslt.par });
1804 if(rslt.par !== $.jstree.root) {
1805 this._node_changed(rslt.par);
1809 // this.get_container_ul().children('.jstree-initial-node').remove();
1812 if(rslt.add.length) {
1813 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
1815 cb.call(this, true);
1817 if(this.settings.core.worker && window.Blob && window.URL && window.Worker) {
1819 if(this._wrk === null) {
1820 this._wrk = window.URL.createObjectURL(
1822 ['self.onmessage = ' + func.toString()],
1823 {type:"text/javascript"}
1827 if(!this._data.core.working || force_processing) {
1828 this._data.core.working = true;
1829 w = new window.Worker(this._wrk);
1830 w.onmessage = $.proxy(function (e) {
1831 rslt.call(this, e.data, true);
1832 try { w.terminate(); w = null; } catch(ignore) { }
1833 if(this._data.core.worker_queue.length) {
1834 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1837 this._data.core.working = false;
1841 if(this._data.core.worker_queue.length) {
1842 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1845 this._data.core.working = false;
1849 w.postMessage(args);
1853 this._data.core.worker_queue.push([dom, data, cb, true]);
1857 rslt.call(this, func(args), false);
1858 if(this._data.core.worker_queue.length) {
1859 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1862 this._data.core.working = false;
1867 rslt.call(this, func(args), false);
1871 * parses a node from a jQuery object and appends them to the in memory tree model. Used internally.
1873 * @name _parse_model_from_html(d [, p, ps])
1874 * @param {jQuery} d the jQuery object to parse
1875 * @param {String} p the parent ID
1876 * @param {Array} ps list of all parents
1877 * @return {String} the ID of the object added to the model
1879 _parse_model_from_html : function (d, p, ps) {
1880 if(!ps) { ps = []; }
1881 else { ps = [].concat(ps); }
1882 if(p) { ps.unshift(p); }
1883 var c, e, m = this._model.data,
1894 li_attr : { id : false },
1895 a_attr : { href : '#' },
1898 for(i in this._model.default_state) {
1899 if(this._model.default_state.hasOwnProperty(i)) {
1900 data.state[i] = this._model.default_state[i];
1903 tmp = $.vakata.attributes(d, true);
1904 $.each(tmp, function (i, v) {
1906 if(!v.length) { return true; }
1907 data.li_attr[i] = v;
1909 data.id = v.toString();
1912 tmp = d.children('a').first();
1914 tmp = $.vakata.attributes(tmp, true);
1915 $.each(tmp, function (i, v) {
1922 tmp = d.children("a").first().length ? d.children("a").first().clone() : d.clone();
1923 tmp.children("ins, i, ul").remove();
1925 tmp = $('<div />').html(tmp);
1926 data.text = this.settings.core.force_text ? tmp.text() : tmp.html();
1928 data.data = tmp ? $.extend(true, {}, tmp) : null;
1929 data.state.opened = d.hasClass('jstree-open');
1930 data.state.selected = d.children('a').hasClass('jstree-clicked');
1931 data.state.disabled = d.children('a').hasClass('jstree-disabled');
1932 if(data.data && data.data.jstree) {
1933 for(i in data.data.jstree) {
1934 if(data.data.jstree.hasOwnProperty(i)) {
1935 data.state[i] = data.data.jstree[i];
1939 tmp = d.children("a").children(".jstree-themeicon");
1941 data.icon = tmp.hasClass('jstree-themeicon-hidden') ? false : tmp.attr('rel');
1943 if(data.state.icon !== undefined) {
1944 data.icon = data.state.icon;
1946 if(data.icon === undefined || data.icon === null || data.icon === "") {
1949 tmp = d.children("ul").children("li");
1951 tid = 'j' + this._id + '_' + (++this._cnt);
1953 data.id = data.li_attr.id ? data.li_attr.id.toString() : tid;
1955 tmp.each($.proxy(function (i, v) {
1956 c = this._parse_model_from_html($(v), data.id, ps);
1957 e = this._model.data[c];
1958 data.children.push(c);
1959 if(e.children_d.length) {
1960 data.children_d = data.children_d.concat(e.children_d);
1963 data.children_d = data.children_d.concat(data.children);
1966 if(d.hasClass('jstree-closed')) {
1967 data.state.loaded = false;
1970 if(data.li_attr['class']) {
1971 data.li_attr['class'] = data.li_attr['class'].replace('jstree-closed','').replace('jstree-open','');
1973 if(data.a_attr['class']) {
1974 data.a_attr['class'] = data.a_attr['class'].replace('jstree-clicked','').replace('jstree-disabled','');
1977 if(data.state.selected) {
1978 this._data.core.selected.push(data.id);
1983 * 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.
1985 * @name _parse_model_from_flat_json(d [, p, ps])
1986 * @param {Object} d the JSON object to parse
1987 * @param {String} p the parent ID
1988 * @param {Array} ps list of all parents
1989 * @return {String} the ID of the object added to the model
1991 _parse_model_from_flat_json : function (d, p, ps) {
1992 if(!ps) { ps = []; }
1993 else { ps = ps.concat(); }
1994 if(p) { ps.unshift(p); }
1995 var tid = d.id.toString(),
1996 m = this._model.data,
1997 df = this._model.default_state,
2001 text : d.text || '',
2002 icon : d.icon !== undefined ? d.icon : true,
2005 children : d.children || [],
2006 children_d : d.children_d || [],
2009 li_attr : { id : false },
2010 a_attr : { href : '#' },
2014 if(df.hasOwnProperty(i)) {
2015 tmp.state[i] = df[i];
2018 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
2019 tmp.icon = d.data.jstree.icon;
2021 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
2027 for(i in d.data.jstree) {
2028 if(d.data.jstree.hasOwnProperty(i)) {
2029 tmp.state[i] = d.data.jstree[i];
2034 if(d && typeof d.state === 'object') {
2035 for (i in d.state) {
2036 if(d.state.hasOwnProperty(i)) {
2037 tmp.state[i] = d.state[i];
2041 if(d && typeof d.li_attr === 'object') {
2042 for (i in d.li_attr) {
2043 if(d.li_attr.hasOwnProperty(i)) {
2044 tmp.li_attr[i] = d.li_attr[i];
2048 if(!tmp.li_attr.id) {
2049 tmp.li_attr.id = tid;
2051 if(d && typeof d.a_attr === 'object') {
2052 for (i in d.a_attr) {
2053 if(d.a_attr.hasOwnProperty(i)) {
2054 tmp.a_attr[i] = d.a_attr[i];
2058 if(d && d.children && d.children === true) {
2059 tmp.state.loaded = false;
2061 tmp.children_d = [];
2064 for(i = 0, j = tmp.children.length; i < j; i++) {
2065 c = this._parse_model_from_flat_json(m[tmp.children[i]], tmp.id, ps);
2067 tmp.children_d.push(c);
2068 if(e.children_d.length) {
2069 tmp.children_d = tmp.children_d.concat(e.children_d);
2074 m[tmp.id].original = d;
2075 if(tmp.state.selected) {
2076 this._data.core.selected.push(tmp.id);
2081 * parses a node from a JSON object and appends it to the in memory tree model. Used internally.
2083 * @name _parse_model_from_json(d [, p, ps])
2084 * @param {Object} d the JSON object to parse
2085 * @param {String} p the parent ID
2086 * @param {Array} ps list of all parents
2087 * @return {String} the ID of the object added to the model
2089 _parse_model_from_json : function (d, p, ps) {
2090 if(!ps) { ps = []; }
2091 else { ps = ps.concat(); }
2092 if(p) { ps.unshift(p); }
2093 var tid = false, i, j, c, e, m = this._model.data, df = this._model.default_state, tmp;
2095 tid = 'j' + this._id + '_' + (++this._cnt);
2100 text : typeof d === 'string' ? d : '',
2101 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true,
2108 li_attr : { id : false },
2109 a_attr : { href : '#' },
2113 if(df.hasOwnProperty(i)) {
2114 tmp.state[i] = df[i];
2117 if(d && d.id) { tmp.id = d.id.toString(); }
2118 if(d && d.text) { tmp.text = d.text; }
2119 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
2120 tmp.icon = d.data.jstree.icon;
2122 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
2128 for(i in d.data.jstree) {
2129 if(d.data.jstree.hasOwnProperty(i)) {
2130 tmp.state[i] = d.data.jstree[i];
2135 if(d && typeof d.state === 'object') {
2136 for (i in d.state) {
2137 if(d.state.hasOwnProperty(i)) {
2138 tmp.state[i] = d.state[i];
2142 if(d && typeof d.li_attr === 'object') {
2143 for (i in d.li_attr) {
2144 if(d.li_attr.hasOwnProperty(i)) {
2145 tmp.li_attr[i] = d.li_attr[i];
2149 if(tmp.li_attr.id && !tmp.id) {
2150 tmp.id = tmp.li_attr.id.toString();
2155 if(!tmp.li_attr.id) {
2156 tmp.li_attr.id = tmp.id;
2158 if(d && typeof d.a_attr === 'object') {
2159 for (i in d.a_attr) {
2160 if(d.a_attr.hasOwnProperty(i)) {
2161 tmp.a_attr[i] = d.a_attr[i];
2165 if(d && d.children && d.children.length) {
2166 for(i = 0, j = d.children.length; i < j; i++) {
2167 c = this._parse_model_from_json(d.children[i], tmp.id, ps);
2169 tmp.children.push(c);
2170 if(e.children_d.length) {
2171 tmp.children_d = tmp.children_d.concat(e.children_d);
2174 tmp.children_d = tmp.children_d.concat(tmp.children);
2176 if(d && d.children && d.children === true) {
2177 tmp.state.loaded = false;
2179 tmp.children_d = [];
2185 if(tmp.state.selected) {
2186 this._data.core.selected.push(tmp.id);
2191 * redraws all nodes that need to be redrawn. Used internally.
2194 * @trigger redraw.jstree
2196 _redraw : function () {
2197 var nodes = this._model.force_full_redraw ? this._model.data[$.jstree.root].children.concat([]) : this._model.changed.concat([]),
2198 f = document.createElement('UL'), tmp, i, j, fe = this._data.core.focused;
2199 for(i = 0, j = nodes.length; i < j; i++) {
2200 tmp = this.redraw_node(nodes[i], true, this._model.force_full_redraw);
2201 if(tmp && this._model.force_full_redraw) {
2205 if(this._model.force_full_redraw) {
2206 f.className = this.get_container_ul()[0].className;
2207 f.setAttribute('role','group');
2208 this.element.empty().append(f);
2209 //this.get_container_ul()[0].appendChild(f);
2212 tmp = this.get_node(fe, true);
2213 if(tmp && tmp.length && tmp.children('.jstree-anchor')[0] !== document.activeElement) {
2214 tmp.children('.jstree-anchor').focus();
2217 this._data.core.focused = null;
2220 this._model.force_full_redraw = false;
2221 this._model.changed = [];
2223 * triggered after nodes are redrawn
2225 * @name redraw.jstree
2226 * @param {array} nodes the redrawn nodes
2228 this.trigger('redraw', { "nodes" : nodes });
2231 * redraws all nodes that need to be redrawn or optionally - the whole tree
2232 * @name redraw([full])
2233 * @param {Boolean} full if set to `true` all nodes are redrawn.
2235 redraw : function (full) {
2237 this._model.force_full_redraw = true;
2239 //if(this._model.redraw_timeout) {
2240 // clearTimeout(this._model.redraw_timeout);
2242 //this._model.redraw_timeout = setTimeout($.proxy(this._redraw, this),0);
2246 * redraws a single node's children. Used internally.
2248 * @name draw_children(node)
2249 * @param {mixed} node the node whose children will be redrawn
2251 draw_children : function (node) {
2252 var obj = this.get_node(node),
2257 if(!obj) { return false; }
2258 if(obj.id === $.jstree.root) { return this.redraw(true); }
2259 node = this.get_node(node, true);
2260 if(!node || !node.length) { return false; } // TODO: quick toggle
2262 node.children('.jstree-children').remove();
2264 if(obj.children.length && obj.state.loaded) {
2265 k = d.createElement('UL');
2266 k.setAttribute('role', 'group');
2267 k.className = 'jstree-children';
2268 for(i = 0, j = obj.children.length; i < j; i++) {
2269 k.appendChild(this.redraw_node(obj.children[i], true, true));
2271 node.appendChild(k);
2275 * redraws a single node. Used internally.
2277 * @name redraw_node(node, deep, is_callback, force_render)
2278 * @param {mixed} node the node to redraw
2279 * @param {Boolean} deep should child nodes be redrawn too
2280 * @param {Boolean} is_callback is this a recursion call
2281 * @param {Boolean} force_render should children of closed parents be drawn anyway
2283 redraw_node : function (node, deep, is_callback, force_render) {
2284 var obj = this.get_node(node),
2293 m = this._model.data,
2299 has_children = false,
2300 last_sibling = false;
2301 if(!obj) { return false; }
2302 if(obj.id === $.jstree.root) { return this.redraw(true); }
2303 deep = deep || obj.children.length === 0;
2304 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);
2307 //node = d.createElement('LI');
2309 par = obj.parent !== $.jstree.root ? $('#' + obj.parent.replace($.jstree.idregex,'\\$&'), this.element)[0] : null;
2310 if(par !== null && (!par || !m[obj.parent].state.opened)) {
2313 ind = $.inArray(obj.id, par === null ? m[$.jstree.root].children : m[obj.parent].children);
2319 par = node.parent().parent()[0];
2320 if(par === this.element[0]) {
2325 // m[obj.id].data = node.data(); // use only node's data, no need to touch jquery storage
2326 if(!deep && obj.children.length && !node.children('.jstree-children').length) {
2330 old = node.children('.jstree-children')[0];
2332 f = node.children('.jstree-anchor')[0] === document.activeElement;
2334 //node = d.createElement('LI');
2337 node = _node.cloneNode(true);
2338 // node is DOM, deep is boolean
2341 for(i in obj.li_attr) {
2342 if(obj.li_attr.hasOwnProperty(i)) {
2343 if(i === 'id') { continue; }
2345 node.setAttribute(i, obj.li_attr[i]);
2348 c += obj.li_attr[i];
2352 if(!obj.a_attr.id) {
2353 obj.a_attr.id = obj.id + '_anchor';
2355 node.setAttribute('aria-selected', !!obj.state.selected);
2356 node.setAttribute('aria-level', obj.parents.length);
2357 node.setAttribute('aria-labelledby', obj.a_attr.id);
2358 if(obj.state.disabled) {
2359 node.setAttribute('aria-disabled', true);
2362 for(i = 0, j = obj.children.length; i < j; i++) {
2363 if(!m[obj.children[i]].state.hidden) {
2364 has_children = true;
2368 if(obj.parent !== null && m[obj.parent] && !obj.state.hidden) {
2369 i = $.inArray(obj.id, m[obj.parent].children);
2370 last_sibling = obj.id;
2373 for(j = m[obj.parent].children.length; i < j; i++) {
2374 if(!m[m[obj.parent].children[i]].state.hidden) {
2375 last_sibling = m[obj.parent].children[i];
2377 if(last_sibling !== obj.id) {
2384 if(obj.state.hidden) {
2385 c += ' jstree-hidden';
2387 if(obj.state.loaded && !has_children) {
2388 c += ' jstree-leaf';
2391 c += obj.state.opened && obj.state.loaded ? ' jstree-open' : ' jstree-closed';
2392 node.setAttribute('aria-expanded', (obj.state.opened && obj.state.loaded) );
2394 if(last_sibling === obj.id) {
2395 c += ' jstree-last';
2399 c = ( obj.state.selected ? ' jstree-clicked' : '') + ( obj.state.disabled ? ' jstree-disabled' : '');
2400 for(j in obj.a_attr) {
2401 if(obj.a_attr.hasOwnProperty(j)) {
2402 if(j === 'href' && obj.a_attr[j] === '#') { continue; }
2404 node.childNodes[1].setAttribute(j, obj.a_attr[j]);
2407 c += ' ' + obj.a_attr[j];
2412 node.childNodes[1].className = 'jstree-anchor ' + c;
2414 if((obj.icon && obj.icon !== true) || obj.icon === false) {
2415 if(obj.icon === false) {
2416 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-hidden';
2418 else if(obj.icon.indexOf('/') === -1 && obj.icon.indexOf('.') === -1) {
2419 node.childNodes[1].childNodes[0].className += ' ' + obj.icon + ' jstree-themeicon-custom';
2422 node.childNodes[1].childNodes[0].style.backgroundImage = 'url('+obj.icon+')';
2423 node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center';
2424 node.childNodes[1].childNodes[0].style.backgroundSize = 'auto';
2425 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom';
2429 if(this.settings.core.force_text) {
2430 node.childNodes[1].appendChild(d.createTextNode(obj.text));
2433 node.childNodes[1].innerHTML += obj.text;
2437 if(deep && obj.children.length && (obj.state.opened || force_render) && obj.state.loaded) {
2438 k = d.createElement('UL');
2439 k.setAttribute('role', 'group');
2440 k.className = 'jstree-children';
2441 for(i = 0, j = obj.children.length; i < j; i++) {
2442 k.appendChild(this.redraw_node(obj.children[i], deep, true));
2444 node.appendChild(k);
2447 node.appendChild(old);
2450 // append back using par / ind
2452 par = this.element[0];
2454 for(i = 0, j = par.childNodes.length; i < j; i++) {
2455 if(par.childNodes[i] && par.childNodes[i].className && par.childNodes[i].className.indexOf('jstree-children') !== -1) {
2456 tmp = par.childNodes[i];
2461 tmp = d.createElement('UL');
2462 tmp.setAttribute('role', 'group');
2463 tmp.className = 'jstree-children';
2464 par.appendChild(tmp);
2468 if(ind < par.childNodes.length) {
2469 par.insertBefore(node, par.childNodes[ind]);
2472 par.appendChild(node);
2475 t = this.element[0].scrollTop;
2476 l = this.element[0].scrollLeft;
2477 node.childNodes[1].focus();
2478 this.element[0].scrollTop = t;
2479 this.element[0].scrollLeft = l;
2482 if(obj.state.opened && !obj.state.loaded) {
2483 obj.state.opened = false;
2484 setTimeout($.proxy(function () {
2485 this.open_node(obj.id, false, 0);
2491 * opens a node, revaling its children. If the node is not loaded it will be loaded and opened once ready.
2492 * @name open_node(obj [, callback, animation])
2493 * @param {mixed} obj the node to open
2494 * @param {Function} callback a function to execute once the node is opened
2495 * @param {Number} animation the animation duration in milliseconds when opening the node (overrides the `core.animation` setting). Use `false` for no animation.
2496 * @trigger open_node.jstree, after_open.jstree, before_open.jstree
2498 open_node : function (obj, callback, animation) {
2500 if($.isArray(obj)) {
2502 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2503 this.open_node(obj[t1], callback, animation);
2507 obj = this.get_node(obj);
2508 if(!obj || obj.id === $.jstree.root) {
2511 animation = animation === undefined ? this.settings.core.animation : animation;
2512 if(!this.is_closed(obj)) {
2514 callback.call(this, obj, false);
2518 if(!this.is_loaded(obj)) {
2519 if(this.is_loading(obj)) {
2520 return setTimeout($.proxy(function () {
2521 this.open_node(obj, callback, animation);
2524 this.load_node(obj, function (o, ok) {
2525 return ok ? this.open_node(o, callback, animation) : (callback ? callback.call(this, o, false) : false);
2529 d = this.get_node(obj, true);
2532 if(animation && d.children(".jstree-children").length) {
2533 d.children(".jstree-children").stop(true, true);
2535 if(obj.children.length && !this._firstChild(d.children('.jstree-children')[0])) {
2536 this.draw_children(obj);
2537 //d = this.get_node(obj, true);
2540 this.trigger('before_open', { "node" : obj });
2541 d[0].className = d[0].className.replace('jstree-closed', 'jstree-open');
2542 d[0].setAttribute("aria-expanded", true);
2545 this.trigger('before_open', { "node" : obj });
2547 .children(".jstree-children").css("display","none").end()
2548 .removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true)
2549 .children(".jstree-children").stop(true, true)
2550 .slideDown(animation, function () {
2551 this.style.display = "";
2552 t.trigger("after_open", { "node" : obj });
2556 obj.state.opened = true;
2558 callback.call(this, obj, true);
2562 * 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)
2564 * @name before_open.jstree
2565 * @param {Object} node the opened node
2567 this.trigger('before_open', { "node" : obj });
2570 * triggered when a node is opened (if there is an animation it will not be completed yet)
2572 * @name open_node.jstree
2573 * @param {Object} node the opened node
2575 this.trigger('open_node', { "node" : obj });
2576 if(!animation || !d.length) {
2578 * triggered when a node is opened and the animation is complete
2580 * @name after_open.jstree
2581 * @param {Object} node the opened node
2583 this.trigger("after_open", { "node" : obj });
2589 * opens every parent of a node (node should be loaded)
2590 * @name _open_to(obj)
2591 * @param {mixed} obj the node to reveal
2594 _open_to : function (obj) {
2595 obj = this.get_node(obj);
2596 if(!obj || obj.id === $.jstree.root) {
2599 var i, j, p = obj.parents;
2600 for(i = 0, j = p.length; i < j; i+=1) {
2601 if(i !== $.jstree.root) {
2602 this.open_node(p[i], false, 0);
2605 return $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element);
2608 * closes a node, hiding its children
2609 * @name close_node(obj [, animation])
2610 * @param {mixed} obj the node to close
2611 * @param {Number} animation the animation duration in milliseconds when closing the node (overrides the `core.animation` setting). Use `false` for no animation.
2612 * @trigger close_node.jstree, after_close.jstree
2614 close_node : function (obj, animation) {
2616 if($.isArray(obj)) {
2618 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2619 this.close_node(obj[t1], animation);
2623 obj = this.get_node(obj);
2624 if(!obj || obj.id === $.jstree.root) {
2627 if(this.is_closed(obj)) {
2630 animation = animation === undefined ? this.settings.core.animation : animation;
2632 d = this.get_node(obj, true);
2635 d[0].className = d[0].className.replace('jstree-open', 'jstree-closed');
2636 d.attr("aria-expanded", false).children('.jstree-children').remove();
2640 .children(".jstree-children").attr("style","display:block !important").end()
2641 .removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false)
2642 .children(".jstree-children").stop(true, true).slideUp(animation, function () {
2643 this.style.display = "";
2644 d.children('.jstree-children').remove();
2645 t.trigger("after_close", { "node" : obj });
2649 obj.state.opened = false;
2651 * triggered when a node is closed (if there is an animation it will not be complete yet)
2653 * @name close_node.jstree
2654 * @param {Object} node the closed node
2656 this.trigger('close_node',{ "node" : obj });
2657 if(!animation || !d.length) {
2659 * triggered when a node is closed and the animation is complete
2661 * @name after_close.jstree
2662 * @param {Object} node the closed node
2664 this.trigger("after_close", { "node" : obj });
2668 * toggles a node - closing it if it is open, opening it if it is closed
2669 * @name toggle_node(obj)
2670 * @param {mixed} obj the node to toggle
2672 toggle_node : function (obj) {
2674 if($.isArray(obj)) {
2676 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2677 this.toggle_node(obj[t1]);
2681 if(this.is_closed(obj)) {
2682 return this.open_node(obj);
2684 if(this.is_open(obj)) {
2685 return this.close_node(obj);
2689 * 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.
2690 * @name open_all([obj, animation, original_obj])
2691 * @param {mixed} obj the node to open recursively, omit to open all nodes in the tree
2692 * @param {Number} animation the animation duration in milliseconds when opening the nodes, the default is no animation
2693 * @param {jQuery} reference to the node that started the process (internal use)
2694 * @trigger open_all.jstree
2696 open_all : function (obj, animation, original_obj) {
2697 if(!obj) { obj = $.jstree.root; }
2698 obj = this.get_node(obj);
2699 if(!obj) { return false; }
2700 var dom = obj.id === $.jstree.root ? this.get_container_ul() : this.get_node(obj, true), i, j, _this;
2702 for(i = 0, j = obj.children_d.length; i < j; i++) {
2703 if(this.is_closed(this._model.data[obj.children_d[i]])) {
2704 this._model.data[obj.children_d[i]].state.opened = true;
2707 return this.trigger('open_all', { "node" : obj });
2709 original_obj = original_obj || dom;
2711 dom = this.is_closed(obj) ? dom.find('.jstree-closed').addBack() : dom.find('.jstree-closed');
2712 dom.each(function () {
2715 function(node, status) { if(status && this.is_parent(node)) { this.open_all(node, animation, original_obj); } },
2719 if(original_obj.find('.jstree-closed').length === 0) {
2721 * triggered when an `open_all` call completes
2723 * @name open_all.jstree
2724 * @param {Object} node the opened node
2726 this.trigger('open_all', { "node" : this.get_node(original_obj) });
2730 * closes all nodes within a node (or the tree), revaling their children
2731 * @name close_all([obj, animation])
2732 * @param {mixed} obj the node to close recursively, omit to close all nodes in the tree
2733 * @param {Number} animation the animation duration in milliseconds when closing the nodes, the default is no animation
2734 * @trigger close_all.jstree
2736 close_all : function (obj, animation) {
2737 if(!obj) { obj = $.jstree.root; }
2738 obj = this.get_node(obj);
2739 if(!obj) { return false; }
2740 var dom = obj.id === $.jstree.root ? this.get_container_ul() : this.get_node(obj, true),
2743 dom = this.is_open(obj) ? dom.find('.jstree-open').addBack() : dom.find('.jstree-open');
2744 $(dom.get().reverse()).each(function () { _this.close_node(this, animation || 0); });
2746 for(i = 0, j = obj.children_d.length; i < j; i++) {
2747 this._model.data[obj.children_d[i]].state.opened = false;
2750 * triggered when an `close_all` call completes
2752 * @name close_all.jstree
2753 * @param {Object} node the closed node
2755 this.trigger('close_all', { "node" : obj });
2758 * checks if a node is disabled (not selectable)
2759 * @name is_disabled(obj)
2760 * @param {mixed} obj
2763 is_disabled : function (obj) {
2764 obj = this.get_node(obj);
2765 return obj && obj.state && obj.state.disabled;
2768 * enables a node - so that it can be selected
2769 * @name enable_node(obj)
2770 * @param {mixed} obj the node to enable
2771 * @trigger enable_node.jstree
2773 enable_node : function (obj) {
2775 if($.isArray(obj)) {
2777 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2778 this.enable_node(obj[t1]);
2782 obj = this.get_node(obj);
2783 if(!obj || obj.id === $.jstree.root) {
2786 obj.state.disabled = false;
2787 this.get_node(obj,true).children('.jstree-anchor').removeClass('jstree-disabled').attr('aria-disabled', false);
2789 * triggered when an node is enabled
2791 * @name enable_node.jstree
2792 * @param {Object} node the enabled node
2794 this.trigger('enable_node', { 'node' : obj });
2797 * disables a node - so that it can not be selected
2798 * @name disable_node(obj)
2799 * @param {mixed} obj the node to disable
2800 * @trigger disable_node.jstree
2802 disable_node : function (obj) {
2804 if($.isArray(obj)) {
2806 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2807 this.disable_node(obj[t1]);
2811 obj = this.get_node(obj);
2812 if(!obj || obj.id === $.jstree.root) {
2815 obj.state.disabled = true;
2816 this.get_node(obj,true).children('.jstree-anchor').addClass('jstree-disabled').attr('aria-disabled', true);
2818 * triggered when an node is disabled
2820 * @name disable_node.jstree
2821 * @param {Object} node the disabled node
2823 this.trigger('disable_node', { 'node' : obj });
2826 * hides a node - it is still in the structure but will not be visible
2827 * @name hide_node(obj)
2828 * @param {mixed} obj the node to hide
2829 * @param {Boolean} redraw internal parameter controlling if redraw is called
2830 * @trigger hide_node.jstree
2832 hide_node : function (obj, skip_redraw) {
2834 if($.isArray(obj)) {
2836 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2837 this.hide_node(obj[t1], true);
2842 obj = this.get_node(obj);
2843 if(!obj || obj.id === $.jstree.root) {
2846 if(!obj.state.hidden) {
2847 obj.state.hidden = true;
2848 this._node_changed(obj.parent);
2853 * triggered when an node is hidden
2855 * @name hide_node.jstree
2856 * @param {Object} node the hidden node
2858 this.trigger('hide_node', { 'node' : obj });
2863 * @name show_node(obj)
2864 * @param {mixed} obj the node to show
2865 * @param {Boolean} skip_redraw internal parameter controlling if redraw is called
2866 * @trigger show_node.jstree
2868 show_node : function (obj, skip_redraw) {
2870 if($.isArray(obj)) {
2872 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2873 this.show_node(obj[t1], true);
2878 obj = this.get_node(obj);
2879 if(!obj || obj.id === $.jstree.root) {
2882 if(obj.state.hidden) {
2883 obj.state.hidden = false;
2884 this._node_changed(obj.parent);
2889 * triggered when an node is shown
2891 * @name show_node.jstree
2892 * @param {Object} node the shown node
2894 this.trigger('show_node', { 'node' : obj });
2900 * @trigger hide_all.jstree
2902 hide_all : function (skip_redraw) {
2903 var i, m = this._model.data, ids = [];
2905 if(m.hasOwnProperty(i) && i !== $.jstree.root && !m[i].state.hidden) {
2906 m[i].state.hidden = true;
2910 this._model.force_full_redraw = true;
2915 * triggered when all nodes are hidden
2917 * @name hide_all.jstree
2918 * @param {Array} nodes the IDs of all hidden nodes
2920 this.trigger('hide_all', { 'nodes' : ids });
2926 * @trigger show_all.jstree
2928 show_all : function (skip_redraw) {
2929 var i, m = this._model.data, ids = [];
2931 if(m.hasOwnProperty(i) && i !== $.jstree.root && m[i].state.hidden) {
2932 m[i].state.hidden = false;
2936 this._model.force_full_redraw = true;
2941 * triggered when all nodes are shown
2943 * @name show_all.jstree
2944 * @param {Array} nodes the IDs of all shown nodes
2946 this.trigger('show_all', { 'nodes' : ids });
2950 * called when a node is selected by the user. Used internally.
2952 * @name activate_node(obj, e)
2953 * @param {mixed} obj the node
2954 * @param {Object} e the related event
2955 * @trigger activate_node.jstree, changed.jstree
2957 activate_node : function (obj, e) {
2958 if(this.is_disabled(obj)) {
2961 if(!e || typeof e !== 'object') {
2965 // 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
2966 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;
2967 if(this._data.core.last_clicked && !this._data.core.last_clicked.state.selected) { this._data.core.last_clicked = null; }
2968 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]); }
2970 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 ) )) {
2971 if(!this.settings.core.multiple && (e.metaKey || e.ctrlKey || e.shiftKey) && this.is_selected(obj)) {
2972 this.deselect_node(obj, false, e);
2975 this.deselect_all(true);
2976 this.select_node(obj, false, false, e);
2977 this._data.core.last_clicked = this.get_node(obj);
2982 var o = this.get_node(obj).id,
2983 l = this._data.core.last_clicked.id,
2984 p = this.get_node(this._data.core.last_clicked.parent).children,
2987 for(i = 0, j = p.length; i < j; i += 1) {
2988 // separate IFs work whem o and l are the same
2995 if(!this.is_disabled(p[i]) && (c || p[i] === o || p[i] === l)) {
2996 this.select_node(p[i], true, false, e);
2999 this.deselect_node(p[i], true, e);
3002 this.trigger('changed', { 'action' : 'select_node', 'node' : this.get_node(obj), 'selected' : this._data.core.selected, 'event' : e });
3005 if(!this.is_selected(obj)) {
3006 this.select_node(obj, false, false, e);
3009 this.deselect_node(obj, false, e);
3014 * triggered when an node is clicked or intercated with by the user
3016 * @name activate_node.jstree
3017 * @param {Object} node
3018 * @param {Object} event the ooriginal event (if any) which triggered the call (may be an empty object)
3020 this.trigger('activate_node', { 'node' : this.get_node(obj), 'event' : e });
3023 * applies the hover state on a node, called when a node is hovered by the user. Used internally.
3025 * @name hover_node(obj)
3026 * @param {mixed} obj
3027 * @trigger hover_node.jstree
3029 hover_node : function (obj) {
3030 obj = this.get_node(obj, true);
3031 if(!obj || !obj.length || obj.children('.jstree-hovered').length) {
3034 var o = this.element.find('.jstree-hovered'), t = this.element;
3035 if(o && o.length) { this.dehover_node(o); }
3037 obj.children('.jstree-anchor').addClass('jstree-hovered');
3039 * triggered when an node is hovered
3041 * @name hover_node.jstree
3042 * @param {Object} node
3044 this.trigger('hover_node', { 'node' : this.get_node(obj) });
3045 setTimeout(function () { t.attr('aria-activedescendant', obj[0].id); }, 0);
3048 * removes the hover state from a nodecalled when a node is no longer hovered by the user. Used internally.
3050 * @name dehover_node(obj)
3051 * @param {mixed} obj
3052 * @trigger dehover_node.jstree
3054 dehover_node : function (obj) {
3055 obj = this.get_node(obj, true);
3056 if(!obj || !obj.length || !obj.children('.jstree-hovered').length) {
3059 obj.children('.jstree-anchor').removeClass('jstree-hovered');
3061 * triggered when an node is no longer hovered
3063 * @name dehover_node.jstree
3064 * @param {Object} node
3066 this.trigger('dehover_node', { 'node' : this.get_node(obj) });
3070 * @name select_node(obj [, supress_event, prevent_open])
3071 * @param {mixed} obj an array can be used to select multiple nodes
3072 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3073 * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened
3074 * @trigger select_node.jstree, changed.jstree
3076 select_node : function (obj, supress_event, prevent_open, e) {
3077 var dom, t1, t2, th;
3078 if($.isArray(obj)) {
3080 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3081 this.select_node(obj[t1], supress_event, prevent_open, e);
3085 obj = this.get_node(obj);
3086 if(!obj || obj.id === $.jstree.root) {
3089 dom = this.get_node(obj, true);
3090 if(!obj.state.selected) {
3091 obj.state.selected = true;
3092 this._data.core.selected.push(obj.id);
3094 dom = this._open_to(obj);
3096 if(dom && dom.length) {
3097 dom.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked');
3100 * triggered when an node is selected
3102 * @name select_node.jstree
3103 * @param {Object} node
3104 * @param {Array} selected the current selection
3105 * @param {Object} event the event (if any) that triggered this select_node
3107 this.trigger('select_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3108 if(!supress_event) {
3110 * triggered when selection changes
3112 * @name changed.jstree
3113 * @param {Object} node
3114 * @param {Object} action the action that caused the selection to change
3115 * @param {Array} selected the current selection
3116 * @param {Object} event the event (if any) that triggered this changed event
3118 this.trigger('changed', { 'action' : 'select_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3124 * @name deselect_node(obj [, supress_event])
3125 * @param {mixed} obj an array can be used to deselect multiple nodes
3126 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3127 * @trigger deselect_node.jstree, changed.jstree
3129 deselect_node : function (obj, supress_event, e) {
3131 if($.isArray(obj)) {
3133 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3134 this.deselect_node(obj[t1], supress_event, e);
3138 obj = this.get_node(obj);
3139 if(!obj || obj.id === $.jstree.root) {
3142 dom = this.get_node(obj, true);
3143 if(obj.state.selected) {
3144 obj.state.selected = false;
3145 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.id);
3147 dom.attr('aria-selected', false).children('.jstree-anchor').removeClass('jstree-clicked');
3150 * triggered when an node is deselected
3152 * @name deselect_node.jstree
3153 * @param {Object} node
3154 * @param {Array} selected the current selection
3155 * @param {Object} event the event (if any) that triggered this deselect_node
3157 this.trigger('deselect_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3158 if(!supress_event) {
3159 this.trigger('changed', { 'action' : 'deselect_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3164 * select all nodes in the tree
3165 * @name select_all([supress_event])
3166 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3167 * @trigger select_all.jstree, changed.jstree
3169 select_all : function (supress_event) {
3170 var tmp = this._data.core.selected.concat([]), i, j;
3171 this._data.core.selected = this._model.data[$.jstree.root].children_d.concat();
3172 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
3173 if(this._model.data[this._data.core.selected[i]]) {
3174 this._model.data[this._data.core.selected[i]].state.selected = true;
3179 * triggered when all nodes are selected
3181 * @name select_all.jstree
3182 * @param {Array} selected the current selection
3184 this.trigger('select_all', { 'selected' : this._data.core.selected });
3185 if(!supress_event) {
3186 this.trigger('changed', { 'action' : 'select_all', 'selected' : this._data.core.selected, 'old_selection' : tmp });
3190 * deselect all selected nodes
3191 * @name deselect_all([supress_event])
3192 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3193 * @trigger deselect_all.jstree, changed.jstree
3195 deselect_all : function (supress_event) {
3196 var tmp = this._data.core.selected.concat([]), i, j;
3197 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
3198 if(this._model.data[this._data.core.selected[i]]) {
3199 this._model.data[this._data.core.selected[i]].state.selected = false;
3202 this._data.core.selected = [];
3203 this.element.find('.jstree-clicked').removeClass('jstree-clicked').parent().attr('aria-selected', false);
3205 * triggered when all nodes are deselected
3207 * @name deselect_all.jstree
3208 * @param {Object} node the previous selection
3209 * @param {Array} selected the current selection
3211 this.trigger('deselect_all', { 'selected' : this._data.core.selected, 'node' : tmp });
3212 if(!supress_event) {
3213 this.trigger('changed', { 'action' : 'deselect_all', 'selected' : this._data.core.selected, 'old_selection' : tmp });
3217 * checks if a node is selected
3218 * @name is_selected(obj)
3219 * @param {mixed} obj
3222 is_selected : function (obj) {
3223 obj = this.get_node(obj);
3224 if(!obj || obj.id === $.jstree.root) {
3227 return obj.state.selected;
3230 * get an array of all selected nodes
3231 * @name get_selected([full])
3232 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3235 get_selected : function (full) {
3236 return full ? $.map(this._data.core.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.core.selected.slice();
3239 * get an array of all top level selected nodes (ignoring children of selected nodes)
3240 * @name get_top_selected([full])
3241 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3244 get_top_selected : function (full) {
3245 var tmp = this.get_selected(true),
3246 obj = {}, i, j, k, l;
3247 for(i = 0, j = tmp.length; i < j; i++) {
3248 obj[tmp[i].id] = tmp[i];
3250 for(i = 0, j = tmp.length; i < j; i++) {
3251 for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
3252 if(obj[tmp[i].children_d[k]]) {
3253 delete obj[tmp[i].children_d[k]];
3259 if(obj.hasOwnProperty(i)) {
3263 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
3266 * get an array of all bottom level selected nodes (ignoring selected parents)
3267 * @name get_bottom_selected([full])
3268 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3271 get_bottom_selected : function (full) {
3272 var tmp = this.get_selected(true),
3274 for(i = 0, j = tmp.length; i < j; i++) {
3275 if(!tmp[i].children.length) {
3276 obj.push(tmp[i].id);
3279 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
3282 * gets the current state of the tree so that it can be restored later with `set_state(state)`. Used internally.
3287 get_state : function () {
3292 'left' : this.element.scrollLeft(),
3293 'top' : this.element.scrollTop()
3297 'name' : this.get_theme(),
3298 'icons' : this._data.core.themes.icons,
3299 'dots' : this._data.core.themes.dots
3305 for(i in this._model.data) {
3306 if(this._model.data.hasOwnProperty(i)) {
3307 if(i !== $.jstree.root) {
3308 if(this._model.data[i].state.opened) {
3309 state.core.open.push(i);
3311 if(this._model.data[i].state.selected) {
3312 state.core.selected.push(i);
3320 * sets the state of the tree. Used internally.
3321 * @name set_state(state [, callback])
3323 * @param {Object} state the state to restore. Keep in mind this object is passed by reference and jstree will modify it.
3324 * @param {Function} callback an optional function to execute once the state is restored.
3325 * @trigger set_state.jstree
3327 set_state : function (state, callback) {
3330 var res, n, t, _this, i;
3331 if(state.core.open) {
3332 if(!$.isArray(state.core.open) || !state.core.open.length) {
3333 delete state.core.open;
3334 this.set_state(state, callback);
3337 this._load_nodes(state.core.open, function (nodes) {
3338 this.open_node(nodes, false, 0);
3339 delete state.core.open;
3340 this.set_state(state, callback);
3345 if(state.core.scroll) {
3346 if(state.core.scroll && state.core.scroll.left !== undefined) {
3347 this.element.scrollLeft(state.core.scroll.left);
3349 if(state.core.scroll && state.core.scroll.top !== undefined) {
3350 this.element.scrollTop(state.core.scroll.top);
3352 delete state.core.scroll;
3353 this.set_state(state, callback);
3356 if(state.core.selected) {
3358 this.deselect_all();
3359 $.each(state.core.selected, function (i, v) {
3360 _this.select_node(v, false, true);
3362 delete state.core.selected;
3363 this.set_state(state, callback);
3367 if(state.hasOwnProperty(i) && i !== "core" && $.inArray(i, this.settings.plugins) === -1) {
3371 if($.isEmptyObject(state.core)) {
3373 this.set_state(state, callback);
3377 if($.isEmptyObject(state)) {
3379 if(callback) { callback.call(this); }
3381 * triggered when a `set_state` call completes
3383 * @name set_state.jstree
3385 this.trigger('set_state');
3393 * refreshes the tree - all nodes are reloaded with calls to `load_node`.
3395 * @param {Boolean} skip_loading an option to skip showing the loading indicator
3396 * @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
3397 * @trigger refresh.jstree
3399 refresh : function (skip_loading, forget_state) {
3400 this._data.core.state = forget_state === true ? {} : this.get_state();
3401 if(forget_state && $.isFunction(forget_state)) { this._data.core.state = forget_state.call(this, this._data.core.state); }
3403 this._model.data = {};
3404 this._model.data[$.jstree.root] = {
3410 state : { loaded : false }
3412 this._data.core.selected = [];
3413 this._data.core.last_clicked = null;
3414 this._data.core.focused = null;
3416 var c = this.get_container_ul()[0].className;
3418 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>");
3419 this.element.attr('aria-activedescendant','j'+this._id+'_loading');
3421 this.load_node($.jstree.root, function (o, s) {
3423 this.get_container_ul()[0].className = c;
3424 if(this._firstChild(this.get_container_ul()[0])) {
3425 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
3427 this.set_state($.extend(true, {}, this._data.core.state), function () {
3429 * triggered when a `refresh` call completes
3431 * @name refresh.jstree
3433 this.trigger('refresh');
3436 this._data.core.state = null;
3440 * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`.
3441 * @name refresh_node(obj)
3442 * @param {mixed} obj the node
3443 * @trigger refresh_node.jstree
3445 refresh_node : function (obj) {
3446 obj = this.get_node(obj);
3447 if(!obj || obj.id === $.jstree.root) { return false; }
3448 var opened = [], to_load = [], s = this._data.core.selected.concat([]);
3449 to_load.push(obj.id);
3450 if(obj.state.opened === true) { opened.push(obj.id); }
3451 this.get_node(obj, true).find('.jstree-open').each(function() { opened.push(this.id); });
3452 this._load_nodes(to_load, $.proxy(function (nodes) {
3453 this.open_node(opened, false, 0);
3454 this.select_node(this._data.core.selected);
3456 * triggered when a node is refreshed
3458 * @name refresh_node.jstree
3459 * @param {Object} node - the refreshed node
3460 * @param {Array} nodes - an array of the IDs of the nodes that were reloaded
3462 this.trigger('refresh_node', { 'node' : obj, 'nodes' : nodes });
3466 * set (change) the ID of a node
3467 * @name set_id(obj, id)
3468 * @param {mixed} obj the node
3469 * @param {String} id the new ID
3472 set_id : function (obj, id) {
3473 obj = this.get_node(obj);
3474 if(!obj || obj.id === $.jstree.root) { return false; }
3475 var i, j, m = this._model.data;
3477 // update parents (replace current ID with new one in children and children_d)
3478 m[obj.parent].children[$.inArray(obj.id, m[obj.parent].children)] = id;
3479 for(i = 0, j = obj.parents.length; i < j; i++) {
3480 m[obj.parents[i]].children_d[$.inArray(obj.id, m[obj.parents[i]].children_d)] = id;
3482 // update children (replace current ID with new one in parent and parents)
3483 for(i = 0, j = obj.children.length; i < j; i++) {
3484 m[obj.children[i]].parent = id;
3486 for(i = 0, j = obj.children_d.length; i < j; i++) {
3487 m[obj.children_d[i]].parents[$.inArray(obj.id, m[obj.children_d[i]].parents)] = id;
3489 i = $.inArray(obj.id, this._data.core.selected);
3490 if(i !== -1) { this._data.core.selected[i] = id; }
3491 // update model and obj itself (obj.id, this._model.data[KEY])
3492 i = this.get_node(obj.id, true);
3494 i.attr('id', id).children('.jstree-anchor').attr('id', id + '_anchor').end().attr('aria-labelledby', id + '_anchor');
3495 if(this.element.attr('aria-activedescendant') === obj.id) {
3496 this.element.attr('aria-activedescendant', id);
3501 obj.li_attr.id = id;
3506 * get the text value of a node
3507 * @name get_text(obj)
3508 * @param {mixed} obj the node
3511 get_text : function (obj) {
3512 obj = this.get_node(obj);
3513 return (!obj || obj.id === $.jstree.root) ? false : obj.text;
3516 * set the text value of a node. Used internally, please use `rename_node(obj, val)`.
3518 * @name set_text(obj, val)
3519 * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes
3520 * @param {String} val the new text value
3522 * @trigger set_text.jstree
3524 set_text : function (obj, val) {
3526 if($.isArray(obj)) {
3528 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3529 this.set_text(obj[t1], val);
3533 obj = this.get_node(obj);
3534 if(!obj || obj.id === $.jstree.root) { return false; }
3536 if(this.get_node(obj, true).length) {
3537 this.redraw_node(obj.id);
3540 * triggered when a node text value is changed
3542 * @name set_text.jstree
3543 * @param {Object} obj
3544 * @param {String} text the new value
3546 this.trigger('set_text',{ "obj" : obj, "text" : val });
3550 * gets a JSON representation of a node (or the whole tree)
3551 * @name get_json([obj, options])
3552 * @param {mixed} obj
3553 * @param {Object} options
3554 * @param {Boolean} options.no_state do not return state information
3555 * @param {Boolean} options.no_id do not return ID
3556 * @param {Boolean} options.no_children do not include children
3557 * @param {Boolean} options.no_data do not include node data
3558 * @param {Boolean} options.flat return flat JSON instead of nested
3561 get_json : function (obj, options, flat) {
3562 obj = this.get_node(obj || $.jstree.root);
3563 if(!obj) { return false; }
3564 if(options && options.flat && !flat) { flat = []; }
3568 'icon' : this.get_icon(obj),
3569 'li_attr' : $.extend(true, {}, obj.li_attr),
3570 'a_attr' : $.extend(true, {}, obj.a_attr),
3572 'data' : options && options.no_data ? false : $.extend(true, {}, obj.data)
3573 //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ),
3575 if(options && options.flat) {
3576 tmp.parent = obj.parent;
3581 if(!options || !options.no_state) {
3582 for(i in obj.state) {
3583 if(obj.state.hasOwnProperty(i)) {
3584 tmp.state[i] = obj.state[i];
3588 if(options && options.no_id) {
3590 if(tmp.li_attr && tmp.li_attr.id) {
3591 delete tmp.li_attr.id;
3593 if(tmp.a_attr && tmp.a_attr.id) {
3594 delete tmp.a_attr.id;
3597 if(options && options.flat && obj.id !== $.jstree.root) {
3600 if(!options || !options.no_children) {
3601 for(i = 0, j = obj.children.length; i < j; i++) {
3602 if(options && options.flat) {
3603 this.get_json(obj.children[i], options, flat);
3606 tmp.children.push(this.get_json(obj.children[i], options));
3610 return options && options.flat ? flat : (obj.id === $.jstree.root ? tmp.children : tmp);
3613 * create a new node (do not confuse with load_node)
3614 * @name create_node([obj, node, pos, callback, is_loaded])
3615 * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`)
3616 * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name)
3617 * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last"
3618 * @param {Function} callback a function to be called once the node is created
3619 * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded
3620 * @return {String} the ID of the newly create node
3621 * @trigger model.jstree, create_node.jstree
3623 create_node : function (par, node, pos, callback, is_loaded) {
3624 if(par === null) { par = $.jstree.root; }
3625 par = this.get_node(par);
3626 if(!par) { return false; }
3627 pos = pos === undefined ? "last" : pos;
3628 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
3629 return this.load_node(par, function () { this.create_node(par, node, pos, callback, true); });
3631 if(!node) { node = { "text" : this.get_string('New node') }; }
3632 if(typeof node === "string") { node = { "text" : node }; }
3633 if(node.text === undefined) { node.text = this.get_string('New node'); }
3636 if(par.id === $.jstree.root) {
3637 if(pos === "before") { pos = "first"; }
3638 if(pos === "after") { pos = "last"; }
3642 tmp = this.get_node(par.parent);
3643 pos = $.inArray(par.id, tmp.children);
3647 tmp = this.get_node(par.parent);
3648 pos = $.inArray(par.id, tmp.children) + 1;
3656 pos = par.children.length;
3659 if(!pos) { pos = 0; }
3662 if(pos > par.children.length) { pos = par.children.length; }
3663 if(!node.id) { node.id = true; }
3664 if(!this.check("create_node", node, par, pos)) {
3665 this.settings.core.error.call(this, this._data.core.last_error);
3668 if(node.id === true) { delete node.id; }
3669 node = this._parse_model_from_json(node, par.id, par.parents.concat());
3670 if(!node) { return false; }
3671 tmp = this.get_node(node);
3674 dpc = dpc.concat(tmp.children_d);
3675 this.trigger('model', { "nodes" : dpc, "parent" : par.id });
3677 par.children_d = par.children_d.concat(dpc);
3678 for(i = 0, j = par.parents.length; i < j; i++) {
3679 this._model.data[par.parents[i]].children_d = this._model.data[par.parents[i]].children_d.concat(dpc);
3683 for(i = 0, j = par.children.length; i < j; i++) {
3684 tmp[i >= pos ? i+1 : i] = par.children[i];
3689 this.redraw_node(par, true);
3690 if(callback) { callback.call(this, this.get_node(node)); }
3692 * triggered when a node is created
3694 * @name create_node.jstree
3695 * @param {Object} node
3696 * @param {String} parent the parent's ID
3697 * @param {Number} position the position of the new node among the parent's children
3699 this.trigger('create_node', { "node" : this.get_node(node), "parent" : par.id, "position" : pos });
3703 * set the text value of a node
3704 * @name rename_node(obj, val)
3705 * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name
3706 * @param {String} val the new text value
3708 * @trigger rename_node.jstree
3710 rename_node : function (obj, val) {
3712 if($.isArray(obj)) {
3714 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3715 this.rename_node(obj[t1], val);
3719 obj = this.get_node(obj);
3720 if(!obj || obj.id === $.jstree.root) { return false; }
3722 if(!this.check("rename_node", obj, this.get_parent(obj), val)) {
3723 this.settings.core.error.call(this, this._data.core.last_error);
3726 this.set_text(obj, val); // .apply(this, Array.prototype.slice.call(arguments))
3728 * triggered when a node is renamed
3730 * @name rename_node.jstree
3731 * @param {Object} node
3732 * @param {String} text the new value
3733 * @param {String} old the old value
3735 this.trigger('rename_node', { "node" : obj, "text" : val, "old" : old });
3740 * @name delete_node(obj)
3741 * @param {mixed} obj the node, you can pass an array to delete multiple nodes
3743 * @trigger delete_node.jstree, changed.jstree
3745 delete_node : function (obj) {
3746 var t1, t2, par, pos, tmp, i, j, k, l, c, top, lft;
3747 if($.isArray(obj)) {
3749 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3750 this.delete_node(obj[t1]);
3754 obj = this.get_node(obj);
3755 if(!obj || obj.id === $.jstree.root) { return false; }
3756 par = this.get_node(obj.parent);
3757 pos = $.inArray(obj.id, par.children);
3759 if(!this.check("delete_node", obj, par, pos)) {
3760 this.settings.core.error.call(this, this._data.core.last_error);
3764 par.children = $.vakata.array_remove(par.children, pos);
3766 tmp = obj.children_d.concat([]);
3768 for(k = 0, l = tmp.length; k < l; k++) {
3769 for(i = 0, j = obj.parents.length; i < j; i++) {
3770 pos = $.inArray(tmp[k], this._model.data[obj.parents[i]].children_d);
3772 this._model.data[obj.parents[i]].children_d = $.vakata.array_remove(this._model.data[obj.parents[i]].children_d, pos);
3775 if(this._model.data[tmp[k]].state.selected) {
3777 pos = $.inArray(tmp[k], this._data.core.selected);
3779 this._data.core.selected = $.vakata.array_remove(this._data.core.selected, pos);
3784 * triggered when a node is deleted
3786 * @name delete_node.jstree
3787 * @param {Object} node
3788 * @param {String} parent the parent's ID
3790 this.trigger('delete_node', { "node" : obj, "parent" : par.id });
3792 this.trigger('changed', { 'action' : 'delete_node', 'node' : obj, 'selected' : this._data.core.selected, 'parent' : par.id });
3794 for(k = 0, l = tmp.length; k < l; k++) {
3795 delete this._model.data[tmp[k]];
3797 if($.inArray(this._data.core.focused, tmp) !== -1) {
3798 this._data.core.focused = null;
3799 top = this.element[0].scrollTop;
3800 lft = this.element[0].scrollLeft;
3801 if(par.id === $.jstree.root) {
3802 this.get_node(this._model.data[$.jstree.root].children[0], true).children('.jstree-anchor').focus();
3805 this.get_node(par, true).children('.jstree-anchor').focus();
3807 this.element[0].scrollTop = top;
3808 this.element[0].scrollLeft = lft;
3810 this.redraw_node(par, true);
3814 * check if an operation is premitted on the tree. Used internally.
3816 * @name check(chk, obj, par, pos)
3817 * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node"
3818 * @param {mixed} obj the node
3819 * @param {mixed} par the parent
3820 * @param {mixed} pos the position to insert at, or if "rename_node" - the new name
3821 * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node
3824 check : function (chk, obj, par, pos, more) {
3825 obj = obj && obj.id ? obj : this.get_node(obj);
3826 par = par && par.id ? par : this.get_node(par);
3827 var tmp = chk.match(/^move_node|copy_node|create_node$/i) ? par : obj,
3828 chc = this.settings.core.check_callback;
3829 if(chk === "move_node" || chk === "copy_node") {
3830 if((!more || !more.is_multi) && (obj.id === par.id || $.inArray(obj.id, par.children) === pos || $.inArray(par.id, obj.children_d) !== -1)) {
3831 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 }) };
3835 if(tmp && tmp.data) { tmp = tmp.data; }
3836 if(tmp && tmp.functions && (tmp.functions[chk] === false || tmp.functions[chk] === true)) {
3837 if(tmp.functions[chk] === false) {
3838 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 }) };
3840 return tmp.functions[chk];
3842 if(chc === false || ($.isFunction(chc) && chc.call(this, chk, obj, par, pos, more) === false) || (chc && chc[chk] === false)) {
3843 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 }) };
3849 * get the last error
3850 * @name last_error()
3853 last_error : function () {
3854 return this._data.core.last_error;
3857 * move a node to a new parent
3858 * @name move_node(obj, par [, pos, callback, is_loaded])
3859 * @param {mixed} obj the node to move, pass an array to move multiple nodes
3860 * @param {mixed} par the new parent
3861 * @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`
3862 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
3863 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
3864 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
3865 * @param {Boolean} instance internal parameter indicating if the node comes from another instance
3866 * @trigger move_node.jstree
3868 move_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) {
3869 var t1, t2, old_par, old_pos, new_par, old_ins, is_multi, dpc, tmp, i, j, k, l, p;
3871 par = this.get_node(par);
3872 pos = pos === undefined ? 0 : pos;
3873 if(!par) { return false; }
3874 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
3875 return this.load_node(par, function () { this.move_node(obj, par, pos, callback, true, false, origin); });
3878 if($.isArray(obj)) {
3879 if(obj.length === 1) {
3883 //obj = obj.slice();
3884 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3885 if((tmp = this.move_node(obj[t1], par, pos, callback, is_loaded, false, origin))) {
3894 obj = obj && obj.id ? obj : this.get_node(obj);
3896 if(!obj || obj.id === $.jstree.root) { return false; }
3898 old_par = (obj.parent || $.jstree.root).toString();
3899 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent);
3900 old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
3901 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
3902 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;
3903 if(old_ins && old_ins._id) {
3904 obj = old_ins._model.data[obj.id];
3908 if((tmp = this.copy_node(obj, par, pos, callback, is_loaded, false, origin))) {
3909 if(old_ins) { old_ins.delete_node(obj); }
3914 //var m = this._model.data;
3915 if(par.id === $.jstree.root) {
3916 if(pos === "before") { pos = "first"; }
3917 if(pos === "after") { pos = "last"; }
3921 pos = $.inArray(par.id, new_par.children);
3924 pos = $.inArray(par.id, new_par.children) + 1;
3931 pos = new_par.children.length;
3934 if(!pos) { pos = 0; }
3937 if(pos > new_par.children.length) { pos = new_par.children.length; }
3938 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) })) {
3939 this.settings.core.error.call(this, this._data.core.last_error);
3942 if(obj.parent === new_par.id) {
3943 dpc = new_par.children.concat();
3944 tmp = $.inArray(obj.id, dpc);
3946 dpc = $.vakata.array_remove(dpc, tmp);
3947 if(pos > tmp) { pos--; }
3950 for(i = 0, j = dpc.length; i < j; i++) {
3951 tmp[i >= pos ? i+1 : i] = dpc[i];
3954 new_par.children = tmp;
3955 this._node_changed(new_par.id);
3956 this.redraw(new_par.id === $.jstree.root);
3959 // clean old parent and up
3960 tmp = obj.children_d.concat();
3962 for(i = 0, j = obj.parents.length; i < j; i++) {
3964 p = old_ins._model.data[obj.parents[i]].children_d;
3965 for(k = 0, l = p.length; k < l; k++) {
3966 if($.inArray(p[k], tmp) === -1) {
3970 old_ins._model.data[obj.parents[i]].children_d = dpc;
3972 old_ins._model.data[old_par].children = $.vakata.array_remove_item(old_ins._model.data[old_par].children, obj.id);
3974 // insert into new parent and up
3975 for(i = 0, j = new_par.parents.length; i < j; i++) {
3976 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(tmp);
3979 for(i = 0, j = new_par.children.length; i < j; i++) {
3980 dpc[i >= pos ? i+1 : i] = new_par.children[i];
3983 new_par.children = dpc;
3984 new_par.children_d.push(obj.id);
3985 new_par.children_d = new_par.children_d.concat(obj.children_d);
3988 obj.parent = new_par.id;
3989 tmp = new_par.parents.concat();
3990 tmp.unshift(new_par.id);
3991 p = obj.parents.length;
3994 // update object children
3996 for(i = 0, j = obj.children_d.length; i < j; i++) {
3997 this._model.data[obj.children_d[i]].parents = this._model.data[obj.children_d[i]].parents.slice(0,p*-1);
3998 Array.prototype.push.apply(this._model.data[obj.children_d[i]].parents, tmp);
4001 if(old_par === $.jstree.root || new_par.id === $.jstree.root) {
4002 this._model.force_full_redraw = true;
4004 if(!this._model.force_full_redraw) {
4005 this._node_changed(old_par);
4006 this._node_changed(new_par.id);
4012 if(callback) { callback.call(this, obj, new_par, pos); }
4014 * triggered when a node is moved
4016 * @name move_node.jstree
4017 * @param {Object} node
4018 * @param {String} parent the parent's ID
4019 * @param {Number} position the position of the node among the parent's children
4020 * @param {String} old_parent the old parent of the node
4021 * @param {Number} old_position the old position of the node
4022 * @param {Boolean} is_multi do the node and new parent belong to different instances
4023 * @param {jsTree} old_instance the instance the node came from
4024 * @param {jsTree} new_instance the instance of the new parent
4026 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 });
4030 * copy a node to a new parent
4031 * @name copy_node(obj, par [, pos, callback, is_loaded])
4032 * @param {mixed} obj the node to copy, pass an array to copy multiple nodes
4033 * @param {mixed} par the new parent
4034 * @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`
4035 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
4036 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
4037 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
4038 * @param {Boolean} instance internal parameter indicating if the node comes from another instance
4039 * @trigger model.jstree copy_node.jstree
4041 copy_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) {
4042 var t1, t2, dpc, tmp, i, j, node, old_par, new_par, old_ins, is_multi;
4044 par = this.get_node(par);
4045 pos = pos === undefined ? 0 : pos;
4046 if(!par) { return false; }
4047 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
4048 return this.load_node(par, function () { this.copy_node(obj, par, pos, callback, true, false, origin); });
4051 if($.isArray(obj)) {
4052 if(obj.length === 1) {
4056 //obj = obj.slice();
4057 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4058 if((tmp = this.copy_node(obj[t1], par, pos, callback, is_loaded, true, origin))) {
4067 obj = obj && obj.id ? obj : this.get_node(obj);
4068 if(!obj || obj.id === $.jstree.root) { return false; }
4070 old_par = (obj.parent || $.jstree.root).toString();
4071 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent);
4072 old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
4073 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
4075 if(old_ins && old_ins._id) {
4076 obj = old_ins._model.data[obj.id];
4079 if(par.id === $.jstree.root) {
4080 if(pos === "before") { pos = "first"; }
4081 if(pos === "after") { pos = "last"; }
4085 pos = $.inArray(par.id, new_par.children);
4088 pos = $.inArray(par.id, new_par.children) + 1;
4095 pos = new_par.children.length;
4098 if(!pos) { pos = 0; }
4101 if(pos > new_par.children.length) { pos = new_par.children.length; }
4102 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) })) {
4103 this.settings.core.error.call(this, this._data.core.last_error);
4106 node = old_ins ? old_ins.get_json(obj, { no_id : true, no_data : true, no_state : true }) : obj;
4107 if(!node) { return false; }
4108 if(node.id === true) { delete node.id; }
4109 node = this._parse_model_from_json(node, new_par.id, new_par.parents.concat());
4110 if(!node) { return false; }
4111 tmp = this.get_node(node);
4112 if(obj && obj.state && obj.state.loaded === false) { tmp.state.loaded = false; }
4115 dpc = dpc.concat(tmp.children_d);
4116 this.trigger('model', { "nodes" : dpc, "parent" : new_par.id });
4118 // insert into new parent and up
4119 for(i = 0, j = new_par.parents.length; i < j; i++) {
4120 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(dpc);
4123 for(i = 0, j = new_par.children.length; i < j; i++) {
4124 dpc[i >= pos ? i+1 : i] = new_par.children[i];
4127 new_par.children = dpc;
4128 new_par.children_d.push(tmp.id);
4129 new_par.children_d = new_par.children_d.concat(tmp.children_d);
4131 if(new_par.id === $.jstree.root) {
4132 this._model.force_full_redraw = true;
4134 if(!this._model.force_full_redraw) {
4135 this._node_changed(new_par.id);
4138 this.redraw(new_par.id === $.jstree.root);
4140 if(callback) { callback.call(this, tmp, new_par, pos); }
4142 * triggered when a node is copied
4144 * @name copy_node.jstree
4145 * @param {Object} node the copied node
4146 * @param {Object} original the original node
4147 * @param {String} parent the parent's ID
4148 * @param {Number} position the position of the node among the parent's children
4149 * @param {String} old_parent the old parent of the node
4150 * @param {Number} old_position the position of the original node
4151 * @param {Boolean} is_multi do the node and new parent belong to different instances
4152 * @param {jsTree} old_instance the instance the node came from
4153 * @param {jsTree} new_instance the instance of the new parent
4155 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 });
4159 * cut a node (a later call to `paste(obj)` would move the node)
4161 * @param {mixed} obj multiple objects can be passed using an array
4162 * @trigger cut.jstree
4164 cut : function (obj) {
4165 if(!obj) { obj = this._data.core.selected.concat(); }
4166 if(!$.isArray(obj)) { obj = [obj]; }
4167 if(!obj.length) { return false; }
4168 var tmp = [], o, t1, t2;
4169 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4170 o = this.get_node(obj[t1]);
4171 if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); }
4173 if(!tmp.length) { return false; }
4176 ccp_mode = 'move_node';
4178 * triggered when nodes are added to the buffer for moving
4181 * @param {Array} node
4183 this.trigger('cut', { "node" : obj });
4186 * copy a node (a later call to `paste(obj)` would copy the node)
4188 * @param {mixed} obj multiple objects can be passed using an array
4189 * @trigger copy.jstree
4191 copy : function (obj) {
4192 if(!obj) { obj = this._data.core.selected.concat(); }
4193 if(!$.isArray(obj)) { obj = [obj]; }
4194 if(!obj.length) { return false; }
4195 var tmp = [], o, t1, t2;
4196 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4197 o = this.get_node(obj[t1]);
4198 if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); }
4200 if(!tmp.length) { return false; }
4203 ccp_mode = 'copy_node';
4205 * triggered when nodes are added to the buffer for copying
4208 * @param {Array} node
4210 this.trigger('copy', { "node" : obj });
4213 * get the current buffer (any nodes that are waiting for a paste operation)
4214 * @name get_buffer()
4215 * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance)
4217 get_buffer : function () {
4218 return { 'mode' : ccp_mode, 'node' : ccp_node, 'inst' : ccp_inst };
4221 * check if there is something in the buffer to paste
4225 can_paste : function () {
4226 return ccp_mode !== false && ccp_node !== false; // && ccp_inst._model.data[ccp_node];
4229 * copy or move the previously cut or copied nodes to a new parent
4230 * @name paste(obj [, pos])
4231 * @param {mixed} obj the new parent
4232 * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0`
4233 * @trigger paste.jstree
4235 paste : function (obj, pos) {
4236 obj = this.get_node(obj);
4237 if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; }
4238 if(this[ccp_mode](ccp_node, obj, pos, false, false, false, ccp_inst)) {
4240 * triggered when paste is invoked
4242 * @name paste.jstree
4243 * @param {String} parent the ID of the receiving node
4244 * @param {Array} node the nodes in the buffer
4245 * @param {String} mode the performed operation - "copy_node" or "move_node"
4247 this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode });
4254 * clear the buffer of previously copied or cut nodes
4255 * @name clear_buffer()
4256 * @trigger clear_buffer.jstree
4258 clear_buffer : function () {
4263 * triggered when the copy / cut buffer is cleared
4265 * @name clear_buffer.jstree
4267 this.trigger('clear_buffer');
4270 * put a node in edit mode (input field to rename the node)
4271 * @name edit(obj [, default_text, callback])
4272 * @param {mixed} obj
4273 * @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)
4274 * @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
4276 edit : function (obj, default_text, callback) {
4277 var rtl, w, a, s, t, h1, h2, fn, tmp, cancel = false;
4278 obj = this.get_node(obj);
4279 if(!obj) { return false; }
4280 if(this.settings.core.check_callback === false) {
4281 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_07', 'reason' : 'Could not edit node because of check_callback' };
4282 this.settings.core.error.call(this, this._data.core.last_error);
4286 default_text = typeof default_text === 'string' ? default_text : obj.text;
4287 this.set_text(obj, "");
4288 obj = this._open_to(obj);
4289 tmp.text = default_text;
4291 rtl = this._data.core.rtl;
4292 w = this.element.width();
4293 this._data.core.focused = tmp.id;
4294 a = obj.children('.jstree-anchor').focus();
4297 oi = obj.children("i:visible"),
4298 ai = a.children("i:visible"),
4299 w1 = oi.width() * oi.length,
4300 w2 = ai.width() * ai.length,
4303 h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body");
4304 h2 = $("<"+"input />", {
4306 "class" : "jstree-rename-input",
4307 // "size" : t.length,
4310 "border" : "1px solid silver",
4311 "box-sizing" : "border-box",
4312 "display" : "inline-block",
4313 "height" : (this._data.core.li_height) + "px",
4314 "lineHeight" : (this._data.core.li_height) + "px",
4315 "width" : "150px" // will be set a bit further down
4317 "blur" : $.proxy(function (e) {
4318 e.stopImmediatePropagation();
4320 var i = s.children(".jstree-rename-input"),
4322 f = this.settings.core.force_text,
4324 if(v === "") { v = t; }
4328 t = f ? t : $('<div></div>').append($.parseHTML(t)).html();
4329 this.set_text(obj, t);
4330 nv = !!this.rename_node(obj, f ? $('<div></div>').text(v).text() : $('<div></div>').append($.parseHTML(v)).html());
4332 this.set_text(obj, t); // move this up? and fix #483
4334 this._data.core.focused = tmp.id;
4335 setTimeout($.proxy(function () {
4336 var node = this.get_node(tmp.id, true);
4338 this._data.core.focused = tmp.id;
4339 node.children('.jstree-anchor').focus();
4343 callback.call(this, tmp, nv, cancel);
4346 "keydown" : function (e) {
4352 if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) {
4353 e.stopImmediatePropagation();
4355 if(key === 27 || key === 13) {
4360 "click" : function (e) { e.stopImmediatePropagation(); },
4361 "mousedown" : function (e) { e.stopImmediatePropagation(); },
4362 "keyup" : function (e) {
4363 h2.width(Math.min(h1.text("pW" + this.value).width(),w));
4365 "keypress" : function(e) {
4366 if(e.which === 13) { return false; }
4370 fontFamily : a.css('fontFamily') || '',
4371 fontSize : a.css('fontSize') || '',
4372 fontWeight : a.css('fontWeight') || '',
4373 fontStyle : a.css('fontStyle') || '',
4374 fontStretch : a.css('fontStretch') || '',
4375 fontVariant : a.css('fontVariant') || '',
4376 letterSpacing : a.css('letterSpacing') || '',
4377 wordSpacing : a.css('wordSpacing') || ''
4379 s.attr('class', a.attr('class')).append(a.contents().clone()).append(h2);
4382 h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select();
4388 * @name set_theme(theme_name [, theme_url])
4389 * @param {String} theme_name the name of the new theme to apply
4390 * @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.
4391 * @trigger set_theme.jstree
4393 set_theme : function (theme_name, theme_url) {
4394 if(!theme_name) { return false; }
4395 if(theme_url === true) {
4396 var dir = this.settings.core.themes.dir;
4397 if(!dir) { dir = $.jstree.path + '/themes'; }
4398 theme_url = dir + '/' + theme_name + '/style.css';
4400 if(theme_url && $.inArray(theme_url, themes_loaded) === -1) {
4401 $('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />');
4402 themes_loaded.push(theme_url);
4404 if(this._data.core.themes.name) {
4405 this.element.removeClass('jstree-' + this._data.core.themes.name);
4407 this._data.core.themes.name = theme_name;
4408 this.element.addClass('jstree-' + theme_name);
4409 this.element[this.settings.core.themes.responsive ? 'addClass' : 'removeClass' ]('jstree-' + theme_name + '-responsive');
4411 * triggered when a theme is set
4413 * @name set_theme.jstree
4414 * @param {String} theme the new theme
4416 this.trigger('set_theme', { 'theme' : theme_name });
4419 * gets the name of the currently applied theme name
4423 get_theme : function () { return this._data.core.themes.name; },
4425 * changes the theme variant (if the theme has variants)
4426 * @name set_theme_variant(variant_name)
4427 * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed)
4429 set_theme_variant : function (variant_name) {
4430 if(this._data.core.themes.variant) {
4431 this.element.removeClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
4433 this._data.core.themes.variant = variant_name;
4435 this.element.addClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
4439 * gets the name of the currently applied theme variant
4443 get_theme_variant : function () { return this._data.core.themes.variant; },
4445 * shows a striped background on the container (if the theme supports it)
4446 * @name show_stripes()
4448 show_stripes : function () { this._data.core.themes.stripes = true; this.get_container_ul().addClass("jstree-striped"); },
4450 * hides the striped background on the container
4451 * @name hide_stripes()
4453 hide_stripes : function () { this._data.core.themes.stripes = false; this.get_container_ul().removeClass("jstree-striped"); },
4455 * toggles the striped background on the container
4456 * @name toggle_stripes()
4458 toggle_stripes : function () { if(this._data.core.themes.stripes) { this.hide_stripes(); } else { this.show_stripes(); } },
4460 * shows the connecting dots (if the theme supports it)
4463 show_dots : function () { this._data.core.themes.dots = true; this.get_container_ul().removeClass("jstree-no-dots"); },
4465 * hides the connecting dots
4468 hide_dots : function () { this._data.core.themes.dots = false; this.get_container_ul().addClass("jstree-no-dots"); },
4470 * toggles the connecting dots
4471 * @name toggle_dots()
4473 toggle_dots : function () { if(this._data.core.themes.dots) { this.hide_dots(); } else { this.show_dots(); } },
4475 * show the node icons
4476 * @name show_icons()
4478 show_icons : function () { this._data.core.themes.icons = true; this.get_container_ul().removeClass("jstree-no-icons"); },
4480 * hide the node icons
4481 * @name hide_icons()
4483 hide_icons : function () { this._data.core.themes.icons = false; this.get_container_ul().addClass("jstree-no-icons"); },
4485 * toggle the node icons
4486 * @name toggle_icons()
4488 toggle_icons : function () { if(this._data.core.themes.icons) { this.hide_icons(); } else { this.show_icons(); } },
4490 * set the node icon for a node
4491 * @name set_icon(obj, icon)
4492 * @param {mixed} obj
4493 * @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
4495 set_icon : function (obj, icon) {
4496 var t1, t2, dom, old;
4497 if($.isArray(obj)) {
4499 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4500 this.set_icon(obj[t1], icon);
4504 obj = this.get_node(obj);
4505 if(!obj || obj.id === $.jstree.root) { return false; }
4507 obj.icon = icon === true || icon === null || icon === undefined || icon === '' ? true : icon;
4508 dom = this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon");
4509 if(icon === false) {
4510 this.hide_icon(obj);
4512 else if(icon === true || icon === null || icon === undefined || icon === '') {
4513 dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel");
4514 if(old === false) { this.show_icon(obj); }
4516 else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) {
4517 dom.removeClass(old).css("background","");
4518 dom.addClass(icon + ' jstree-themeicon-custom').attr("rel",icon);
4519 if(old === false) { this.show_icon(obj); }
4522 dom.removeClass(old).css("background","");
4523 dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon);
4524 if(old === false) { this.show_icon(obj); }
4529 * get the node icon for a node
4530 * @name get_icon(obj)
4531 * @param {mixed} obj
4534 get_icon : function (obj) {
4535 obj = this.get_node(obj);
4536 return (!obj || obj.id === $.jstree.root) ? false : obj.icon;
4539 * hide the icon on an individual node
4540 * @name hide_icon(obj)
4541 * @param {mixed} obj
4543 hide_icon : function (obj) {
4545 if($.isArray(obj)) {
4547 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4548 this.hide_icon(obj[t1]);
4552 obj = this.get_node(obj);
4553 if(!obj || obj === $.jstree.root) { return false; }
4555 this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden');
4559 * show the icon on an individual node
4560 * @name show_icon(obj)
4561 * @param {mixed} obj
4563 show_icon : function (obj) {
4565 if($.isArray(obj)) {
4567 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4568 this.show_icon(obj[t1]);
4572 obj = this.get_node(obj);
4573 if(!obj || obj === $.jstree.root) { return false; }
4574 dom = this.get_node(obj, true);
4575 obj.icon = dom.length ? dom.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true;
4576 if(!obj.icon) { obj.icon = true; }
4577 dom.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden');
4584 // collect attributes
4585 $.vakata.attributes = function(node, with_values) {
4587 var attr = with_values ? {} : [];
4588 if(node && node.attributes) {
4589 $.each(node.attributes, function (i, v) {
4590 if($.inArray(v.name.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; }
4591 if(v.value !== null && $.trim(v.value) !== '') {
4592 if(with_values) { attr[v.name] = v.value; }
4593 else { attr.push(v.name); }
4599 $.vakata.array_unique = function(array) {
4600 var a = [], i, j, l, o = {};
4601 for(i = 0, l = array.length; i < l; i++) {
4602 if(o[array[i]] === undefined) {
4609 // remove item from array
4610 $.vakata.array_remove = function(array, from, to) {
4611 var rest = array.slice((to || from) + 1 || array.length);
4612 array.length = from < 0 ? array.length + from : from;
4613 array.push.apply(array, rest);
4616 // remove item from array
4617 $.vakata.array_remove_item = function(array, item) {
4618 var tmp = $.inArray(item, array);
4619 return tmp !== -1 ? $.vakata.array_remove(array, tmp) : array;
4624 * ### Changed plugin
4626 * 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.
4629 $.jstree.plugins.changed = function (options, parent) {
4631 this.trigger = function (ev, data) {
4636 if(ev.replace('.jstree','') === 'changed') {
4637 data.changed = { selected : [], deselected : [] };
4639 for(i = 0, j = last.length; i < j; i++) {
4642 for(i = 0, j = data.selected.length; i < j; i++) {
4643 if(!tmp[data.selected[i]]) {
4644 data.changed.selected.push(data.selected[i]);
4647 tmp[data.selected[i]] = 2;
4650 for(i = 0, j = last.length; i < j; i++) {
4651 if(tmp[last[i]] === 1) {
4652 data.changed.deselected.push(last[i]);
4655 last = data.selected.slice();
4658 * triggered when selection changes (the "changed" plugin enhances the original event with more data)
4660 * @name changed.jstree
4661 * @param {Object} node
4662 * @param {Object} action the action that caused the selection to change
4663 * @param {Array} selected the current selection
4664 * @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
4665 * @param {Object} event the event (if any) that triggered this changed event
4668 parent.trigger.call(this, ev, data);
4670 this.refresh = function (skip_loading, forget_state) {
4672 return parent.refresh.apply(this, arguments);
4677 * ### Checkbox plugin
4679 * This plugin renders checkbox icons in front of each node, making multiple selection much easier.
4680 * 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.
4683 var _i = document.createElement('I');
4684 _i.className = 'jstree-icon jstree-checkbox';
4685 _i.setAttribute('role', 'presentation');
4687 * stores all defaults for the checkbox plugin
4688 * @name $.jstree.defaults.checkbox
4691 $.jstree.defaults.checkbox = {
4693 * a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`.
4694 * @name $.jstree.defaults.checkbox.visible
4699 * a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`.
4700 * @name $.jstree.defaults.checkbox.three_state
4705 * a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`.
4706 * @name $.jstree.defaults.checkbox.whole_node
4711 * a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`.
4712 * @name $.jstree.defaults.checkbox.keep_selected_style
4715 keep_selected_style : true,
4717 * This setting controls how cascading and undetermined nodes are applied.
4718 * 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.
4719 * If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''.
4720 * @name $.jstree.defaults.checkbox.cascade
4725 * 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.
4726 * @name $.jstree.defaults.checkbox.tie_selection
4729 tie_selection : true
4731 $.jstree.plugins.checkbox = function (options, parent) {
4732 this.bind = function () {
4733 parent.bind.call(this);
4734 this._data.checkbox.uto = false;
4735 this._data.checkbox.selected = [];
4736 if(this.settings.checkbox.three_state) {
4737 this.settings.checkbox.cascade = 'up+down+undetermined';
4740 .on("init.jstree", $.proxy(function () {
4741 this._data.checkbox.visible = this.settings.checkbox.visible;
4742 if(!this.settings.checkbox.keep_selected_style) {
4743 this.element.addClass('jstree-checkbox-no-clicked');
4745 if(this.settings.checkbox.tie_selection) {
4746 this.element.addClass('jstree-checkbox-selection');
4749 .on("loading.jstree", $.proxy(function () {
4750 this[ this._data.checkbox.visible ? 'show_checkboxes' : 'hide_checkboxes' ]();
4752 if(this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
4754 .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 () {
4755 // only if undetermined is in setting
4756 if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
4757 this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
4760 if(!this.settings.checkbox.tie_selection) {
4762 .on('model.jstree', $.proxy(function (e, data) {
4763 var m = this._model.data,
4767 for(i = 0, j = dpc.length; i < j; i++) {
4768 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);
4769 if(m[dpc[i]].state.checked) {
4770 this._data.checkbox.selected.push(dpc[i]);
4775 if(this.settings.checkbox.cascade.indexOf('up') !== -1 || this.settings.checkbox.cascade.indexOf('down') !== -1) {
4777 .on('model.jstree', $.proxy(function (e, data) {
4778 var m = this._model.data,
4782 c, i, j, k, l, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
4784 if(s.indexOf('down') !== -1) {
4786 if(p.state[ t ? 'selected' : 'checked' ]) {
4787 for(i = 0, j = dpc.length; i < j; i++) {
4788 m[dpc[i]].state[ t ? 'selected' : 'checked' ] = true;
4790 this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(dpc);
4793 for(i = 0, j = dpc.length; i < j; i++) {
4794 if(m[dpc[i]].state[ t ? 'selected' : 'checked' ]) {
4795 for(k = 0, l = m[dpc[i]].children_d.length; k < l; k++) {
4796 m[m[dpc[i]].children_d[k]].state[ t ? 'selected' : 'checked' ] = true;
4798 this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(m[dpc[i]].children_d);
4804 if(s.indexOf('up') !== -1) {
4806 for(i = 0, j = p.children_d.length; i < j; i++) {
4807 if(!m[p.children_d[i]].children.length) {
4808 chd.push(m[p.children_d[i]].parent);
4811 chd = $.vakata.array_unique(chd);
4812 for(k = 0, l = chd.length; k < l; k++) {
4814 while(p && p.id !== $.jstree.root) {
4816 for(i = 0, j = p.children.length; i < j; i++) {
4817 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4820 p.state[ t ? 'selected' : 'checked' ] = true;
4821 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4822 tmp = this.get_node(p, true);
4823 if(tmp && tmp.length) {
4824 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass( t ? 'jstree-clicked' : 'jstree-checked');
4830 p = this.get_node(p.parent);
4835 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected);
4837 .on(this.settings.checkbox.tie_selection ? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e, data) {
4838 var obj = data.node,
4839 m = this._model.data,
4840 par = this.get_node(obj.parent),
4841 dom = this.get_node(obj, true),
4842 i, j, c, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
4845 if(s.indexOf('down') !== -1) {
4846 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d));
4847 for(i = 0, j = obj.children_d.length; i < j; i++) {
4848 tmp = m[obj.children_d[i]];
4849 tmp.state[ t ? 'selected' : 'checked' ] = true;
4850 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4851 tmp.original.state.undetermined = false;
4857 if(s.indexOf('up') !== -1) {
4858 while(par && par.id !== $.jstree.root) {
4860 for(i = 0, j = par.children.length; i < j; i++) {
4861 c += m[par.children[i]].state[ t ? 'selected' : 'checked' ];
4864 par.state[ t ? 'selected' : 'checked' ] = true;
4865 this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id);
4866 tmp = this.get_node(par, true);
4867 if(tmp && tmp.length) {
4868 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4874 par = this.get_node(par.parent);
4878 // apply down (process .children separately?)
4879 if(s.indexOf('down') !== -1 && dom.length) {
4880 dom.find('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', true);
4883 .on(this.settings.checkbox.tie_selection ? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e, data) {
4884 var obj = this.get_node($.jstree.root),
4885 m = this._model.data,
4887 for(i = 0, j = obj.children_d.length; i < j; i++) {
4888 tmp = m[obj.children_d[i]];
4889 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4890 tmp.original.state.undetermined = false;
4894 .on(this.settings.checkbox.tie_selection ? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e, data) {
4895 var obj = data.node,
4896 dom = this.get_node(obj, true),
4897 i, j, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
4898 if(obj && obj.original && obj.original.state && obj.original.state.undetermined) {
4899 obj.original.state.undetermined = false;
4903 if(s.indexOf('down') !== -1) {
4904 for(i = 0, j = obj.children_d.length; i < j; i++) {
4905 tmp = this._model.data[obj.children_d[i]];
4906 tmp.state[ t ? 'selected' : 'checked' ] = false;
4907 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4908 tmp.original.state.undetermined = false;
4914 if(s.indexOf('up') !== -1) {
4915 for(i = 0, j = obj.parents.length; i < j; i++) {
4916 tmp = this._model.data[obj.parents[i]];
4917 tmp.state[ t ? 'selected' : 'checked' ] = false;
4918 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4919 tmp.original.state.undetermined = false;
4921 tmp = this.get_node(obj.parents[i], true);
4922 if(tmp && tmp.length) {
4923 tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
4928 for(i = 0, j = this._data[ t ? 'core' : 'checkbox' ].selected.length; i < j; i++) {
4929 // apply down + apply up
4931 (s.indexOf('down') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.children_d) === -1) &&
4932 (s.indexOf('up') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.parents) === -1)
4934 tmp.push(this._data[ t ? 'core' : 'checkbox' ].selected[i]);
4937 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(tmp);
4939 // apply down (process .children separately?)
4940 if(s.indexOf('down') !== -1 && dom.length) {
4941 dom.find('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', false);
4945 if(this.settings.checkbox.cascade.indexOf('up') !== -1) {
4947 .on('delete_node.jstree', $.proxy(function (e, data) {
4948 // apply up (whole handler)
4949 var p = this.get_node(data.parent),
4950 m = this._model.data,
4951 i, j, c, tmp, t = this.settings.checkbox.tie_selection;
4952 while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) {
4954 for(i = 0, j = p.children.length; i < j; i++) {
4955 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4957 if(j > 0 && c === j) {
4958 p.state[ t ? 'selected' : 'checked' ] = true;
4959 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4960 tmp = this.get_node(p, true);
4961 if(tmp && tmp.length) {
4962 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4968 p = this.get_node(p.parent);
4971 .on('move_node.jstree', $.proxy(function (e, data) {
4972 // apply up (whole handler)
4973 var is_multi = data.is_multi,
4974 old_par = data.old_parent,
4975 new_par = this.get_node(data.parent),
4976 m = this._model.data,
4977 p, c, i, j, tmp, t = this.settings.checkbox.tie_selection;
4979 p = this.get_node(old_par);
4980 while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) {
4982 for(i = 0, j = p.children.length; i < j; i++) {
4983 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4985 if(j > 0 && c === j) {
4986 p.state[ t ? 'selected' : 'checked' ] = true;
4987 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4988 tmp = this.get_node(p, true);
4989 if(tmp && tmp.length) {
4990 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4996 p = this.get_node(p.parent);
5000 while(p && p.id !== $.jstree.root) {
5002 for(i = 0, j = p.children.length; i < j; i++) {
5003 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
5006 if(!p.state[ t ? 'selected' : 'checked' ]) {
5007 p.state[ t ? 'selected' : 'checked' ] = true;
5008 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
5009 tmp = this.get_node(p, true);
5010 if(tmp && tmp.length) {
5011 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
5016 if(p.state[ t ? 'selected' : 'checked' ]) {
5017 p.state[ t ? 'selected' : 'checked' ] = false;
5018 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_remove_item(this._data[ t ? 'core' : 'checkbox' ].selected, p.id);
5019 tmp = this.get_node(p, true);
5020 if(tmp && tmp.length) {
5021 tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
5028 p = this.get_node(p.parent);
5034 * set the undetermined state where and if necessary. Used internally.
5036 * @name _undetermined()
5039 this._undetermined = function () {
5040 if(this.element === null) { return; }
5041 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;
5042 for(i = 0, j = s.length; i < j; i++) {
5043 if(m[s[i]] && m[s[i]].parents) {
5044 for(k = 0, l = m[s[i]].parents.length; k < l; k++) {
5045 if(o[m[s[i]].parents[k]] === undefined && m[s[i]].parents[k] !== $.jstree.root) {
5046 o[m[s[i]].parents[k]] = true;
5047 p.push(m[s[i]].parents[k]);
5052 // attempt for server side undetermined state
5053 this.element.find('.jstree-closed').not(':has(.jstree-children)')
5055 var tmp = tt.get_node(this), tmp2;
5056 if(!tmp.state.loaded) {
5057 if(tmp.original && tmp.original.state && tmp.original.state.undetermined && tmp.original.state.undetermined === true) {
5058 if(o[tmp.id] === undefined && tmp.id !== $.jstree.root) {
5062 for(k = 0, l = tmp.parents.length; k < l; k++) {
5063 if(o[tmp.parents[k]] === undefined && tmp.parents[k] !== $.jstree.root) {
5064 o[tmp.parents[k]] = true;
5065 p.push(tmp.parents[k]);
5071 for(i = 0, j = tmp.children_d.length; i < j; i++) {
5072 tmp2 = m[tmp.children_d[i]];
5073 if(!tmp2.state.loaded && tmp2.original && tmp2.original.state && tmp2.original.state.undetermined && tmp2.original.state.undetermined === true) {
5074 if(o[tmp2.id] === undefined && tmp2.id !== $.jstree.root) {
5078 for(k = 0, l = tmp2.parents.length; k < l; k++) {
5079 if(o[tmp2.parents[k]] === undefined && tmp2.parents[k] !== $.jstree.root) {
5080 o[tmp2.parents[k]] = true;
5081 p.push(tmp2.parents[k]);
5089 this.element.find('.jstree-undetermined').removeClass('jstree-undetermined');
5090 for(i = 0, j = p.length; i < j; i++) {
5091 if(!m[p[i]].state[ t ? 'selected' : 'checked' ]) {
5092 s = this.get_node(p[i], true);
5094 s.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined');
5099 this.redraw_node = function(obj, deep, is_callback, force_render) {
5100 obj = parent.redraw_node.apply(this, arguments);
5102 var i, j, tmp = null, icon = null;
5103 for(i = 0, j = obj.childNodes.length; i < j; i++) {
5104 if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
5105 tmp = obj.childNodes[i];
5110 if(!this.settings.checkbox.tie_selection && this._model.data[obj.id].state.checked) { tmp.className += ' jstree-checked'; }
5111 icon = _i.cloneNode(false);
5112 if(this._model.data[obj.id].state.checkbox_disabled) { icon.className += ' jstree-checkbox-disabled'; }
5113 tmp.insertBefore(icon, tmp.childNodes[0]);
5116 if(!is_callback && this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
5117 if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
5118 this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
5123 * show the node checkbox icons
5124 * @name show_checkboxes()
5127 this.show_checkboxes = function () { this._data.core.themes.checkboxes = true; this.get_container_ul().removeClass("jstree-no-checkboxes"); };
5129 * hide the node checkbox icons
5130 * @name hide_checkboxes()
5133 this.hide_checkboxes = function () { this._data.core.themes.checkboxes = false; this.get_container_ul().addClass("jstree-no-checkboxes"); };
5135 * toggle the node icons
5136 * @name toggle_checkboxes()
5139 this.toggle_checkboxes = function () { if(this._data.core.themes.checkboxes) { this.hide_checkboxes(); } else { this.show_checkboxes(); } };
5141 * checks if a node is in an undetermined state
5142 * @name is_undetermined(obj)
5143 * @param {mixed} obj
5146 this.is_undetermined = function (obj) {
5147 obj = this.get_node(obj);
5148 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;
5149 if(!obj || obj.state[ t ? 'selected' : 'checked' ] === true || s.indexOf('undetermined') === -1 || (s.indexOf('down') === -1 && s.indexOf('up') === -1)) {
5152 if(!obj.state.loaded && obj.original.state.undetermined === true) {
5155 for(i = 0, j = obj.children_d.length; i < j; i++) {
5156 if($.inArray(obj.children_d[i], d) !== -1 || (!m[obj.children_d[i]].state.loaded && m[obj.children_d[i]].original.state.undetermined)) {
5163 * disable a node's checkbox
5164 * @name disable_checkbox(obj)
5165 * @param {mixed} obj an array can be used too
5166 * @trigger disable_checkbox.jstree
5169 this.disable_checkbox = function (obj) {
5171 if($.isArray(obj)) {
5173 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
5174 this.disable_checkbox(obj[t1]);
5178 obj = this.get_node(obj);
5179 if(!obj || obj.id === $.jstree.root) {
5182 dom = this.get_node(obj, true);
5183 if(!obj.state.checkbox_disabled) {
5184 obj.state.checkbox_disabled = true;
5185 if(dom && dom.length) {
5186 dom.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-checkbox-disabled');
5189 * triggered when an node's checkbox is disabled
5191 * @name disable_checkbox.jstree
5192 * @param {Object} node
5195 this.trigger('disable_checkbox', { 'node' : obj });
5199 * enable a node's checkbox
5200 * @name disable_checkbox(obj)
5201 * @param {mixed} obj an array can be used too
5202 * @trigger enable_checkbox.jstree
5205 this.enable_checkbox = function (obj) {
5207 if($.isArray(obj)) {
5209 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
5210 this.enable_checkbox(obj[t1]);
5214 obj = this.get_node(obj);
5215 if(!obj || obj.id === $.jstree.root) {
5218 dom = this.get_node(obj, true);
5219 if(obj.state.checkbox_disabled) {
5220 obj.state.checkbox_disabled = false;
5221 if(dom && dom.length) {
5222 dom.children('.jstree-anchor').children('.jstree-checkbox').removeClass('jstree-checkbox-disabled');
5225 * triggered when an node's checkbox is enabled
5227 * @name enable_checkbox.jstree
5228 * @param {Object} node
5231 this.trigger('enable_checkbox', { 'node' : obj });
5235 this.activate_node = function (obj, e) {
5236 if($(e.target).hasClass('jstree-checkbox-disabled')) {
5239 if(this.settings.checkbox.tie_selection && (this.settings.checkbox.whole_node || $(e.target).hasClass('jstree-checkbox'))) {
5242 if(this.settings.checkbox.tie_selection || (!this.settings.checkbox.whole_node && !$(e.target).hasClass('jstree-checkbox'))) {
5243 return parent.activate_node.call(this, obj, e);
5245 if(this.is_disabled(obj)) {
5248 if(this.is_checked(obj)) {
5249 this.uncheck_node(obj, e);
5252 this.check_node(obj, e);
5254 this.trigger('activate_node', { 'node' : this.get_node(obj) });
5258 * check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally)
5259 * @name check_node(obj)
5260 * @param {mixed} obj an array can be used to check multiple nodes
5261 * @trigger check_node.jstree
5264 this.check_node = function (obj, e) {
5265 if(this.settings.checkbox.tie_selection) { return this.select_node(obj, false, true, e); }
5266 var dom, t1, t2, th;
5267 if($.isArray(obj)) {
5269 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
5270 this.check_node(obj[t1], e);
5274 obj = this.get_node(obj);
5275 if(!obj || obj.id === $.jstree.root) {
5278 dom = this.get_node(obj, true);
5279 if(!obj.state.checked) {
5280 obj.state.checked = true;
5281 this._data.checkbox.selected.push(obj.id);
5282 if(dom && dom.length) {
5283 dom.children('.jstree-anchor').addClass('jstree-checked');
5286 * triggered when an node is checked (only if tie_selection in checkbox settings is false)
5288 * @name check_node.jstree
5289 * @param {Object} node
5290 * @param {Array} selected the current selection
5291 * @param {Object} event the event (if any) that triggered this check_node
5294 this.trigger('check_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
5298 * uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally)
5299 * @name uncheck_node(obj)
5300 * @param {mixed} obj an array can be used to uncheck multiple nodes
5301 * @trigger uncheck_node.jstree
5304 this.uncheck_node = function (obj, e) {
5305 if(this.settings.checkbox.tie_selection) { return this.deselect_node(obj, false, e); }
5307 if($.isArray(obj)) {
5309 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
5310 this.uncheck_node(obj[t1], e);
5314 obj = this.get_node(obj);
5315 if(!obj || obj.id === $.jstree.root) {
5318 dom = this.get_node(obj, true);
5319 if(obj.state.checked) {
5320 obj.state.checked = false;
5321 this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, obj.id);
5323 dom.children('.jstree-anchor').removeClass('jstree-checked');
5326 * triggered when an node is unchecked (only if tie_selection in checkbox settings is false)
5328 * @name uncheck_node.jstree
5329 * @param {Object} node
5330 * @param {Array} selected the current selection
5331 * @param {Object} event the event (if any) that triggered this uncheck_node
5334 this.trigger('uncheck_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
5338 * checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally)
5340 * @trigger check_all.jstree, changed.jstree
5343 this.check_all = function () {
5344 if(this.settings.checkbox.tie_selection) { return this.select_all(); }
5345 var tmp = this._data.checkbox.selected.concat([]), i, j;
5346 this._data.checkbox.selected = this._model.data[$.jstree.root].children_d.concat();
5347 for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
5348 if(this._model.data[this._data.checkbox.selected[i]]) {
5349 this._model.data[this._data.checkbox.selected[i]].state.checked = true;
5354 * triggered when all nodes are checked (only if tie_selection in checkbox settings is false)
5356 * @name check_all.jstree
5357 * @param {Array} selected the current selection
5360 this.trigger('check_all', { 'selected' : this._data.checkbox.selected });
5363 * uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally)
5364 * @name uncheck_all()
5365 * @trigger uncheck_all.jstree
5368 this.uncheck_all = function () {
5369 if(this.settings.checkbox.tie_selection) { return this.deselect_all(); }
5370 var tmp = this._data.checkbox.selected.concat([]), i, j;
5371 for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
5372 if(this._model.data[this._data.checkbox.selected[i]]) {
5373 this._model.data[this._data.checkbox.selected[i]].state.checked = false;
5376 this._data.checkbox.selected = [];
5377 this.element.find('.jstree-checked').removeClass('jstree-checked');
5379 * triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false)
5381 * @name uncheck_all.jstree
5382 * @param {Object} node the previous selection
5383 * @param {Array} selected the current selection
5386 this.trigger('uncheck_all', { 'selected' : this._data.checkbox.selected, 'node' : tmp });
5389 * checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected)
5390 * @name is_checked(obj)
5391 * @param {mixed} obj
5395 this.is_checked = function (obj) {
5396 if(this.settings.checkbox.tie_selection) { return this.is_selected(obj); }
5397 obj = this.get_node(obj);
5398 if(!obj || obj.id === $.jstree.root) { return false; }
5399 return obj.state.checked;
5402 * get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected)
5403 * @name get_checked([full])
5404 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
5408 this.get_checked = function (full) {
5409 if(this.settings.checkbox.tie_selection) { return this.get_selected(full); }
5410 return full ? $.map(this._data.checkbox.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.checkbox.selected;
5413 * 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)
5414 * @name get_top_checked([full])
5415 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
5419 this.get_top_checked = function (full) {
5420 if(this.settings.checkbox.tie_selection) { return this.get_top_selected(full); }
5421 var tmp = this.get_checked(true),
5422 obj = {}, i, j, k, l;
5423 for(i = 0, j = tmp.length; i < j; i++) {
5424 obj[tmp[i].id] = tmp[i];
5426 for(i = 0, j = tmp.length; i < j; i++) {
5427 for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
5428 if(obj[tmp[i].children_d[k]]) {
5429 delete obj[tmp[i].children_d[k]];
5435 if(obj.hasOwnProperty(i)) {
5439 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
5442 * 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)
5443 * @name get_bottom_checked([full])
5444 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
5448 this.get_bottom_checked = function (full) {
5449 if(this.settings.checkbox.tie_selection) { return this.get_bottom_selected(full); }
5450 var tmp = this.get_checked(true),
5452 for(i = 0, j = tmp.length; i < j; i++) {
5453 if(!tmp[i].children.length) {
5454 obj.push(tmp[i].id);
5457 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
5459 this.load_node = function (obj, callback) {
5460 var k, l, i, j, c, tmp;
5461 if(!$.isArray(obj) && !this.settings.checkbox.tie_selection) {
5462 tmp = this.get_node(obj);
5463 if(tmp && tmp.state.loaded) {
5464 for(k = 0, l = tmp.children_d.length; k < l; k++) {
5465 if(this._model.data[tmp.children_d[k]].state.checked) {
5467 this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, tmp.children_d[k]);
5472 return parent.load_node.apply(this, arguments);
5474 this.get_state = function () {
5475 var state = parent.get_state.apply(this, arguments);
5476 if(this.settings.checkbox.tie_selection) { return state; }
5477 state.checkbox = this._data.checkbox.selected.slice();
5480 this.set_state = function (state, callback) {
5481 var res = parent.set_state.apply(this, arguments);
5482 if(res && state.checkbox) {
5483 if(!this.settings.checkbox.tie_selection) {
5486 $.each(state.checkbox, function (i, v) {
5487 _this.check_node(v);
5490 delete state.checkbox;
5491 this.set_state(state, callback);
5496 this.refresh = function (skip_loading, forget_state) {
5497 if(!this.settings.checkbox.tie_selection) {
5498 this._data.checkbox.selected = [];
5500 return parent.refresh.apply(this, arguments);
5504 // include the checkbox plugin by default
5505 // $.jstree.defaults.plugins.push("checkbox");
5508 * ### Conditionalselect plugin
5510 * This plugin allows defining a callback to allow or deny node selection by user input (activate node method).
5514 * 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`.
5515 * @name $.jstree.defaults.checkbox.visible
5518 $.jstree.defaults.conditionalselect = function () { return true; };
5519 $.jstree.plugins.conditionalselect = function (options, parent) {
5521 this.activate_node = function (obj, e) {
5522 if(this.settings.conditionalselect.call(this, this.get_node(obj), e)) {
5523 parent.activate_node.call(this, obj, e);
5530 * ### Contextmenu plugin
5532 * Shows a context menu when a node is right-clicked.
5536 * stores all defaults for the contextmenu plugin
5537 * @name $.jstree.defaults.contextmenu
5538 * @plugin contextmenu
5540 $.jstree.defaults.contextmenu = {
5542 * a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`.
5543 * @name $.jstree.defaults.contextmenu.select_node
5544 * @plugin contextmenu
5548 * a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used.
5549 * @name $.jstree.defaults.contextmenu.show_at_node
5550 * @plugin contextmenu
5552 show_at_node : true,
5554 * 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).
5556 * 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):
5558 * * `separator_before` - a boolean indicating if there should be a separator before this item
5559 * * `separator_after` - a boolean indicating if there should be a separator after this item
5560 * * `_disabled` - a boolean indicating if this action should be disabled
5561 * * `label` - a string - the name of the action (could be a function returning a string)
5562 * * `action` - a function to be executed if this item is chosen
5563 * * `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
5564 * * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2)
5565 * * `shortcut_label` - shortcut label (like for example `F2` for rename)
5567 * @name $.jstree.defaults.contextmenu.items
5568 * @plugin contextmenu
5570 items : function (o, cb) { // Could be an object directly
5573 "separator_before" : false,
5574 "separator_after" : true,
5575 "_disabled" : false, //(this.check("create_node", data.reference, {}, "last")),
5577 "action" : function (data) {
5578 var inst = $.jstree.reference(data.reference),
5579 obj = inst.get_node(data.reference);
5580 inst.create_node(obj, {}, "last", function (new_node) {
5581 setTimeout(function () { inst.edit(new_node); },0);
5586 "separator_before" : false,
5587 "separator_after" : false,
5588 "_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")),
5592 "shortcut_label" : 'F2',
5593 "icon" : "glyphicon glyphicon-leaf",
5595 "action" : function (data) {
5596 var inst = $.jstree.reference(data.reference),
5597 obj = inst.get_node(data.reference);
5602 "separator_before" : false,
5604 "separator_after" : false,
5605 "_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")),
5607 "action" : function (data) {
5608 var inst = $.jstree.reference(data.reference),
5609 obj = inst.get_node(data.reference);
5610 if(inst.is_selected(obj)) {
5611 inst.delete_node(inst.get_selected());
5614 inst.delete_node(obj);
5619 "separator_before" : true,
5621 "separator_after" : false,
5626 "separator_before" : false,
5627 "separator_after" : false,
5629 "action" : function (data) {
5630 var inst = $.jstree.reference(data.reference),
5631 obj = inst.get_node(data.reference);
5632 if(inst.is_selected(obj)) {
5633 inst.cut(inst.get_top_selected());
5641 "separator_before" : false,
5643 "separator_after" : false,
5645 "action" : function (data) {
5646 var inst = $.jstree.reference(data.reference),
5647 obj = inst.get_node(data.reference);
5648 if(inst.is_selected(obj)) {
5649 inst.copy(inst.get_top_selected());
5657 "separator_before" : false,
5659 "_disabled" : function (data) {
5660 return !$.jstree.reference(data.reference).can_paste();
5662 "separator_after" : false,
5664 "action" : function (data) {
5665 var inst = $.jstree.reference(data.reference),
5666 obj = inst.get_node(data.reference);
5676 $.jstree.plugins.contextmenu = function (options, parent) {
5677 this.bind = function () {
5678 parent.bind.call(this);
5680 var last_ts = 0, cto = null, ex, ey;
5682 .on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e, data) {
5684 last_ts = e.ctrlKey ? +new Date() : 0;
5686 last_ts = (+new Date()) + 10000;
5691 if(!this.is_loading(e.currentTarget)) {
5692 this.show_contextmenu(e.currentTarget, e.pageX, e.pageY, e);
5695 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
5696 if(this._data.contextmenu.visible && (!last_ts || (+new Date()) - last_ts > 250)) { // work around safari & macOS ctrl+click
5697 $.vakata.context.hide();
5701 .on("touchstart.jstree", ".jstree-anchor", function (e) {
5702 if(!e.originalEvent || !e.originalEvent.changedTouches || !e.originalEvent.changedTouches[0]) {
5707 cto = setTimeout(function () {
5708 $(e.currentTarget).trigger('contextmenu', true);
5711 .on('touchmove.vakata.jstree', function (e) {
5712 if(cto && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0] && (Math.abs(ex - e.pageX) > 50 || Math.abs(ey - e.pageY) > 50)) {
5716 .on('touchend.vakata.jstree', function (e) {
5723 if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) {
5724 var el = null, tm = null;
5726 .on("touchstart", ".jstree-anchor", function (e) {
5727 el = e.currentTarget;
5729 $(document).one("touchend", function (e) {
5730 e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset);
5731 e.currentTarget = e.target;
5732 tm = ((+(new Date())) - tm);
5733 if(e.target === el && tm > 600 && tm < 1000) {
5735 $(el).trigger('contextmenu', e);
5743 $(document).on("context_hide.vakata.jstree", $.proxy(function () { this._data.contextmenu.visible = false; }, this));
5745 this.teardown = function () {
5746 if(this._data.contextmenu.visible) {
5747 $.vakata.context.hide();
5749 parent.teardown.call(this);
5753 * prepare and show the context menu for a node
5754 * @name show_contextmenu(obj [, x, y])
5755 * @param {mixed} obj the node
5756 * @param {Number} x the x-coordinate relative to the document to show the menu at
5757 * @param {Number} y the y-coordinate relative to the document to show the menu at
5758 * @param {Object} e the event if available that triggered the contextmenu
5759 * @plugin contextmenu
5760 * @trigger show_contextmenu.jstree
5762 this.show_contextmenu = function (obj, x, y, e) {
5763 obj = this.get_node(obj);
5764 if(!obj || obj.id === $.jstree.root) { return false; }
5765 var s = this.settings.contextmenu,
5766 d = this.get_node(obj, true),
5767 a = d.children(".jstree-anchor"),
5770 if(s.show_at_node || x === undefined || y === undefined) {
5773 y = o.top + this._data.core.li_height;
5775 if(this.settings.contextmenu.select_node && !this.is_selected(obj)) {
5776 this.activate_node(obj, e);
5780 if($.isFunction(i)) {
5781 i = i.call(this, obj, $.proxy(function (i) {
5782 this._show_contextmenu(obj, x, y, i);
5785 if($.isPlainObject(i)) {
5786 this._show_contextmenu(obj, x, y, i);
5790 * show the prepared context menu for a node
5791 * @name _show_contextmenu(obj, x, y, i)
5792 * @param {mixed} obj the node
5793 * @param {Number} x the x-coordinate relative to the document to show the menu at
5794 * @param {Number} y the y-coordinate relative to the document to show the menu at
5795 * @param {Number} i the object of items to show
5796 * @plugin contextmenu
5797 * @trigger show_contextmenu.jstree
5800 this._show_contextmenu = function (obj, x, y, i) {
5801 var d = this.get_node(obj, true),
5802 a = d.children(".jstree-anchor");
5803 $(document).one("context_show.vakata.jstree", $.proxy(function (e, data) {
5804 var cls = 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu';
5805 $(data.element).addClass(cls);
5807 this._data.contextmenu.visible = true;
5808 $.vakata.context.show(a, { 'x' : x, 'y' : y }, i);
5810 * triggered when the contextmenu is shown for a node
5812 * @name show_contextmenu.jstree
5813 * @param {Object} node the node
5814 * @param {Number} x the x-coordinate of the menu relative to the document
5815 * @param {Number} y the y-coordinate of the menu relative to the document
5816 * @plugin contextmenu
5818 this.trigger('show_contextmenu', { "node" : obj, "x" : x, "y" : y });
5822 // contextmenu helper
5824 var right_to_left = false,
5835 $.vakata.context = {
5837 hide_onmouseleave : 0,
5840 _trigger : function (event_name) {
5841 $(document).triggerHandler("context_" + event_name + ".vakata", {
5842 "reference" : vakata_context.reference,
5843 "element" : vakata_context.element,
5845 "x" : vakata_context.position_x,
5846 "y" : vakata_context.position_y
5850 _execute : function (i) {
5851 i = vakata_context.items[i];
5852 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, {
5854 "reference" : vakata_context.reference,
5855 "element" : vakata_context.element,
5857 "x" : vakata_context.position_x,
5858 "y" : vakata_context.position_y
5862 _parse : function (o, is_callback) {
5863 if(!o) { return false; }
5865 vakata_context.html = "";
5866 vakata_context.items = [];
5872 if(is_callback) { str += "<"+"ul>"; }
5873 $.each(o, function (i, val) {
5874 if(!val) { return true; }
5875 vakata_context.items.push(val);
5876 if(!sep && val.separator_before) {
5877 str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
5880 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+"' ":'')+">";
5881 str += "<"+"a href='#' rel='" + (vakata_context.items.length - 1) + "'>";
5882 if($.vakata.context.settings.icons) {
5885 if(val.icon.indexOf("/") !== -1 || val.icon.indexOf(".") !== -1) { str += " style='background:url(\"" + val.icon + "\") center center no-repeat' "; }
5886 else { str += " class='" + val.icon + "' "; }
5888 str += "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>";
5890 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>";
5892 tmp = $.vakata.context._parse(val.submenu, true);
5893 if(tmp) { str += tmp; }
5896 if(val.separator_after) {
5897 str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
5901 str = str.replace(/<li class\='vakata-context-separator'\><\/li\>$/,"");
5902 if(is_callback) { str += "</ul>"; }
5904 * triggered on the document when the contextmenu is parsed (HTML is built)
5906 * @plugin contextmenu
5907 * @name context_parse.vakata
5908 * @param {jQuery} reference the element that was right clicked
5909 * @param {jQuery} element the DOM element of the menu itself
5910 * @param {Object} position the x & y coordinates of the menu
5912 if(!is_callback) { vakata_context.html = str; $.vakata.context._trigger("parse"); }
5913 return str.length > 10 ? str : false;
5915 _show_submenu : function (o) {
5917 if(!o.length || !o.children("ul").length) { return; }
5918 var e = o.children("ul"),
5919 x = o.offset().left + o.outerWidth(),
5923 dw = $(window).width() + $(window).scrollLeft(),
5924 dh = $(window).height() + $(window).scrollTop();
5925 // може да се спести е една проверка - дали няма някой от класовете вече нагоре
5927 o[x - (w + 10 + o.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left");
5930 o[x + w + 10 > dw ? "addClass" : "removeClass"]("vakata-context-right");
5932 if(y + h + 10 > dh) {
5933 e.css("bottom","-1px");
5937 show : function (reference, position, data) {
5938 var o, e, x, y, w, h, dw, dh, cond = true;
5939 if(vakata_context.element && vakata_context.element.length) {
5940 vakata_context.element.width('');
5943 case (!position && !reference):
5945 case (!!position && !!reference):
5946 vakata_context.reference = reference;
5947 vakata_context.position_x = position.x;
5948 vakata_context.position_y = position.y;
5950 case (!position && !!reference):
5951 vakata_context.reference = reference;
5952 o = reference.offset();
5953 vakata_context.position_x = o.left + reference.outerHeight();
5954 vakata_context.position_y = o.top;
5956 case (!!position && !reference):
5957 vakata_context.position_x = position.x;
5958 vakata_context.position_y = position.y;
5961 if(!!reference && !data && $(reference).data('vakata_contextmenu')) {
5962 data = $(reference).data('vakata_contextmenu');
5964 if($.vakata.context._parse(data)) {
5965 vakata_context.element.html(vakata_context.html);
5967 if(vakata_context.items.length) {
5968 vakata_context.element.appendTo("body");
5969 e = vakata_context.element;
5970 x = vakata_context.position_x;
5971 y = vakata_context.position_y;
5974 dw = $(window).width() + $(window).scrollLeft();
5975 dh = $(window).height() + $(window).scrollTop();
5977 x -= (e.outerWidth() - $(reference).outerWidth());
5978 if(x < $(window).scrollLeft() + 20) {
5979 x = $(window).scrollLeft() + 20;
5982 if(x + w + 20 > dw) {
5985 if(y + h + 20 > dh) {
5989 vakata_context.element
5990 .css({ "left" : x, "top" : y })
5992 .find('a').first().focus().parent().addClass("vakata-context-hover");
5993 vakata_context.is_visible = true;
5995 * triggered on the document when the contextmenu is shown
5997 * @plugin contextmenu
5998 * @name context_show.vakata
5999 * @param {jQuery} reference the element that was right clicked
6000 * @param {jQuery} element the DOM element of the menu itself
6001 * @param {Object} position the x & y coordinates of the menu
6003 $.vakata.context._trigger("show");
6006 hide : function () {
6007 if(vakata_context.is_visible) {
6008 vakata_context.element.hide().find("ul").hide().end().find(':focus').blur().end().detach();
6009 vakata_context.is_visible = false;
6011 * triggered on the document when the contextmenu is hidden
6013 * @plugin contextmenu
6014 * @name context_hide.vakata
6015 * @param {jQuery} reference the element that was right clicked
6016 * @param {jQuery} element the DOM element of the menu itself
6017 * @param {Object} position the x & y coordinates of the menu
6019 $.vakata.context._trigger("hide");
6024 right_to_left = $("body").css("direction") === "rtl";
6027 vakata_context.element = $("<ul class='vakata-context'></ul>");
6028 vakata_context.element
6029 .on("mouseenter", "li", function (e) {
6030 e.stopImmediatePropagation();
6032 if($.contains(this, e.relatedTarget)) {
6033 // премахнато заради delegate mouseleave по-долу
6034 // $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
6038 if(to) { clearTimeout(to); }
6039 vakata_context.element.find(".vakata-context-hover").removeClass("vakata-context-hover").end();
6042 .siblings().find("ul").hide().end().end()
6043 .parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover");
6044 $.vakata.context._show_submenu(this);
6046 // тестово - дали не натоварва?
6047 .on("mouseleave", "li", function (e) {
6048 if($.contains(this, e.relatedTarget)) { return; }
6049 $(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover");
6051 .on("mouseleave", function (e) {
6052 $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
6053 if($.vakata.context.settings.hide_onmouseleave) {
6056 return function () { $.vakata.context.hide(); };
6057 }(this)), $.vakata.context.settings.hide_onmouseleave);
6060 .on("click", "a", function (e) {
6063 //.on("mouseup", "a", function (e) {
6064 if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata.context._execute($(this).attr("rel")) !== false) {
6065 $.vakata.context.hide();
6068 .on('keydown', 'a', function (e) {
6075 $(e.currentTarget).trigger(e);
6078 if(vakata_context.is_visible) {
6079 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();
6080 e.stopImmediatePropagation();
6085 if(vakata_context.is_visible) {
6086 o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first();
6087 if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); }
6088 o.addClass("vakata-context-hover").children('a').focus();
6089 e.stopImmediatePropagation();
6094 if(vakata_context.is_visible) {
6095 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();
6096 e.stopImmediatePropagation();
6101 if(vakata_context.is_visible) {
6102 o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first();
6103 if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); }
6104 o.addClass("vakata-context-hover").children('a').focus();
6105 e.stopImmediatePropagation();
6110 $.vakata.context.hide();
6114 //console.log(e.which);
6118 .on('keydown', function (e) {
6120 var a = vakata_context.element.find('.vakata-contextmenu-shortcut-' + e.which).parent();
6121 if(a.parent().not('.vakata-context-disabled')) {
6127 .on("mousedown.vakata.jstree", function (e) {
6128 if(vakata_context.is_visible && !$.contains(vakata_context.element[0], e.target)) {
6129 $.vakata.context.hide();
6132 .on("context_show.vakata.jstree", function (e, data) {
6133 vakata_context.element.find("li:has(ul)").children("a").addClass("vakata-context-parent");
6135 vakata_context.element.addClass("vakata-context-rtl").css("direction", "rtl");
6137 // also apply a RTL class?
6138 vakata_context.element.find("ul").hide().end();
6142 // $.jstree.defaults.plugins.push("contextmenu");
6145 * ### Drag'n'drop plugin
6147 * Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations.
6151 * stores all defaults for the drag'n'drop plugin
6152 * @name $.jstree.defaults.dnd
6155 $.jstree.defaults.dnd = {
6157 * a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`.
6158 * @name $.jstree.defaults.dnd.copy
6163 * a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`.
6164 * @name $.jstree.defaults.dnd.open_timeout
6169 * 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
6170 * @name $.jstree.defaults.dnd.is_draggable
6173 is_draggable : true,
6175 * 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`
6176 * @name $.jstree.defaults.dnd.check_while_dragging
6179 check_while_dragging : true,
6181 * a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false`
6182 * @name $.jstree.defaults.dnd.always_copy
6185 always_copy : false,
6187 * 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`
6188 * @name $.jstree.defaults.dnd.inside_pos
6193 * 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
6194 * @name $.jstree.defaults.dnd.drag_selection
6197 drag_selection : true,
6199 * 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.
6200 * @name $.jstree.defaults.dnd.touch
6205 * 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.
6206 * @name $.jstree.defaults.dnd.large_drop_target
6209 large_drop_target : false,
6211 * 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".
6212 * @name $.jstree.defaults.dnd.large_drag_target
6215 large_drag_target : false
6217 // TODO: now check works by checking for each node individually, how about max_children, unique, etc?
6218 $.jstree.plugins.dnd = function (options, parent) {
6219 this.bind = function () {
6220 parent.bind.call(this);
6223 .on('mousedown.jstree touchstart.jstree', this.settings.dnd.large_drag_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) {
6224 if(this.settings.dnd.large_drag_target && $(e.target).closest('.jstree-node')[0] !== e.currentTarget) {
6227 if(e.type === "touchstart" && (!this.settings.dnd.touch || (this.settings.dnd.touch === 'selected' && !$(e.currentTarget).closest('.jstree-node').children('.jstree-anchor').hasClass('jstree-clicked')))) {
6230 var obj = this.get_node(e.target),
6231 mlt = this.is_selected(obj) && this.settings.dnd.drag_selection ? this.get_top_selected().length : 1,
6232 txt = (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget));
6233 if(this.settings.core.force_text) {
6234 txt = $.vakata.html.escape(txt);
6236 if(obj && obj.id && obj.id !== $.jstree.root && (e.which === 1 || e.type === "touchstart") &&
6237 (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)))
6239 this.element.trigger('mousedown.jstree');
6240 return $.vakata.dnd.start(e, { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_top_selected() : [obj.id] }, '<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>');
6247 // bind only once for all instances
6252 marker = $('<div id="jstree-marker"> </div>').hide(); //.appendTo('body');
6255 .on('dnd_start.vakata.jstree', function (e, data) {
6258 if(!data || !data.data || !data.data.jstree) { return; }
6259 marker.appendTo('body'); //.show();
6261 .on('dnd_move.vakata.jstree', function (e, data) {
6262 if(opento) { clearTimeout(opento); }
6263 if(!data || !data.data || !data.data.jstree) { return; }
6265 // if we are hovering the marker image do nothing (can happen on "inside" drags)
6266 if(data.event.target.id && data.event.target.id === 'jstree-marker') {
6269 lastev = data.event;
6271 var ins = $.jstree.reference(data.event.target),
6275 tmp, l, t, h, p, i, o, ok, t1, t2, op, ps, pr, ip, tm;
6276 // if we are over an instance
6277 if(ins && ins._data && ins._data.dnd) {
6278 marker.attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ));
6280 .children().attr('class', 'jstree-' + ins.get_theme() + ' jstree-' + ins.get_theme() + '-' + ins.get_theme_variant() + ' ' + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ))
6281 .find('.jstree-copy').first()[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'show' : 'hide' ]();
6284 // if are hovering the container itself add a new root node
6285 if( (data.event.target === ins.element[0] || data.event.target === ins.get_container_ul()[0]) && ins.get_container_ul().children().length === 0) {
6287 for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
6288 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) });
6292 lastmv = { 'ins' : ins, 'par' : $.jstree.root, 'pos' : 'last' };
6294 data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
6299 // if we are hovering a tree node
6300 ref = ins.settings.dnd.large_drop_target ? $(data.event.target).closest('.jstree-node').children('.jstree-anchor') : $(data.event.target).closest('.jstree-anchor');
6301 if(ref && ref.length && ref.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) {
6303 rel = data.event.pageY - off.top;
6304 h = ref.outerHeight();
6306 o = ['b', 'i', 'a'];
6308 else if(rel > h - h / 3) {
6309 o = ['a', 'i', 'b'];
6312 o = rel > h / 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a'];
6314 $.each(o, function (j, v) {
6319 p = ins.get_parent(ref);
6320 i = ref.parent().index();
6323 ip = ins.settings.dnd.inside_pos;
6324 tm = ins.get_node(ref.parent());
6326 t = off.top + h / 2 + 1;
6328 i = ip === 'first' ? 0 : (ip === 'last' ? tm.children.length : Math.min(ip, tm.children.length));
6333 p = ins.get_parent(ref);
6334 i = ref.parent().index() + 1;
6338 for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
6339 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";
6341 if(op === "move_node" && v === 'a' && (data.data.origin && data.data.origin === ins) && p === ins.get_parent(data.data.nodes[t1])) {
6342 pr = ins.get_node(p);
6343 if(ps > $.inArray(data.data.nodes[t1], pr.children)) {
6347 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) }) );
6349 if(ins && ins.last_error) { laster = ins.last_error(); }
6353 if(v === 'i' && ref.parent().is('.jstree-closed') && ins.settings.dnd.open_timeout) {
6354 opento = setTimeout((function (x, z) { return function () { x.open_node(z); }; }(ins, ref)), ins.settings.dnd.open_timeout);
6357 lastmv = { 'ins' : ins, 'par' : p, 'pos' : v === 'i' && ip === 'last' && i === 0 && !ins.is_loaded(tm) ? 'last' : i };
6358 marker.css({ 'left' : l + 'px', 'top' : t + 'px' }).show();
6359 data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
6365 if(o === true) { return; }
6370 data.helper.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er');
6373 .on('dnd_scroll.vakata.jstree', function (e, data) {
6374 if(!data || !data.data || !data.data.jstree) { return; }
6378 data.helper.find('.jstree-icon').first().removeClass('jstree-ok').addClass('jstree-er');
6380 .on('dnd_stop.vakata.jstree', function (e, data) {
6381 if(opento) { clearTimeout(opento); }
6382 if(!data || !data.data || !data.data.jstree) { return; }
6383 marker.hide().detach();
6384 var i, j, nodes = [];
6386 for(i = 0, j = data.data.nodes.length; i < j; i++) {
6387 nodes[i] = data.data.origin ? data.data.origin.get_node(data.data.nodes[i]) : data.data.nodes[i];
6389 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);
6392 i = $(data.event.target).closest('.jstree');
6393 if(i.length && laster && laster.error && laster.error === 'check') {
6396 i.settings.core.error.call(this, laster);
6403 .on('keyup.jstree keydown.jstree', function (e, data) {
6404 data = $.vakata.dnd._get();
6405 if(data && data.data && data.data.jstree) {
6406 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' ]();
6408 lastev.metaKey = e.metaKey;
6409 lastev.ctrlKey = e.ctrlKey;
6410 $.vakata.dnd._trigger('move', lastev);
6420 escape : function (str) {
6421 return $.vakata.html.div.text(str).html();
6423 strip : function (str) {
6424 return $.vakata.html.div.empty().append($.parseHTML(str)).text();
6447 scroll_proximity : 20,
6451 threshold_touch : 50
6453 _trigger : function (event_name, e) {
6454 var data = $.vakata.dnd._get();
6456 $(document).triggerHandler("dnd_" + event_name + ".vakata", data);
6458 _get : function () {
6460 "data" : vakata_dnd.data,
6461 "element" : vakata_dnd.element,
6462 "helper" : vakata_dnd.helper
6465 _clean : function () {
6466 if(vakata_dnd.helper) { vakata_dnd.helper.remove(); }
6467 if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
6484 $(document).off("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag);
6485 $(document).off("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop);
6487 _scroll : function (init_only) {
6488 if(!vakata_dnd.scroll_e || (!vakata_dnd.scroll_l && !vakata_dnd.scroll_t)) {
6489 if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
6492 if(!vakata_dnd.scroll_i) {
6493 vakata_dnd.scroll_i = setInterval($.vakata.dnd._scroll, 100);
6496 if(init_only === true) { return false; }
6498 var i = vakata_dnd.scroll_e.scrollTop(),
6499 j = vakata_dnd.scroll_e.scrollLeft();
6500 vakata_dnd.scroll_e.scrollTop(i + vakata_dnd.scroll_t * $.vakata.dnd.settings.scroll_speed);
6501 vakata_dnd.scroll_e.scrollLeft(j + vakata_dnd.scroll_l * $.vakata.dnd.settings.scroll_speed);
6502 if(i !== vakata_dnd.scroll_e.scrollTop() || j !== vakata_dnd.scroll_e.scrollLeft()) {
6504 * triggered on the document when a drag causes an element to scroll
6507 * @name dnd_scroll.vakata
6508 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
6509 * @param {DOM} element the DOM element being dragged
6510 * @param {jQuery} helper the helper shown next to the mouse
6511 * @param {jQuery} event the element that is scrolling
6513 $.vakata.dnd._trigger("scroll", vakata_dnd.scroll_e);
6516 start : function (e, data, html) {
6517 if(e.type === "touchstart" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
6518 e.pageX = e.originalEvent.changedTouches[0].pageX;
6519 e.pageY = e.originalEvent.changedTouches[0].pageY;
6520 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
6522 if(vakata_dnd.is_drag) { $.vakata.dnd.stop({}); }
6524 e.currentTarget.unselectable = "on";
6525 e.currentTarget.onselectstart = function() { return false; };
6526 if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; }
6528 vakata_dnd.init_x = e.pageX;
6529 vakata_dnd.init_y = e.pageY;
6530 vakata_dnd.data = data;
6531 vakata_dnd.is_down = true;
6532 vakata_dnd.element = e.currentTarget;
6533 vakata_dnd.target = e.target;
6534 vakata_dnd.is_touch = e.type === "touchstart";
6535 if(html !== false) {
6536 vakata_dnd.helper = $("<div id='vakata-dnd'></div>").html(html).css({
6537 "display" : "block",
6540 "position" : "absolute",
6542 "lineHeight" : "16px",
6546 $(document).on("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag);
6547 $(document).on("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop);
6550 drag : function (e) {
6551 if(e.type === "touchmove" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
6552 e.pageX = e.originalEvent.changedTouches[0].pageX;
6553 e.pageY = e.originalEvent.changedTouches[0].pageY;
6554 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
6556 if(!vakata_dnd.is_down) { return; }
6557 if(!vakata_dnd.is_drag) {
6559 Math.abs(e.pageX - vakata_dnd.init_x) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) ||
6560 Math.abs(e.pageY - vakata_dnd.init_y) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold)
6562 if(vakata_dnd.helper) {
6563 vakata_dnd.helper.appendTo("body");
6564 vakata_dnd.helper_w = vakata_dnd.helper.outerWidth();
6566 vakata_dnd.is_drag = true;
6568 * triggered on the document when a drag starts
6571 * @name dnd_start.vakata
6572 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
6573 * @param {DOM} element the DOM element being dragged
6574 * @param {jQuery} helper the helper shown next to the mouse
6575 * @param {Object} event the event that caused the start (probably mousemove)
6577 $.vakata.dnd._trigger("start", e);
6582 var d = false, w = false,
6583 dh = false, wh = false,
6584 dw = false, ww = false,
6585 dt = false, dl = false,
6586 ht = false, hl = false;
6588 vakata_dnd.scroll_t = 0;
6589 vakata_dnd.scroll_l = 0;
6590 vakata_dnd.scroll_e = false;
6591 $($(e.target).parentsUntil("body").addBack().get().reverse())
6592 .filter(function () {
6593 return (/^auto|scroll$/).test($(this).css("overflow")) &&
6594 (this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth);
6597 var t = $(this), o = t.offset();
6598 if(this.scrollHeight > this.offsetHeight) {
6599 if(o.top + t.height() - e.pageY < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
6600 if(e.pageY - o.top < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
6602 if(this.scrollWidth > this.offsetWidth) {
6603 if(o.left + t.width() - e.pageX < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
6604 if(e.pageX - o.left < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
6606 if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
6607 vakata_dnd.scroll_e = $(this);
6612 if(!vakata_dnd.scroll_e) {
6613 d = $(document); w = $(window);
6614 dh = d.height(); wh = w.height();
6615 dw = d.width(); ww = w.width();
6616 dt = d.scrollTop(); dl = d.scrollLeft();
6617 if(dh > wh && e.pageY - dt < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
6618 if(dh > wh && wh - (e.pageY - dt) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
6619 if(dw > ww && e.pageX - dl < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
6620 if(dw > ww && ww - (e.pageX - dl) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
6621 if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
6622 vakata_dnd.scroll_e = d;
6625 if(vakata_dnd.scroll_e) { $.vakata.dnd._scroll(true); }
6627 if(vakata_dnd.helper) {
6628 ht = parseInt(e.pageY + $.vakata.dnd.settings.helper_top, 10);
6629 hl = parseInt(e.pageX + $.vakata.dnd.settings.helper_left, 10);
6630 if(dh && ht + 25 > dh) { ht = dh - 50; }
6631 if(dw && hl + vakata_dnd.helper_w > dw) { hl = dw - (vakata_dnd.helper_w + 2); }
6632 vakata_dnd.helper.css({
6638 * triggered on the document when a drag is in progress
6641 * @name dnd_move.vakata
6642 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
6643 * @param {DOM} element the DOM element being dragged
6644 * @param {jQuery} helper the helper shown next to the mouse
6645 * @param {Object} event the event that caused this to trigger (most likely mousemove)
6647 $.vakata.dnd._trigger("move", e);
6650 stop : function (e) {
6651 if(e.type === "touchend" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
6652 e.pageX = e.originalEvent.changedTouches[0].pageX;
6653 e.pageY = e.originalEvent.changedTouches[0].pageY;
6654 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
6656 if(vakata_dnd.is_drag) {
6658 * triggered on the document when a drag stops (the dragged element is dropped)
6661 * @name dnd_stop.vakata
6662 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
6663 * @param {DOM} element the DOM element being dragged
6664 * @param {jQuery} helper the helper shown next to the mouse
6665 * @param {Object} event the event that caused the stop
6667 $.vakata.dnd._trigger("stop", e);
6670 if(e.type === "touchend" && e.target === vakata_dnd.target) {
6671 var to = setTimeout(function () { $(e.target).click(); }, 100);
6672 $(e.target).one('click', function() { if(to) { clearTimeout(to); } });
6675 $.vakata.dnd._clean();
6681 // include the dnd plugin by default
6682 // $.jstree.defaults.plugins.push("dnd");
6686 * ### Massload plugin
6688 * Adds massload functionality to jsTree, so that multiple nodes can be loaded in a single request (only useful with lazy loading).
6692 * massload configuration
6694 * It is possible to set this to a standard jQuery-like AJAX config.
6695 * 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.
6697 * 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.
6699 * 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.
6702 * "id1" : [{ "text" : "Child of ID1", "id" : "c1" }, { "text" : "Another child of ID1", "id" : "c2" }],
6703 * "id2" : [{ "text" : "Child of ID2", "id" : "c3" }]
6706 * @name $.jstree.defaults.massload
6709 $.jstree.defaults.massload = null;
6710 $.jstree.plugins.massload = function (options, parent) {
6711 this.init = function (el, options) {
6712 parent.init.call(this, el, options);
6713 this._data.massload = {};
6715 this._load_nodes = function (nodes, callback, is_callback) {
6716 var s = this.settings.massload;
6717 if(is_callback && !$.isEmptyObject(this._data.massload)) {
6718 return parent._load_nodes.call(this, nodes, callback, is_callback);
6720 if($.isFunction(s)) {
6721 return s.call(this, nodes, $.proxy(function (data) {
6723 for(var i in data) {
6724 if(data.hasOwnProperty(i)) {
6725 this._data.massload[i] = data[i];
6729 parent._load_nodes.call(this, nodes, callback, is_callback);
6732 if(typeof s === 'object' && s && s.url) {
6733 s = $.extend(true, {}, s);
6734 if($.isFunction(s.url)) {
6735 s.url = s.url.call(this, nodes);
6737 if($.isFunction(s.data)) {
6738 s.data = s.data.call(this, nodes);
6741 .done($.proxy(function (data,t,x) {
6743 for(var i in data) {
6744 if(data.hasOwnProperty(i)) {
6745 this._data.massload[i] = data[i];
6749 parent._load_nodes.call(this, nodes, callback, is_callback);
6751 .fail($.proxy(function (f) {
6752 parent._load_nodes.call(this, nodes, callback, is_callback);
6755 return parent._load_nodes.call(this, nodes, callback, is_callback);
6757 this._load_node = function (obj, callback) {
6758 var d = this._data.massload[obj.id];
6760 return this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }) : d, function (status) {
6761 callback.call(this, status);
6762 delete this._data.massload[obj.id];
6765 return parent._load_node.call(this, obj, callback);
6772 * Adds search functionality to jsTree.
6776 * stores all defaults for the search plugin
6777 * @name $.jstree.defaults.search
6780 $.jstree.defaults.search = {
6782 * a jQuery-like AJAX config, which jstree uses if a server should be queried for results.
6784 * 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.
6785 * 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
6786 * @name $.jstree.defaults.search.ajax
6791 * Indicates if the search should be fuzzy or not (should `chnd3` match `child node 3`). Default is `false`.
6792 * @name $.jstree.defaults.search.fuzzy
6797 * Indicates if the search should be case sensitive. Default is `false`.
6798 * @name $.jstree.defaults.search.case_sensitive
6801 case_sensitive : false,
6803 * 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).
6804 * This setting can be changed at runtime when calling the search method. Default is `false`.
6805 * @name $.jstree.defaults.search.show_only_matches
6808 show_only_matches : false,
6810 * Indicates if the children of matched element are shown (when show_only_matches is true)
6811 * This setting can be changed at runtime when calling the search method. Default is `false`.
6812 * @name $.jstree.defaults.search.show_only_matches_children
6815 show_only_matches_children : false,
6817 * 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`.
6818 * @name $.jstree.defaults.search.close_opened_onclear
6821 close_opened_onclear : true,
6823 * Indicates if only leaf nodes should be included in search results. Default is `false`.
6824 * @name $.jstree.defaults.search.search_leaves_only
6827 search_leaves_only : false,
6829 * 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).
6830 * 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`.
6831 * @name $.jstree.defaults.search.search_callback
6834 search_callback : false
6837 $.jstree.plugins.search = function (options, parent) {
6838 this.bind = function () {
6839 parent.bind.call(this);
6841 this._data.search.str = "";
6842 this._data.search.dom = $();
6843 this._data.search.res = [];
6844 this._data.search.opn = [];
6845 this._data.search.som = false;
6846 this._data.search.smc = false;
6847 this._data.search.hdn = [];
6850 .on("search.jstree", $.proxy(function (e, data) {
6851 if(this._data.search.som && data.res.length) {
6852 var m = this._model.data, i, j, p = [];
6853 for(i = 0, j = data.res.length; i < j; i++) {
6854 if(m[data.res[i]] && !m[data.res[i]].state.hidden) {
6855 p.push(data.res[i]);
6856 p = p.concat(m[data.res[i]].parents);
6857 if(this._data.search.smc) {
6858 p = p.concat(m[data.res[i]].children_d);
6862 p = $.vakata.array_remove_item($.vakata.array_unique(p), $.jstree.root);
6863 this._data.search.hdn = this.hide_all(true);
6867 .on("clear_search.jstree", $.proxy(function (e, data) {
6868 if(this._data.search.som && data.res.length) {
6869 this.show_node(this._data.search.hdn);
6874 * used to search the tree nodes for a given string
6875 * @name search(str [, skip_async])
6876 * @param {String} str the search string
6877 * @param {Boolean} skip_async if set to true server will not be queried even if configured
6878 * @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)
6879 * @param {mixed} inside an optional node to whose children to limit the search
6880 * @param {Boolean} append if set to true the results of this search are appended to the previous search
6882 * @trigger search.jstree
6884 this.search = function (str, skip_async, show_only_matches, inside, append, show_only_matches_children) {
6885 if(str === false || $.trim(str.toString()) === "") {
6886 return this.clear_search();
6888 inside = this.get_node(inside);
6889 inside = inside && inside.id ? inside.id : null;
6890 str = str.toString();
6891 var s = this.settings.search,
6892 a = s.ajax ? s.ajax : false,
6893 m = this._model.data,
6897 if(this._data.search.res.length && !append) {
6898 this.clear_search();
6900 if(show_only_matches === undefined) {
6901 show_only_matches = s.show_only_matches;
6903 if(show_only_matches_children === undefined) {
6904 show_only_matches_children = s.show_only_matches_children;
6906 if(!skip_async && a !== false) {
6907 if($.isFunction(a)) {
6908 return a.call(this, str, $.proxy(function (d) {
6909 if(d && d.d) { d = d.d; }
6910 this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () {
6911 this.search(str, true, show_only_matches, inside, append);
6916 a = $.extend({}, a);
6917 if(!a.data) { a.data = {}; }
6920 a.data.inside = inside;
6923 .fail($.proxy(function () {
6924 this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'search', 'id' : 'search_01', 'reason' : 'Could not load search parents', 'data' : JSON.stringify(a) };
6925 this.settings.core.error.call(this, this._data.core.last_error);
6927 .done($.proxy(function (d) {
6928 if(d && d.d) { d = d.d; }
6929 this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () {
6930 this.search(str, true, show_only_matches, inside, append);
6936 this._data.search.str = str;
6937 this._data.search.dom = $();
6938 this._data.search.res = [];
6939 this._data.search.opn = [];
6940 this._data.search.som = show_only_matches;
6941 this._data.search.smc = show_only_matches_children;
6944 f = new $.vakata.search(str, true, { caseSensitive : s.case_sensitive, fuzzy : s.fuzzy });
6945 $.each(m[inside ? inside : $.jstree.root].children_d, function (ii, i) {
6947 if(v.text && (!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) ) ) {
6949 p = p.concat(v.parents);
6953 p = $.vakata.array_unique(p);
6954 for(i = 0, j = p.length; i < j; i++) {
6955 if(p[i] !== $.jstree.root && m[p[i]] && this.open_node(p[i], null, 0) === true) {
6956 this._data.search.opn.push(p[i]);
6960 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(', #')));
6961 this._data.search.res = r;
6964 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(', #'))));
6965 this._data.search.res = $.vakata.array_unique(this._data.search.res.concat(r));
6967 this._data.search.dom.children(".jstree-anchor").addClass('jstree-search');
6970 * triggered after search is complete
6972 * @name search.jstree
6973 * @param {jQuery} nodes a jQuery collection of matching nodes
6974 * @param {String} str the search string
6975 * @param {Array} res a collection of objects represeing the matching nodes
6978 this.trigger('search', { nodes : this._data.search.dom, str : str, res : this._data.search.res, show_only_matches : show_only_matches });
6981 * used to clear the last search (removes classes and shows all nodes if filtering is on)
6982 * @name clear_search()
6984 * @trigger clear_search.jstree
6986 this.clear_search = function () {
6987 if(this.settings.search.close_opened_onclear) {
6988 this.close_node(this._data.search.opn, 0);
6991 * triggered after search is complete
6993 * @name clear_search.jstree
6994 * @param {jQuery} nodes a jQuery collection of matching nodes (the result from the last search)
6995 * @param {String} str the search string (the last search string)
6996 * @param {Array} res a collection of objects represeing the matching nodes (the result from the last search)
6999 this.trigger('clear_search', { 'nodes' : this._data.search.dom, str : this._data.search.str, res : this._data.search.res });
7000 if(this._data.search.res.length) {
7001 this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(this._data.search.res, function (v) {
7002 return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&');
7004 this._data.search.dom.children(".jstree-anchor").removeClass("jstree-search");
7006 this._data.search.str = "";
7007 this._data.search.res = [];
7008 this._data.search.opn = [];
7009 this._data.search.dom = $();
7012 this.redraw_node = function(obj, deep, callback, force_render) {
7013 obj = parent.redraw_node.apply(this, arguments);
7015 if($.inArray(obj.id, this._data.search.res) !== -1) {
7016 var i, j, tmp = null;
7017 for(i = 0, j = obj.childNodes.length; i < j; i++) {
7018 if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
7019 tmp = obj.childNodes[i];
7024 tmp.className += ' jstree-search';
7034 // from http://kiro.me/projects/fuse.html
7035 $.vakata.search = function(pattern, txt, options) {
7036 options = options || {};
7037 options = $.extend({}, $.vakata.search.defaults, options);
7038 if(options.fuzzy !== false) {
7039 options.fuzzy = true;
7041 pattern = options.caseSensitive ? pattern : pattern.toLowerCase();
7042 var MATCH_LOCATION = options.location,
7043 MATCH_DISTANCE = options.distance,
7044 MATCH_THRESHOLD = options.threshold,
7045 patternLen = pattern.length,
7046 matchmask, pattern_alphabet, match_bitapScore, search;
7047 if(patternLen > 32) {
7048 options.fuzzy = false;
7051 matchmask = 1 << (patternLen - 1);
7052 pattern_alphabet = (function () {
7055 for (i = 0; i < patternLen; i++) {
7056 mask[pattern.charAt(i)] = 0;
7058 for (i = 0; i < patternLen; i++) {
7059 mask[pattern.charAt(i)] |= 1 << (patternLen - i - 1);
7063 match_bitapScore = function (e, x) {
7064 var accuracy = e / patternLen,
7065 proximity = Math.abs(MATCH_LOCATION - x);
7066 if(!MATCH_DISTANCE) {
7067 return proximity ? 1.0 : accuracy;
7069 return accuracy + (proximity / MATCH_DISTANCE);
7072 search = function (text) {
7073 text = options.caseSensitive ? text : text.toLowerCase();
7074 if(pattern === text || text.indexOf(pattern) !== -1) {
7080 if(!options.fuzzy) {
7087 textLen = text.length,
7088 scoreThreshold = MATCH_THRESHOLD,
7089 bestLoc = text.indexOf(pattern, MATCH_LOCATION),
7091 binMax = patternLen + textLen,
7092 lastRd, start, finish, rd, charMatch,
7095 if (bestLoc !== -1) {
7096 scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold);
7097 bestLoc = text.lastIndexOf(pattern, MATCH_LOCATION + patternLen);
7098 if (bestLoc !== -1) {
7099 scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold);
7103 for (i = 0; i < patternLen; i++) {
7106 while (binMin < binMid) {
7107 if (match_bitapScore(i, MATCH_LOCATION + binMid) <= scoreThreshold) {
7112 binMid = Math.floor((binMax - binMin) / 2 + binMin);
7115 start = Math.max(1, MATCH_LOCATION - binMid + 1);
7116 finish = Math.min(MATCH_LOCATION + binMid, textLen) + patternLen;
7117 rd = new Array(finish + 2);
7118 rd[finish + 1] = (1 << i) - 1;
7119 for (j = finish; j >= start; j--) {
7120 charMatch = pattern_alphabet[text.charAt(j - 1)];
7122 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
7124 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | (((lastRd[j + 1] | lastRd[j]) << 1) | 1) | lastRd[j + 1];
7126 if (rd[j] & matchmask) {
7127 score = match_bitapScore(i, j - 1);
7128 if (score <= scoreThreshold) {
7129 scoreThreshold = score;
7131 locations.push(bestLoc);
7132 if (bestLoc > MATCH_LOCATION) {
7133 start = Math.max(1, 2 * MATCH_LOCATION - bestLoc);
7140 if (match_bitapScore(i + 1, MATCH_LOCATION) > scoreThreshold) {
7146 isMatch: bestLoc >= 0,
7150 return txt === true ? { 'search' : search } : search(txt);
7152 $.vakata.search.defaults = {
7157 caseSensitive : false
7161 // include the search plugin by default
7162 // $.jstree.defaults.plugins.push("search");
7168 * Automatically sorts all siblings in the tree according to a sorting function.
7172 * the settings function used to sort the nodes.
7173 * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`.
7174 * @name $.jstree.defaults.sort
7177 $.jstree.defaults.sort = function (a, b) {
7178 //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);
7179 return this.get_text(a) > this.get_text(b) ? 1 : -1;
7181 $.jstree.plugins.sort = function (options, parent) {
7182 this.bind = function () {
7183 parent.bind.call(this);
7185 .on("model.jstree", $.proxy(function (e, data) {
7186 this.sort(data.parent, true);
7188 .on("rename_node.jstree create_node.jstree", $.proxy(function (e, data) {
7189 this.sort(data.parent || data.node.parent, false);
7190 this.redraw_node(data.parent || data.node.parent, true);
7192 .on("move_node.jstree copy_node.jstree", $.proxy(function (e, data) {
7193 this.sort(data.parent, false);
7194 this.redraw_node(data.parent, true);
7198 * used to sort a node's children
7200 * @name sort(obj [, deep])
7201 * @param {mixed} obj the node
7202 * @param {Boolean} deep if set to `true` nodes are sorted recursively.
7204 * @trigger search.jstree
7206 this.sort = function (obj, deep) {
7208 obj = this.get_node(obj);
7209 if(obj && obj.children && obj.children.length) {
7210 obj.children.sort($.proxy(this.settings.sort, this));
7212 for(i = 0, j = obj.children_d.length; i < j; i++) {
7213 this.sort(obj.children_d[i], false);
7220 // include the sort plugin by default
7221 // $.jstree.defaults.plugins.push("sort");
7226 * Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc)
7231 * stores all defaults for the state plugin
7232 * @name $.jstree.defaults.state
7235 $.jstree.defaults.state = {
7237 * A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`.
7238 * @name $.jstree.defaults.state.key
7243 * A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`.
7244 * @name $.jstree.defaults.state.events
7247 events : 'changed.jstree open_node.jstree close_node.jstree check_node.jstree uncheck_node.jstree',
7249 * Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire.
7250 * @name $.jstree.defaults.state.ttl
7255 * 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.
7256 * @name $.jstree.defaults.state.filter
7261 $.jstree.plugins.state = function (options, parent) {
7262 this.bind = function () {
7263 parent.bind.call(this);
7264 var bind = $.proxy(function () {
7265 this.element.on(this.settings.state.events, $.proxy(function () {
7266 if(to) { clearTimeout(to); }
7267 to = setTimeout($.proxy(function () { this.save_state(); }, this), 100);
7270 * triggered when the state plugin is finished restoring the state (and immediately after ready if there is no state to restore).
7272 * @name state_ready.jstree
7275 this.trigger('state_ready');
7278 .on("ready.jstree", $.proxy(function (e, data) {
7279 this.element.one("restore_state.jstree", bind);
7280 if(!this.restore_state()) { bind(); }
7285 * @name save_state()
7288 this.save_state = function () {
7289 var st = { 'state' : this.get_state(), 'ttl' : this.settings.state.ttl, 'sec' : +(new Date()) };
7290 $.vakata.storage.set(this.settings.state.key, JSON.stringify(st));
7293 * restore the state from the user's computer
7294 * @name restore_state()
7297 this.restore_state = function () {
7298 var k = $.vakata.storage.get(this.settings.state.key);
7299 if(!!k) { try { k = JSON.parse(k); } catch(ex) { return false; } }
7300 if(!!k && k.ttl && k.sec && +(new Date()) - k.sec > k.ttl) { return false; }
7301 if(!!k && k.state) { k = k.state; }
7302 if(!!k && $.isFunction(this.settings.state.filter)) { k = this.settings.state.filter.call(this, k); }
7304 this.element.one("set_state.jstree", function (e, data) { data.instance.trigger('restore_state', { 'state' : $.extend(true, {}, k) }); });
7311 * clear the state on the user's computer
7312 * @name clear_state()
7315 this.clear_state = function () {
7316 return $.vakata.storage.del(this.settings.state.key);
7320 (function ($, undefined) {
7321 $.vakata.storage = {
7322 // simply specifying the functions in FF throws an error
7323 set : function (key, val) { return window.localStorage.setItem(key, val); },
7324 get : function (key) { return window.localStorage.getItem(key); },
7325 del : function (key) { return window.localStorage.removeItem(key); }
7329 // include the state plugin by default
7330 // $.jstree.defaults.plugins.push("state");
7335 * 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.
7339 * 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).
7341 * * `max_children` the maximum number of immediate children this node type can have. Do not specify or set to `-1` for unlimited.
7342 * * `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.
7343 * * `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.
7344 * * `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.
7346 * There are two predefined types:
7348 * * `#` represents the root of the tree, for example `max_children` would control the maximum number of root nodes.
7349 * * `default` represents the default node - any settings here will be applied to all nodes that do not have a type specified.
7351 * @name $.jstree.defaults.types
7354 $.jstree.defaults.types = {
7357 $.jstree.defaults.types[$.jstree.root] = {};
7359 $.jstree.plugins.types = function (options, parent) {
7360 this.init = function (el, options) {
7362 if(options && options.types && options.types['default']) {
7363 for(i in options.types) {
7364 if(i !== "default" && i !== $.jstree.root && options.types.hasOwnProperty(i)) {
7365 for(j in options.types['default']) {
7366 if(options.types['default'].hasOwnProperty(j) && options.types[i][j] === undefined) {
7367 options.types[i][j] = options.types['default'][j];
7373 parent.init.call(this, el, options);
7374 this._model.data[$.jstree.root].type = $.jstree.root;
7376 this.refresh = function (skip_loading, forget_state) {
7377 parent.refresh.call(this, skip_loading, forget_state);
7378 this._model.data[$.jstree.root].type = $.jstree.root;
7380 this.bind = function () {
7382 .on('model.jstree', $.proxy(function (e, data) {
7383 var m = this._model.data,
7385 t = this.settings.types,
7386 i, j, c = 'default';
7387 for(i = 0, j = dpc.length; i < j; i++) {
7389 if(m[dpc[i]].original && m[dpc[i]].original.type && t[m[dpc[i]].original.type]) {
7390 c = m[dpc[i]].original.type;
7392 if(m[dpc[i]].data && m[dpc[i]].data.jstree && m[dpc[i]].data.jstree.type && t[m[dpc[i]].data.jstree.type]) {
7393 c = m[dpc[i]].data.jstree.type;
7396 if(m[dpc[i]].icon === true && t[c].icon !== undefined) {
7397 m[dpc[i]].icon = t[c].icon;
7400 m[$.jstree.root].type = $.jstree.root;
7402 parent.bind.call(this);
7404 this.get_json = function (obj, options, flat) {
7406 m = this._model.data,
7407 opt = options ? $.extend(true, {}, options, {no_id:false}) : {},
7408 tmp = parent.get_json.call(this, obj, opt, flat);
7409 if(tmp === false) { return false; }
7410 if($.isArray(tmp)) {
7411 for(i = 0, j = tmp.length; i < j; i++) {
7412 tmp[i].type = tmp[i].id && m[tmp[i].id] && m[tmp[i].id].type ? m[tmp[i].id].type : "default";
7413 if(options && options.no_id) {
7415 if(tmp[i].li_attr && tmp[i].li_attr.id) {
7416 delete tmp[i].li_attr.id;
7418 if(tmp[i].a_attr && tmp[i].a_attr.id) {
7419 delete tmp[i].a_attr.id;
7425 tmp.type = tmp.id && m[tmp.id] && m[tmp.id].type ? m[tmp.id].type : "default";
7426 if(options && options.no_id) {
7427 tmp = this._delete_ids(tmp);
7432 this._delete_ids = function (tmp) {
7433 if($.isArray(tmp)) {
7434 for(var i = 0, j = tmp.length; i < j; i++) {
7435 tmp[i] = this._delete_ids(tmp[i]);
7440 if(tmp.li_attr && tmp.li_attr.id) {
7441 delete tmp.li_attr.id;
7443 if(tmp.a_attr && tmp.a_attr.id) {
7444 delete tmp.a_attr.id;
7446 if(tmp.children && $.isArray(tmp.children)) {
7447 tmp.children = this._delete_ids(tmp.children);
7451 this.check = function (chk, obj, par, pos, more) {
7452 if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; }
7453 obj = obj && obj.id ? obj : this.get_node(obj);
7454 par = par && par.id ? par : this.get_node(par);
7455 var m = obj && obj.id ? (more && more.origin ? more.origin : $.jstree.reference(obj.id)) : null, tmp, d, i, j;
7456 m = m && m._model && m._model.data ? m._model.data : null;
7461 if(chk !== 'move_node' || $.inArray(obj.id, par.children) === -1) {
7462 tmp = this.get_rules(par);
7463 if(tmp.max_children !== undefined && tmp.max_children !== -1 && tmp.max_children === par.children.length) {
7464 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 }) };
7467 if(tmp.valid_children !== undefined && tmp.valid_children !== -1 && $.inArray((obj.type || 'default'), tmp.valid_children) === -1) {
7468 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 }) };
7471 if(m && obj.children_d && obj.parents) {
7473 for(i = 0, j = obj.children_d.length; i < j; i++) {
7474 d = Math.max(d, m[obj.children_d[i]].parents.length);
7476 d = d - obj.parents.length + 1;
7478 if(d <= 0 || d === undefined) { d = 1; }
7480 if(tmp.max_depth !== undefined && tmp.max_depth !== -1 && tmp.max_depth < d) {
7481 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 }) };
7484 par = this.get_node(par.parent);
7485 tmp = this.get_rules(par);
7494 * used to retrieve the type settings object for a node
7495 * @name get_rules(obj)
7496 * @param {mixed} obj the node to find the rules for
7500 this.get_rules = function (obj) {
7501 obj = this.get_node(obj);
7502 if(!obj) { return false; }
7503 var tmp = this.get_type(obj, true);
7504 if(tmp.max_depth === undefined) { tmp.max_depth = -1; }
7505 if(tmp.max_children === undefined) { tmp.max_children = -1; }
7506 if(tmp.valid_children === undefined) { tmp.valid_children = -1; }
7510 * used to retrieve the type string or settings object for a node
7511 * @name get_type(obj [, rules])
7512 * @param {mixed} obj the node to find the rules for
7513 * @param {Boolean} rules if set to `true` instead of a string the settings object will be returned
7514 * @return {String|Object}
7517 this.get_type = function (obj, rules) {
7518 obj = this.get_node(obj);
7519 return (!obj) ? false : ( rules ? $.extend({ 'type' : obj.type }, this.settings.types[obj.type]) : obj.type);
7522 * used to change a node's type
7523 * @name set_type(obj, type)
7524 * @param {mixed} obj the node to change
7525 * @param {String} type the new type
7528 this.set_type = function (obj, type) {
7529 var t, t1, t2, old_type, old_icon;
7530 if($.isArray(obj)) {
7532 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
7533 this.set_type(obj[t1], type);
7537 t = this.settings.types;
7538 obj = this.get_node(obj);
7539 if(!t[type] || !obj) { return false; }
7540 old_type = obj.type;
7541 old_icon = this.get_icon(obj);
7543 if(old_icon === true || (t[old_type] && t[old_type].icon !== undefined && old_icon === t[old_type].icon)) {
7544 this.set_icon(obj, t[type].icon !== undefined ? t[type].icon : true);
7549 // include the types plugin by default
7550 // $.jstree.defaults.plugins.push("types");
7555 * Enforces that no nodes with the same name can coexist as siblings.
7559 * stores all defaults for the unique plugin
7560 * @name $.jstree.defaults.unique
7563 $.jstree.defaults.unique = {
7565 * Indicates if the comparison should be case sensitive. Default is `false`.
7566 * @name $.jstree.defaults.unique.case_sensitive
7569 case_sensitive : false,
7571 * 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)`.
7572 * @name $.jstree.defaults.unique.duplicate
7575 duplicate : function (name, counter) {
7576 return name + ' (' + counter + ')';
7580 $.jstree.plugins.unique = function (options, parent) {
7581 this.check = function (chk, obj, par, pos, more) {
7582 if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; }
7583 obj = obj && obj.id ? obj : this.get_node(obj);
7584 par = par && par.id ? par : this.get_node(par);
7585 if(!par || !par.children) { return true; }
7586 var n = chk === "rename_node" ? pos : obj.text,
7588 s = this.settings.unique.case_sensitive,
7589 m = this._model.data, i, j;
7590 for(i = 0, j = par.children.length; i < j; i++) {
7591 c.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase());
7593 if(!s) { n = n.toLowerCase(); }
7598 i = ($.inArray(n, c) === -1 || (obj.text && obj.text[ s ? 'toString' : 'toLowerCase']() === n));
7600 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 }) };
7604 i = ($.inArray(n, c) === -1);
7606 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 }) };
7610 i = ($.inArray(n, c) === -1);
7612 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 }) };
7616 i = ( (obj.parent === par.id && (!more || !more.is_multi)) || $.inArray(n, c) === -1);
7618 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 }) };
7624 this.create_node = function (par, node, pos, callback, is_loaded) {
7625 if(!node || node.text === undefined) {
7627 par = $.jstree.root;
7629 par = this.get_node(par);
7631 return parent.create_node.call(this, par, node, pos, callback, is_loaded);
7633 pos = pos === undefined ? "last" : pos;
7634 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
7635 return parent.create_node.call(this, par, node, pos, callback, is_loaded);
7637 if(!node) { node = {}; }
7638 var tmp, n, dpc, i, j, m = this._model.data, s = this.settings.unique.case_sensitive, cb = this.settings.unique.duplicate;
7639 n = tmp = this.get_string('New node');
7641 for(i = 0, j = par.children.length; i < j; i++) {
7642 dpc.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase());
7645 while($.inArray(s ? n : n.toLowerCase(), dpc) !== -1) {
7646 n = cb.call(this, tmp, (++i)).toString();
7650 return parent.create_node.call(this, par, node, pos, callback, is_loaded);
7654 // include the unique plugin by default
7655 // $.jstree.defaults.plugins.push("unique");
7659 * ### Wholerow plugin
7661 * Makes each node appear block level. Making selection easier. May cause slow down for large trees in old browsers.
7664 var div = document.createElement('DIV');
7665 div.setAttribute('unselectable','on');
7666 div.setAttribute('role','presentation');
7667 div.className = 'jstree-wholerow';
7668 div.innerHTML = ' ';
7669 $.jstree.plugins.wholerow = function (options, parent) {
7670 this.bind = function () {
7671 parent.bind.call(this);
7674 .on('ready.jstree set_state.jstree', $.proxy(function () {
7677 .on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
7678 //div.style.height = this._data.core.li_height + 'px';
7679 this.get_container_ul().addClass('jstree-wholerow-ul');
7681 .on("deselect_all.jstree", $.proxy(function (e, data) {
7682 this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
7684 .on("changed.jstree", $.proxy(function (e, data) {
7685 this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
7686 var tmp = false, i, j;
7687 for(i = 0, j = data.selected.length; i < j; i++) {
7688 tmp = this.get_node(data.selected[i], true);
7689 if(tmp && tmp.length) {
7690 tmp.children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
7694 .on("open_node.jstree", $.proxy(function (e, data) {
7695 this.get_node(data.node, true).find('.jstree-clicked').parent().children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
7697 .on("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) {
7698 if(e.type === "hover_node" && this.is_disabled(data.node)) { return; }
7699 this.get_node(data.node, true).children('.jstree-wholerow')[e.type === "hover_node"?"addClass":"removeClass"]('jstree-wholerow-hovered');
7701 .on("contextmenu.jstree", ".jstree-wholerow", $.proxy(function (e) {
7703 var tmp = $.Event('contextmenu', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey, pageX : e.pageX, pageY : e.pageY });
7704 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp);
7707 .on("mousedown.jstree touchstart.jstree", ".jstree-wholerow", function (e) {
7708 if(e.target === e.currentTarget) {
7709 var a = $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor");
7715 .on("click.jstree", ".jstree-wholerow", function (e) {
7716 e.stopImmediatePropagation();
7717 var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
7718 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
7720 .on("click.jstree", ".jstree-leaf > .jstree-ocl", $.proxy(function (e) {
7721 e.stopImmediatePropagation();
7722 var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
7723 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
7725 .on("mouseover.jstree", ".jstree-wholerow, .jstree-icon", $.proxy(function (e) {
7726 e.stopImmediatePropagation();
7727 if(!this.is_disabled(e.currentTarget)) {
7728 this.hover_node(e.currentTarget);
7732 .on("mouseleave.jstree", ".jstree-node", $.proxy(function (e) {
7733 this.dehover_node(e.currentTarget);
7736 this.teardown = function () {
7737 if(this.settings.wholerow) {
7738 this.element.find(".jstree-wholerow").remove();
7740 parent.teardown.call(this);
7742 this.redraw_node = function(obj, deep, callback, force_render) {
7743 obj = parent.redraw_node.apply(this, arguments);
7745 var tmp = div.cloneNode(true);
7746 //tmp.style.height = this._data.core.li_height + 'px';
7747 if($.inArray(obj.id, this._data.core.selected) !== -1) { tmp.className += ' jstree-wholerow-clicked'; }
7748 if(this._data.core.focused && this._data.core.focused === obj.id) { tmp.className += ' jstree-wholerow-hovered'; }
7749 obj.insertBefore(tmp, obj.childNodes[0]);
7754 // include the wholerow plugin by default
7755 // $.jstree.defaults.plugins.push("wholerow");
7756 if(document.registerElement && Object && Object.create) {
7757 var proto = Object.create(HTMLElement.prototype);
7758 proto.createdCallback = function () {
7759 var c = { core : {}, plugins : [] }, i;
7760 for(i in $.jstree.plugins) {
7761 if($.jstree.plugins.hasOwnProperty(i) && this.attributes[i]) {
7763 if(this.getAttribute(i) && JSON.parse(this.getAttribute(i))) {
7764 c[i] = JSON.parse(this.getAttribute(i));
7768 for(i in $.jstree.defaults.core) {
7769 if($.jstree.defaults.core.hasOwnProperty(i) && this.attributes[i]) {
7770 c.core[i] = JSON.parse(this.getAttribute(i)) || this.getAttribute(i);
7775 // proto.attributeChangedCallback = function (name, previous, value) { };
7777 document.registerElement("vakata-jstree", { prototype: proto });