1 /*globals jQuery, define, exports, require, window, document, postMessage */
4 if (typeof define === 'function' && define.amd) {
5 define(['jquery'], factory);
7 else if(typeof exports === 'object') {
8 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 _d = document, _node = _d.createElement('LI'), _temp1, _temp2;
47 _node.setAttribute('role', 'treeitem');
48 _temp1 = _d.createElement('I');
49 _temp1.className = 'jstree-icon jstree-ocl';
50 _temp1.setAttribute('role', 'presentation');
51 _node.appendChild(_temp1);
52 _temp1 = _d.createElement('A');
53 _temp1.className = 'jstree-anchor';
54 _temp1.setAttribute('href','#');
55 _temp1.setAttribute('tabindex','-1');
56 _temp2 = _d.createElement('I');
57 _temp2.className = 'jstree-icon jstree-themeicon';
58 _temp2.setAttribute('role', 'presentation');
59 _temp1.appendChild(_temp2);
60 _node.appendChild(_temp1);
61 _temp1 = _temp2 = null;
65 * holds all jstree related functions and variables, including the actual class and methods to create, access and manipulate instances.
70 * specifies the jstree version in use
71 * @name $.jstree.version
75 * holds all the default options used when creating new instances
76 * @name $.jstree.defaults
80 * 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 `[]`
81 * @name $.jstree.defaults.plugins
86 * stores all loaded jstree plugins (used internally)
87 * @name $.jstree.plugins
90 path : src && src.indexOf('/') !== -1 ? src.replace(/\/[^\/]+$/,'') : '',
91 idregex : /[\\:&!^|()\[\]<>@*'+~#";.,=\- \/${}%]/g
94 * creates a jstree instance
95 * @name $.jstree.create(el [, options])
96 * @param {DOMElement|jQuery|String} el the element to create the instance on, can be jQuery extended or a selector
97 * @param {Object} options options for this instance (extends `$.jstree.defaults`)
98 * @return {jsTree} the new instance
100 $.jstree.create = function (el, options) {
101 var tmp = new $.jstree.core(++instance_counter),
103 options = $.extend(true, {}, $.jstree.defaults, options);
104 if(opt && opt.plugins) {
105 options.plugins = opt.plugins;
107 $.each(options.plugins, function (i, k) {
109 tmp = tmp.plugin(k, options[k]);
112 tmp.init(el, options);
116 * remove all traces of jstree from the DOM and destroy all instances
117 * @name $.jstree.destroy()
119 $.jstree.destroy = function () {
120 $('.jstree:jstree').jstree('destroy');
121 $(document).off('.jstree');
124 * the jstree class constructor, used only internally
126 * @name $.jstree.core(id)
127 * @param {Number} id this instance's index
129 $.jstree.core = function (id) {
149 * get a reference to an existing instance
153 * // provided a container with an ID of "tree", and a nested node with an ID of "branch"
154 * // all of there will return the same instance
155 * $.jstree.reference('tree');
156 * $.jstree.reference('#tree');
157 * $.jstree.reference($('#tree'));
158 * $.jstree.reference(document.getElementByID('tree'));
159 * $.jstree.reference('branch');
160 * $.jstree.reference('#branch');
161 * $.jstree.reference($('#branch'));
162 * $.jstree.reference(document.getElementByID('branch'));
164 * @name $.jstree.reference(needle)
165 * @param {DOMElement|jQuery|String} needle
166 * @return {jsTree|null} the instance or `null` if not found
168 $.jstree.reference = function (needle) {
171 if(needle && needle.id) { needle = needle.id; }
173 if(!obj || !obj.length) {
174 try { obj = $(needle); } catch (ignore) { }
176 if(!obj || !obj.length) {
177 try { obj = $('#' + needle.replace($.jstree.idregex,'\\$&')); } catch (ignore) { }
179 if(obj && obj.length && (obj = obj.closest('.jstree')).length && (obj = obj.data('jstree'))) {
183 $('.jstree').each(function () {
184 var inst = $(this).data('jstree');
185 if(inst && inst._model.data[needle]) {
194 * Create an instance, get an instance or invoke a command on a instance.
196 * 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).
198 * 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).
200 * If there is an existing instance and `arg` is not a string the instance itself is returned (similar to `$.jstree.reference`).
202 * In any other case - nothing is returned and chaining is not broken.
206 * $('#tree1').jstree(); // creates an instance
207 * $('#tree2').jstree({ plugins : [] }); // create an instance with some options
208 * $('#tree1').jstree('open_node', '#branch_1'); // call a method on an existing instance, passing additional arguments
209 * $('#tree2').jstree(); // get an existing instance (or create an instance)
210 * $('#tree2').jstree(true); // get an existing instance (will not create new instance)
211 * $('#branch_1').jstree().select_node('#branch_1'); // get an instance (using a nested element and call a method)
213 * @name $().jstree([arg])
214 * @param {String|Object} arg
217 $.fn.jstree = function (arg) {
218 // check for string argument
219 var is_method = (typeof arg === 'string'),
220 args = Array.prototype.slice.call(arguments, 1),
222 this.each(function () {
223 // get the instance (if there is one) and method (if it exists)
224 var instance = $.jstree.reference(this),
225 method = is_method && instance ? instance[arg] : null;
226 // if calling a method, and method is available - execute on the instance
227 result = is_method && method ?
228 method.apply(instance, args) :
230 // if there is no instance and no method is being called - create one
231 if(!instance && !is_method && (arg === undefined || $.isPlainObject(arg))) {
232 $(this).data('jstree', new $.jstree.create(this, arg));
234 // if there is an instance and no method is called - return the instance
235 if( (instance && !is_method) || arg === true ) {
236 result = instance || false;
238 // if there was a method call which returned a result - break and return the value
239 if(result !== null && result !== undefined) {
243 // if there was a method call with a valid return value - return that, otherwise continue the chain
244 return result !== null && result !== undefined ?
248 * used to find elements containing an instance
252 * $('div:jstree').each(function () {
253 * $(this).jstree('destroy');
259 $.expr[':'].jstree = $.expr.createPseudo(function(search) {
261 return $(a).hasClass('jstree') &&
262 $(a).data('jstree') !== undefined;
267 * stores all defaults for the core
268 * @name $.jstree.defaults.core
270 $.jstree.defaults.core = {
274 * 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).
276 * You can also pass in a HTML string or a JSON array here.
278 * 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.
279 * 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.
281 * 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.
286 * $('#tree').jstree({
289 * 'url' : '/get/children/',
290 * 'data' : function (node) {
291 * return { 'id' : node.id };
297 * $('#tree').jstree({
300 * 'Simple root node',
303 * 'text' : 'Root node with options',
304 * 'state' : { 'opened' : true, 'selected' : true },
305 * 'children' : [ { 'text' : 'Child 1' }, 'Child 2']
311 * $('#tree').jstree({
313 * 'data' : function (obj, callback) {
314 * callback.call(this, ['Root 1', 'Root 2']);
318 * @name $.jstree.defaults.core.data
322 * configure the various strings used throughout the tree
324 * You can use an object where the key is the string you need to replace and the value is your replacement.
325 * Another option is to specify a function which will be called with an argument of the needed string and should return the replacement.
326 * If left as `false` no replacement is made.
330 * $('#tree').jstree({
333 * 'Loading ...' : 'Please wait ...'
338 * @name $.jstree.defaults.core.strings
342 * determines what happens when a user tries to modify the structure of the tree
343 * If left as `false` all operations like create, rename, delete, move or copy are prevented.
344 * You can set this to `true` to allow all interactions or use a function to have better control.
348 * $('#tree').jstree({
350 * 'check_callback' : function (operation, node, node_parent, node_position, more) {
351 * // operation can be 'create_node', 'rename_node', 'delete_node', 'move_node' or 'copy_node'
352 * // in case of 'rename_node' node_position is filled with the new node name
353 * return operation === 'rename_node' ? true : false;
358 * @name $.jstree.defaults.core.check_callback
360 check_callback : false,
362 * a callback called with a single object parameter in the instance's scope when something goes wrong (operation prevented, ajax failed, etc)
363 * @name $.jstree.defaults.core.error
367 * the open / close animation duration in milliseconds - set this to `false` to disable the animation (default is `200`)
368 * @name $.jstree.defaults.core.animation
372 * a boolean indicating if multiple nodes can be selected
373 * @name $.jstree.defaults.core.multiple
377 * theme configuration object
378 * @name $.jstree.defaults.core.themes
382 * the name of the theme to use (if left as `false` the default theme is used)
383 * @name $.jstree.defaults.core.themes.name
387 * 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.
388 * @name $.jstree.defaults.core.themes.url
392 * the location of all jstree themes - only used if `url` is set to `true`
393 * @name $.jstree.defaults.core.themes.dir
397 * a boolean indicating if connecting dots are shown
398 * @name $.jstree.defaults.core.themes.dots
402 * a boolean indicating if node icons are shown
403 * @name $.jstree.defaults.core.themes.icons
407 * a boolean indicating if the tree background is striped
408 * @name $.jstree.defaults.core.themes.stripes
412 * a string (or boolean `false`) specifying the theme variant to use (if the theme supports variants)
413 * @name $.jstree.defaults.core.themes.variant
417 * a boolean specifying if a reponsive version of the theme should kick in on smaller screens (if the theme supports it). Defaults to `false`.
418 * @name $.jstree.defaults.core.themes.responsive
423 * 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)
424 * @name $.jstree.defaults.core.expand_selected_onload
426 expand_selected_onload : true,
428 * 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`
429 * @name $.jstree.defaults.core.worker
433 * Force node text to plain text (and escape HTML). Defaults to `false`
434 * @name $.jstree.defaults.core.force_text
438 $.jstree.core.prototype = {
440 * used to decorate an instance with a plugin. Used internally.
442 * @name plugin(deco [, opts])
443 * @param {String} deco the plugin to decorate with
444 * @param {Object} opts options for the plugin
447 plugin : function (deco, opts) {
448 var Child = $.jstree.plugins[deco];
450 this._data[deco] = {};
451 Child.prototype = this;
452 return new Child(opts, this);
457 * used to decorate an instance with a plugin. Used internally.
459 * @name init(el, optons)
460 * @param {DOMElement|jQuery|String} el the element we are transforming
461 * @param {Object} options options for this instance
462 * @trigger init.jstree, loading.jstree, loaded.jstree, ready.jstree, changed.jstree
464 init : function (el, options) {
473 state : { loaded : false }
477 force_full_redraw : false,
478 redraw_timeout : false,
487 this.element = $(el).addClass('jstree jstree-' + this._id);
488 this.settings = options;
489 this.element.bind("destroyed", $.proxy(this.teardown, this));
491 this._data.core.ready = false;
492 this._data.core.loaded = false;
493 this._data.core.rtl = (this.element.css("direction") === "rtl");
494 this.element[this._data.core.rtl ? 'addClass' : 'removeClass']("jstree-rtl");
495 this.element.attr('role','tree');
496 if(this.settings.core.multiple) {
497 this.element.attr('aria-multiselectable', true);
499 if(!this.element.attr('tabindex')) {
500 this.element.attr('tabindex','0');
505 * triggered after all events are bound
509 this.trigger("init");
511 this._data.core.original_container_html = this.element.find(" > ul > li").clone(true);
512 this._data.core.original_container_html
513 .find("li").addBack()
514 .contents().filter(function() {
515 return this.nodeType === 3 && (!this.nodeValue || /^\s+$/.test(this.nodeValue));
518 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>");
519 this.element.attr('aria-activedescendant','j' + this._id + '_loading');
520 this._data.core.li_height = this.get_container_ul().children("li").first().height() || 24;
522 * triggered after the loading text is shown and before loading starts
524 * @name loading.jstree
526 this.trigger("loading");
530 * destroy an instance
532 * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact
534 destroy : function (keep_html) {
537 window.URL.revokeObjectURL(this._wrk);
542 if(!keep_html) { this.element.empty(); }
543 this.element.unbind("destroyed", this.teardown);
547 * part of the destroying of an instance. Used internally.
551 teardown : function () {
554 .removeClass('jstree')
555 .removeData('jstree')
556 .find("[class^='jstree']")
558 .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); });
562 * bind all events. Used internally.
568 .on("dblclick.jstree", function () {
569 if(document.selection && document.selection.empty) {
570 document.selection.empty();
573 if(window.getSelection) {
574 var sel = window.getSelection();
576 sel.removeAllRanges();
582 .on("click.jstree", ".jstree-ocl", $.proxy(function (e) {
583 this.toggle_node(e.target);
585 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
587 if(e.currentTarget !== document.activeElement) { $(e.currentTarget).focus(); }
588 this.activate_node(e.currentTarget, e);
590 .on('keydown.jstree', '.jstree-anchor', $.proxy(function (e) {
591 if(e.target.tagName === "INPUT") { return true; }
597 $(e.currentTarget).trigger(e);
601 if(this.is_open(e.currentTarget)) {
602 this.close_node(e.currentTarget);
605 o = this.get_prev_dom(e.currentTarget);
606 if(o && o.length) { o.children('.jstree-anchor').focus(); }
611 o = this.get_prev_dom(e.currentTarget);
612 if(o && o.length) { o.children('.jstree-anchor').focus(); }
616 if(this.is_closed(e.currentTarget)) {
617 this.open_node(e.currentTarget, function (o) { this.get_node(o, true).children('.jstree-anchor').focus(); });
620 o = this.get_next_dom(e.currentTarget);
621 if(o && o.length) { o.children('.jstree-anchor').focus(); }
626 o = this.get_next_dom(e.currentTarget);
627 if(o && o.length) { o.children('.jstree-anchor').focus(); }
632 o = this.get_node(e.currentTarget);
633 if(o && o.id && o.id !== '#') {
634 o = this.is_selected(o) ? this.get_selected() : o;
635 // this.delete_node(o);
641 o = this.get_node(e.currentTarget);
643 if(o && o.id && o.id !== '#') {
649 // console.log(e.which);
653 .on("load_node.jstree", $.proxy(function (e, data) {
655 if(data.node.id === '#' && !this._data.core.loaded) {
656 this._data.core.loaded = true;
657 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
659 * triggered after the root node is loaded for the first time
661 * @name loaded.jstree
663 this.trigger("loaded");
665 if(!this._data.core.ready && !this.get_container_ul().find('.jstree-loading').length) {
666 this._data.core.ready = true;
667 if(this._data.core.selected.length) {
668 if(this.settings.core.expand_selected_onload) {
670 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
671 tmp = tmp.concat(this._model.data[this._data.core.selected[i]].parents);
673 tmp = $.vakata.array_unique(tmp);
674 for(i = 0, j = tmp.length; i < j; i++) {
675 this.open_node(tmp[i], false, 0);
678 this.trigger('changed', { 'action' : 'ready', 'selected' : this._data.core.selected });
681 * triggered after all nodes are finished loading
685 setTimeout($.proxy(function () { this.trigger("ready"); }, this), 0);
690 .on("init.jstree", $.proxy(function () {
691 var s = this.settings.core.themes;
692 this._data.core.themes.dots = s.dots;
693 this._data.core.themes.stripes = s.stripes;
694 this._data.core.themes.icons = s.icons;
695 this.set_theme(s.name || "default", s.url);
696 this.set_theme_variant(s.variant);
698 .on("loading.jstree", $.proxy(function () {
699 this[ this._data.core.themes.dots ? "show_dots" : "hide_dots" ]();
700 this[ this._data.core.themes.icons ? "show_icons" : "hide_icons" ]();
701 this[ this._data.core.themes.stripes ? "show_stripes" : "hide_stripes" ]();
703 .on('blur.jstree', '.jstree-anchor', $.proxy(function (e) {
704 this._data.core.focused = null;
705 $(e.currentTarget).filter('.jstree-hovered').mouseleave();
707 .on('focus.jstree', '.jstree-anchor', $.proxy(function (e) {
708 var tmp = this.get_node(e.currentTarget);
710 this._data.core.focused = tmp.id;
712 this.element.find('.jstree-hovered').not(e.currentTarget).mouseleave();
713 $(e.currentTarget).mouseenter();
715 .on('focus.jstree', $.proxy(function () {
716 if(!this._data.core.focused) {
717 this.get_node(this.element.attr('aria-activedescendant'), true).find('> .jstree-anchor').focus();
720 .on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e) {
721 this.hover_node(e.currentTarget);
723 .on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e) {
724 this.dehover_node(e.currentTarget);
728 * part of the destroying of an instance. Used internally.
732 unbind : function () {
733 this.element.off('.jstree');
734 $(document).off('.jstree-' + this._id);
737 * trigger an event. Used internally.
739 * @name trigger(ev [, data])
740 * @param {String} ev the name of the event to trigger
741 * @param {Object} data additional data to pass with the event
743 trigger : function (ev, data) {
747 data.instance = this;
748 this.element.triggerHandler(ev.replace('.jstree','') + '.jstree', data);
751 * returns the jQuery extended instance container
752 * @name get_container()
755 get_container : function () {
759 * returns the jQuery extended main UL node inside the instance container. Used internally.
761 * @name get_container_ul()
764 get_container_ul : function () {
765 return this.element.children(".jstree-children").first();
768 * gets string replacements (localization). Used internally.
770 * @name get_string(key)
771 * @param {String} key
774 get_string : function (key) {
775 var a = this.settings.core.strings;
776 if($.isFunction(a)) { return a.call(this, key); }
777 if(a && a[key]) { return a[key]; }
781 * gets the first child of a DOM node. Used internally.
783 * @name _firstChild(dom)
784 * @param {DOMElement} dom
785 * @return {DOMElement}
787 _firstChild : function (dom) {
788 dom = dom ? dom.firstChild : null;
789 while(dom !== null && dom.nodeType !== 1) {
790 dom = dom.nextSibling;
795 * gets the next sibling of a DOM node. Used internally.
797 * @name _nextSibling(dom)
798 * @param {DOMElement} dom
799 * @return {DOMElement}
801 _nextSibling : function (dom) {
802 dom = dom ? dom.nextSibling : null;
803 while(dom !== null && dom.nodeType !== 1) {
804 dom = dom.nextSibling;
809 * gets the previous sibling of a DOM node. Used internally.
811 * @name _previousSibling(dom)
812 * @param {DOMElement} dom
813 * @return {DOMElement}
815 _previousSibling : function (dom) {
816 dom = dom ? dom.previousSibling : null;
817 while(dom !== null && dom.nodeType !== 1) {
818 dom = dom.previousSibling;
823 * 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)
824 * @name get_node(obj [, as_dom])
826 * @param {Boolean} as_dom
827 * @return {Object|jQuery}
829 get_node : function (obj, as_dom) {
835 if(this._model.data[obj]) {
836 obj = this._model.data[obj];
838 else if(typeof obj === "string" && (dom = $('#' + obj.replace($.jstree.idregex,'\\$&'), this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) {
839 obj = this._model.data[dom.closest('.jstree-node').attr('id')];
841 else if((dom = $(obj, this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) {
842 obj = this._model.data[dom.closest('.jstree-node').attr('id')];
844 else if((dom = $(obj, this.element)).length && dom.hasClass('jstree')) {
845 obj = this._model.data['#'];
852 obj = obj.id === '#' ? this.element : $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element);
855 } catch (ex) { return false; }
858 * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array)
859 * @name get_path(obj [, glue, ids])
860 * @param {mixed} obj the node
861 * @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
862 * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used
865 get_path : function (obj, glue, ids) {
866 obj = obj.parents ? obj : this.get_node(obj);
867 if(!obj || obj.id === '#' || !obj.parents) {
871 p.push(ids ? obj.id : obj.text);
872 for(i = 0, j = obj.parents.length; i < j; i++) {
873 p.push(ids ? obj.parents[i] : this.get_text(obj.parents[i]));
875 p = p.reverse().slice(1);
876 return glue ? p.join(glue) : p;
879 * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
880 * @name get_next_dom(obj [, strict])
882 * @param {Boolean} strict
885 get_next_dom : function (obj, strict) {
887 obj = this.get_node(obj, true);
888 if(obj[0] === this.element[0]) {
889 tmp = this._firstChild(this.get_container_ul()[0]);
890 while (tmp && tmp.offsetHeight === 0) {
891 tmp = this._nextSibling(tmp);
893 return tmp ? $(tmp) : false;
895 if(!obj || !obj.length) {
901 tmp = this._nextSibling(tmp);
902 } while (tmp && tmp.offsetHeight === 0);
903 return tmp ? $(tmp) : false;
905 if(obj.hasClass("jstree-open")) {
906 tmp = this._firstChild(obj.children('.jstree-children')[0]);
907 while (tmp && tmp.offsetHeight === 0) {
908 tmp = this._nextSibling(tmp);
916 tmp = this._nextSibling(tmp);
917 } while (tmp && tmp.offsetHeight === 0);
921 return obj.parentsUntil(".jstree",".jstree-node").next(".jstree-node:visible").first();
924 * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
925 * @name get_prev_dom(obj [, strict])
927 * @param {Boolean} strict
930 get_prev_dom : function (obj, strict) {
932 obj = this.get_node(obj, true);
933 if(obj[0] === this.element[0]) {
934 tmp = this.get_container_ul()[0].lastChild;
935 while (tmp && tmp.offsetHeight === 0) {
936 tmp = this._previousSibling(tmp);
938 return tmp ? $(tmp) : false;
940 if(!obj || !obj.length) {
946 tmp = this._previousSibling(tmp);
947 } while (tmp && tmp.offsetHeight === 0);
948 return tmp ? $(tmp) : false;
952 tmp = this._previousSibling(tmp);
953 } while (tmp && tmp.offsetHeight === 0);
956 while(obj.hasClass("jstree-open")) {
957 obj = obj.children(".jstree-children").first().children(".jstree-node:visible:last");
961 tmp = obj[0].parentNode.parentNode;
962 return tmp && tmp.className && tmp.className.indexOf('jstree-node') !== -1 ? $(tmp) : false;
965 * get the parent ID of a node
966 * @name get_parent(obj)
970 get_parent : function (obj) {
971 obj = this.get_node(obj);
972 if(!obj || obj.id === '#') {
978 * get a jQuery collection of all the children of a node (node must be rendered)
979 * @name get_children_dom(obj)
983 get_children_dom : function (obj) {
984 obj = this.get_node(obj, true);
985 if(obj[0] === this.element[0]) {
986 return this.get_container_ul().children(".jstree-node");
988 if(!obj || !obj.length) {
991 return obj.children(".jstree-children").children(".jstree-node");
994 * checks if a node has children
995 * @name is_parent(obj)
999 is_parent : function (obj) {
1000 obj = this.get_node(obj);
1001 return obj && (obj.state.loaded === false || obj.children.length > 0);
1004 * checks if a node is loaded (its children are available)
1005 * @name is_loaded(obj)
1006 * @param {mixed} obj
1009 is_loaded : function (obj) {
1010 obj = this.get_node(obj);
1011 return obj && obj.state.loaded;
1014 * check if a node is currently loading (fetching children)
1015 * @name is_loading(obj)
1016 * @param {mixed} obj
1019 is_loading : function (obj) {
1020 obj = this.get_node(obj);
1021 return obj && obj.state && obj.state.loading;
1024 * check if a node is opened
1025 * @name is_open(obj)
1026 * @param {mixed} obj
1029 is_open : function (obj) {
1030 obj = this.get_node(obj);
1031 return obj && obj.state.opened;
1034 * check if a node is in a closed state
1035 * @name is_closed(obj)
1036 * @param {mixed} obj
1039 is_closed : function (obj) {
1040 obj = this.get_node(obj);
1041 return obj && this.is_parent(obj) && !obj.state.opened;
1044 * check if a node has no children
1045 * @name is_leaf(obj)
1046 * @param {mixed} obj
1049 is_leaf : function (obj) {
1050 return !this.is_parent(obj);
1053 * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array.
1054 * @name load_node(obj [, callback])
1055 * @param {mixed} obj
1056 * @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
1058 * @trigger load_node.jstree
1060 load_node : function (obj, callback) {
1062 if($.isArray(obj)) {
1063 this._load_nodes(obj.slice(), callback);
1066 obj = this.get_node(obj);
1068 if(callback) { callback.call(this, obj, false); }
1071 // 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?
1072 if(obj.state.loaded) {
1073 obj.state.loaded = false;
1074 for(k = 0, l = obj.children_d.length; k < l; k++) {
1075 for(i = 0, j = obj.parents.length; i < j; i++) {
1076 this._model.data[obj.parents[i]].children_d = $.vakata.array_remove_item(this._model.data[obj.parents[i]].children_d, obj.children_d[k]);
1078 if(this._model.data[obj.children_d[k]].state.selected) {
1080 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.children_d[k]);
1082 delete this._model.data[obj.children_d[k]];
1085 obj.children_d = [];
1087 this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected });
1090 obj.state.loading = true;
1091 this.get_node(obj, true).addClass("jstree-loading").attr('aria-busy',true);
1092 this._load_node(obj, $.proxy(function (status) {
1093 obj = this._model.data[obj.id];
1094 obj.state.loading = false;
1095 obj.state.loaded = status;
1096 var dom = this.get_node(obj, true);
1097 if(obj.state.loaded && !obj.children.length && dom && dom.length && !dom.hasClass('jstree-leaf')) {
1098 dom.removeClass('jstree-closed jstree-open').addClass('jstree-leaf');
1100 dom.removeClass("jstree-loading").attr('aria-busy',false);
1102 * triggered after a node is loaded
1104 * @name load_node.jstree
1105 * @param {Object} node the node that was loading
1106 * @param {Boolean} status was the node loaded successfully
1108 this.trigger('load_node', { "node" : obj, "status" : status });
1110 callback.call(this, obj, status);
1116 * load an array of nodes (will also load unavailable nodes as soon as the appear in the structure). Used internally.
1118 * @name _load_nodes(nodes [, callback])
1119 * @param {array} nodes
1120 * @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
1122 _load_nodes : function (nodes, callback, is_callback) {
1124 c = function () { this._load_nodes(nodes, callback, true); },
1125 m = this._model.data, i, j;
1126 for(i = 0, j = nodes.length; i < j; i++) {
1127 if(m[nodes[i]] && (!m[nodes[i]].state.loaded || !is_callback)) {
1128 if(!this.is_loading(nodes[i])) {
1129 this.load_node(nodes[i], c);
1135 if(callback && !callback.done) {
1136 callback.call(this, nodes);
1137 callback.done = true;
1142 * handles the actual loading of a node. Used only internally.
1144 * @name _load_node(obj [, callback])
1145 * @param {mixed} obj
1146 * @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
1149 _load_node : function (obj, callback) {
1150 var s = this.settings.core.data, t;
1151 // use original HTML
1153 if(obj.id === '#') {
1154 return this._append_html_data(obj, this._data.core.original_container_html.clone(true), function (status) {
1155 callback.call(this, status);
1159 return callback.call(this, false);
1161 // return callback.call(this, obj.id === '#' ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false);
1163 if($.isFunction(s)) {
1164 return s.call(this, obj, $.proxy(function (d) {
1166 callback.call(this, false);
1168 this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $(d) : d, function (status) {
1169 callback.call(this, status);
1171 // 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));
1174 if(typeof s === 'object') {
1176 s = $.extend(true, {}, s);
1177 if($.isFunction(s.url)) {
1178 s.url = s.url.call(this, obj);
1180 if($.isFunction(s.data)) {
1181 s.data = s.data.call(this, obj);
1184 .done($.proxy(function (d,t,x) {
1185 var type = x.getResponseHeader('Content-Type');
1186 if(type.indexOf('json') !== -1 || typeof d === "object") {
1187 return this._append_json_data(obj, d, function (status) { callback.call(this, status); });
1188 //return callback.call(this, this._append_json_data(obj, d));
1190 if(type.indexOf('html') !== -1 || typeof d === "string") {
1191 return this._append_html_data(obj, $(d), function (status) { callback.call(this, status); });
1192 // return callback.call(this, this._append_html_data(obj, $(d)));
1194 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 }) };
1195 this.settings.core.error.call(this, this._data.core.last_error);
1196 return callback.call(this, false);
1198 .fail($.proxy(function (f) {
1199 callback.call(this, false);
1200 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 }) };
1201 this.settings.core.error.call(this, this._data.core.last_error);
1204 t = ($.isArray(s) || $.isPlainObject(s)) ? JSON.parse(JSON.stringify(s)) : s;
1205 if(obj.id === '#') {
1206 return this._append_json_data(obj, t, function (status) {
1207 callback.call(this, status);
1211 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
1212 this.settings.core.error.call(this, this._data.core.last_error);
1213 return callback.call(this, false);
1215 //return callback.call(this, (obj.id === "#" ? this._append_json_data(obj, t) : false) );
1217 if(typeof s === 'string') {
1218 if(obj.id === '#') {
1219 return this._append_html_data(obj, $(s), function (status) {
1220 callback.call(this, status);
1224 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
1225 this.settings.core.error.call(this, this._data.core.last_error);
1226 return callback.call(this, false);
1228 //return callback.call(this, (obj.id === "#" ? this._append_html_data(obj, $(s)) : false) );
1230 return callback.call(this, false);
1233 * adds a node to the list of nodes to redraw. Used only internally.
1235 * @name _node_changed(obj [, callback])
1236 * @param {mixed} obj
1238 _node_changed : function (obj) {
1239 obj = this.get_node(obj);
1241 this._model.changed.push(obj.id);
1245 * appends HTML content to the tree. Used internally.
1247 * @name _append_html_data(obj, data)
1248 * @param {mixed} obj the node to append to
1249 * @param {String} data the HTML string to parse and append
1250 * @trigger model.jstree, changed.jstree
1252 _append_html_data : function (dom, data, cb) {
1253 dom = this.get_node(dom);
1255 dom.children_d = [];
1256 var dat = data.is('ul') ? data.children() : data,
1260 m = this._model.data,
1262 s = this._data.core.selected.length,
1264 dat.each($.proxy(function (i, v) {
1265 tmp = this._parse_model_from_html($(v), par, p.parents.concat());
1269 if(m[tmp].children_d.length) {
1270 dpc = dpc.concat(m[tmp].children_d);
1276 for(i = 0, j = p.parents.length; i < j; i++) {
1277 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1280 * triggered when new data is inserted to the tree model
1282 * @name model.jstree
1283 * @param {Array} nodes an array of node IDs
1284 * @param {String} parent the parent ID of the nodes
1286 this.trigger('model', { "nodes" : dpc, 'parent' : par });
1288 this._node_changed(par);
1292 this.get_container_ul().children('.jstree-initial-node').remove();
1295 if(this._data.core.selected.length !== s) {
1296 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
1298 cb.call(this, true);
1301 * appends JSON content to the tree. Used internally.
1303 * @name _append_json_data(obj, data)
1304 * @param {mixed} obj the node to append to
1305 * @param {String} data the JSON object to parse and append
1306 * @param {Boolean} force_processing internal param - do not set
1307 * @trigger model.jstree, changed.jstree
1309 _append_json_data : function (dom, data, cb, force_processing) {
1310 dom = this.get_node(dom);
1312 dom.children_d = [];
1316 if(typeof data === "string") {
1317 data = JSON.parse(data);
1320 if(!$.isArray(data)) { data = [data]; }
1323 'df' : this._model.default_state,
1326 'm' : this._model.data,
1328 't_cnt' : this._cnt,
1329 'sel' : this._data.core.selected
1331 func = function (data, undefined) {
1332 if(data.data) { data = data.data; }
1345 parse_flat = function (d, p, ps) {
1346 if(!ps) { ps = []; }
1347 else { ps = ps.concat(); }
1348 if(p) { ps.unshift(p); }
1349 var tid = d.id.toString(),
1353 text : d.text || '',
1354 icon : d.icon !== undefined ? d.icon : true,
1357 children : d.children || [],
1358 children_d : d.children_d || [],
1361 li_attr : { id : false },
1362 a_attr : { href : '#' },
1366 if(df.hasOwnProperty(i)) {
1367 tmp.state[i] = df[i];
1370 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1371 tmp.icon = d.data.jstree.icon;
1376 for(i in d.data.jstree) {
1377 if(d.data.jstree.hasOwnProperty(i)) {
1378 tmp.state[i] = d.data.jstree[i];
1383 if(d && typeof d.state === 'object') {
1384 for (i in d.state) {
1385 if(d.state.hasOwnProperty(i)) {
1386 tmp.state[i] = d.state[i];
1390 if(d && typeof d.li_attr === 'object') {
1391 for (i in d.li_attr) {
1392 if(d.li_attr.hasOwnProperty(i)) {
1393 tmp.li_attr[i] = d.li_attr[i];
1397 if(!tmp.li_attr.id) {
1398 tmp.li_attr.id = tid;
1400 if(d && typeof d.a_attr === 'object') {
1401 for (i in d.a_attr) {
1402 if(d.a_attr.hasOwnProperty(i)) {
1403 tmp.a_attr[i] = d.a_attr[i];
1407 if(d && d.children && d.children === true) {
1408 tmp.state.loaded = false;
1410 tmp.children_d = [];
1413 for(i = 0, j = tmp.children.length; i < j; i++) {
1414 c = parse_flat(m[tmp.children[i]], tmp.id, ps);
1416 tmp.children_d.push(c);
1417 if(e.children_d.length) {
1418 tmp.children_d = tmp.children_d.concat(e.children_d);
1423 m[tmp.id].original = d;
1424 if(tmp.state.selected) {
1429 parse_nest = function (d, p, ps) {
1430 if(!ps) { ps = []; }
1431 else { ps = ps.concat(); }
1432 if(p) { ps.unshift(p); }
1433 var tid = false, i, j, c, e, tmp;
1435 tid = 'j' + t_id + '_' + (++t_cnt);
1440 text : typeof d === 'string' ? d : '',
1441 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true,
1448 li_attr : { id : false },
1449 a_attr : { href : '#' },
1453 if(df.hasOwnProperty(i)) {
1454 tmp.state[i] = df[i];
1457 if(d && d.id) { tmp.id = d.id.toString(); }
1458 if(d && d.text) { tmp.text = d.text; }
1459 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1460 tmp.icon = d.data.jstree.icon;
1465 for(i in d.data.jstree) {
1466 if(d.data.jstree.hasOwnProperty(i)) {
1467 tmp.state[i] = d.data.jstree[i];
1472 if(d && typeof d.state === 'object') {
1473 for (i in d.state) {
1474 if(d.state.hasOwnProperty(i)) {
1475 tmp.state[i] = d.state[i];
1479 if(d && typeof d.li_attr === 'object') {
1480 for (i in d.li_attr) {
1481 if(d.li_attr.hasOwnProperty(i)) {
1482 tmp.li_attr[i] = d.li_attr[i];
1486 if(tmp.li_attr.id && !tmp.id) {
1487 tmp.id = tmp.li_attr.id.toString();
1492 if(!tmp.li_attr.id) {
1493 tmp.li_attr.id = tmp.id;
1495 if(d && typeof d.a_attr === 'object') {
1496 for (i in d.a_attr) {
1497 if(d.a_attr.hasOwnProperty(i)) {
1498 tmp.a_attr[i] = d.a_attr[i];
1502 if(d && d.children && d.children.length) {
1503 for(i = 0, j = d.children.length; i < j; i++) {
1504 c = parse_nest(d.children[i], tmp.id, ps);
1506 tmp.children.push(c);
1507 if(e.children_d.length) {
1508 tmp.children_d = tmp.children_d.concat(e.children_d);
1511 tmp.children_d = tmp.children_d.concat(tmp.children);
1513 if(d && d.children && d.children === true) {
1514 tmp.state.loaded = false;
1516 tmp.children_d = [];
1522 if(tmp.state.selected) {
1528 if(dat.length && dat[0].id !== undefined && dat[0].parent !== undefined) {
1529 // Flat JSON support (for easy import from DB):
1530 // 1) convert to object (foreach)
1531 for(i = 0, j = dat.length; i < j; i++) {
1532 if(!dat[i].children) {
1533 dat[i].children = [];
1535 m[dat[i].id.toString()] = dat[i];
1537 // 2) populate children (foreach)
1538 for(i = 0, j = dat.length; i < j; i++) {
1539 m[dat[i].parent.toString()].children.push(dat[i].id.toString());
1540 // populate parent.children_d
1541 p.children_d.push(dat[i].id.toString());
1543 // 3) normalize && populate parents and children_d with recursion
1544 for(i = 0, j = p.children.length; i < j; i++) {
1545 tmp = parse_flat(m[p.children[i]], par, p.parents.concat());
1547 if(m[tmp].children_d.length) {
1548 dpc = dpc.concat(m[tmp].children_d);
1551 for(i = 0, j = p.parents.length; i < j; i++) {
1552 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1554 // ?) three_state selection - p.state.selected && t - (if three_state foreach(dat => ch) -> foreach(parents) if(parent.selected) child.selected = true;
1565 for(i = 0, j = dat.length; i < j; i++) {
1566 tmp = parse_nest(dat[i], par, p.parents.concat());
1570 if(m[tmp].children_d.length) {
1571 dpc = dpc.concat(m[tmp].children_d);
1577 for(i = 0, j = p.parents.length; i < j; i++) {
1578 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1589 if(typeof window === 'undefined' || typeof window.document === 'undefined') {
1596 rslt = function (rslt, worker) {
1597 this._cnt = rslt.cnt;
1598 this._model.data = rslt.mod; // breaks the reference in load_node - careful
1601 var i, j, a = rslt.add, r = rslt.sel, s = this._data.core.selected.slice(), m = this._model.data;
1602 // if selection was changed while calculating in worker
1603 if(r.length !== s.length || $.vakata.array_unique(r.concat(s)).length !== r.length) {
1604 // deselect nodes that are no longer selected
1605 for(i = 0, j = r.length; i < j; i++) {
1606 if($.inArray(r[i], a) === -1 && $.inArray(r[i], s) === -1) {
1607 m[r[i]].state.selected = false;
1610 // select nodes that were selected in the mean time
1611 for(i = 0, j = s.length; i < j; i++) {
1612 if($.inArray(s[i], r) === -1) {
1613 m[s[i]].state.selected = true;
1618 if(rslt.add.length) {
1619 this._data.core.selected = this._data.core.selected.concat(rslt.add);
1622 this.trigger('model', { "nodes" : rslt.dpc, 'parent' : rslt.par });
1624 if(rslt.par !== '#') {
1625 this._node_changed(rslt.par);
1629 // this.get_container_ul().children('.jstree-initial-node').remove();
1632 if(rslt.add.length) {
1633 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
1635 cb.call(this, true);
1637 if(this.settings.core.worker && window.Blob && window.URL && window.Worker) {
1639 if(this._wrk === null) {
1640 this._wrk = window.URL.createObjectURL(
1642 ['self.onmessage = ' + func.toString()],
1643 {type:"text/javascript"}
1647 if(!this._data.core.working || force_processing) {
1648 this._data.core.working = true;
1649 w = new window.Worker(this._wrk);
1650 w.onmessage = $.proxy(function (e) {
1651 rslt.call(this, e.data, true);
1652 try { w.terminate(); w = null; } catch(ignore) { }
1653 if(this._data.core.worker_queue.length) {
1654 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1657 this._data.core.working = false;
1661 if(this._data.core.worker_queue.length) {
1662 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1665 this._data.core.working = false;
1669 w.postMessage(args);
1673 this._data.core.worker_queue.push([dom, data, cb, true]);
1677 rslt.call(this, func(args), false);
1678 if(this._data.core.worker_queue.length) {
1679 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1682 this._data.core.working = false;
1687 rslt.call(this, func(args), false);
1691 * parses a node from a jQuery object and appends them to the in memory tree model. Used internally.
1693 * @name _parse_model_from_html(d [, p, ps])
1694 * @param {jQuery} d the jQuery object to parse
1695 * @param {String} p the parent ID
1696 * @param {Array} ps list of all parents
1697 * @return {String} the ID of the object added to the model
1699 _parse_model_from_html : function (d, p, ps) {
1700 if(!ps) { ps = []; }
1701 else { ps = [].concat(ps); }
1702 if(p) { ps.unshift(p); }
1703 var c, e, m = this._model.data,
1714 li_attr : { id : false },
1715 a_attr : { href : '#' },
1718 for(i in this._model.default_state) {
1719 if(this._model.default_state.hasOwnProperty(i)) {
1720 data.state[i] = this._model.default_state[i];
1723 tmp = $.vakata.attributes(d, true);
1724 $.each(tmp, function (i, v) {
1726 if(!v.length) { return true; }
1727 data.li_attr[i] = v;
1729 data.id = v.toString();
1732 tmp = d.children('a').first();
1734 tmp = $.vakata.attributes(tmp, true);
1735 $.each(tmp, function (i, v) {
1742 tmp = d.children("a").first().length ? d.children("a").first().clone() : d.clone();
1743 tmp.children("ins, i, ul").remove();
1745 tmp = $('<div />').html(tmp);
1746 data.text = this.settings.core.force_text ? tmp.text() : tmp.html();
1748 data.data = tmp ? $.extend(true, {}, tmp) : null;
1749 data.state.opened = d.hasClass('jstree-open');
1750 data.state.selected = d.children('a').hasClass('jstree-clicked');
1751 data.state.disabled = d.children('a').hasClass('jstree-disabled');
1752 if(data.data && data.data.jstree) {
1753 for(i in data.data.jstree) {
1754 if(data.data.jstree.hasOwnProperty(i)) {
1755 data.state[i] = data.data.jstree[i];
1759 tmp = d.children("a").children(".jstree-themeicon");
1761 data.icon = tmp.hasClass('jstree-themeicon-hidden') ? false : tmp.attr('rel');
1763 if(data.state.icon) {
1764 data.icon = data.state.icon;
1766 tmp = d.children("ul").children("li");
1768 tid = 'j' + this._id + '_' + (++this._cnt);
1770 data.id = data.li_attr.id ? data.li_attr.id.toString() : tid;
1772 tmp.each($.proxy(function (i, v) {
1773 c = this._parse_model_from_html($(v), data.id, ps);
1774 e = this._model.data[c];
1775 data.children.push(c);
1776 if(e.children_d.length) {
1777 data.children_d = data.children_d.concat(e.children_d);
1780 data.children_d = data.children_d.concat(data.children);
1783 if(d.hasClass('jstree-closed')) {
1784 data.state.loaded = false;
1787 if(data.li_attr['class']) {
1788 data.li_attr['class'] = data.li_attr['class'].replace('jstree-closed','').replace('jstree-open','');
1790 if(data.a_attr['class']) {
1791 data.a_attr['class'] = data.a_attr['class'].replace('jstree-clicked','').replace('jstree-disabled','');
1794 if(data.state.selected) {
1795 this._data.core.selected.push(data.id);
1800 * 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.
1802 * @name _parse_model_from_flat_json(d [, p, ps])
1803 * @param {Object} d the JSON object to parse
1804 * @param {String} p the parent ID
1805 * @param {Array} ps list of all parents
1806 * @return {String} the ID of the object added to the model
1808 _parse_model_from_flat_json : function (d, p, ps) {
1809 if(!ps) { ps = []; }
1810 else { ps = ps.concat(); }
1811 if(p) { ps.unshift(p); }
1812 var tid = d.id.toString(),
1813 m = this._model.data,
1814 df = this._model.default_state,
1818 text : d.text || '',
1819 icon : d.icon !== undefined ? d.icon : true,
1822 children : d.children || [],
1823 children_d : d.children_d || [],
1826 li_attr : { id : false },
1827 a_attr : { href : '#' },
1831 if(df.hasOwnProperty(i)) {
1832 tmp.state[i] = df[i];
1835 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1836 tmp.icon = d.data.jstree.icon;
1841 for(i in d.data.jstree) {
1842 if(d.data.jstree.hasOwnProperty(i)) {
1843 tmp.state[i] = d.data.jstree[i];
1848 if(d && typeof d.state === 'object') {
1849 for (i in d.state) {
1850 if(d.state.hasOwnProperty(i)) {
1851 tmp.state[i] = d.state[i];
1855 if(d && typeof d.li_attr === 'object') {
1856 for (i in d.li_attr) {
1857 if(d.li_attr.hasOwnProperty(i)) {
1858 tmp.li_attr[i] = d.li_attr[i];
1862 if(!tmp.li_attr.id) {
1863 tmp.li_attr.id = tid;
1865 if(d && typeof d.a_attr === 'object') {
1866 for (i in d.a_attr) {
1867 if(d.a_attr.hasOwnProperty(i)) {
1868 tmp.a_attr[i] = d.a_attr[i];
1872 if(d && d.children && d.children === true) {
1873 tmp.state.loaded = false;
1875 tmp.children_d = [];
1878 for(i = 0, j = tmp.children.length; i < j; i++) {
1879 c = this._parse_model_from_flat_json(m[tmp.children[i]], tmp.id, ps);
1881 tmp.children_d.push(c);
1882 if(e.children_d.length) {
1883 tmp.children_d = tmp.children_d.concat(e.children_d);
1888 m[tmp.id].original = d;
1889 if(tmp.state.selected) {
1890 this._data.core.selected.push(tmp.id);
1895 * parses a node from a JSON object and appends it to the in memory tree model. Used internally.
1897 * @name _parse_model_from_json(d [, p, ps])
1898 * @param {Object} d the JSON object to parse
1899 * @param {String} p the parent ID
1900 * @param {Array} ps list of all parents
1901 * @return {String} the ID of the object added to the model
1903 _parse_model_from_json : function (d, p, ps) {
1904 if(!ps) { ps = []; }
1905 else { ps = ps.concat(); }
1906 if(p) { ps.unshift(p); }
1907 var tid = false, i, j, c, e, m = this._model.data, df = this._model.default_state, tmp;
1909 tid = 'j' + this._id + '_' + (++this._cnt);
1914 text : typeof d === 'string' ? d : '',
1915 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true,
1922 li_attr : { id : false },
1923 a_attr : { href : '#' },
1927 if(df.hasOwnProperty(i)) {
1928 tmp.state[i] = df[i];
1931 if(d && d.id) { tmp.id = d.id.toString(); }
1932 if(d && d.text) { tmp.text = d.text; }
1933 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1934 tmp.icon = d.data.jstree.icon;
1939 for(i in d.data.jstree) {
1940 if(d.data.jstree.hasOwnProperty(i)) {
1941 tmp.state[i] = d.data.jstree[i];
1946 if(d && typeof d.state === 'object') {
1947 for (i in d.state) {
1948 if(d.state.hasOwnProperty(i)) {
1949 tmp.state[i] = d.state[i];
1953 if(d && typeof d.li_attr === 'object') {
1954 for (i in d.li_attr) {
1955 if(d.li_attr.hasOwnProperty(i)) {
1956 tmp.li_attr[i] = d.li_attr[i];
1960 if(tmp.li_attr.id && !tmp.id) {
1961 tmp.id = tmp.li_attr.id.toString();
1966 if(!tmp.li_attr.id) {
1967 tmp.li_attr.id = tmp.id;
1969 if(d && typeof d.a_attr === 'object') {
1970 for (i in d.a_attr) {
1971 if(d.a_attr.hasOwnProperty(i)) {
1972 tmp.a_attr[i] = d.a_attr[i];
1976 if(d && d.children && d.children.length) {
1977 for(i = 0, j = d.children.length; i < j; i++) {
1978 c = this._parse_model_from_json(d.children[i], tmp.id, ps);
1980 tmp.children.push(c);
1981 if(e.children_d.length) {
1982 tmp.children_d = tmp.children_d.concat(e.children_d);
1985 tmp.children_d = tmp.children_d.concat(tmp.children);
1987 if(d && d.children && d.children === true) {
1988 tmp.state.loaded = false;
1990 tmp.children_d = [];
1996 if(tmp.state.selected) {
1997 this._data.core.selected.push(tmp.id);
2002 * redraws all nodes that need to be redrawn. Used internally.
2005 * @trigger redraw.jstree
2007 _redraw : function () {
2008 var nodes = this._model.force_full_redraw ? this._model.data['#'].children.concat([]) : this._model.changed.concat([]),
2009 f = document.createElement('UL'), tmp, i, j, fe = this._data.core.focused;
2010 for(i = 0, j = nodes.length; i < j; i++) {
2011 tmp = this.redraw_node(nodes[i], true, this._model.force_full_redraw);
2012 if(tmp && this._model.force_full_redraw) {
2016 if(this._model.force_full_redraw) {
2017 f.className = this.get_container_ul()[0].className;
2018 f.setAttribute('role','group');
2019 this.element.empty().append(f);
2020 //this.get_container_ul()[0].appendChild(f);
2023 tmp = this.get_node(fe, true);
2024 if(tmp && tmp.length && tmp.children('.jstree-anchor')[0] !== document.activeElement) {
2025 tmp.children('.jstree-anchor').focus();
2028 this._data.core.focused = null;
2031 this._model.force_full_redraw = false;
2032 this._model.changed = [];
2034 * triggered after nodes are redrawn
2036 * @name redraw.jstree
2037 * @param {array} nodes the redrawn nodes
2039 this.trigger('redraw', { "nodes" : nodes });
2042 * redraws all nodes that need to be redrawn or optionally - the whole tree
2043 * @name redraw([full])
2044 * @param {Boolean} full if set to `true` all nodes are redrawn.
2046 redraw : function (full) {
2048 this._model.force_full_redraw = true;
2050 //if(this._model.redraw_timeout) {
2051 // clearTimeout(this._model.redraw_timeout);
2053 //this._model.redraw_timeout = setTimeout($.proxy(this._redraw, this),0);
2057 * redraws a single node. Used internally.
2059 * @name redraw_node(node, deep, is_callback, force_render)
2060 * @param {mixed} node the node to redraw
2061 * @param {Boolean} deep should child nodes be redrawn too
2062 * @param {Boolean} is_callback is this a recursion call
2063 * @param {Boolean} force_render should children of closed parents be drawn anyway
2065 redraw_node : function (node, deep, is_callback, force_render) {
2066 var obj = this.get_node(node),
2075 m = this._model.data,
2079 if(!obj) { return false; }
2080 if(obj.id === '#') { return this.redraw(true); }
2081 deep = deep || obj.children.length === 0;
2082 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);
2085 //node = d.createElement('LI');
2087 par = obj.parent !== '#' ? $('#' + obj.parent.replace($.jstree.idregex,'\\$&'), this.element)[0] : null;
2088 if(par !== null && (!par || !m[obj.parent].state.opened)) {
2091 ind = $.inArray(obj.id, par === null ? m['#'].children : m[obj.parent].children);
2097 par = node.parent().parent()[0];
2098 if(par === this.element[0]) {
2103 // m[obj.id].data = node.data(); // use only node's data, no need to touch jquery storage
2104 if(!deep && obj.children.length && !node.children('.jstree-children').length) {
2108 old = node.children('.jstree-children')[0];
2110 f = node.children('.jstree-anchor')[0] === document.activeElement;
2112 //node = d.createElement('LI');
2115 node = _node.cloneNode(true);
2116 // node is DOM, deep is boolean
2119 for(i in obj.li_attr) {
2120 if(obj.li_attr.hasOwnProperty(i)) {
2121 if(i === 'id') { continue; }
2123 node.setAttribute(i, obj.li_attr[i]);
2126 c += obj.li_attr[i];
2130 if(!obj.a_attr.id) {
2131 obj.a_attr.id = obj.id + '_anchor';
2133 node.setAttribute('aria-selected', !!obj.state.selected);
2134 node.setAttribute('aria-level', obj.parents.length);
2135 node.setAttribute('aria-labelledby', obj.a_attr.id);
2136 if(obj.state.disabled) {
2137 node.setAttribute('aria-disabled', true);
2140 if(obj.state.loaded && !obj.children.length) {
2141 c += ' jstree-leaf';
2144 c += obj.state.opened && obj.state.loaded ? ' jstree-open' : ' jstree-closed';
2145 node.setAttribute('aria-expanded', (obj.state.opened && obj.state.loaded) );
2147 if(obj.parent !== null && m[obj.parent].children[m[obj.parent].children.length - 1] === obj.id) {
2148 c += ' jstree-last';
2152 c = ( obj.state.selected ? ' jstree-clicked' : '') + ( obj.state.disabled ? ' jstree-disabled' : '');
2153 for(j in obj.a_attr) {
2154 if(obj.a_attr.hasOwnProperty(j)) {
2155 if(j === 'href' && obj.a_attr[j] === '#') { continue; }
2157 node.childNodes[1].setAttribute(j, obj.a_attr[j]);
2160 c += ' ' + obj.a_attr[j];
2165 node.childNodes[1].className = 'jstree-anchor ' + c;
2167 if((obj.icon && obj.icon !== true) || obj.icon === false) {
2168 if(obj.icon === false) {
2169 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-hidden';
2171 else if(obj.icon.indexOf('/') === -1 && obj.icon.indexOf('.') === -1) {
2172 node.childNodes[1].childNodes[0].className += ' ' + obj.icon + ' jstree-themeicon-custom';
2175 node.childNodes[1].childNodes[0].style.backgroundImage = 'url('+obj.icon+')';
2176 node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center';
2177 node.childNodes[1].childNodes[0].style.backgroundSize = 'auto';
2178 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom';
2182 if(this.settings.core.force_text) {
2183 node.childNodes[1].appendChild(d.createTextNode(obj.text));
2186 node.childNodes[1].innerHTML += obj.text;
2189 if(deep && obj.children.length && (obj.state.opened || force_render) && obj.state.loaded) {
2190 k = d.createElement('UL');
2191 k.setAttribute('role', 'group');
2192 k.className = 'jstree-children';
2193 for(i = 0, j = obj.children.length; i < j; i++) {
2194 k.appendChild(this.redraw_node(obj.children[i], deep, true));
2196 node.appendChild(k);
2199 node.appendChild(old);
2202 // append back using par / ind
2204 par = this.element[0];
2206 for(i = 0, j = par.childNodes.length; i < j; i++) {
2207 if(par.childNodes[i] && par.childNodes[i].className && par.childNodes[i].className.indexOf('jstree-children') !== -1) {
2208 tmp = par.childNodes[i];
2213 tmp = d.createElement('UL');
2214 tmp.setAttribute('role', 'group');
2215 tmp.className = 'jstree-children';
2216 par.appendChild(tmp);
2220 if(ind < par.childNodes.length) {
2221 par.insertBefore(node, par.childNodes[ind]);
2224 par.appendChild(node);
2227 node.childNodes[1].focus();
2230 if(obj.state.opened && !obj.state.loaded) {
2231 obj.state.opened = false;
2232 setTimeout($.proxy(function () {
2233 this.open_node(obj.id, false, 0);
2239 * opens a node, revaling its children. If the node is not loaded it will be loaded and opened once ready.
2240 * @name open_node(obj [, callback, animation])
2241 * @param {mixed} obj the node to open
2242 * @param {Function} callback a function to execute once the node is opened
2243 * @param {Number} animation the animation duration in milliseconds when opening the node (overrides the `core.animation` setting). Use `false` for no animation.
2244 * @trigger open_node.jstree, after_open.jstree, before_open.jstree
2246 open_node : function (obj, callback, animation) {
2248 if($.isArray(obj)) {
2250 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2251 this.open_node(obj[t1], callback, animation);
2255 obj = this.get_node(obj);
2256 if(!obj || obj.id === '#') {
2259 animation = animation === undefined ? this.settings.core.animation : animation;
2260 if(!this.is_closed(obj)) {
2262 callback.call(this, obj, false);
2266 if(!this.is_loaded(obj)) {
2267 if(this.is_loading(obj)) {
2268 return setTimeout($.proxy(function () {
2269 this.open_node(obj, callback, animation);
2272 this.load_node(obj, function (o, ok) {
2273 return ok ? this.open_node(o, callback, animation) : (callback ? callback.call(this, o, false) : false);
2277 d = this.get_node(obj, true);
2280 if(obj.children.length && !this._firstChild(d.children('.jstree-children')[0])) {
2281 this.redraw_node(obj, true, false, true);
2282 d = this.get_node(obj, true);
2285 this.trigger('before_open', { "node" : obj });
2286 d[0].className = d[0].className.replace('jstree-closed', 'jstree-open');
2287 d[0].setAttribute("aria-expanded", true);
2290 this.trigger('before_open', { "node" : obj });
2292 .children(".jstree-children").css("display","none").end()
2293 .removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true)
2294 .children(".jstree-children").stop(true, true)
2295 .slideDown(animation, function () {
2296 this.style.display = "";
2297 t.trigger("after_open", { "node" : obj });
2301 obj.state.opened = true;
2303 callback.call(this, obj, true);
2307 * 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)
2309 * @name before_open.jstree
2310 * @param {Object} node the opened node
2312 this.trigger('before_open', { "node" : obj });
2315 * triggered when a node is opened (if there is an animation it will not be completed yet)
2317 * @name open_node.jstree
2318 * @param {Object} node the opened node
2320 this.trigger('open_node', { "node" : obj });
2321 if(!animation || !d.length) {
2323 * triggered when a node is opened and the animation is complete
2325 * @name after_open.jstree
2326 * @param {Object} node the opened node
2328 this.trigger("after_open", { "node" : obj });
2333 * opens every parent of a node (node should be loaded)
2334 * @name _open_to(obj)
2335 * @param {mixed} obj the node to reveal
2338 _open_to : function (obj) {
2339 obj = this.get_node(obj);
2340 if(!obj || obj.id === '#') {
2343 var i, j, p = obj.parents;
2344 for(i = 0, j = p.length; i < j; i+=1) {
2346 this.open_node(p[i], false, 0);
2349 return $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element);
2352 * closes a node, hiding its children
2353 * @name close_node(obj [, animation])
2354 * @param {mixed} obj the node to close
2355 * @param {Number} animation the animation duration in milliseconds when closing the node (overrides the `core.animation` setting). Use `false` for no animation.
2356 * @trigger close_node.jstree, after_close.jstree
2358 close_node : function (obj, animation) {
2360 if($.isArray(obj)) {
2362 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2363 this.close_node(obj[t1], animation);
2367 obj = this.get_node(obj);
2368 if(!obj || obj.id === '#') {
2371 if(this.is_closed(obj)) {
2374 animation = animation === undefined ? this.settings.core.animation : animation;
2376 d = this.get_node(obj, true);
2379 d[0].className = d[0].className.replace('jstree-open', 'jstree-closed');
2380 d.attr("aria-expanded", false).children('.jstree-children').remove();
2384 .children(".jstree-children").attr("style","display:block !important").end()
2385 .removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false)
2386 .children(".jstree-children").stop(true, true).slideUp(animation, function () {
2387 this.style.display = "";
2388 d.children('.jstree-children').remove();
2389 t.trigger("after_close", { "node" : obj });
2393 obj.state.opened = false;
2395 * triggered when a node is closed (if there is an animation it will not be complete yet)
2397 * @name close_node.jstree
2398 * @param {Object} node the closed node
2400 this.trigger('close_node',{ "node" : obj });
2401 if(!animation || !d.length) {
2403 * triggered when a node is closed and the animation is complete
2405 * @name after_close.jstree
2406 * @param {Object} node the closed node
2408 this.trigger("after_close", { "node" : obj });
2412 * toggles a node - closing it if it is open, opening it if it is closed
2413 * @name toggle_node(obj)
2414 * @param {mixed} obj the node to toggle
2416 toggle_node : function (obj) {
2418 if($.isArray(obj)) {
2420 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2421 this.toggle_node(obj[t1]);
2425 if(this.is_closed(obj)) {
2426 return this.open_node(obj);
2428 if(this.is_open(obj)) {
2429 return this.close_node(obj);
2433 * 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.
2434 * @name open_all([obj, animation, original_obj])
2435 * @param {mixed} obj the node to open recursively, omit to open all nodes in the tree
2436 * @param {Number} animation the animation duration in milliseconds when opening the nodes, the default is no animation
2437 * @param {jQuery} reference to the node that started the process (internal use)
2438 * @trigger open_all.jstree
2440 open_all : function (obj, animation, original_obj) {
2441 if(!obj) { obj = '#'; }
2442 obj = this.get_node(obj);
2443 if(!obj) { return false; }
2444 var dom = obj.id === '#' ? this.get_container_ul() : this.get_node(obj, true), i, j, _this;
2446 for(i = 0, j = obj.children_d.length; i < j; i++) {
2447 if(this.is_closed(this._model.data[obj.children_d[i]])) {
2448 this._model.data[obj.children_d[i]].state.opened = true;
2451 return this.trigger('open_all', { "node" : obj });
2453 original_obj = original_obj || dom;
2455 dom = this.is_closed(obj) ? dom.find('.jstree-closed').addBack() : dom.find('.jstree-closed');
2456 dom.each(function () {
2459 function(node, status) { if(status && this.is_parent(node)) { this.open_all(node, animation, original_obj); } },
2463 if(original_obj.find('.jstree-closed').length === 0) {
2465 * triggered when an `open_all` call completes
2467 * @name open_all.jstree
2468 * @param {Object} node the opened node
2470 this.trigger('open_all', { "node" : this.get_node(original_obj) });
2474 * closes all nodes within a node (or the tree), revaling their children
2475 * @name close_all([obj, animation])
2476 * @param {mixed} obj the node to close recursively, omit to close all nodes in the tree
2477 * @param {Number} animation the animation duration in milliseconds when closing the nodes, the default is no animation
2478 * @trigger close_all.jstree
2480 close_all : function (obj, animation) {
2481 if(!obj) { obj = '#'; }
2482 obj = this.get_node(obj);
2483 if(!obj) { return false; }
2484 var dom = obj.id === '#' ? this.get_container_ul() : this.get_node(obj, true),
2487 for(i = 0, j = obj.children_d.length; i < j; i++) {
2488 this._model.data[obj.children_d[i]].state.opened = false;
2490 return this.trigger('close_all', { "node" : obj });
2492 dom = this.is_open(obj) ? dom.find('.jstree-open').addBack() : dom.find('.jstree-open');
2493 $(dom.get().reverse()).each(function () { _this.close_node(this, animation || 0); });
2495 * triggered when an `close_all` call completes
2497 * @name close_all.jstree
2498 * @param {Object} node the closed node
2500 this.trigger('close_all', { "node" : obj });
2503 * checks if a node is disabled (not selectable)
2504 * @name is_disabled(obj)
2505 * @param {mixed} obj
2508 is_disabled : function (obj) {
2509 obj = this.get_node(obj);
2510 return obj && obj.state && obj.state.disabled;
2513 * enables a node - so that it can be selected
2514 * @name enable_node(obj)
2515 * @param {mixed} obj the node to enable
2516 * @trigger enable_node.jstree
2518 enable_node : function (obj) {
2520 if($.isArray(obj)) {
2522 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2523 this.enable_node(obj[t1]);
2527 obj = this.get_node(obj);
2528 if(!obj || obj.id === '#') {
2531 obj.state.disabled = false;
2532 this.get_node(obj,true).children('.jstree-anchor').removeClass('jstree-disabled').attr('aria-disabled', false);
2534 * triggered when an node is enabled
2536 * @name enable_node.jstree
2537 * @param {Object} node the enabled node
2539 this.trigger('enable_node', { 'node' : obj });
2542 * disables a node - so that it can not be selected
2543 * @name disable_node(obj)
2544 * @param {mixed} obj the node to disable
2545 * @trigger disable_node.jstree
2547 disable_node : function (obj) {
2549 if($.isArray(obj)) {
2551 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2552 this.disable_node(obj[t1]);
2556 obj = this.get_node(obj);
2557 if(!obj || obj.id === '#') {
2560 obj.state.disabled = true;
2561 this.get_node(obj,true).children('.jstree-anchor').addClass('jstree-disabled').attr('aria-disabled', true);
2563 * triggered when an node is disabled
2565 * @name disable_node.jstree
2566 * @param {Object} node the disabled node
2568 this.trigger('disable_node', { 'node' : obj });
2571 * called when a node is selected by the user. Used internally.
2573 * @name activate_node(obj, e)
2574 * @param {mixed} obj the node
2575 * @param {Object} e the related event
2576 * @trigger activate_node.jstree, changed.jstree
2578 activate_node : function (obj, e) {
2579 if(this.is_disabled(obj)) {
2583 // 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
2584 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;
2585 if(this._data.core.last_clicked && !this._data.core.last_clicked.state.selected) { this._data.core.last_clicked = null; }
2586 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]); }
2588 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 ) )) {
2589 if(!this.settings.core.multiple && (e.metaKey || e.ctrlKey || e.shiftKey) && this.is_selected(obj)) {
2590 this.deselect_node(obj, false, e);
2593 this.deselect_all(true);
2594 this.select_node(obj, false, false, e);
2595 this._data.core.last_clicked = this.get_node(obj);
2600 var o = this.get_node(obj).id,
2601 l = this._data.core.last_clicked.id,
2602 p = this.get_node(this._data.core.last_clicked.parent).children,
2605 for(i = 0, j = p.length; i < j; i += 1) {
2606 // separate IFs work whem o and l are the same
2613 if(c || p[i] === o || p[i] === l) {
2614 this.select_node(p[i], true, false, e);
2617 this.deselect_node(p[i], true, e);
2620 this.trigger('changed', { 'action' : 'select_node', 'node' : this.get_node(obj), 'selected' : this._data.core.selected, 'event' : e });
2623 if(!this.is_selected(obj)) {
2624 this.select_node(obj, false, false, e);
2627 this.deselect_node(obj, false, e);
2632 * triggered when an node is clicked or intercated with by the user
2634 * @name activate_node.jstree
2635 * @param {Object} node
2637 this.trigger('activate_node', { 'node' : this.get_node(obj) });
2640 * applies the hover state on a node, called when a node is hovered by the user. Used internally.
2642 * @name hover_node(obj)
2643 * @param {mixed} obj
2644 * @trigger hover_node.jstree
2646 hover_node : function (obj) {
2647 obj = this.get_node(obj, true);
2648 if(!obj || !obj.length || obj.children('.jstree-hovered').length) {
2651 var o = this.element.find('.jstree-hovered'), t = this.element;
2652 if(o && o.length) { this.dehover_node(o); }
2654 obj.children('.jstree-anchor').addClass('jstree-hovered');
2656 * triggered when an node is hovered
2658 * @name hover_node.jstree
2659 * @param {Object} node
2661 this.trigger('hover_node', { 'node' : this.get_node(obj) });
2662 setTimeout(function () { t.attr('aria-activedescendant', obj[0].id); }, 0);
2665 * removes the hover state from a nodecalled when a node is no longer hovered by the user. Used internally.
2667 * @name dehover_node(obj)
2668 * @param {mixed} obj
2669 * @trigger dehover_node.jstree
2671 dehover_node : function (obj) {
2672 obj = this.get_node(obj, true);
2673 if(!obj || !obj.length || !obj.children('.jstree-hovered').length) {
2676 obj.children('.jstree-anchor').removeClass('jstree-hovered');
2678 * triggered when an node is no longer hovered
2680 * @name dehover_node.jstree
2681 * @param {Object} node
2683 this.trigger('dehover_node', { 'node' : this.get_node(obj) });
2687 * @name select_node(obj [, supress_event, prevent_open])
2688 * @param {mixed} obj an array can be used to select multiple nodes
2689 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
2690 * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened
2691 * @trigger select_node.jstree, changed.jstree
2693 select_node : function (obj, supress_event, prevent_open, e) {
2694 var dom, t1, t2, th;
2695 if($.isArray(obj)) {
2697 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2698 this.select_node(obj[t1], supress_event, prevent_open, e);
2702 obj = this.get_node(obj);
2703 if(!obj || obj.id === '#') {
2706 dom = this.get_node(obj, true);
2707 if(!obj.state.selected) {
2708 obj.state.selected = true;
2709 this._data.core.selected.push(obj.id);
2711 dom = this._open_to(obj);
2713 if(dom && dom.length) {
2714 dom.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked');
2717 * triggered when an node is selected
2719 * @name select_node.jstree
2720 * @param {Object} node
2721 * @param {Array} selected the current selection
2722 * @param {Object} event the event (if any) that triggered this select_node
2724 this.trigger('select_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
2725 if(!supress_event) {
2727 * triggered when selection changes
2729 * @name changed.jstree
2730 * @param {Object} node
2731 * @param {Object} action the action that caused the selection to change
2732 * @param {Array} selected the current selection
2733 * @param {Object} event the event (if any) that triggered this changed event
2735 this.trigger('changed', { 'action' : 'select_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
2741 * @name deselect_node(obj [, supress_event])
2742 * @param {mixed} obj an array can be used to deselect multiple nodes
2743 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
2744 * @trigger deselect_node.jstree, changed.jstree
2746 deselect_node : function (obj, supress_event, e) {
2748 if($.isArray(obj)) {
2750 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2751 this.deselect_node(obj[t1], supress_event, e);
2755 obj = this.get_node(obj);
2756 if(!obj || obj.id === '#') {
2759 dom = this.get_node(obj, true);
2760 if(obj.state.selected) {
2761 obj.state.selected = false;
2762 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.id);
2764 dom.attr('aria-selected', false).children('.jstree-anchor').removeClass('jstree-clicked');
2767 * triggered when an node is deselected
2769 * @name deselect_node.jstree
2770 * @param {Object} node
2771 * @param {Array} selected the current selection
2772 * @param {Object} event the event (if any) that triggered this deselect_node
2774 this.trigger('deselect_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
2775 if(!supress_event) {
2776 this.trigger('changed', { 'action' : 'deselect_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
2781 * select all nodes in the tree
2782 * @name select_all([supress_event])
2783 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
2784 * @trigger select_all.jstree, changed.jstree
2786 select_all : function (supress_event) {
2787 var tmp = this._data.core.selected.concat([]), i, j;
2788 this._data.core.selected = this._model.data['#'].children_d.concat();
2789 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
2790 if(this._model.data[this._data.core.selected[i]]) {
2791 this._model.data[this._data.core.selected[i]].state.selected = true;
2796 * triggered when all nodes are selected
2798 * @name select_all.jstree
2799 * @param {Array} selected the current selection
2801 this.trigger('select_all', { 'selected' : this._data.core.selected });
2802 if(!supress_event) {
2803 this.trigger('changed', { 'action' : 'select_all', 'selected' : this._data.core.selected, 'old_selection' : tmp });
2807 * deselect all selected nodes
2808 * @name deselect_all([supress_event])
2809 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
2810 * @trigger deselect_all.jstree, changed.jstree
2812 deselect_all : function (supress_event) {
2813 var tmp = this._data.core.selected.concat([]), i, j;
2814 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
2815 if(this._model.data[this._data.core.selected[i]]) {
2816 this._model.data[this._data.core.selected[i]].state.selected = false;
2819 this._data.core.selected = [];
2820 this.element.find('.jstree-clicked').removeClass('jstree-clicked').parent().attr('aria-selected', false);
2822 * triggered when all nodes are deselected
2824 * @name deselect_all.jstree
2825 * @param {Object} node the previous selection
2826 * @param {Array} selected the current selection
2828 this.trigger('deselect_all', { 'selected' : this._data.core.selected, 'node' : tmp });
2829 if(!supress_event) {
2830 this.trigger('changed', { 'action' : 'deselect_all', 'selected' : this._data.core.selected, 'old_selection' : tmp });
2834 * checks if a node is selected
2835 * @name is_selected(obj)
2836 * @param {mixed} obj
2839 is_selected : function (obj) {
2840 obj = this.get_node(obj);
2841 if(!obj || obj.id === '#') {
2844 return obj.state.selected;
2847 * get an array of all selected nodes
2848 * @name get_selected([full])
2849 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
2852 get_selected : function (full) {
2853 return full ? $.map(this._data.core.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.core.selected.slice();
2856 * get an array of all top level selected nodes (ignoring children of selected nodes)
2857 * @name get_top_selected([full])
2858 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
2861 get_top_selected : function (full) {
2862 var tmp = this.get_selected(true),
2863 obj = {}, i, j, k, l;
2864 for(i = 0, j = tmp.length; i < j; i++) {
2865 obj[tmp[i].id] = tmp[i];
2867 for(i = 0, j = tmp.length; i < j; i++) {
2868 for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
2869 if(obj[tmp[i].children_d[k]]) {
2870 delete obj[tmp[i].children_d[k]];
2876 if(obj.hasOwnProperty(i)) {
2880 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
2883 * get an array of all bottom level selected nodes (ignoring selected parents)
2884 * @name get_bottom_selected([full])
2885 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
2888 get_bottom_selected : function (full) {
2889 var tmp = this.get_selected(true),
2891 for(i = 0, j = tmp.length; i < j; i++) {
2892 if(!tmp[i].children.length) {
2893 obj.push(tmp[i].id);
2896 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
2899 * gets the current state of the tree so that it can be restored later with `set_state(state)`. Used internally.
2904 get_state : function () {
2909 'left' : this.element.scrollLeft(),
2910 'top' : this.element.scrollTop()
2914 'name' : this.get_theme(),
2915 'icons' : this._data.core.themes.icons,
2916 'dots' : this._data.core.themes.dots
2922 for(i in this._model.data) {
2923 if(this._model.data.hasOwnProperty(i)) {
2925 if(this._model.data[i].state.opened) {
2926 state.core.open.push(i);
2928 if(this._model.data[i].state.selected) {
2929 state.core.selected.push(i);
2937 * sets the state of the tree. Used internally.
2938 * @name set_state(state [, callback])
2940 * @param {Object} state the state to restore
2941 * @param {Function} callback an optional function to execute once the state is restored.
2942 * @trigger set_state.jstree
2944 set_state : function (state, callback) {
2947 var res, n, t, _this;
2948 if(state.core.open) {
2949 if(!$.isArray(state.core.open)) {
2950 delete state.core.open;
2951 this.set_state(state, callback);
2957 $.each(state.core.open.concat([]), function (i, v) {
2960 if(t.is_loaded(v)) {
2961 if(t.is_closed(v)) {
2962 t.open_node(v, false, 0);
2964 if(state && state.core && state.core.open) {
2965 $.vakata.array_remove_item(state.core.open, v);
2969 if(!t.is_loading(v)) {
2970 t.open_node(v, $.proxy(function (o, s) {
2971 if(!s && state && state.core && state.core.open) {
2972 $.vakata.array_remove_item(state.core.open, o.id);
2974 this.set_state(state, callback);
2977 // there will be some async activity - so wait for it
2983 delete state.core.open;
2984 this.set_state(state, callback);
2988 if(state.core.scroll) {
2989 if(state.core.scroll && state.core.scroll.left !== undefined) {
2990 this.element.scrollLeft(state.core.scroll.left);
2992 if(state.core.scroll && state.core.scroll.top !== undefined) {
2993 this.element.scrollTop(state.core.scroll.top);
2995 delete state.core.scroll;
2996 this.set_state(state, callback);
3000 if(state.core.themes) {
3001 if(state.core.themes.name) {
3002 this.set_theme(state.core.themes.name);
3004 if(typeof state.core.themes.dots !== 'undefined') {
3005 this[ state.core.themes.dots ? "show_dots" : "hide_dots" ]();
3007 if(typeof state.core.themes.icons !== 'undefined') {
3008 this[ state.core.themes.icons ? "show_icons" : "hide_icons" ]();
3010 delete state.core.themes;
3011 delete state.core.open;
3012 this.set_state(state, callback);
3016 if(state.core.selected) {
3018 this.deselect_all();
3019 $.each(state.core.selected, function (i, v) {
3020 _this.select_node(v);
3022 delete state.core.selected;
3023 this.set_state(state, callback);
3026 if($.isEmptyObject(state.core)) {
3028 this.set_state(state, callback);
3032 if($.isEmptyObject(state)) {
3034 if(callback) { callback.call(this); }
3036 * triggered when a `set_state` call completes
3038 * @name set_state.jstree
3040 this.trigger('set_state');
3048 * refreshes the tree - all nodes are reloaded with calls to `load_node`.
3050 * @param {Boolean} skip_loading an option to skip showing the loading indicator
3051 * @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
3052 * @trigger refresh.jstree
3054 refresh : function (skip_loading, forget_state) {
3055 this._data.core.state = forget_state === true ? {} : this.get_state();
3056 if(forget_state && $.isFunction(forget_state)) { this._data.core.state = forget_state.call(this, this._data.core.state); }
3058 this._model.data = {
3065 state : { loaded : false }
3068 var c = this.get_container_ul()[0].className;
3070 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>");
3071 this.element.attr('aria-activedescendant','j'+this._id+'_loading');
3073 this.load_node('#', function (o, s) {
3075 this.get_container_ul()[0].className = c;
3076 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
3077 this.set_state($.extend(true, {}, this._data.core.state), function () {
3079 * triggered when a `refresh` call completes
3081 * @name refresh.jstree
3083 this.trigger('refresh');
3086 this._data.core.state = null;
3090 * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`.
3091 * @name refresh_node(obj)
3092 * @param {mixed} obj the node
3093 * @trigger refresh_node.jstree
3095 refresh_node : function (obj) {
3096 obj = this.get_node(obj);
3097 if(!obj || obj.id === '#') { return false; }
3098 var opened = [], to_load = [], s = this._data.core.selected.concat([]);
3099 to_load.push(obj.id);
3100 if(obj.state.opened === true) { opened.push(obj.id); }
3101 this.get_node(obj, true).find('.jstree-open').each(function() { opened.push(this.id); });
3102 this._load_nodes(to_load, $.proxy(function (nodes) {
3103 this.open_node(opened, false, 0);
3104 this.select_node(this._data.core.selected);
3106 * triggered when a node is refreshed
3108 * @name refresh_node.jstree
3109 * @param {Object} node - the refreshed node
3110 * @param {Array} nodes - an array of the IDs of the nodes that were reloaded
3112 this.trigger('refresh_node', { 'node' : obj, 'nodes' : nodes });
3116 * set (change) the ID of a node
3117 * @name set_id(obj, id)
3118 * @param {mixed} obj the node
3119 * @param {String} id the new ID
3122 set_id : function (obj, id) {
3123 obj = this.get_node(obj);
3124 if(!obj || obj.id === '#') { return false; }
3125 var i, j, m = this._model.data;
3127 // update parents (replace current ID with new one in children and children_d)
3128 m[obj.parent].children[$.inArray(obj.id, m[obj.parent].children)] = id;
3129 for(i = 0, j = obj.parents.length; i < j; i++) {
3130 m[obj.parents[i]].children_d[$.inArray(obj.id, m[obj.parents[i]].children_d)] = id;
3132 // update children (replace current ID with new one in parent and parents)
3133 for(i = 0, j = obj.children.length; i < j; i++) {
3134 m[obj.children[i]].parent = id;
3136 for(i = 0, j = obj.children_d.length; i < j; i++) {
3137 m[obj.children_d[i]].parents[$.inArray(obj.id, m[obj.children_d[i]].parents)] = id;
3139 i = $.inArray(obj.id, this._data.core.selected);
3140 if(i !== -1) { this._data.core.selected[i] = id; }
3141 // update model and obj itself (obj.id, this._model.data[KEY])
3142 i = this.get_node(obj.id, true);
3152 * get the text value of a node
3153 * @name get_text(obj)
3154 * @param {mixed} obj the node
3157 get_text : function (obj) {
3158 obj = this.get_node(obj);
3159 return (!obj || obj.id === '#') ? false : obj.text;
3162 * set the text value of a node. Used internally, please use `rename_node(obj, val)`.
3164 * @name set_text(obj, val)
3165 * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes
3166 * @param {String} val the new text value
3168 * @trigger set_text.jstree
3170 set_text : function (obj, val) {
3172 if($.isArray(obj)) {
3174 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3175 this.set_text(obj[t1], val);
3179 obj = this.get_node(obj);
3180 if(!obj || obj.id === '#') { return false; }
3182 if(this.get_node(obj, true).length) {
3183 this.redraw_node(obj.id);
3186 * triggered when a node text value is changed
3188 * @name set_text.jstree
3189 * @param {Object} obj
3190 * @param {String} text the new value
3192 this.trigger('set_text',{ "obj" : obj, "text" : val });
3196 * gets a JSON representation of a node (or the whole tree)
3197 * @name get_json([obj, options])
3198 * @param {mixed} obj
3199 * @param {Object} options
3200 * @param {Boolean} options.no_state do not return state information
3201 * @param {Boolean} options.no_id do not return ID
3202 * @param {Boolean} options.no_children do not include children
3203 * @param {Boolean} options.no_data do not include node data
3204 * @param {Boolean} options.flat return flat JSON instead of nested
3207 get_json : function (obj, options, flat) {
3208 obj = this.get_node(obj || '#');
3209 if(!obj) { return false; }
3210 if(options && options.flat && !flat) { flat = []; }
3214 'icon' : this.get_icon(obj),
3215 'li_attr' : $.extend(true, {}, obj.li_attr),
3216 'a_attr' : $.extend(true, {}, obj.a_attr),
3218 'data' : options && options.no_data ? false : $.extend(true, {}, obj.data)
3219 //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ),
3221 if(options && options.flat) {
3222 tmp.parent = obj.parent;
3227 if(!options || !options.no_state) {
3228 for(i in obj.state) {
3229 if(obj.state.hasOwnProperty(i)) {
3230 tmp.state[i] = obj.state[i];
3234 if(options && options.no_id) {
3236 if(tmp.li_attr && tmp.li_attr.id) {
3237 delete tmp.li_attr.id;
3240 if(options && options.flat && obj.id !== '#') {
3243 if(!options || !options.no_children) {
3244 for(i = 0, j = obj.children.length; i < j; i++) {
3245 if(options && options.flat) {
3246 this.get_json(obj.children[i], options, flat);
3249 tmp.children.push(this.get_json(obj.children[i], options));
3253 return options && options.flat ? flat : (obj.id === '#' ? tmp.children : tmp);
3256 * create a new node (do not confuse with load_node)
3257 * @name create_node([obj, node, pos, callback, is_loaded])
3258 * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`)
3259 * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name)
3260 * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last"
3261 * @param {Function} callback a function to be called once the node is created
3262 * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded
3263 * @return {String} the ID of the newly create node
3264 * @trigger model.jstree, create_node.jstree
3266 create_node : function (par, node, pos, callback, is_loaded) {
3267 if(par === null) { par = "#"; }
3268 par = this.get_node(par);
3269 if(!par) { return false; }
3270 pos = pos === undefined ? "last" : pos;
3271 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
3272 return this.load_node(par, function () { this.create_node(par, node, pos, callback, true); });
3274 if(!node) { node = { "text" : this.get_string('New node') }; }
3275 if(node.text === undefined) { node.text = this.get_string('New node'); }
3278 if(par.id === '#') {
3279 if(pos === "before") { pos = "first"; }
3280 if(pos === "after") { pos = "last"; }
3284 tmp = this.get_node(par.parent);
3285 pos = $.inArray(par.id, tmp.children);
3289 tmp = this.get_node(par.parent);
3290 pos = $.inArray(par.id, tmp.children) + 1;
3298 pos = par.children.length;
3301 if(!pos) { pos = 0; }
3304 if(pos > par.children.length) { pos = par.children.length; }
3305 if(!node.id) { node.id = true; }
3306 if(!this.check("create_node", node, par, pos)) {
3307 this.settings.core.error.call(this, this._data.core.last_error);
3310 if(node.id === true) { delete node.id; }
3311 node = this._parse_model_from_json(node, par.id, par.parents.concat());
3312 if(!node) { return false; }
3313 tmp = this.get_node(node);
3316 dpc = dpc.concat(tmp.children_d);
3317 this.trigger('model', { "nodes" : dpc, "parent" : par.id });
3319 par.children_d = par.children_d.concat(dpc);
3320 for(i = 0, j = par.parents.length; i < j; i++) {
3321 this._model.data[par.parents[i]].children_d = this._model.data[par.parents[i]].children_d.concat(dpc);
3325 for(i = 0, j = par.children.length; i < j; i++) {
3326 tmp[i >= pos ? i+1 : i] = par.children[i];
3331 this.redraw_node(par, true);
3332 if(callback) { callback.call(this, this.get_node(node)); }
3334 * triggered when a node is created
3336 * @name create_node.jstree
3337 * @param {Object} node
3338 * @param {String} parent the parent's ID
3339 * @param {Number} position the position of the new node among the parent's children
3341 this.trigger('create_node', { "node" : this.get_node(node), "parent" : par.id, "position" : pos });
3345 * set the text value of a node
3346 * @name rename_node(obj, val)
3347 * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name
3348 * @param {String} val the new text value
3350 * @trigger rename_node.jstree
3352 rename_node : function (obj, val) {
3354 if($.isArray(obj)) {
3356 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3357 this.rename_node(obj[t1], val);
3361 obj = this.get_node(obj);
3362 if(!obj || obj.id === '#') { return false; }
3364 if(!this.check("rename_node", obj, this.get_parent(obj), val)) {
3365 this.settings.core.error.call(this, this._data.core.last_error);
3368 this.set_text(obj, val); // .apply(this, Array.prototype.slice.call(arguments))
3370 * triggered when a node is renamed
3372 * @name rename_node.jstree
3373 * @param {Object} node
3374 * @param {String} text the new value
3375 * @param {String} old the old value
3377 this.trigger('rename_node', { "node" : obj, "text" : val, "old" : old });
3382 * @name delete_node(obj)
3383 * @param {mixed} obj the node, you can pass an array to delete multiple nodes
3385 * @trigger delete_node.jstree, changed.jstree
3387 delete_node : function (obj) {
3388 var t1, t2, par, pos, tmp, i, j, k, l, c;
3389 if($.isArray(obj)) {
3391 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3392 this.delete_node(obj[t1]);
3396 obj = this.get_node(obj);
3397 if(!obj || obj.id === '#') { return false; }
3398 par = this.get_node(obj.parent);
3399 pos = $.inArray(obj.id, par.children);
3401 if(!this.check("delete_node", obj, par, pos)) {
3402 this.settings.core.error.call(this, this._data.core.last_error);
3406 par.children = $.vakata.array_remove(par.children, pos);
3408 tmp = obj.children_d.concat([]);
3410 for(k = 0, l = tmp.length; k < l; k++) {
3411 for(i = 0, j = obj.parents.length; i < j; i++) {
3412 pos = $.inArray(tmp[k], this._model.data[obj.parents[i]].children_d);
3414 this._model.data[obj.parents[i]].children_d = $.vakata.array_remove(this._model.data[obj.parents[i]].children_d, pos);
3417 if(this._model.data[tmp[k]].state.selected) {
3419 pos = $.inArray(tmp[k], this._data.core.selected);
3421 this._data.core.selected = $.vakata.array_remove(this._data.core.selected, pos);
3426 * triggered when a node is deleted
3428 * @name delete_node.jstree
3429 * @param {Object} node
3430 * @param {String} parent the parent's ID
3432 this.trigger('delete_node', { "node" : obj, "parent" : par.id });
3434 this.trigger('changed', { 'action' : 'delete_node', 'node' : obj, 'selected' : this._data.core.selected, 'parent' : par.id });
3436 for(k = 0, l = tmp.length; k < l; k++) {
3437 delete this._model.data[tmp[k]];
3439 this.redraw_node(par, true);
3443 * check if an operation is premitted on the tree. Used internally.
3445 * @name check(chk, obj, par, pos)
3446 * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node"
3447 * @param {mixed} obj the node
3448 * @param {mixed} par the parent
3449 * @param {mixed} pos the position to insert at, or if "rename_node" - the new name
3450 * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node
3453 check : function (chk, obj, par, pos, more) {
3454 obj = obj && obj.id ? obj : this.get_node(obj);
3455 par = par && par.id ? par : this.get_node(par);
3456 var tmp = chk.match(/^move_node|copy_node|create_node$/i) ? par : obj,
3457 chc = this.settings.core.check_callback;
3458 if(chk === "move_node" || chk === "copy_node") {
3459 if((!more || !more.is_multi) && (obj.id === par.id || $.inArray(obj.id, par.children) === pos || $.inArray(par.id, obj.children_d) !== -1)) {
3460 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 }) };
3464 if(tmp && tmp.data) { tmp = tmp.data; }
3465 if(tmp && tmp.functions && (tmp.functions[chk] === false || tmp.functions[chk] === true)) {
3466 if(tmp.functions[chk] === false) {
3467 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 }) };
3469 return tmp.functions[chk];
3471 if(chc === false || ($.isFunction(chc) && chc.call(this, chk, obj, par, pos, more) === false) || (chc && chc[chk] === false)) {
3472 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 }) };
3478 * get the last error
3479 * @name last_error()
3482 last_error : function () {
3483 return this._data.core.last_error;
3486 * move a node to a new parent
3487 * @name move_node(obj, par [, pos, callback, is_loaded])
3488 * @param {mixed} obj the node to move, pass an array to move multiple nodes
3489 * @param {mixed} par the new parent
3490 * @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`
3491 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
3492 * @param {Boolean} internal parameter indicating if the parent node has been loaded
3493 * @param {Boolean} internal parameter indicating if the tree should be redrawn
3494 * @trigger move_node.jstree
3496 move_node : function (obj, par, pos, callback, is_loaded, skip_redraw) {
3497 var t1, t2, old_par, old_pos, new_par, old_ins, is_multi, dpc, tmp, i, j, k, l, p;
3499 par = this.get_node(par);
3500 pos = pos === undefined ? 0 : pos;
3501 if(!par) { return false; }
3502 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
3503 return this.load_node(par, function () { this.move_node(obj, par, pos, callback, true); });
3506 if($.isArray(obj)) {
3508 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3509 if(this.move_node(obj[t1], par, pos, callback, is_loaded, true)) {
3517 obj = obj && obj.id ? obj : this.get_node(obj);
3519 if(!obj || obj.id === '#') { return false; }
3521 old_par = (obj.parent || '#').toString();
3522 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === '#') ? par : this.get_node(par.parent);
3523 old_ins = obj.instance ? obj.instance : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
3524 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
3525 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;
3527 if(this.copy_node(obj, par, pos, callback, is_loaded)) {
3528 if(old_ins) { old_ins.delete_node(obj); }
3533 //var m = this._model.data;
3534 if(par.id === '#') {
3535 if(pos === "before") { pos = "first"; }
3536 if(pos === "after") { pos = "last"; }
3540 pos = $.inArray(par.id, new_par.children);
3543 pos = $.inArray(par.id, new_par.children) + 1;
3550 pos = new_par.children.length;
3553 if(!pos) { pos = 0; }
3556 if(pos > new_par.children.length) { pos = new_par.children.length; }
3557 if(!this.check("move_node", obj, new_par, pos, { 'core' : true, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) {
3558 this.settings.core.error.call(this, this._data.core.last_error);
3561 if(obj.parent === new_par.id) {
3562 dpc = new_par.children.concat();
3563 tmp = $.inArray(obj.id, dpc);
3565 dpc = $.vakata.array_remove(dpc, tmp);
3566 if(pos > tmp) { pos--; }
3569 for(i = 0, j = dpc.length; i < j; i++) {
3570 tmp[i >= pos ? i+1 : i] = dpc[i];
3573 new_par.children = tmp;
3574 this._node_changed(new_par.id);
3575 this.redraw(new_par.id === '#');
3578 // clean old parent and up
3579 tmp = obj.children_d.concat();
3581 for(i = 0, j = obj.parents.length; i < j; i++) {
3583 p = old_ins._model.data[obj.parents[i]].children_d;
3584 for(k = 0, l = p.length; k < l; k++) {
3585 if($.inArray(p[k], tmp) === -1) {
3589 old_ins._model.data[obj.parents[i]].children_d = dpc;
3591 old_ins._model.data[old_par].children = $.vakata.array_remove_item(old_ins._model.data[old_par].children, obj.id);
3593 // insert into new parent and up
3594 for(i = 0, j = new_par.parents.length; i < j; i++) {
3595 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(tmp);
3598 for(i = 0, j = new_par.children.length; i < j; i++) {
3599 dpc[i >= pos ? i+1 : i] = new_par.children[i];
3602 new_par.children = dpc;
3603 new_par.children_d.push(obj.id);
3604 new_par.children_d = new_par.children_d.concat(obj.children_d);
3607 obj.parent = new_par.id;
3608 tmp = new_par.parents.concat();
3609 tmp.unshift(new_par.id);
3610 p = obj.parents.length;
3613 // update object children
3615 for(i = 0, j = obj.children_d.length; i < j; i++) {
3616 this._model.data[obj.children_d[i]].parents = this._model.data[obj.children_d[i]].parents.slice(0,p*-1);
3617 Array.prototype.push.apply(this._model.data[obj.children_d[i]].parents, tmp);
3620 if(old_par === '#' || new_par.id === '#') {
3621 this._model.force_full_redraw = true;
3623 if(!this._model.force_full_redraw) {
3624 this._node_changed(old_par);
3625 this._node_changed(new_par.id);
3631 if(callback) { callback.call(this, obj, new_par, pos); }
3633 * triggered when a node is moved
3635 * @name move_node.jstree
3636 * @param {Object} node
3637 * @param {String} parent the parent's ID
3638 * @param {Number} position the position of the node among the parent's children
3639 * @param {String} old_parent the old parent of the node
3640 * @param {Number} old_position the old position of the node
3641 * @param {Boolean} is_multi do the node and new parent belong to different instances
3642 * @param {jsTree} old_instance the instance the node came from
3643 * @param {jsTree} new_instance the instance of the new parent
3645 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 });
3649 * copy a node to a new parent
3650 * @name copy_node(obj, par [, pos, callback, is_loaded])
3651 * @param {mixed} obj the node to copy, pass an array to copy multiple nodes
3652 * @param {mixed} par the new parent
3653 * @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`
3654 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
3655 * @param {Boolean} internal parameter indicating if the parent node has been loaded
3656 * @param {Boolean} internal parameter indicating if the tree should be redrawn
3657 * @trigger model.jstree copy_node.jstree
3659 copy_node : function (obj, par, pos, callback, is_loaded, skip_redraw) {
3660 var t1, t2, dpc, tmp, i, j, node, old_par, new_par, old_ins, is_multi;
3662 par = this.get_node(par);
3663 pos = pos === undefined ? 0 : pos;
3664 if(!par) { return false; }
3665 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
3666 return this.load_node(par, function () { this.copy_node(obj, par, pos, callback, true); });
3669 if($.isArray(obj)) {
3671 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3672 tmp = this.copy_node(obj[t1], par, pos, callback, is_loaded, true);
3681 obj = obj && obj.id ? obj : this.get_node(obj);
3682 if(!obj || obj.id === '#') { return false; }
3684 old_par = (obj.parent || '#').toString();
3685 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === '#') ? par : this.get_node(par.parent);
3686 old_ins = obj.instance ? obj.instance : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
3687 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
3688 if(par.id === '#') {
3689 if(pos === "before") { pos = "first"; }
3690 if(pos === "after") { pos = "last"; }
3694 pos = $.inArray(par.id, new_par.children);
3697 pos = $.inArray(par.id, new_par.children) + 1;
3704 pos = new_par.children.length;
3707 if(!pos) { pos = 0; }
3710 if(pos > new_par.children.length) { pos = new_par.children.length; }
3711 if(!this.check("copy_node", obj, new_par, pos, { 'core' : true, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) {
3712 this.settings.core.error.call(this, this._data.core.last_error);
3715 node = old_ins ? old_ins.get_json(obj, { no_id : true, no_data : true, no_state : true }) : obj;
3716 if(!node) { return false; }
3717 if(node.id === true) { delete node.id; }
3718 node = this._parse_model_from_json(node, new_par.id, new_par.parents.concat());
3719 if(!node) { return false; }
3720 tmp = this.get_node(node);
3721 if(obj && obj.state && obj.state.loaded === false) { tmp.state.loaded = false; }
3724 dpc = dpc.concat(tmp.children_d);
3725 this.trigger('model', { "nodes" : dpc, "parent" : new_par.id });
3727 // insert into new parent and up
3728 for(i = 0, j = new_par.parents.length; i < j; i++) {
3729 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(dpc);
3732 for(i = 0, j = new_par.children.length; i < j; i++) {
3733 dpc[i >= pos ? i+1 : i] = new_par.children[i];
3736 new_par.children = dpc;
3737 new_par.children_d.push(tmp.id);
3738 new_par.children_d = new_par.children_d.concat(tmp.children_d);
3740 if(new_par.id === '#') {
3741 this._model.force_full_redraw = true;
3743 if(!this._model.force_full_redraw) {
3744 this._node_changed(new_par.id);
3747 this.redraw(new_par.id === '#');
3749 if(callback) { callback.call(this, tmp, new_par, pos); }
3751 * triggered when a node is copied
3753 * @name copy_node.jstree
3754 * @param {Object} node the copied node
3755 * @param {Object} original the original node
3756 * @param {String} parent the parent's ID
3757 * @param {Number} position the position of the node among the parent's children
3758 * @param {String} old_parent the old parent of the node
3759 * @param {Number} old_position the position of the original node
3760 * @param {Boolean} is_multi do the node and new parent belong to different instances
3761 * @param {jsTree} old_instance the instance the node came from
3762 * @param {jsTree} new_instance the instance of the new parent
3764 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 });
3768 * cut a node (a later call to `paste(obj)` would move the node)
3770 * @param {mixed} obj multiple objects can be passed using an array
3771 * @trigger cut.jstree
3773 cut : function (obj) {
3774 if(!obj) { obj = this._data.core.selected.concat(); }
3775 if(!$.isArray(obj)) { obj = [obj]; }
3776 if(!obj.length) { return false; }
3777 var tmp = [], o, t1, t2;
3778 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3779 o = this.get_node(obj[t1]);
3780 if(o && o.id && o.id !== '#') { tmp.push(o); }
3782 if(!tmp.length) { return false; }
3785 ccp_mode = 'move_node';
3787 * triggered when nodes are added to the buffer for moving
3790 * @param {Array} node
3792 this.trigger('cut', { "node" : obj });
3795 * copy a node (a later call to `paste(obj)` would copy the node)
3797 * @param {mixed} obj multiple objects can be passed using an array
3798 * @trigger copy.jstre
3800 copy : function (obj) {
3801 if(!obj) { obj = this._data.core.selected.concat(); }
3802 if(!$.isArray(obj)) { obj = [obj]; }
3803 if(!obj.length) { return false; }
3804 var tmp = [], o, t1, t2;
3805 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3806 o = this.get_node(obj[t1]);
3807 if(o && o.id && o.id !== '#') { tmp.push(o); }
3809 if(!tmp.length) { return false; }
3812 ccp_mode = 'copy_node';
3814 * triggered when nodes are added to the buffer for copying
3817 * @param {Array} node
3819 this.trigger('copy', { "node" : obj });
3822 * get the current buffer (any nodes that are waiting for a paste operation)
3823 * @name get_buffer()
3824 * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance)
3826 get_buffer : function () {
3827 return { 'mode' : ccp_mode, 'node' : ccp_node, 'inst' : ccp_inst };
3830 * check if there is something in the buffer to paste
3834 can_paste : function () {
3835 return ccp_mode !== false && ccp_node !== false; // && ccp_inst._model.data[ccp_node];
3838 * copy or move the previously cut or copied nodes to a new parent
3839 * @name paste(obj [, pos])
3840 * @param {mixed} obj the new parent
3841 * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0`
3842 * @trigger paste.jstree
3844 paste : function (obj, pos) {
3845 obj = this.get_node(obj);
3846 if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; }
3847 if(this[ccp_mode](ccp_node, obj, pos)) {
3849 * triggered when paste is invoked
3851 * @name paste.jstree
3852 * @param {String} parent the ID of the receiving node
3853 * @param {Array} node the nodes in the buffer
3854 * @param {String} mode the performed operation - "copy_node" or "move_node"
3856 this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode });
3863 * clear the buffer of previously copied or cut nodes
3864 * @name clear_buffer()
3865 * @trigger clear_buffer.jstree
3867 clear_buffer : function () {
3872 * triggered when the copy / cut buffer is cleared
3874 * @name clear_buffer.jstree
3876 this.trigger('clear_buffer');
3879 * put a node in edit mode (input field to rename the node)
3880 * @name edit(obj [, default_text])
3881 * @param {mixed} obj
3882 * @param {String} default_text the text to populate the input with (if omitted the node text value is used)
3884 edit : function (obj, default_text) {
3885 obj = this.get_node(obj);
3886 if(!obj) { return false; }
3887 if(this.settings.core.check_callback === false) {
3888 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_07', 'reason' : 'Could not edit node because of check_callback' };
3889 this.settings.core.error.call(this, this._data.core.last_error);
3892 default_text = typeof default_text === 'string' ? default_text : obj.text;
3893 this.set_text(obj, "");
3894 obj = this._open_to(obj);
3896 var rtl = this._data.core.rtl,
3897 w = this.element.width(),
3898 a = obj.children('.jstree-anchor'),
3901 oi = obj.children("i:visible"),
3902 ai = a.children("i:visible"),
3903 w1 = oi.width() * oi.length,
3904 w2 = ai.width() * ai.length,
3907 h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"),
3908 h2 = $("<"+"input />", {
3910 "class" : "jstree-rename-input",
3911 // "size" : t.length,
3914 "border" : "1px solid silver",
3915 "box-sizing" : "border-box",
3916 "display" : "inline-block",
3917 "height" : (this._data.core.li_height) + "px",
3918 "lineHeight" : (this._data.core.li_height) + "px",
3919 "width" : "150px" // will be set a bit further down
3921 "blur" : $.proxy(function () {
3922 var i = s.children(".jstree-rename-input"),
3924 if(v === "") { v = t; }
3928 this.set_text(obj, t);
3929 if(this.rename_node(obj, $('<div></div>').text(v)[this.settings.core.force_text ? 'text' : 'html']()) === false) {
3930 this.set_text(obj, t); // move this up? and fix #483
3933 "keydown" : function (event) {
3934 var key = event.which;
3938 if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) {
3939 event.stopImmediatePropagation();
3941 if(key === 27 || key === 13) {
3942 event.preventDefault();
3946 "click" : function (e) { e.stopImmediatePropagation(); },
3947 "mousedown" : function (e) { e.stopImmediatePropagation(); },
3948 "keyup" : function (event) {
3949 h2.width(Math.min(h1.text("pW" + this.value).width(),w));
3951 "keypress" : function(event) {
3952 if(event.which === 13) { return false; }
3956 fontFamily : a.css('fontFamily') || '',
3957 fontSize : a.css('fontSize') || '',
3958 fontWeight : a.css('fontWeight') || '',
3959 fontStyle : a.css('fontStyle') || '',
3960 fontStretch : a.css('fontStretch') || '',
3961 fontVariant : a.css('fontVariant') || '',
3962 letterSpacing : a.css('letterSpacing') || '',
3963 wordSpacing : a.css('wordSpacing') || ''
3965 s.attr('class', a.attr('class')).append(a.contents().clone()).append(h2);
3968 h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select();
3974 * @name set_theme(theme_name [, theme_url])
3975 * @param {String} theme_name the name of the new theme to apply
3976 * @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.
3977 * @trigger set_theme.jstree
3979 set_theme : function (theme_name, theme_url) {
3980 if(!theme_name) { return false; }
3981 if(theme_url === true) {
3982 var dir = this.settings.core.themes.dir;
3983 if(!dir) { dir = $.jstree.path + '/themes'; }
3984 theme_url = dir + '/' + theme_name + '/style.css';
3986 if(theme_url && $.inArray(theme_url, themes_loaded) === -1) {
3987 $('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />');
3988 themes_loaded.push(theme_url);
3990 if(this._data.core.themes.name) {
3991 this.element.removeClass('jstree-' + this._data.core.themes.name);
3993 this._data.core.themes.name = theme_name;
3994 this.element.addClass('jstree-' + theme_name);
3995 this.element[this.settings.core.themes.responsive ? 'addClass' : 'removeClass' ]('jstree-' + theme_name + '-responsive');
3997 * triggered when a theme is set
3999 * @name set_theme.jstree
4000 * @param {String} theme the new theme
4002 this.trigger('set_theme', { 'theme' : theme_name });
4005 * gets the name of the currently applied theme name
4009 get_theme : function () { return this._data.core.themes.name; },
4011 * changes the theme variant (if the theme has variants)
4012 * @name set_theme_variant(variant_name)
4013 * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed)
4015 set_theme_variant : function (variant_name) {
4016 if(this._data.core.themes.variant) {
4017 this.element.removeClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
4019 this._data.core.themes.variant = variant_name;
4021 this.element.addClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
4025 * gets the name of the currently applied theme variant
4029 get_theme_variant : function () { return this._data.core.themes.variant; },
4031 * shows a striped background on the container (if the theme supports it)
4032 * @name show_stripes()
4034 show_stripes : function () { this._data.core.themes.stripes = true; this.get_container_ul().addClass("jstree-striped"); },
4036 * hides the striped background on the container
4037 * @name hide_stripes()
4039 hide_stripes : function () { this._data.core.themes.stripes = false; this.get_container_ul().removeClass("jstree-striped"); },
4041 * toggles the striped background on the container
4042 * @name toggle_stripes()
4044 toggle_stripes : function () { if(this._data.core.themes.stripes) { this.hide_stripes(); } else { this.show_stripes(); } },
4046 * shows the connecting dots (if the theme supports it)
4049 show_dots : function () { this._data.core.themes.dots = true; this.get_container_ul().removeClass("jstree-no-dots"); },
4051 * hides the connecting dots
4054 hide_dots : function () { this._data.core.themes.dots = false; this.get_container_ul().addClass("jstree-no-dots"); },
4056 * toggles the connecting dots
4057 * @name toggle_dots()
4059 toggle_dots : function () { if(this._data.core.themes.dots) { this.hide_dots(); } else { this.show_dots(); } },
4061 * show the node icons
4062 * @name show_icons()
4064 show_icons : function () { this._data.core.themes.icons = true; this.get_container_ul().removeClass("jstree-no-icons"); },
4066 * hide the node icons
4067 * @name hide_icons()
4069 hide_icons : function () { this._data.core.themes.icons = false; this.get_container_ul().addClass("jstree-no-icons"); },
4071 * toggle the node icons
4072 * @name toggle_icons()
4074 toggle_icons : function () { if(this._data.core.themes.icons) { this.hide_icons(); } else { this.show_icons(); } },
4076 * set the node icon for a node
4077 * @name set_icon(obj, icon)
4078 * @param {mixed} obj
4079 * @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
4081 set_icon : function (obj, icon) {
4082 var t1, t2, dom, old;
4083 if($.isArray(obj)) {
4085 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4086 this.set_icon(obj[t1], icon);
4090 obj = this.get_node(obj);
4091 if(!obj || obj.id === '#') { return false; }
4094 dom = this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon");
4095 if(icon === false) {
4096 this.hide_icon(obj);
4098 else if(icon === true) {
4099 dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel");
4101 else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) {
4102 dom.removeClass(old).css("background","");
4103 dom.addClass(icon + ' jstree-themeicon-custom').attr("rel",icon);
4106 dom.removeClass(old).css("background","");
4107 dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon);
4112 * get the node icon for a node
4113 * @name get_icon(obj)
4114 * @param {mixed} obj
4117 get_icon : function (obj) {
4118 obj = this.get_node(obj);
4119 return (!obj || obj.id === '#') ? false : obj.icon;
4122 * hide the icon on an individual node
4123 * @name hide_icon(obj)
4124 * @param {mixed} obj
4126 hide_icon : function (obj) {
4128 if($.isArray(obj)) {
4130 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4131 this.hide_icon(obj[t1]);
4135 obj = this.get_node(obj);
4136 if(!obj || obj === '#') { return false; }
4138 this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden');
4142 * show the icon on an individual node
4143 * @name show_icon(obj)
4144 * @param {mixed} obj
4146 show_icon : function (obj) {
4148 if($.isArray(obj)) {
4150 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4151 this.show_icon(obj[t1]);
4155 obj = this.get_node(obj);
4156 if(!obj || obj === '#') { return false; }
4157 dom = this.get_node(obj, true);
4158 obj.icon = dom.length ? dom.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true;
4159 if(!obj.icon) { obj.icon = true; }
4160 dom.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden');
4167 // collect attributes
4168 $.vakata.attributes = function(node, with_values) {
4170 var attr = with_values ? {} : [];
4171 if(node && node.attributes) {
4172 $.each(node.attributes, function (i, v) {
4173 if($.inArray(v.name.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; }
4174 if(v.value !== null && $.trim(v.value) !== '') {
4175 if(with_values) { attr[v.name] = v.value; }
4176 else { attr.push(v.name); }
4182 $.vakata.array_unique = function(array) {
4183 var a = [], i, j, l;
4184 for(i = 0, l = array.length; i < l; i++) {
4185 for(j = 0; j <= i; j++) {
4186 if(array[i] === array[j]) {
4190 if(j === i) { a.push(array[i]); }
4194 // remove item from array
4195 $.vakata.array_remove = function(array, from, to) {
4196 var rest = array.slice((to || from) + 1 || array.length);
4197 array.length = from < 0 ? array.length + from : from;
4198 array.push.apply(array, rest);
4201 // remove item from array
4202 $.vakata.array_remove_item = function(array, item) {
4203 var tmp = $.inArray(item, array);
4204 return tmp !== -1 ? $.vakata.array_remove(array, tmp) : array;
4208 * ### Checkbox plugin
4210 * This plugin renders checkbox icons in front of each node, making multiple selection much easier.
4211 * 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.
4214 var _i = document.createElement('I');
4215 _i.className = 'jstree-icon jstree-checkbox';
4216 _i.setAttribute('role', 'presentation');
4218 * stores all defaults for the checkbox plugin
4219 * @name $.jstree.defaults.checkbox
4222 $.jstree.defaults.checkbox = {
4224 * a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`.
4225 * @name $.jstree.defaults.checkbox.visible
4230 * a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`.
4231 * @name $.jstree.defaults.checkbox.three_state
4236 * a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`.
4237 * @name $.jstree.defaults.checkbox.whole_node
4242 * a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`.
4243 * @name $.jstree.defaults.checkbox.keep_selected_style
4246 keep_selected_style : true,
4248 * This setting controls how cascading and undetermined nodes are applied.
4249 * 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.
4250 * If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''.
4251 * @name $.jstree.defaults.checkbox.cascade
4256 * 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.
4257 * @name $.jstree.defaults.checkbox.tie_selection
4260 tie_selection : true
4262 $.jstree.plugins.checkbox = function (options, parent) {
4263 this.bind = function () {
4264 parent.bind.call(this);
4265 this._data.checkbox.uto = false;
4266 this._data.checkbox.selected = [];
4267 if(this.settings.checkbox.three_state) {
4268 this.settings.checkbox.cascade = 'up+down+undetermined';
4271 .on("init.jstree", $.proxy(function () {
4272 this._data.checkbox.visible = this.settings.checkbox.visible;
4273 if(!this.settings.checkbox.keep_selected_style) {
4274 this.element.addClass('jstree-checkbox-no-clicked');
4276 if(this.settings.checkbox.tie_selection) {
4277 this.element.addClass('jstree-checkbox-selection');
4280 .on("loading.jstree", $.proxy(function () {
4281 this[ this._data.checkbox.visible ? 'show_checkboxes' : 'hide_checkboxes' ]();
4283 if(this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
4285 .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 () {
4286 // only if undetermined is in setting
4287 if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
4288 this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
4291 if(!this.settings.checkbox.tie_selection) {
4293 .on('model.jstree', $.proxy(function (e, data) {
4294 var m = this._model.data,
4298 for(i = 0, j = dpc.length; i < j; i++) {
4299 m[dpc[i]].state.checked = (m[dpc[i]].original && m[dpc[i]].original.state && m[dpc[i]].original.state.checked);
4300 if(m[dpc[i]].state.checked) {
4301 this._data.checkbox.selected.push(dpc[i]);
4306 if(this.settings.checkbox.cascade.indexOf('up') !== -1 || this.settings.checkbox.cascade.indexOf('down') !== -1) {
4308 .on('model.jstree', $.proxy(function (e, data) {
4309 var m = this._model.data,
4313 c, i, j, k, l, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
4315 if(s.indexOf('down') !== -1) {
4317 if(p.state[ t ? 'selected' : 'checked' ]) {
4318 for(i = 0, j = dpc.length; i < j; i++) {
4319 m[dpc[i]].state[ t ? 'selected' : 'checked' ] = true;
4321 this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(dpc);
4324 for(i = 0, j = dpc.length; i < j; i++) {
4325 if(m[dpc[i]].state[ t ? 'selected' : 'checked' ]) {
4326 for(k = 0, l = m[dpc[i]].children_d.length; k < l; k++) {
4327 m[m[dpc[i]].children_d[k]].state[ t ? 'selected' : 'checked' ] = true;
4329 this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(m[dpc[i]].children_d);
4335 if(s.indexOf('up') !== -1) {
4337 for(i = 0, j = p.children_d.length; i < j; i++) {
4338 if(!m[p.children_d[i]].children.length) {
4339 chd.push(m[p.children_d[i]].parent);
4342 chd = $.vakata.array_unique(chd);
4343 for(k = 0, l = chd.length; k < l; k++) {
4345 while(p && p.id !== '#') {
4347 for(i = 0, j = p.children.length; i < j; i++) {
4348 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4351 p.state[ t ? 'selected' : 'checked' ] = true;
4352 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4353 tmp = this.get_node(p, true);
4354 if(tmp && tmp.length) {
4355 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass( t ? 'jstree-clicked' : 'jstree-checked');
4361 p = this.get_node(p.parent);
4366 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected);
4368 .on(this.settings.checkbox.tie_selection ? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e, data) {
4369 var obj = data.node,
4370 m = this._model.data,
4371 par = this.get_node(obj.parent),
4372 dom = this.get_node(obj, true),
4373 i, j, c, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
4376 if(s.indexOf('down') !== -1) {
4377 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d));
4378 for(i = 0, j = obj.children_d.length; i < j; i++) {
4379 tmp = m[obj.children_d[i]];
4380 tmp.state[ t ? 'selected' : 'checked' ] = true;
4381 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4382 tmp.original.state.undetermined = false;
4388 if(s.indexOf('up') !== -1) {
4389 while(par && par.id !== '#') {
4391 for(i = 0, j = par.children.length; i < j; i++) {
4392 c += m[par.children[i]].state[ t ? 'selected' : 'checked' ];
4395 par.state[ t ? 'selected' : 'checked' ] = true;
4396 this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id);
4397 tmp = this.get_node(par, true);
4398 if(tmp && tmp.length) {
4399 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4405 par = this.get_node(par.parent);
4409 // apply down (process .children separately?)
4410 if(s.indexOf('down') !== -1 && dom.length) {
4411 dom.find('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', true);
4414 .on(this.settings.checkbox.tie_selection ? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e, data) {
4415 var obj = this.get_node('#'),
4416 m = this._model.data,
4418 for(i = 0, j = obj.children_d.length; i < j; i++) {
4419 tmp = m[obj.children_d[i]];
4420 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4421 tmp.original.state.undetermined = false;
4425 .on(this.settings.checkbox.tie_selection ? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e, data) {
4426 var obj = data.node,
4427 dom = this.get_node(obj, true),
4428 i, j, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
4429 if(obj && obj.original && obj.original.state && obj.original.state.undetermined) {
4430 obj.original.state.undetermined = false;
4434 if(s.indexOf('down') !== -1) {
4435 for(i = 0, j = obj.children_d.length; i < j; i++) {
4436 tmp = this._model.data[obj.children_d[i]];
4437 tmp.state[ t ? 'selected' : 'checked' ] = false;
4438 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4439 tmp.original.state.undetermined = false;
4445 if(s.indexOf('up') !== -1) {
4446 for(i = 0, j = obj.parents.length; i < j; i++) {
4447 tmp = this._model.data[obj.parents[i]];
4448 tmp.state[ t ? 'selected' : 'checked' ] = false;
4449 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
4450 tmp.original.state.undetermined = false;
4452 tmp = this.get_node(obj.parents[i], true);
4453 if(tmp && tmp.length) {
4454 tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
4459 for(i = 0, j = this._data[ t ? 'core' : 'checkbox' ].selected.length; i < j; i++) {
4460 // apply down + apply up
4462 (s.indexOf('down') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.children_d) === -1) &&
4463 (s.indexOf('up') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.parents) === -1)
4465 tmp.push(this._data[ t ? 'core' : 'checkbox' ].selected[i]);
4468 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(tmp);
4470 // apply down (process .children separately?)
4471 if(s.indexOf('down') !== -1 && dom.length) {
4472 dom.find('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', false);
4476 if(this.settings.checkbox.cascade.indexOf('up') !== -1) {
4478 .on('delete_node.jstree', $.proxy(function (e, data) {
4479 // apply up (whole handler)
4480 var p = this.get_node(data.parent),
4481 m = this._model.data,
4482 i, j, c, tmp, t = this.settings.checkbox.tie_selection;
4483 while(p && p.id !== '#') {
4485 for(i = 0, j = p.children.length; i < j; i++) {
4486 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4489 p.state[ t ? 'selected' : 'checked' ] = true;
4490 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4491 tmp = this.get_node(p, true);
4492 if(tmp && tmp.length) {
4493 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4499 p = this.get_node(p.parent);
4502 .on('move_node.jstree', $.proxy(function (e, data) {
4503 // apply up (whole handler)
4504 var is_multi = data.is_multi,
4505 old_par = data.old_parent,
4506 new_par = this.get_node(data.parent),
4507 m = this._model.data,
4508 p, c, i, j, tmp, t = this.settings.checkbox.tie_selection;
4510 p = this.get_node(old_par);
4511 while(p && p.id !== '#') {
4513 for(i = 0, j = p.children.length; i < j; i++) {
4514 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4517 p.state[ t ? 'selected' : 'checked' ] = true;
4518 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4519 tmp = this.get_node(p, true);
4520 if(tmp && tmp.length) {
4521 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4527 p = this.get_node(p.parent);
4531 while(p && p.id !== '#') {
4533 for(i = 0, j = p.children.length; i < j; i++) {
4534 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
4537 if(!p.state[ t ? 'selected' : 'checked' ]) {
4538 p.state[ t ? 'selected' : 'checked' ] = true;
4539 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
4540 tmp = this.get_node(p, true);
4541 if(tmp && tmp.length) {
4542 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
4547 if(p.state[ t ? 'selected' : 'checked' ]) {
4548 p.state[ t ? 'selected' : 'checked' ] = false;
4549 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_remove_item(this._data[ t ? 'core' : 'checkbox' ].selected, p.id);
4550 tmp = this.get_node(p, true);
4551 if(tmp && tmp.length) {
4552 tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
4559 p = this.get_node(p.parent);
4565 * set the undetermined state where and if necessary. Used internally.
4567 * @name _undetermined()
4570 this._undetermined = function () {
4571 var i, j, m = this._model.data, t = this.settings.checkbox.tie_selection, s = this._data[ t ? 'core' : 'checkbox' ].selected, p = [], tt = this;
4572 for(i = 0, j = s.length; i < j; i++) {
4573 if(m[s[i]] && m[s[i]].parents) {
4574 p = p.concat(m[s[i]].parents);
4577 // attempt for server side undetermined state
4578 this.element.find('.jstree-closed').not(':has(.jstree-children)')
4580 var tmp = tt.get_node(this), tmp2;
4581 if(!tmp.state.loaded) {
4582 if(tmp.original && tmp.original.state && tmp.original.state.undetermined && tmp.original.state.undetermined === true) {
4584 p = p.concat(tmp.parents);
4588 for(i = 0, j = tmp.children_d.length; i < j; i++) {
4589 tmp2 = m[tmp.children_d[i]];
4590 if(!tmp2.state.loaded && tmp2.original && tmp2.original.state && tmp2.original.state.undetermined && tmp2.original.state.undetermined === true) {
4592 p = p.concat(tmp2.parents);
4597 p = $.vakata.array_unique(p);
4598 p = $.vakata.array_remove_item(p,'#');
4600 this.element.find('.jstree-undetermined').removeClass('jstree-undetermined');
4601 for(i = 0, j = p.length; i < j; i++) {
4602 if(!m[p[i]].state[ t ? 'selected' : 'checked' ]) {
4603 s = this.get_node(p[i], true);
4605 s.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined');
4610 this.redraw_node = function(obj, deep, is_callback, force_render) {
4611 obj = parent.redraw_node.apply(this, arguments);
4613 var i, j, tmp = null;
4614 for(i = 0, j = obj.childNodes.length; i < j; i++) {
4615 if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
4616 tmp = obj.childNodes[i];
4621 if(!this.settings.checkbox.tie_selection && this._model.data[obj.id].state.checked) { tmp.className += ' jstree-checked'; }
4622 tmp.insertBefore(_i.cloneNode(false), tmp.childNodes[0]);
4625 if(!is_callback && this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
4626 if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
4627 this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
4632 * show the node checkbox icons
4633 * @name show_checkboxes()
4636 this.show_checkboxes = function () { this._data.core.themes.checkboxes = true; this.get_container_ul().removeClass("jstree-no-checkboxes"); };
4638 * hide the node checkbox icons
4639 * @name hide_checkboxes()
4642 this.hide_checkboxes = function () { this._data.core.themes.checkboxes = false; this.get_container_ul().addClass("jstree-no-checkboxes"); };
4644 * toggle the node icons
4645 * @name toggle_checkboxes()
4648 this.toggle_checkboxes = function () { if(this._data.core.themes.checkboxes) { this.hide_checkboxes(); } else { this.show_checkboxes(); } };
4650 * checks if a node is in an undetermined state
4651 * @name is_undetermined(obj)
4652 * @param {mixed} obj
4655 this.is_undetermined = function (obj) {
4656 obj = this.get_node(obj);
4657 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;
4658 if(!obj || obj.state[ t ? 'selected' : 'checked' ] === true || s.indexOf('undetermined') === -1 || (s.indexOf('down') === -1 && s.indexOf('up') === -1)) {
4661 if(!obj.state.loaded && obj.original.state.undetermined === true) {
4664 for(i = 0, j = obj.children_d.length; i < j; i++) {
4665 if($.inArray(obj.children_d[i], d) !== -1 || (!m[obj.children_d[i]].state.loaded && m[obj.children_d[i]].original.state.undetermined)) {
4672 this.activate_node = function (obj, e) {
4673 if(this.settings.checkbox.tie_selection && (this.settings.checkbox.whole_node || $(e.target).hasClass('jstree-checkbox'))) {
4676 if(this.settings.checkbox.tie_selection || (!this.settings.checkbox.whole_node && !$(e.target).hasClass('jstree-checkbox'))) {
4677 return parent.activate_node.call(this, obj, e);
4679 if(this.is_checked(obj)) {
4680 this.uncheck_node(obj, e);
4683 this.check_node(obj, e);
4685 this.trigger('activate_node', { 'node' : this.get_node(obj) });
4689 * check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally)
4690 * @name check_node(obj)
4691 * @param {mixed} obj an array can be used to check multiple nodes
4692 * @trigger check_node.jstree
4695 this.check_node = function (obj, e) {
4696 if(this.settings.checkbox.tie_selection) { return this.select_node(obj, false, true, e); }
4697 var dom, t1, t2, th;
4698 if($.isArray(obj)) {
4700 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4701 this.check_node(obj[t1], e);
4705 obj = this.get_node(obj);
4706 if(!obj || obj.id === '#') {
4709 dom = this.get_node(obj, true);
4710 if(!obj.state.checked) {
4711 obj.state.checked = true;
4712 this._data.checkbox.selected.push(obj.id);
4713 if(dom && dom.length) {
4714 dom.children('.jstree-anchor').addClass('jstree-checked');
4717 * triggered when an node is checked (only if tie_selection in checkbox settings is false)
4719 * @name check_node.jstree
4720 * @param {Object} node
4721 * @param {Array} selected the current selection
4722 * @param {Object} event the event (if any) that triggered this check_node
4725 this.trigger('check_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
4729 * uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally)
4730 * @name deselect_node(obj)
4731 * @param {mixed} obj an array can be used to deselect multiple nodes
4732 * @trigger uncheck_node.jstree
4735 this.uncheck_node = function (obj, e) {
4736 if(this.settings.checkbox.tie_selection) { return this.deselect_node(obj, false, e); }
4738 if($.isArray(obj)) {
4740 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4741 this.uncheck_node(obj[t1], e);
4745 obj = this.get_node(obj);
4746 if(!obj || obj.id === '#') {
4749 dom = this.get_node(obj, true);
4750 if(obj.state.checked) {
4751 obj.state.checked = false;
4752 this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, obj.id);
4754 dom.children('.jstree-anchor').removeClass('jstree-checked');
4757 * triggered when an node is unchecked (only if tie_selection in checkbox settings is false)
4759 * @name uncheck_node.jstree
4760 * @param {Object} node
4761 * @param {Array} selected the current selection
4762 * @param {Object} event the event (if any) that triggered this uncheck_node
4765 this.trigger('uncheck_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
4769 * checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally)
4771 * @trigger check_all.jstree, changed.jstree
4774 this.check_all = function () {
4775 if(this.settings.checkbox.tie_selection) { return this.select_all(); }
4776 var tmp = this._data.checkbox.selected.concat([]), i, j;
4777 this._data.checkbox.selected = this._model.data['#'].children_d.concat();
4778 for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
4779 if(this._model.data[this._data.checkbox.selected[i]]) {
4780 this._model.data[this._data.checkbox.selected[i]].state.checked = true;
4785 * triggered when all nodes are checked (only if tie_selection in checkbox settings is false)
4787 * @name check_all.jstree
4788 * @param {Array} selected the current selection
4791 this.trigger('check_all', { 'selected' : this._data.checkbox.selected });
4794 * uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally)
4795 * @name uncheck_all()
4796 * @trigger uncheck_all.jstree
4799 this.uncheck_all = function () {
4800 if(this.settings.checkbox.tie_selection) { return this.deselect_all(); }
4801 var tmp = this._data.checkbox.selected.concat([]), i, j;
4802 for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
4803 if(this._model.data[this._data.checkbox.selected[i]]) {
4804 this._model.data[this._data.checkbox.selected[i]].state.checked = false;
4807 this._data.checkbox.selected = [];
4808 this.element.find('.jstree-checked').removeClass('jstree-checked');
4810 * triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false)
4812 * @name uncheck_all.jstree
4813 * @param {Object} node the previous selection
4814 * @param {Array} selected the current selection
4817 this.trigger('uncheck_all', { 'selected' : this._data.checkbox.selected, 'node' : tmp });
4820 * checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected)
4821 * @name is_checked(obj)
4822 * @param {mixed} obj
4826 this.is_checked = function (obj) {
4827 if(this.settings.checkbox.tie_selection) { return this.is_selected(obj); }
4828 obj = this.get_node(obj);
4829 if(!obj || obj.id === '#') { return false; }
4830 return obj.state.checked;
4833 * get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected)
4834 * @name get_checked([full])
4835 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
4839 this.get_checked = function (full) {
4840 if(this.settings.checkbox.tie_selection) { return this.get_selected(full); }
4841 return full ? $.map(this._data.checkbox.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.checkbox.selected;
4844 * 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)
4845 * @name get_top_checked([full])
4846 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
4850 this.get_top_checked = function (full) {
4851 if(this.settings.checkbox.tie_selection) { return this.get_top_selected(full); }
4852 var tmp = this.get_checked(true),
4853 obj = {}, i, j, k, l;
4854 for(i = 0, j = tmp.length; i < j; i++) {
4855 obj[tmp[i].id] = tmp[i];
4857 for(i = 0, j = tmp.length; i < j; i++) {
4858 for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
4859 if(obj[tmp[i].children_d[k]]) {
4860 delete obj[tmp[i].children_d[k]];
4866 if(obj.hasOwnProperty(i)) {
4870 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
4873 * 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)
4874 * @name get_bottom_checked([full])
4875 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
4879 this.get_bottom_checked = function (full) {
4880 if(this.settings.checkbox.tie_selection) { return this.get_bottom_selected(full); }
4881 var tmp = this.get_checked(true),
4883 for(i = 0, j = tmp.length; i < j; i++) {
4884 if(!tmp[i].children.length) {
4885 obj.push(tmp[i].id);
4888 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
4892 // include the checkbox plugin by default
4893 // $.jstree.defaults.plugins.push("checkbox");
4896 * ### Contextmenu plugin
4898 * Shows a context menu when a node is right-clicked.
4902 * stores all defaults for the contextmenu plugin
4903 * @name $.jstree.defaults.contextmenu
4904 * @plugin contextmenu
4906 $.jstree.defaults.contextmenu = {
4908 * a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`.
4909 * @name $.jstree.defaults.contextmenu.select_node
4910 * @plugin contextmenu
4914 * a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used.
4915 * @name $.jstree.defaults.contextmenu.show_at_node
4916 * @plugin contextmenu
4918 show_at_node : true,
4920 * 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).
4922 * 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):
4924 * * `separator_before` - a boolean indicating if there should be a separator before this item
4925 * * `separator_after` - a boolean indicating if there should be a separator after this item
4926 * * `_disabled` - a boolean indicating if this action should be disabled
4927 * * `label` - a string - the name of the action (could be a function returning a string)
4928 * * `action` - a function to be executed if this item is chosen
4929 * * `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
4930 * * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2)
4931 * * `shortcut_label` - shortcut label (like for example `F2` for rename)
4933 * @name $.jstree.defaults.contextmenu.items
4934 * @plugin contextmenu
4936 items : function (o, cb) { // Could be an object directly
4939 "separator_before" : false,
4940 "separator_after" : true,
4941 "_disabled" : false, //(this.check("create_node", data.reference, {}, "last")),
4943 "action" : function (data) {
4944 var inst = $.jstree.reference(data.reference),
4945 obj = inst.get_node(data.reference);
4946 inst.create_node(obj, {}, "last", function (new_node) {
4947 setTimeout(function () { inst.edit(new_node); },0);
4952 "separator_before" : false,
4953 "separator_after" : false,
4954 "_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")),
4958 "shortcut_label" : 'F2',
4959 "icon" : "glyphicon glyphicon-leaf",
4961 "action" : function (data) {
4962 var inst = $.jstree.reference(data.reference),
4963 obj = inst.get_node(data.reference);
4968 "separator_before" : false,
4970 "separator_after" : false,
4971 "_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")),
4973 "action" : function (data) {
4974 var inst = $.jstree.reference(data.reference),
4975 obj = inst.get_node(data.reference);
4976 if(inst.is_selected(obj)) {
4977 inst.delete_node(inst.get_selected());
4980 inst.delete_node(obj);
4985 "separator_before" : true,
4987 "separator_after" : false,
4992 "separator_before" : false,
4993 "separator_after" : false,
4995 "action" : function (data) {
4996 var inst = $.jstree.reference(data.reference),
4997 obj = inst.get_node(data.reference);
4998 if(inst.is_selected(obj)) {
4999 inst.cut(inst.get_selected());
5007 "separator_before" : false,
5009 "separator_after" : false,
5011 "action" : function (data) {
5012 var inst = $.jstree.reference(data.reference),
5013 obj = inst.get_node(data.reference);
5014 if(inst.is_selected(obj)) {
5015 inst.copy(inst.get_selected());
5023 "separator_before" : false,
5025 "_disabled" : function (data) {
5026 return !$.jstree.reference(data.reference).can_paste();
5028 "separator_after" : false,
5030 "action" : function (data) {
5031 var inst = $.jstree.reference(data.reference),
5032 obj = inst.get_node(data.reference);
5042 $.jstree.plugins.contextmenu = function (options, parent) {
5043 this.bind = function () {
5044 parent.bind.call(this);
5048 .on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e) {
5050 last_ts = e.ctrlKey ? e.timeStamp : 0;
5051 if(!this.is_loading(e.currentTarget)) {
5052 this.show_contextmenu(e.currentTarget, e.pageX, e.pageY, e);
5055 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
5056 if(this._data.contextmenu.visible && (!last_ts || e.timeStamp - last_ts > 250)) { // work around safari & macOS ctrl+click
5057 $.vakata.context.hide();
5061 if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) {
5062 var el = null, tm = null;
5064 .on("touchstart", ".jstree-anchor", function (e) {
5065 el = e.currentTarget;
5067 $(document).one("touchend", function (e) {
5068 e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset);
5069 e.currentTarget = e.target;
5070 tm = ((+(new Date())) - tm);
5071 if(e.target === el && tm > 600 && tm < 1000) {
5073 $(el).trigger('contextmenu', e);
5081 $(document).on("context_hide.vakata.jstree", $.proxy(function () { this._data.contextmenu.visible = false; }, this));
5083 this.teardown = function () {
5084 if(this._data.contextmenu.visible) {
5085 $.vakata.context.hide();
5087 parent.teardown.call(this);
5091 * prepare and show the context menu for a node
5092 * @name show_contextmenu(obj [, x, y])
5093 * @param {mixed} obj the node
5094 * @param {Number} x the x-coordinate relative to the document to show the menu at
5095 * @param {Number} y the y-coordinate relative to the document to show the menu at
5096 * @param {Object} e the event if available that triggered the contextmenu
5097 * @plugin contextmenu
5098 * @trigger show_contextmenu.jstree
5100 this.show_contextmenu = function (obj, x, y, e) {
5101 obj = this.get_node(obj);
5102 if(!obj || obj.id === '#') { return false; }
5103 var s = this.settings.contextmenu,
5104 d = this.get_node(obj, true),
5105 a = d.children(".jstree-anchor"),
5108 if(s.show_at_node || x === undefined || y === undefined) {
5111 y = o.top + this._data.core.li_height;
5113 if(this.settings.contextmenu.select_node && !this.is_selected(obj)) {
5114 this.activate_node(obj, e);
5118 if($.isFunction(i)) {
5119 i = i.call(this, obj, $.proxy(function (i) {
5120 this._show_contextmenu(obj, x, y, i);
5123 if($.isPlainObject(i)) {
5124 this._show_contextmenu(obj, x, y, i);
5128 * show the prepared context menu for a node
5129 * @name _show_contextmenu(obj, x, y, i)
5130 * @param {mixed} obj the node
5131 * @param {Number} x the x-coordinate relative to the document to show the menu at
5132 * @param {Number} y the y-coordinate relative to the document to show the menu at
5133 * @param {Number} i the object of items to show
5134 * @plugin contextmenu
5135 * @trigger show_contextmenu.jstree
5138 this._show_contextmenu = function (obj, x, y, i) {
5139 var d = this.get_node(obj, true),
5140 a = d.children(".jstree-anchor");
5141 $(document).one("context_show.vakata.jstree", $.proxy(function (e, data) {
5142 var cls = 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu';
5143 $(data.element).addClass(cls);
5145 this._data.contextmenu.visible = true;
5146 $.vakata.context.show(a, { 'x' : x, 'y' : y }, i);
5148 * triggered when the contextmenu is shown for a node
5150 * @name show_contextmenu.jstree
5151 * @param {Object} node the node
5152 * @param {Number} x the x-coordinate of the menu relative to the document
5153 * @param {Number} y the y-coordinate of the menu relative to the document
5154 * @plugin contextmenu
5156 this.trigger('show_contextmenu', { "node" : obj, "x" : x, "y" : y });
5160 // contextmenu helper
5162 var right_to_left = false,
5173 $.vakata.context = {
5175 hide_onmouseleave : 0,
5178 _trigger : function (event_name) {
5179 $(document).triggerHandler("context_" + event_name + ".vakata", {
5180 "reference" : vakata_context.reference,
5181 "element" : vakata_context.element,
5183 "x" : vakata_context.position_x,
5184 "y" : vakata_context.position_y
5188 _execute : function (i) {
5189 i = vakata_context.items[i];
5190 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, {
5192 "reference" : vakata_context.reference,
5193 "element" : vakata_context.element,
5195 "x" : vakata_context.position_x,
5196 "y" : vakata_context.position_y
5200 _parse : function (o, is_callback) {
5201 if(!o) { return false; }
5203 vakata_context.html = "";
5204 vakata_context.items = [];
5210 if(is_callback) { str += "<"+"ul>"; }
5211 $.each(o, function (i, val) {
5212 if(!val) { return true; }
5213 vakata_context.items.push(val);
5214 if(!sep && val.separator_before) {
5215 str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
5218 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+"' ":'')+">";
5219 str += "<"+"a href='#' rel='" + (vakata_context.items.length - 1) + "'>";
5220 if($.vakata.context.settings.icons) {
5223 if(val.icon.indexOf("/") !== -1 || val.icon.indexOf(".") !== -1) { str += " style='background:url(\"" + val.icon + "\") center center no-repeat' "; }
5224 else { str += " class='" + val.icon + "' "; }
5226 str += "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>";
5228 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>";
5230 tmp = $.vakata.context._parse(val.submenu, true);
5231 if(tmp) { str += tmp; }
5234 if(val.separator_after) {
5235 str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
5239 str = str.replace(/<li class\='vakata-context-separator'\><\/li\>$/,"");
5240 if(is_callback) { str += "</ul>"; }
5242 * triggered on the document when the contextmenu is parsed (HTML is built)
5244 * @plugin contextmenu
5245 * @name context_parse.vakata
5246 * @param {jQuery} reference the element that was right clicked
5247 * @param {jQuery} element the DOM element of the menu itself
5248 * @param {Object} position the x & y coordinates of the menu
5250 if(!is_callback) { vakata_context.html = str; $.vakata.context._trigger("parse"); }
5251 return str.length > 10 ? str : false;
5253 _show_submenu : function (o) {
5255 if(!o.length || !o.children("ul").length) { return; }
5256 var e = o.children("ul"),
5257 x = o.offset().left + o.outerWidth(),
5261 dw = $(window).width() + $(window).scrollLeft(),
5262 dh = $(window).height() + $(window).scrollTop();
5263 // може да се спести е една проверка - дали няма някой от класовете вече нагоре
5265 o[x - (w + 10 + o.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left");
5268 o[x + w + 10 > dw ? "addClass" : "removeClass"]("vakata-context-right");
5270 if(y + h + 10 > dh) {
5271 e.css("bottom","-1px");
5275 show : function (reference, position, data) {
5276 var o, e, x, y, w, h, dw, dh, cond = true;
5277 if(vakata_context.element && vakata_context.element.length) {
5278 vakata_context.element.width('');
5281 case (!position && !reference):
5283 case (!!position && !!reference):
5284 vakata_context.reference = reference;
5285 vakata_context.position_x = position.x;
5286 vakata_context.position_y = position.y;
5288 case (!position && !!reference):
5289 vakata_context.reference = reference;
5290 o = reference.offset();
5291 vakata_context.position_x = o.left + reference.outerHeight();
5292 vakata_context.position_y = o.top;
5294 case (!!position && !reference):
5295 vakata_context.position_x = position.x;
5296 vakata_context.position_y = position.y;
5299 if(!!reference && !data && $(reference).data('vakata_contextmenu')) {
5300 data = $(reference).data('vakata_contextmenu');
5302 if($.vakata.context._parse(data)) {
5303 vakata_context.element.html(vakata_context.html);
5305 if(vakata_context.items.length) {
5306 vakata_context.element.appendTo("body");
5307 e = vakata_context.element;
5308 x = vakata_context.position_x;
5309 y = vakata_context.position_y;
5312 dw = $(window).width() + $(window).scrollLeft();
5313 dh = $(window).height() + $(window).scrollTop();
5315 x -= e.outerWidth();
5316 if(x < $(window).scrollLeft() + 20) {
5317 x = $(window).scrollLeft() + 20;
5320 if(x + w + 20 > dw) {
5323 if(y + h + 20 > dh) {
5327 vakata_context.element
5328 .css({ "left" : x, "top" : y })
5330 .find('a').first().focus().parent().addClass("vakata-context-hover");
5331 vakata_context.is_visible = true;
5333 * triggered on the document when the contextmenu is shown
5335 * @plugin contextmenu
5336 * @name context_show.vakata
5337 * @param {jQuery} reference the element that was right clicked
5338 * @param {jQuery} element the DOM element of the menu itself
5339 * @param {Object} position the x & y coordinates of the menu
5341 $.vakata.context._trigger("show");
5344 hide : function () {
5345 if(vakata_context.is_visible) {
5346 vakata_context.element.hide().find("ul").hide().end().find(':focus').blur().end().detach();
5347 vakata_context.is_visible = false;
5349 * triggered on the document when the contextmenu is hidden
5351 * @plugin contextmenu
5352 * @name context_hide.vakata
5353 * @param {jQuery} reference the element that was right clicked
5354 * @param {jQuery} element the DOM element of the menu itself
5355 * @param {Object} position the x & y coordinates of the menu
5357 $.vakata.context._trigger("hide");
5362 right_to_left = $("body").css("direction") === "rtl";
5365 vakata_context.element = $("<ul class='vakata-context'></ul>");
5366 vakata_context.element
5367 .on("mouseenter", "li", function (e) {
5368 e.stopImmediatePropagation();
5370 if($.contains(this, e.relatedTarget)) {
5371 // премахнато заради delegate mouseleave по-долу
5372 // $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
5376 if(to) { clearTimeout(to); }
5377 vakata_context.element.find(".vakata-context-hover").removeClass("vakata-context-hover").end();
5380 .siblings().find("ul").hide().end().end()
5381 .parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover");
5382 $.vakata.context._show_submenu(this);
5384 // тестово - дали не натоварва?
5385 .on("mouseleave", "li", function (e) {
5386 if($.contains(this, e.relatedTarget)) { return; }
5387 $(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover");
5389 .on("mouseleave", function (e) {
5390 $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
5391 if($.vakata.context.settings.hide_onmouseleave) {
5394 return function () { $.vakata.context.hide(); };
5395 }(this)), $.vakata.context.settings.hide_onmouseleave);
5398 .on("click", "a", function (e) {
5401 //.on("mouseup", "a", function (e) {
5402 if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata.context._execute($(this).attr("rel")) !== false) {
5403 $.vakata.context.hide();
5406 .on('keydown', 'a', function (e) {
5413 $(e.currentTarget).trigger(e);
5416 if(vakata_context.is_visible) {
5417 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();
5418 e.stopImmediatePropagation();
5423 if(vakata_context.is_visible) {
5424 o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first();
5425 if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); }
5426 o.addClass("vakata-context-hover").children('a').focus();
5427 e.stopImmediatePropagation();
5432 if(vakata_context.is_visible) {
5433 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();
5434 e.stopImmediatePropagation();
5439 if(vakata_context.is_visible) {
5440 o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first();
5441 if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); }
5442 o.addClass("vakata-context-hover").children('a').focus();
5443 e.stopImmediatePropagation();
5448 $.vakata.context.hide();
5452 //console.log(e.which);
5456 .on('keydown', function (e) {
5458 var a = vakata_context.element.find('.vakata-contextmenu-shortcut-' + e.which).parent();
5459 if(a.parent().not('.vakata-context-disabled')) {
5465 .on("mousedown.vakata.jstree", function (e) {
5466 if(vakata_context.is_visible && !$.contains(vakata_context.element[0], e.target)) { $.vakata.context.hide(); }
5468 .on("context_show.vakata.jstree", function (e, data) {
5469 vakata_context.element.find("li:has(ul)").children("a").addClass("vakata-context-parent");
5471 vakata_context.element.addClass("vakata-context-rtl").css("direction", "rtl");
5473 // also apply a RTL class?
5474 vakata_context.element.find("ul").hide().end();
5478 // $.jstree.defaults.plugins.push("contextmenu");
5481 * ### Drag'n'drop plugin
5483 * Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations.
5487 * stores all defaults for the drag'n'drop plugin
5488 * @name $.jstree.defaults.dnd
5491 $.jstree.defaults.dnd = {
5493 * a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`.
5494 * @name $.jstree.defaults.dnd.copy
5499 * a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`.
5500 * @name $.jstree.defaults.dnd.open_timeout
5505 * 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) - return `false` to prevent dragging
5506 * @name $.jstree.defaults.dnd.is_draggable
5509 is_draggable : true,
5511 * 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`
5512 * @name $.jstree.defaults.dnd.check_while_dragging
5515 check_while_dragging : true,
5517 * a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false`
5518 * @name $.jstree.defaults.dnd.always_copy
5521 always_copy : false,
5523 * 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`
5524 * @name $.jstree.defaults.dnd.inside_pos
5529 // TODO: now check works by checking for each node individually, how about max_children, unique, etc?
5530 $.jstree.plugins.dnd = function (options, parent) {
5531 this.bind = function () {
5532 parent.bind.call(this);
5535 .on('mousedown.jstree touchstart.jstree', '.jstree-anchor', $.proxy(function (e) {
5536 var obj = this.get_node(e.target),
5537 mlt = this.is_selected(obj) ? this.get_selected().length : 1;
5538 if(obj && obj.id && obj.id !== "#" && (e.which === 1 || e.type === "touchstart") &&
5539 (this.settings.dnd.is_draggable === true || ($.isFunction(this.settings.dnd.is_draggable) && this.settings.dnd.is_draggable.call(this, (mlt > 1 ? this.get_selected(true) : [obj]))))
5541 this.element.trigger('mousedown.jstree');
5542 return $.vakata.dnd.start(e, { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_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>' + (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget, true)) + '<ins class="jstree-copy" style="display:none;">+</ins></div>');
5549 // bind only once for all instances
5553 marker = $('<div id="jstree-marker"> </div>').hide(); //.appendTo('body');
5556 .on('dnd_start.vakata.jstree', function (e, data) {
5558 if(!data || !data.data || !data.data.jstree) { return; }
5559 marker.appendTo('body'); //.show();
5561 .on('dnd_move.vakata.jstree', function (e, data) {
5562 if(opento) { clearTimeout(opento); }
5563 if(!data || !data.data || !data.data.jstree) { return; }
5565 // if we are hovering the marker image do nothing (can happen on "inside" drags)
5566 if(data.event.target.id && data.event.target.id === 'jstree-marker') {
5570 var ins = $.jstree.reference(data.event.target),
5574 l, t, h, p, i, o, ok, t1, t2, op, ps, pr, ip, tm;
5575 // if we are over an instance
5576 if(ins && ins._data && ins._data.dnd) {
5577 marker.attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ));
5579 .children().attr('class', 'jstree-' + ins.get_theme() + ' jstree-' + ins.get_theme() + '-' + ins.get_theme_variant() + ' ' + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ))
5580 .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' ]();
5583 // if are hovering the container itself add a new root node
5584 if( (data.event.target === ins.element[0] || data.event.target === ins.get_container_ul()[0]) && ins.get_container_ul().children().length === 0) {
5586 for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
5587 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]), '#', 'last', { 'dnd' : true, 'ref' : ins.get_node('#'), 'pos' : 'i', 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) });
5591 lastmv = { 'ins' : ins, 'par' : '#', 'pos' : 'last' };
5593 data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
5598 // if we are hovering a tree node
5599 ref = $(data.event.target).closest('.jstree-anchor');
5600 if(ref && ref.length && ref.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) {
5602 rel = data.event.pageY - off.top;
5605 o = ['b', 'i', 'a'];
5607 else if(rel > h - h / 3) {
5608 o = ['a', 'i', 'b'];
5611 o = rel > h / 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a'];
5613 $.each(o, function (j, v) {
5618 p = ins.get_parent(ref);
5619 i = ref.parent().index();
5622 ip = ins.settings.dnd.inside_pos;
5623 tm = ins.get_node(ref.parent());
5625 t = off.top + h / 2 + 1;
5627 i = ip === 'first' ? 0 : (ip === 'last' ? tm.children.length : Math.min(ip, tm.children.length));
5632 p = ins.get_parent(ref);
5633 i = ref.parent().index() + 1;
5637 for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) {
5638 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";
5640 if(op === "move_node" && v === 'a' && (data.data.origin && data.data.origin === ins) && p === ins.get_parent(data.data.nodes[t1])) {
5641 pr = ins.get_node(p);
5642 if(ps > $.inArray(data.data.nodes[t1], pr.children)) {
5646 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, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }) );
5648 if(ins && ins.last_error) { laster = ins.last_error(); }
5652 if(v === 'i' && ref.parent().is('.jstree-closed') && ins.settings.dnd.open_timeout) {
5653 opento = setTimeout((function (x, z) { return function () { x.open_node(z); }; }(ins, ref)), ins.settings.dnd.open_timeout);
5656 lastmv = { 'ins' : ins, 'par' : p, 'pos' : v === 'i' && ip === 'last' && i === 0 && !ins.is_loaded(tm) ? 'last' : i };
5657 marker.css({ 'left' : l + 'px', 'top' : t + 'px' }).show();
5658 data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok');
5664 if(o === true) { return; }
5669 data.helper.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er');
5672 .on('dnd_scroll.vakata.jstree', function (e, data) {
5673 if(!data || !data.data || !data.data.jstree) { return; }
5676 data.helper.find('.jstree-icon').first().removeClass('jstree-ok').addClass('jstree-er');
5678 .on('dnd_stop.vakata.jstree', function (e, data) {
5679 if(opento) { clearTimeout(opento); }
5680 if(!data || !data.data || !data.data.jstree) { return; }
5681 marker.hide().detach();
5682 var i, j, nodes = [];
5684 for(i = 0, j = data.data.nodes.length; i < j; i++) {
5685 nodes[i] = data.data.origin ? data.data.origin.get_node(data.data.nodes[i]) : data.data.nodes[i];
5686 if(data.data.origin) {
5687 nodes[i].instance = data.data.origin;
5690 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);
5691 for(i = 0, j = nodes.length; i < j; i++) {
5692 if(nodes[i].instance) {
5693 nodes[i].instance = null;
5698 i = $(data.event.target).closest('.jstree');
5699 if(i.length && laster && laster.error && laster.error === 'check') {
5702 i.settings.core.error.call(this, laster);
5707 .on('keyup.jstree keydown.jstree', function (e, data) {
5708 data = $.vakata.dnd._get();
5709 if(data && data.data && data.data.jstree) {
5710 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' ]();
5737 scroll_proximity : 20,
5741 threshold_touch : 50
5743 _trigger : function (event_name, e) {
5744 var data = $.vakata.dnd._get();
5746 $(document).triggerHandler("dnd_" + event_name + ".vakata", data);
5748 _get : function () {
5750 "data" : vakata_dnd.data,
5751 "element" : vakata_dnd.element,
5752 "helper" : vakata_dnd.helper
5755 _clean : function () {
5756 if(vakata_dnd.helper) { vakata_dnd.helper.remove(); }
5757 if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
5774 $(document).off("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag);
5775 $(document).off("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop);
5777 _scroll : function (init_only) {
5778 if(!vakata_dnd.scroll_e || (!vakata_dnd.scroll_l && !vakata_dnd.scroll_t)) {
5779 if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; }
5782 if(!vakata_dnd.scroll_i) {
5783 vakata_dnd.scroll_i = setInterval($.vakata.dnd._scroll, 100);
5786 if(init_only === true) { return false; }
5788 var i = vakata_dnd.scroll_e.scrollTop(),
5789 j = vakata_dnd.scroll_e.scrollLeft();
5790 vakata_dnd.scroll_e.scrollTop(i + vakata_dnd.scroll_t * $.vakata.dnd.settings.scroll_speed);
5791 vakata_dnd.scroll_e.scrollLeft(j + vakata_dnd.scroll_l * $.vakata.dnd.settings.scroll_speed);
5792 if(i !== vakata_dnd.scroll_e.scrollTop() || j !== vakata_dnd.scroll_e.scrollLeft()) {
5794 * triggered on the document when a drag causes an element to scroll
5797 * @name dnd_scroll.vakata
5798 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
5799 * @param {DOM} element the DOM element being dragged
5800 * @param {jQuery} helper the helper shown next to the mouse
5801 * @param {jQuery} event the element that is scrolling
5803 $.vakata.dnd._trigger("scroll", vakata_dnd.scroll_e);
5806 start : function (e, data, html) {
5807 if(e.type === "touchstart" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
5808 e.pageX = e.originalEvent.changedTouches[0].pageX;
5809 e.pageY = e.originalEvent.changedTouches[0].pageY;
5810 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
5812 if(vakata_dnd.is_drag) { $.vakata.dnd.stop({}); }
5814 e.currentTarget.unselectable = "on";
5815 e.currentTarget.onselectstart = function() { return false; };
5816 if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; }
5818 vakata_dnd.init_x = e.pageX;
5819 vakata_dnd.init_y = e.pageY;
5820 vakata_dnd.data = data;
5821 vakata_dnd.is_down = true;
5822 vakata_dnd.element = e.currentTarget;
5823 vakata_dnd.target = e.target;
5824 vakata_dnd.is_touch = e.type === "touchstart";
5825 if(html !== false) {
5826 vakata_dnd.helper = $("<div id='vakata-dnd'></div>").html(html).css({
5827 "display" : "block",
5830 "position" : "absolute",
5832 "lineHeight" : "16px",
5836 $(document).on("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag);
5837 $(document).on("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop);
5840 drag : function (e) {
5841 if(e.type === "touchmove" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
5842 e.pageX = e.originalEvent.changedTouches[0].pageX;
5843 e.pageY = e.originalEvent.changedTouches[0].pageY;
5844 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
5846 if(!vakata_dnd.is_down) { return; }
5847 if(!vakata_dnd.is_drag) {
5849 Math.abs(e.pageX - vakata_dnd.init_x) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) ||
5850 Math.abs(e.pageY - vakata_dnd.init_y) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold)
5852 if(vakata_dnd.helper) {
5853 vakata_dnd.helper.appendTo("body");
5854 vakata_dnd.helper_w = vakata_dnd.helper.outerWidth();
5856 vakata_dnd.is_drag = true;
5858 * triggered on the document when a drag starts
5861 * @name dnd_start.vakata
5862 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
5863 * @param {DOM} element the DOM element being dragged
5864 * @param {jQuery} helper the helper shown next to the mouse
5865 * @param {Object} event the event that caused the start (probably mousemove)
5867 $.vakata.dnd._trigger("start", e);
5872 var d = false, w = false,
5873 dh = false, wh = false,
5874 dw = false, ww = false,
5875 dt = false, dl = false,
5876 ht = false, hl = false;
5878 vakata_dnd.scroll_t = 0;
5879 vakata_dnd.scroll_l = 0;
5880 vakata_dnd.scroll_e = false;
5881 $($(e.target).parentsUntil("body").addBack().get().reverse())
5882 .filter(function () {
5883 return (/^auto|scroll$/).test($(this).css("overflow")) &&
5884 (this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth);
5887 var t = $(this), o = t.offset();
5888 if(this.scrollHeight > this.offsetHeight) {
5889 if(o.top + t.height() - e.pageY < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
5890 if(e.pageY - o.top < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
5892 if(this.scrollWidth > this.offsetWidth) {
5893 if(o.left + t.width() - e.pageX < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
5894 if(e.pageX - o.left < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
5896 if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
5897 vakata_dnd.scroll_e = $(this);
5902 if(!vakata_dnd.scroll_e) {
5903 d = $(document); w = $(window);
5904 dh = d.height(); wh = w.height();
5905 dw = d.width(); ww = w.width();
5906 dt = d.scrollTop(); dl = d.scrollLeft();
5907 if(dh > wh && e.pageY - dt < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; }
5908 if(dh > wh && wh - (e.pageY - dt) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; }
5909 if(dw > ww && e.pageX - dl < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; }
5910 if(dw > ww && ww - (e.pageX - dl) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; }
5911 if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) {
5912 vakata_dnd.scroll_e = d;
5915 if(vakata_dnd.scroll_e) { $.vakata.dnd._scroll(true); }
5917 if(vakata_dnd.helper) {
5918 ht = parseInt(e.pageY + $.vakata.dnd.settings.helper_top, 10);
5919 hl = parseInt(e.pageX + $.vakata.dnd.settings.helper_left, 10);
5920 if(dh && ht + 25 > dh) { ht = dh - 50; }
5921 if(dw && hl + vakata_dnd.helper_w > dw) { hl = dw - (vakata_dnd.helper_w + 2); }
5922 vakata_dnd.helper.css({
5928 * triggered on the document when a drag is in progress
5931 * @name dnd_move.vakata
5932 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
5933 * @param {DOM} element the DOM element being dragged
5934 * @param {jQuery} helper the helper shown next to the mouse
5935 * @param {Object} event the event that caused this to trigger (most likely mousemove)
5937 $.vakata.dnd._trigger("move", e);
5940 stop : function (e) {
5941 if(e.type === "touchend" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) {
5942 e.pageX = e.originalEvent.changedTouches[0].pageX;
5943 e.pageY = e.originalEvent.changedTouches[0].pageY;
5944 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset);
5946 if(vakata_dnd.is_drag) {
5948 * triggered on the document when a drag stops (the dragged element is dropped)
5951 * @name dnd_stop.vakata
5952 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start
5953 * @param {DOM} element the DOM element being dragged
5954 * @param {jQuery} helper the helper shown next to the mouse
5955 * @param {Object} event the event that caused the stop
5957 $.vakata.dnd._trigger("stop", e);
5960 if(e.type === "touchend" && e.target === vakata_dnd.target) {
5961 var to = setTimeout(function () { $(e.target).click(); }, 100);
5962 $(e.target).one('click', function() { if(to) { clearTimeout(to); } });
5965 $.vakata.dnd._clean();
5971 // include the dnd plugin by default
5972 // $.jstree.defaults.plugins.push("dnd");
5978 * Adds search functionality to jsTree.
5982 * stores all defaults for the search plugin
5983 * @name $.jstree.defaults.search
5986 $.jstree.defaults.search = {
5988 * a jQuery-like AJAX config, which jstree uses if a server should be queried for results.
5990 * A `str` (which is the search string) parameter will be added with the request. The expected result is a JSON array with nodes that need to be opened so that matching nodes will be revealed.
5991 * 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 2 parameters - the search string and the callback to call with the array of nodes to load.
5992 * @name $.jstree.defaults.search.ajax
5997 * Indicates if the search should be fuzzy or not (should `chnd3` match `child node 3`). Default is `false`.
5998 * @name $.jstree.defaults.search.fuzzy
6003 * Indicates if the search should be case sensitive. Default is `false`.
6004 * @name $.jstree.defaults.search.case_sensitive
6007 case_sensitive : false,
6009 * 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).
6010 * This setting can be changed at runtime when calling the search method. Default is `false`.
6011 * @name $.jstree.defaults.search.show_only_matches
6014 show_only_matches : false,
6016 * 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`.
6017 * @name $.jstree.defaults.search.close_opened_onclear
6020 close_opened_onclear : true,
6022 * Indicates if only leaf nodes should be included in search results. Default is `false`.
6023 * @name $.jstree.defaults.search.search_leaves_only
6026 search_leaves_only : false,
6028 * 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).
6029 * 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`.
6030 * @name $.jstree.defaults.search.search_callback
6033 search_callback : false
6036 $.jstree.plugins.search = function (options, parent) {
6037 this.bind = function () {
6038 parent.bind.call(this);
6040 this._data.search.str = "";
6041 this._data.search.dom = $();
6042 this._data.search.res = [];
6043 this._data.search.opn = [];
6044 this._data.search.som = false;
6047 .on('before_open.jstree', $.proxy(function (e, data) {
6048 var i, j, f, r = this._data.search.res, s = [], o = $();
6050 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(', #')));
6051 this._data.search.dom.children(".jstree-anchor").addClass('jstree-search');
6052 if(this._data.search.som && this._data.search.res.length) {
6053 for(i = 0, j = r.length; i < j; i++) {
6054 s = s.concat(this.get_node(r[i]).parents);
6056 s = $.vakata.array_remove_item($.vakata.array_unique(s),'#');
6057 o = s.length ? $(this.element[0].querySelectorAll('#' + $.map(s, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #'))) : $();
6059 this.element.find(".jstree-node").hide().filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last');
6060 o = o.add(this._data.search.dom);
6061 o.parentsUntil(".jstree").addBack().show()
6062 .filter(".jstree-children").each(function () { $(this).children(".jstree-node:visible").eq(-1).addClass("jstree-last"); });
6066 .on("search.jstree", $.proxy(function (e, data) {
6067 if(this._data.search.som) {
6068 if(data.nodes.length) {
6069 this.element.find(".jstree-node").hide().filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last');
6070 data.nodes.parentsUntil(".jstree").addBack().show()
6071 .filter(".jstree-children").each(function () { $(this).children(".jstree-node:visible").eq(-1).addClass("jstree-last"); });
6075 .on("clear_search.jstree", $.proxy(function (e, data) {
6076 if(this._data.search.som && data.nodes.length) {
6077 this.element.find(".jstree-node").css("display","").filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last');
6082 * used to search the tree nodes for a given string
6083 * @name search(str [, skip_async])
6084 * @param {String} str the search string
6085 * @param {Boolean} skip_async if set to true server will not be queried even if configured
6086 * @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)
6088 * @trigger search.jstree
6090 this.search = function (str, skip_async, show_only_matches) {
6091 if(str === false || $.trim(str.toString()) === "") {
6092 return this.clear_search();
6094 str = str.toString();
6095 var s = this.settings.search,
6096 a = s.ajax ? s.ajax : false,
6100 if(this._data.search.res.length) {
6101 this.clear_search();
6103 if(show_only_matches === undefined) {
6104 show_only_matches = s.show_only_matches;
6106 if(!skip_async && a !== false) {
6107 if($.isFunction(a)) {
6108 return a.call(this, str, $.proxy(function (d) {
6109 if(d && d.d) { d = d.d; }
6110 this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () {
6111 this.search(str, true, show_only_matches);
6116 a = $.extend({}, a);
6117 if(!a.data) { a.data = {}; }
6120 .fail($.proxy(function () {
6121 this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'search', 'id' : 'search_01', 'reason' : 'Could not load search parents', 'data' : JSON.stringify(a) };
6122 this.settings.core.error.call(this, this._data.core.last_error);
6124 .done($.proxy(function (d) {
6125 if(d && d.d) { d = d.d; }
6126 this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () {
6127 this.search(str, true, show_only_matches);
6132 this._data.search.str = str;
6133 this._data.search.dom = $();
6134 this._data.search.res = [];
6135 this._data.search.opn = [];
6136 this._data.search.som = show_only_matches;
6138 f = new $.vakata.search(str, true, { caseSensitive : s.case_sensitive, fuzzy : s.fuzzy });
6140 $.each(this._model.data, function (i, v) {
6141 if(v.text && ( (s.search_callback && s.search_callback.call(this, str, v)) || (!s.search_callback && f.search(v.text).isMatch) ) && (!s.search_leaves_only || (v.state.loaded && v.children.length === 0)) ) {
6143 p = p.concat(v.parents);
6147 p = $.vakata.array_unique(p);
6148 this._search_open(p);
6149 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(', #')));
6150 this._data.search.res = r;
6151 this._data.search.dom.children(".jstree-anchor").addClass('jstree-search');
6154 * triggered after search is complete
6156 * @name search.jstree
6157 * @param {jQuery} nodes a jQuery collection of matching nodes
6158 * @param {String} str the search string
6159 * @param {Array} res a collection of objects represeing the matching nodes
6162 this.trigger('search', { nodes : this._data.search.dom, str : str, res : this._data.search.res, show_only_matches : show_only_matches });
6165 * used to clear the last search (removes classes and shows all nodes if filtering is on)
6166 * @name clear_search()
6168 * @trigger clear_search.jstree
6170 this.clear_search = function () {
6171 this._data.search.dom.children(".jstree-anchor").removeClass("jstree-search");
6172 if(this.settings.search.close_opened_onclear) {
6173 this.close_node(this._data.search.opn, 0);
6176 * triggered after search is complete
6178 * @name clear_search.jstree
6179 * @param {jQuery} nodes a jQuery collection of matching nodes (the result from the last search)
6180 * @param {String} str the search string (the last search string)
6181 * @param {Array} res a collection of objects represeing the matching nodes (the result from the last search)
6184 this.trigger('clear_search', { 'nodes' : this._data.search.dom, str : this._data.search.str, res : this._data.search.res });
6185 this._data.search.str = "";
6186 this._data.search.res = [];
6187 this._data.search.opn = [];
6188 this._data.search.dom = $();
6191 * opens nodes that need to be opened to reveal the search results. Used only internally.
6193 * @name _search_open(d)
6194 * @param {Array} d an array of node IDs
6197 this._search_open = function (d) {
6199 $.each(d.concat([]), function (i, v) {
6200 if(v === "#") { return true; }
6201 try { v = $('#' + v.replace($.jstree.idregex,'\\$&'), t.element); } catch(ignore) { }
6203 if(t.is_closed(v)) {
6204 t._data.search.opn.push(v[0].id);
6205 t.open_node(v, function () { t._search_open(d); }, 0);
6214 // from http://kiro.me/projects/fuse.html
6215 $.vakata.search = function(pattern, txt, options) {
6216 options = options || {};
6217 if(options.fuzzy !== false) {
6218 options.fuzzy = true;
6220 pattern = options.caseSensitive ? pattern : pattern.toLowerCase();
6221 var MATCH_LOCATION = options.location || 0,
6222 MATCH_DISTANCE = options.distance || 100,
6223 MATCH_THRESHOLD = options.threshold || 0.6,
6224 patternLen = pattern.length,
6225 matchmask, pattern_alphabet, match_bitapScore, search;
6226 if(patternLen > 32) {
6227 options.fuzzy = false;
6230 matchmask = 1 << (patternLen - 1);
6231 pattern_alphabet = (function () {
6234 for (i = 0; i < patternLen; i++) {
6235 mask[pattern.charAt(i)] = 0;
6237 for (i = 0; i < patternLen; i++) {
6238 mask[pattern.charAt(i)] |= 1 << (patternLen - i - 1);
6242 match_bitapScore = function (e, x) {
6243 var accuracy = e / patternLen,
6244 proximity = Math.abs(MATCH_LOCATION - x);
6245 if(!MATCH_DISTANCE) {
6246 return proximity ? 1.0 : accuracy;
6248 return accuracy + (proximity / MATCH_DISTANCE);
6251 search = function (text) {
6252 text = options.caseSensitive ? text : text.toLowerCase();
6253 if(pattern === text || text.indexOf(pattern) !== -1) {
6259 if(!options.fuzzy) {
6266 textLen = text.length,
6267 scoreThreshold = MATCH_THRESHOLD,
6268 bestLoc = text.indexOf(pattern, MATCH_LOCATION),
6270 binMax = patternLen + textLen,
6271 lastRd, start, finish, rd, charMatch,
6274 if (bestLoc !== -1) {
6275 scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold);
6276 bestLoc = text.lastIndexOf(pattern, MATCH_LOCATION + patternLen);
6277 if (bestLoc !== -1) {
6278 scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold);
6282 for (i = 0; i < patternLen; i++) {
6285 while (binMin < binMid) {
6286 if (match_bitapScore(i, MATCH_LOCATION + binMid) <= scoreThreshold) {
6291 binMid = Math.floor((binMax - binMin) / 2 + binMin);
6294 start = Math.max(1, MATCH_LOCATION - binMid + 1);
6295 finish = Math.min(MATCH_LOCATION + binMid, textLen) + patternLen;
6296 rd = new Array(finish + 2);
6297 rd[finish + 1] = (1 << i) - 1;
6298 for (j = finish; j >= start; j--) {
6299 charMatch = pattern_alphabet[text.charAt(j - 1)];
6301 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
6303 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | (((lastRd[j + 1] | lastRd[j]) << 1) | 1) | lastRd[j + 1];
6305 if (rd[j] & matchmask) {
6306 score = match_bitapScore(i, j - 1);
6307 if (score <= scoreThreshold) {
6308 scoreThreshold = score;
6310 locations.push(bestLoc);
6311 if (bestLoc > MATCH_LOCATION) {
6312 start = Math.max(1, 2 * MATCH_LOCATION - bestLoc);
6319 if (match_bitapScore(i + 1, MATCH_LOCATION) > scoreThreshold) {
6325 isMatch: bestLoc >= 0,
6329 return txt === true ? { 'search' : search } : search(txt);
6333 // include the search plugin by default
6334 // $.jstree.defaults.plugins.push("search");
6339 * Automatically sorts all siblings in the tree according to a sorting function.
6343 * the settings function used to sort the nodes.
6344 * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`.
6345 * @name $.jstree.defaults.sort
6348 $.jstree.defaults.sort = function (a, b) {
6349 //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);
6350 return this.get_text(a) > this.get_text(b) ? 1 : -1;
6352 $.jstree.plugins.sort = function (options, parent) {
6353 this.bind = function () {
6354 parent.bind.call(this);
6356 .on("model.jstree", $.proxy(function (e, data) {
6357 this.sort(data.parent, true);
6359 .on("rename_node.jstree create_node.jstree", $.proxy(function (e, data) {
6360 this.sort(data.parent || data.node.parent, false);
6361 this.redraw_node(data.parent || data.node.parent, true);
6363 .on("move_node.jstree copy_node.jstree", $.proxy(function (e, data) {
6364 this.sort(data.parent, false);
6365 this.redraw_node(data.parent, true);
6369 * used to sort a node's children
6371 * @name sort(obj [, deep])
6372 * @param {mixed} obj the node
6373 * @param {Boolean} deep if set to `true` nodes are sorted recursively.
6375 * @trigger search.jstree
6377 this.sort = function (obj, deep) {
6379 obj = this.get_node(obj);
6380 if(obj && obj.children && obj.children.length) {
6381 obj.children.sort($.proxy(this.settings.sort, this));
6383 for(i = 0, j = obj.children_d.length; i < j; i++) {
6384 this.sort(obj.children_d[i], false);
6391 // include the sort plugin by default
6392 // $.jstree.defaults.plugins.push("sort");
6397 * Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc)
6402 * stores all defaults for the state plugin
6403 * @name $.jstree.defaults.state
6406 $.jstree.defaults.state = {
6408 * A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`.
6409 * @name $.jstree.defaults.state.key
6414 * A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`.
6415 * @name $.jstree.defaults.state.events
6418 events : 'changed.jstree open_node.jstree close_node.jstree',
6420 * Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire.
6421 * @name $.jstree.defaults.state.ttl
6426 * 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.
6427 * @name $.jstree.defaults.state.filter
6432 $.jstree.plugins.state = function (options, parent) {
6433 this.bind = function () {
6434 parent.bind.call(this);
6435 var bind = $.proxy(function () {
6436 this.element.on(this.settings.state.events, $.proxy(function () {
6437 if(to) { clearTimeout(to); }
6438 to = setTimeout($.proxy(function () { this.save_state(); }, this), 100);
6442 .on("ready.jstree", $.proxy(function (e, data) {
6443 this.element.one("restore_state.jstree", bind);
6444 if(!this.restore_state()) { bind(); }
6449 * @name save_state()
6452 this.save_state = function () {
6453 var st = { 'state' : this.get_state(), 'ttl' : this.settings.state.ttl, 'sec' : +(new Date()) };
6454 $.vakata.storage.set(this.settings.state.key, JSON.stringify(st));
6457 * restore the state from the user's computer
6458 * @name restore_state()
6461 this.restore_state = function () {
6462 var k = $.vakata.storage.get(this.settings.state.key);
6463 if(!!k) { try { k = JSON.parse(k); } catch(ex) { return false; } }
6464 if(!!k && k.ttl && k.sec && +(new Date()) - k.sec > k.ttl) { return false; }
6465 if(!!k && k.state) { k = k.state; }
6466 if(!!k && $.isFunction(this.settings.state.filter)) { k = this.settings.state.filter.call(this, k); }
6468 this.element.one("set_state.jstree", function (e, data) { data.instance.trigger('restore_state', { 'state' : $.extend(true, {}, k) }); });
6475 * clear the state on the user's computer
6476 * @name clear_state()
6479 this.clear_state = function () {
6480 return $.vakata.storage.del(this.settings.state.key);
6484 (function ($, undefined) {
6485 $.vakata.storage = {
6486 // simply specifying the functions in FF throws an error
6487 set : function (key, val) { return window.localStorage.setItem(key, val); },
6488 get : function (key) { return window.localStorage.getItem(key); },
6489 del : function (key) { return window.localStorage.removeItem(key); }
6493 // include the state plugin by default
6494 // $.jstree.defaults.plugins.push("state");
6499 * 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.
6503 * 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).
6505 * * `max_children` the maximum number of immediate children this node type can have. Do not specify or set to `-1` for unlimited.
6506 * * `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.
6507 * * `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.
6508 * * `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.
6510 * There are two predefined types:
6512 * * `#` represents the root of the tree, for example `max_children` would control the maximum number of root nodes.
6513 * * `default` represents the default node - any settings here will be applied to all nodes that do not have a type specified.
6515 * @name $.jstree.defaults.types
6518 $.jstree.defaults.types = {
6523 $.jstree.plugins.types = function (options, parent) {
6524 this.init = function (el, options) {
6526 if(options && options.types && options.types['default']) {
6527 for(i in options.types) {
6528 if(i !== "default" && i !== "#" && options.types.hasOwnProperty(i)) {
6529 for(j in options.types['default']) {
6530 if(options.types['default'].hasOwnProperty(j) && options.types[i][j] === undefined) {
6531 options.types[i][j] = options.types['default'][j];
6537 parent.init.call(this, el, options);
6538 this._model.data['#'].type = '#';
6540 this.refresh = function (skip_loading, forget_state) {
6541 parent.refresh.call(this, skip_loading, forget_state);
6542 this._model.data['#'].type = '#';
6544 this.bind = function () {
6546 .on('model.jstree', $.proxy(function (e, data) {
6547 var m = this._model.data,
6549 t = this.settings.types,
6550 i, j, c = 'default';
6551 for(i = 0, j = dpc.length; i < j; i++) {
6553 if(m[dpc[i]].original && m[dpc[i]].original.type && t[m[dpc[i]].original.type]) {
6554 c = m[dpc[i]].original.type;
6556 if(m[dpc[i]].data && m[dpc[i]].data.jstree && m[dpc[i]].data.jstree.type && t[m[dpc[i]].data.jstree.type]) {
6557 c = m[dpc[i]].data.jstree.type;
6560 if(m[dpc[i]].icon === true && t[c].icon !== undefined) {
6561 m[dpc[i]].icon = t[c].icon;
6566 parent.bind.call(this);
6568 this.get_json = function (obj, options, flat) {
6570 m = this._model.data,
6571 opt = options ? $.extend(true, {}, options, {no_id:false}) : {},
6572 tmp = parent.get_json.call(this, obj, opt, flat);
6573 if(tmp === false) { return false; }
6574 if($.isArray(tmp)) {
6575 for(i = 0, j = tmp.length; i < j; i++) {
6576 tmp[i].type = tmp[i].id && m[tmp[i].id] && m[tmp[i].id].type ? m[tmp[i].id].type : "default";
6577 if(options && options.no_id) {
6579 if(tmp[i].li_attr && tmp[i].li_attr.id) {
6580 delete tmp[i].li_attr.id;
6586 tmp.type = tmp.id && m[tmp.id] && m[tmp.id].type ? m[tmp.id].type : "default";
6587 if(options && options.no_id) {
6588 tmp = this._delete_ids(tmp);
6593 this._delete_ids = function (tmp) {
6594 if($.isArray(tmp)) {
6595 for(var i = 0, j = tmp.length; i < j; i++) {
6596 tmp[i] = this._delete_ids(tmp[i]);
6601 if(tmp.li_attr && tmp.li_attr.id) {
6602 delete tmp.li_attr.id;
6604 if(tmp.children && $.isArray(tmp.children)) {
6605 tmp.children = this._delete_ids(tmp.children);
6609 this.check = function (chk, obj, par, pos, more) {
6610 if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; }
6611 obj = obj && obj.id ? obj : this.get_node(obj);
6612 par = par && par.id ? par : this.get_node(par);
6613 var m = obj && obj.id ? $.jstree.reference(obj.id) : null, tmp, d, i, j;
6614 m = m && m._model && m._model.data ? m._model.data : null;
6619 if(chk !== 'move_node' || $.inArray(obj.id, par.children) === -1) {
6620 tmp = this.get_rules(par);
6621 if(tmp.max_children !== undefined && tmp.max_children !== -1 && tmp.max_children === par.children.length) {
6622 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 }) };
6625 if(tmp.valid_children !== undefined && tmp.valid_children !== -1 && $.inArray((obj.type || 'default'), tmp.valid_children) === -1) {
6626 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 }) };
6629 if(m && obj.children_d && obj.parents) {
6631 for(i = 0, j = obj.children_d.length; i < j; i++) {
6632 d = Math.max(d, m[obj.children_d[i]].parents.length);
6634 d = d - obj.parents.length + 1;
6636 if(d <= 0 || d === undefined) { d = 1; }
6638 if(tmp.max_depth !== undefined && tmp.max_depth !== -1 && tmp.max_depth < d) {
6639 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 }) };
6642 par = this.get_node(par.parent);
6643 tmp = this.get_rules(par);
6652 * used to retrieve the type settings object for a node
6653 * @name get_rules(obj)
6654 * @param {mixed} obj the node to find the rules for
6658 this.get_rules = function (obj) {
6659 obj = this.get_node(obj);
6660 if(!obj) { return false; }
6661 var tmp = this.get_type(obj, true);
6662 if(tmp.max_depth === undefined) { tmp.max_depth = -1; }
6663 if(tmp.max_children === undefined) { tmp.max_children = -1; }
6664 if(tmp.valid_children === undefined) { tmp.valid_children = -1; }
6668 * used to retrieve the type string or settings object for a node
6669 * @name get_type(obj [, rules])
6670 * @param {mixed} obj the node to find the rules for
6671 * @param {Boolean} rules if set to `true` instead of a string the settings object will be returned
6672 * @return {String|Object}
6675 this.get_type = function (obj, rules) {
6676 obj = this.get_node(obj);
6677 return (!obj) ? false : ( rules ? $.extend({ 'type' : obj.type }, this.settings.types[obj.type]) : obj.type);
6680 * used to change a node's type
6681 * @name set_type(obj, type)
6682 * @param {mixed} obj the node to change
6683 * @param {String} type the new type
6686 this.set_type = function (obj, type) {
6687 var t, t1, t2, old_type, old_icon;
6688 if($.isArray(obj)) {
6690 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
6691 this.set_type(obj[t1], type);
6695 t = this.settings.types;
6696 obj = this.get_node(obj);
6697 if(!t[type] || !obj) { return false; }
6698 old_type = obj.type;
6699 old_icon = this.get_icon(obj);
6701 if(old_icon === true || (t[old_type] && t[old_type].icon && old_icon === t[old_type].icon)) {
6702 this.set_icon(obj, t[type].icon !== undefined ? t[type].icon : true);
6707 // include the types plugin by default
6708 // $.jstree.defaults.plugins.push("types");
6713 * Enforces that no nodes with the same name can coexist as siblings.
6717 * stores all defaults for the unique plugin
6718 * @name $.jstree.defaults.unique
6721 $.jstree.defaults.unique = {
6723 * Indicates if the comparison should be case sensitive. Default is `false`.
6724 * @name $.jstree.defaults.unique.case_sensitive
6727 case_sensitive : false,
6729 * 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)`.
6730 * @name $.jstree.defaults.unique.duplicate
6733 duplicate : function (name, counter) {
6734 return name + ' (' + counter + ')';
6738 $.jstree.plugins.unique = function (options, parent) {
6739 this.check = function (chk, obj, par, pos, more) {
6740 if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; }
6741 obj = obj && obj.id ? obj : this.get_node(obj);
6742 par = par && par.id ? par : this.get_node(par);
6743 if(!par || !par.children) { return true; }
6744 var n = chk === "rename_node" ? pos : obj.text,
6746 s = this.settings.unique.case_sensitive,
6747 m = this._model.data, i, j;
6748 for(i = 0, j = par.children.length; i < j; i++) {
6749 c.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase());
6751 if(!s) { n = n.toLowerCase(); }
6756 i = ($.inArray(n, c) === -1 || (obj.text && obj.text[ s ? 'toString' : 'toLowerCase']() === n));
6758 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 }) };
6762 i = ($.inArray(n, c) === -1);
6764 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 }) };
6768 i = ($.inArray(n, c) === -1);
6770 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 }) };
6774 i = (obj.parent === par.id || $.inArray(n, c) === -1);
6776 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 }) };
6782 this.create_node = function (par, node, pos, callback, is_loaded) {
6783 if(!node || node.text === undefined) {
6787 par = this.get_node(par);
6789 return parent.create_node.call(this, par, node, pos, callback, is_loaded);
6791 pos = pos === undefined ? "last" : pos;
6792 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
6793 return parent.create_node.call(this, par, node, pos, callback, is_loaded);
6795 if(!node) { node = {}; }
6796 var tmp, n, dpc, i, j, m = this._model.data, s = this.settings.unique.case_sensitive, cb = this.settings.unique.duplicate;
6797 n = tmp = this.get_string('New node');
6799 for(i = 0, j = par.children.length; i < j; i++) {
6800 dpc.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase());
6803 while($.inArray(s ? n : n.toLowerCase(), dpc) !== -1) {
6804 n = cb.call(this, tmp, (++i)).toString();
6808 return parent.create_node.call(this, par, node, pos, callback, is_loaded);
6812 // include the unique plugin by default
6813 // $.jstree.defaults.plugins.push("unique");
6817 * ### Wholerow plugin
6819 * Makes each node appear block level. Making selection easier. May cause slow down for large trees in old browsers.
6822 var div = document.createElement('DIV');
6823 div.setAttribute('unselectable','on');
6824 div.setAttribute('role','presentation');
6825 div.className = 'jstree-wholerow';
6826 div.innerHTML = ' ';
6827 $.jstree.plugins.wholerow = function (options, parent) {
6828 this.bind = function () {
6829 parent.bind.call(this);
6832 .on('ready.jstree set_state.jstree', $.proxy(function () {
6835 .on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
6836 //div.style.height = this._data.core.li_height + 'px';
6837 this.get_container_ul().addClass('jstree-wholerow-ul');
6839 .on("deselect_all.jstree", $.proxy(function (e, data) {
6840 this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
6842 .on("changed.jstree", $.proxy(function (e, data) {
6843 this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked');
6844 var tmp = false, i, j;
6845 for(i = 0, j = data.selected.length; i < j; i++) {
6846 tmp = this.get_node(data.selected[i], true);
6847 if(tmp && tmp.length) {
6848 tmp.children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
6852 .on("open_node.jstree", $.proxy(function (e, data) {
6853 this.get_node(data.node, true).find('.jstree-clicked').parent().children('.jstree-wholerow').addClass('jstree-wholerow-clicked');
6855 .on("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) {
6856 if(e.type === "hover_node" && this.is_disabled(data.node)) { return; }
6857 this.get_node(data.node, true).children('.jstree-wholerow')[e.type === "hover_node"?"addClass":"removeClass"]('jstree-wholerow-hovered');
6859 .on("contextmenu.jstree", ".jstree-wholerow", $.proxy(function (e) {
6861 var tmp = $.Event('contextmenu', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey, pageX : e.pageX, pageY : e.pageY });
6862 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp);
6864 .on("click.jstree", ".jstree-wholerow", function (e) {
6865 e.stopImmediatePropagation();
6866 var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
6867 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
6869 .on("click.jstree", ".jstree-leaf > .jstree-ocl", $.proxy(function (e) {
6870 e.stopImmediatePropagation();
6871 var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey });
6872 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus();
6874 .on("mouseover.jstree", ".jstree-wholerow, .jstree-icon", $.proxy(function (e) {
6875 e.stopImmediatePropagation();
6876 if(!this.is_disabled(e.currentTarget)) {
6877 this.hover_node(e.currentTarget);
6881 .on("mouseleave.jstree", ".jstree-node", $.proxy(function (e) {
6882 this.dehover_node(e.currentTarget);
6885 this.teardown = function () {
6886 if(this.settings.wholerow) {
6887 this.element.find(".jstree-wholerow").remove();
6889 parent.teardown.call(this);
6891 this.redraw_node = function(obj, deep, callback, force_render) {
6892 obj = parent.redraw_node.apply(this, arguments);
6894 var tmp = div.cloneNode(true);
6895 //tmp.style.height = this._data.core.li_height + 'px';
6896 if($.inArray(obj.id, this._data.core.selected) !== -1) { tmp.className += ' jstree-wholerow-clicked'; }
6897 obj.insertBefore(tmp, obj.childNodes[0]);
6902 // include the wholerow plugin by default
6903 // $.jstree.defaults.plugins.push("wholerow");
6907 if(document.registerElement && Object && Object.create) {
6908 var proto = Object.create(HTMLElement.prototype);
6909 proto.createdCallback = function () {
6910 var c = { core : {}, plugins : [] }, i;
6911 for(i in $.jstree.plugins) {
6912 if($.jstree.plugins.hasOwnProperty(i) && this.attributes[i]) {
6914 if(this.getAttribute(i) && JSON.parse(this.getAttribute(i))) {
6915 c[i] = JSON.parse(this.getAttribute(i));
6919 for(i in $.jstree.defaults.core) {
6920 if($.jstree.defaults.core.hasOwnProperty(i) && this.attributes[i]) {
6921 c.core[i] = JSON.parse(this.getAttribute(i)) || this.getAttribute(i);
6924 jQuery(this).jstree(c);
6926 // proto.attributeChangedCallback = function (name, previous, value) { };
6928 document.registerElement("vakata-jstree", { prototype: proto });