comparison vakata-jstree-3.3.5/src/jstree.js @ 5:aacd5f53ac99 draft

v2.0.0
author mingchen0919
date Wed, 18 Apr 2018 13:17:28 -0400
parents
children
comparison
equal deleted inserted replaced
4:0fdb0d5f53ce 5:aacd5f53ac99
1 /*!
2 * jsTree {{VERSION}}
3 * http://jstree.com/
4 *
5 * Copyright (c) 2014 Ivan Bozhanov (http://vakata.com)
6 *
7 * Licensed same as jquery - under the terms of the MIT License
8 * http://www.opensource.org/licenses/mit-license.php
9 */
10 /*!
11 * if using jslint please allow for the jQuery global and use following options:
12 * jslint: loopfunc: true, browser: true, ass: true, bitwise: true, continue: true, nomen: true, plusplus: true, regexp: true, unparam: true, todo: true, white: true
13 */
14 /*jshint -W083 */
15 /*globals jQuery, define, module, exports, require, window, document, postMessage */
16 (function (factory) {
17 "use strict";
18 if (typeof define === 'function' && define.amd) {
19 define(['jquery'], factory);
20 }
21 else if(typeof module !== 'undefined' && module.exports) {
22 module.exports = factory(require('jquery'));
23 }
24 else {
25 factory(jQuery);
26 }
27 }(function ($, undefined) {
28 "use strict";
29
30 // prevent another load? maybe there is a better way?
31 if($.jstree) {
32 return;
33 }
34
35 /**
36 * ### jsTree core functionality
37 */
38
39 // internal variables
40 var instance_counter = 0,
41 ccp_node = false,
42 ccp_mode = false,
43 ccp_inst = false,
44 themes_loaded = [],
45 src = $('script:last').attr('src'),
46 document = window.document; // local variable is always faster to access then a global
47
48 /**
49 * holds all jstree related functions and variables, including the actual class and methods to create, access and manipulate instances.
50 * @name $.jstree
51 */
52 $.jstree = {
53 /**
54 * specifies the jstree version in use
55 * @name $.jstree.version
56 */
57 version : '{{VERSION}}',
58 /**
59 * holds all the default options used when creating new instances
60 * @name $.jstree.defaults
61 */
62 defaults : {
63 /**
64 * configure which plugins will be active on an instance. Should be an array of strings, where each element is a plugin name. The default is `[]`
65 * @name $.jstree.defaults.plugins
66 */
67 plugins : []
68 },
69 /**
70 * stores all loaded jstree plugins (used internally)
71 * @name $.jstree.plugins
72 */
73 plugins : {},
74 path : src && src.indexOf('/') !== -1 ? src.replace(/\/[^\/]+$/,'') : '',
75 idregex : /[\\:&!^|()\[\]<>@*'+~#";.,=\- \/${}%?`]/g,
76 root : '#'
77 };
78
79 /**
80 * creates a jstree instance
81 * @name $.jstree.create(el [, options])
82 * @param {DOMElement|jQuery|String} el the element to create the instance on, can be jQuery extended or a selector
83 * @param {Object} options options for this instance (extends `$.jstree.defaults`)
84 * @return {jsTree} the new instance
85 */
86 $.jstree.create = function (el, options) {
87 var tmp = new $.jstree.core(++instance_counter),
88 opt = options;
89 options = $.extend(true, {}, $.jstree.defaults, options);
90 if(opt && opt.plugins) {
91 options.plugins = opt.plugins;
92 }
93 $.each(options.plugins, function (i, k) {
94 if(i !== 'core') {
95 tmp = tmp.plugin(k, options[k]);
96 }
97 });
98 $(el).data('jstree', tmp);
99 tmp.init(el, options);
100 return tmp;
101 };
102 /**
103 * remove all traces of jstree from the DOM and destroy all instances
104 * @name $.jstree.destroy()
105 */
106 $.jstree.destroy = function () {
107 $('.jstree:jstree').jstree('destroy');
108 $(document).off('.jstree');
109 };
110 /**
111 * the jstree class constructor, used only internally
112 * @private
113 * @name $.jstree.core(id)
114 * @param {Number} id this instance's index
115 */
116 $.jstree.core = function (id) {
117 this._id = id;
118 this._cnt = 0;
119 this._wrk = null;
120 this._data = {
121 core : {
122 themes : {
123 name : false,
124 dots : false,
125 icons : false,
126 ellipsis : false
127 },
128 selected : [],
129 last_error : {},
130 working : false,
131 worker_queue : [],
132 focused : null
133 }
134 };
135 };
136 /**
137 * get a reference to an existing instance
138 *
139 * __Examples__
140 *
141 * // provided a container with an ID of "tree", and a nested node with an ID of "branch"
142 * // all of there will return the same instance
143 * $.jstree.reference('tree');
144 * $.jstree.reference('#tree');
145 * $.jstree.reference($('#tree'));
146 * $.jstree.reference(document.getElementByID('tree'));
147 * $.jstree.reference('branch');
148 * $.jstree.reference('#branch');
149 * $.jstree.reference($('#branch'));
150 * $.jstree.reference(document.getElementByID('branch'));
151 *
152 * @name $.jstree.reference(needle)
153 * @param {DOMElement|jQuery|String} needle
154 * @return {jsTree|null} the instance or `null` if not found
155 */
156 $.jstree.reference = function (needle) {
157 var tmp = null,
158 obj = null;
159 if(needle && needle.id && (!needle.tagName || !needle.nodeType)) { needle = needle.id; }
160
161 if(!obj || !obj.length) {
162 try { obj = $(needle); } catch (ignore) { }
163 }
164 if(!obj || !obj.length) {
165 try { obj = $('#' + needle.replace($.jstree.idregex,'\\$&')); } catch (ignore) { }
166 }
167 if(obj && obj.length && (obj = obj.closest('.jstree')).length && (obj = obj.data('jstree'))) {
168 tmp = obj;
169 }
170 else {
171 $('.jstree').each(function () {
172 var inst = $(this).data('jstree');
173 if(inst && inst._model.data[needle]) {
174 tmp = inst;
175 return false;
176 }
177 });
178 }
179 return tmp;
180 };
181 /**
182 * Create an instance, get an instance or invoke a command on a instance.
183 *
184 * If there is no instance associated with the current node a new one is created and `arg` is used to extend `$.jstree.defaults` for this new instance. There would be no return value (chaining is not broken).
185 *
186 * If there is an existing instance and `arg` is a string the command specified by `arg` is executed on the instance, with any additional arguments passed to the function. If the function returns a value it will be returned (chaining could break depending on function).
187 *
188 * If there is an existing instance and `arg` is not a string the instance itself is returned (similar to `$.jstree.reference`).
189 *
190 * In any other case - nothing is returned and chaining is not broken.
191 *
192 * __Examples__
193 *
194 * $('#tree1').jstree(); // creates an instance
195 * $('#tree2').jstree({ plugins : [] }); // create an instance with some options
196 * $('#tree1').jstree('open_node', '#branch_1'); // call a method on an existing instance, passing additional arguments
197 * $('#tree2').jstree(); // get an existing instance (or create an instance)
198 * $('#tree2').jstree(true); // get an existing instance (will not create new instance)
199 * $('#branch_1').jstree().select_node('#branch_1'); // get an instance (using a nested element and call a method)
200 *
201 * @name $().jstree([arg])
202 * @param {String|Object} arg
203 * @return {Mixed}
204 */
205 $.fn.jstree = function (arg) {
206 // check for string argument
207 var is_method = (typeof arg === 'string'),
208 args = Array.prototype.slice.call(arguments, 1),
209 result = null;
210 if(arg === true && !this.length) { return false; }
211 this.each(function () {
212 // get the instance (if there is one) and method (if it exists)
213 var instance = $.jstree.reference(this),
214 method = is_method && instance ? instance[arg] : null;
215 // if calling a method, and method is available - execute on the instance
216 result = is_method && method ?
217 method.apply(instance, args) :
218 null;
219 // if there is no instance and no method is being called - create one
220 if(!instance && !is_method && (arg === undefined || $.isPlainObject(arg))) {
221 $.jstree.create(this, arg);
222 }
223 // if there is an instance and no method is called - return the instance
224 if( (instance && !is_method) || arg === true ) {
225 result = instance || false;
226 }
227 // if there was a method call which returned a result - break and return the value
228 if(result !== null && result !== undefined) {
229 return false;
230 }
231 });
232 // if there was a method call with a valid return value - return that, otherwise continue the chain
233 return result !== null && result !== undefined ?
234 result : this;
235 };
236 /**
237 * used to find elements containing an instance
238 *
239 * __Examples__
240 *
241 * $('div:jstree').each(function () {
242 * $(this).jstree('destroy');
243 * });
244 *
245 * @name $(':jstree')
246 * @return {jQuery}
247 */
248 $.expr.pseudos.jstree = $.expr.createPseudo(function(search) {
249 return function(a) {
250 return $(a).hasClass('jstree') &&
251 $(a).data('jstree') !== undefined;
252 };
253 });
254
255 /**
256 * stores all defaults for the core
257 * @name $.jstree.defaults.core
258 */
259 $.jstree.defaults.core = {
260 /**
261 * data configuration
262 *
263 * If left as `false` the HTML inside the jstree container element is used to populate the tree (that should be an unordered list with list items).
264 *
265 * You can also pass in a HTML string or a JSON array here.
266 *
267 * It is possible to pass in a standard jQuery-like AJAX config and jstree will automatically determine if the response is JSON or HTML and use that to populate the tree.
268 * In addition to the standard jQuery ajax options here you can suppy functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node is being loaded, the return value of those functions will be used.
269 *
270 * The last option is to specify a function, that function will receive the node being loaded as argument and a second param which is a function which should be called with the result.
271 *
272 * __Examples__
273 *
274 * // AJAX
275 * $('#tree').jstree({
276 * 'core' : {
277 * 'data' : {
278 * 'url' : '/get/children/',
279 * 'data' : function (node) {
280 * return { 'id' : node.id };
281 * }
282 * }
283 * });
284 *
285 * // direct data
286 * $('#tree').jstree({
287 * 'core' : {
288 * 'data' : [
289 * 'Simple root node',
290 * {
291 * 'id' : 'node_2',
292 * 'text' : 'Root node with options',
293 * 'state' : { 'opened' : true, 'selected' : true },
294 * 'children' : [ { 'text' : 'Child 1' }, 'Child 2']
295 * }
296 * ]
297 * }
298 * });
299 *
300 * // function
301 * $('#tree').jstree({
302 * 'core' : {
303 * 'data' : function (obj, callback) {
304 * callback.call(this, ['Root 1', 'Root 2']);
305 * }
306 * });
307 *
308 * @name $.jstree.defaults.core.data
309 */
310 data : false,
311 /**
312 * configure the various strings used throughout the tree
313 *
314 * You can use an object where the key is the string you need to replace and the value is your replacement.
315 * Another option is to specify a function which will be called with an argument of the needed string and should return the replacement.
316 * If left as `false` no replacement is made.
317 *
318 * __Examples__
319 *
320 * $('#tree').jstree({
321 * 'core' : {
322 * 'strings' : {
323 * 'Loading ...' : 'Please wait ...'
324 * }
325 * }
326 * });
327 *
328 * @name $.jstree.defaults.core.strings
329 */
330 strings : false,
331 /**
332 * determines what happens when a user tries to modify the structure of the tree
333 * If left as `false` all operations like create, rename, delete, move or copy are prevented.
334 * You can set this to `true` to allow all interactions or use a function to have better control.
335 *
336 * __Examples__
337 *
338 * $('#tree').jstree({
339 * 'core' : {
340 * 'check_callback' : function (operation, node, node_parent, node_position, more) {
341 * // operation can be 'create_node', 'rename_node', 'delete_node', 'move_node', 'copy_node' or 'edit'
342 * // in case of 'rename_node' node_position is filled with the new node name
343 * return operation === 'rename_node' ? true : false;
344 * }
345 * }
346 * });
347 *
348 * @name $.jstree.defaults.core.check_callback
349 */
350 check_callback : false,
351 /**
352 * a callback called with a single object parameter in the instance's scope when something goes wrong (operation prevented, ajax failed, etc)
353 * @name $.jstree.defaults.core.error
354 */
355 error : $.noop,
356 /**
357 * the open / close animation duration in milliseconds - set this to `false` to disable the animation (default is `200`)
358 * @name $.jstree.defaults.core.animation
359 */
360 animation : 200,
361 /**
362 * a boolean indicating if multiple nodes can be selected
363 * @name $.jstree.defaults.core.multiple
364 */
365 multiple : true,
366 /**
367 * theme configuration object
368 * @name $.jstree.defaults.core.themes
369 */
370 themes : {
371 /**
372 * the name of the theme to use (if left as `false` the default theme is used)
373 * @name $.jstree.defaults.core.themes.name
374 */
375 name : false,
376 /**
377 * the URL of the theme's CSS file, leave this as `false` if you have manually included the theme CSS (recommended). You can set this to `true` too which will try to autoload the theme.
378 * @name $.jstree.defaults.core.themes.url
379 */
380 url : false,
381 /**
382 * the location of all jstree themes - only used if `url` is set to `true`
383 * @name $.jstree.defaults.core.themes.dir
384 */
385 dir : false,
386 /**
387 * a boolean indicating if connecting dots are shown
388 * @name $.jstree.defaults.core.themes.dots
389 */
390 dots : true,
391 /**
392 * a boolean indicating if node icons are shown
393 * @name $.jstree.defaults.core.themes.icons
394 */
395 icons : true,
396 /**
397 * a boolean indicating if node ellipsis should be shown - this only works with a fixed with on the container
398 * @name $.jstree.defaults.core.themes.ellipsis
399 */
400 ellipsis : false,
401 /**
402 * a boolean indicating if the tree background is striped
403 * @name $.jstree.defaults.core.themes.stripes
404 */
405 stripes : false,
406 /**
407 * a string (or boolean `false`) specifying the theme variant to use (if the theme supports variants)
408 * @name $.jstree.defaults.core.themes.variant
409 */
410 variant : false,
411 /**
412 * a boolean specifying if a reponsive version of the theme should kick in on smaller screens (if the theme supports it). Defaults to `false`.
413 * @name $.jstree.defaults.core.themes.responsive
414 */
415 responsive : false
416 },
417 /**
418 * if left as `true` all parents of all selected nodes will be opened once the tree loads (so that all selected nodes are visible to the user)
419 * @name $.jstree.defaults.core.expand_selected_onload
420 */
421 expand_selected_onload : true,
422 /**
423 * if left as `true` web workers will be used to parse incoming JSON data where possible, so that the UI will not be blocked by large requests. Workers are however about 30% slower. Defaults to `true`
424 * @name $.jstree.defaults.core.worker
425 */
426 worker : true,
427 /**
428 * Force node text to plain text (and escape HTML). Defaults to `false`
429 * @name $.jstree.defaults.core.force_text
430 */
431 force_text : false,
432 /**
433 * Should the node should be toggled if the text is double clicked . Defaults to `true`
434 * @name $.jstree.defaults.core.dblclick_toggle
435 */
436 dblclick_toggle : true,
437 /**
438 * Should the loaded nodes be part of the state. Defaults to `false`
439 * @name $.jstree.defaults.core.loaded_state
440 */
441 loaded_state : false,
442 /**
443 * Should the last active node be focused when the tree container is blurred and the focused again. This helps working with screen readers. Defaults to `true`
444 * @name $.jstree.defaults.core.restore_focus
445 */
446 restore_focus : true,
447 /**
448 * Default keyboard shortcuts (an object where each key is the button name or combo - like 'enter', 'ctrl-space', 'p', etc and the value is the function to execute in the instance's scope)
449 * @name $.jstree.defaults.core.keyboard
450 */
451 keyboard : {
452 'ctrl-space': function (e) {
453 // aria defines space only with Ctrl
454 e.type = "click";
455 $(e.currentTarget).trigger(e);
456 },
457 'enter': function (e) {
458 // enter
459 e.type = "click";
460 $(e.currentTarget).trigger(e);
461 },
462 'left': function (e) {
463 // left
464 e.preventDefault();
465 if(this.is_open(e.currentTarget)) {
466 this.close_node(e.currentTarget);
467 }
468 else {
469 var o = this.get_parent(e.currentTarget);
470 if(o && o.id !== $.jstree.root) { this.get_node(o, true).children('.jstree-anchor').focus(); }
471 }
472 },
473 'up': function (e) {
474 // up
475 e.preventDefault();
476 var o = this.get_prev_dom(e.currentTarget);
477 if(o && o.length) { o.children('.jstree-anchor').focus(); }
478 },
479 'right': function (e) {
480 // right
481 e.preventDefault();
482 if(this.is_closed(e.currentTarget)) {
483 this.open_node(e.currentTarget, function (o) { this.get_node(o, true).children('.jstree-anchor').focus(); });
484 }
485 else if (this.is_open(e.currentTarget)) {
486 var o = this.get_node(e.currentTarget, true).children('.jstree-children')[0];
487 if(o) { $(this._firstChild(o)).children('.jstree-anchor').focus(); }
488 }
489 },
490 'down': function (e) {
491 // down
492 e.preventDefault();
493 var o = this.get_next_dom(e.currentTarget);
494 if(o && o.length) { o.children('.jstree-anchor').focus(); }
495 },
496 '*': function (e) {
497 // aria defines * on numpad as open_all - not very common
498 this.open_all();
499 },
500 'home': function (e) {
501 // home
502 e.preventDefault();
503 var o = this._firstChild(this.get_container_ul()[0]);
504 if(o) { $(o).children('.jstree-anchor').filter(':visible').focus(); }
505 },
506 'end': function (e) {
507 // end
508 e.preventDefault();
509 this.element.find('.jstree-anchor').filter(':visible').last().focus();
510 },
511 'f2': function (e) {
512 // f2 - safe to include - if check_callback is false it will fail
513 e.preventDefault();
514 this.edit(e.currentTarget);
515 }
516 }
517 };
518 $.jstree.core.prototype = {
519 /**
520 * used to decorate an instance with a plugin. Used internally.
521 * @private
522 * @name plugin(deco [, opts])
523 * @param {String} deco the plugin to decorate with
524 * @param {Object} opts options for the plugin
525 * @return {jsTree}
526 */
527 plugin : function (deco, opts) {
528 var Child = $.jstree.plugins[deco];
529 if(Child) {
530 this._data[deco] = {};
531 Child.prototype = this;
532 return new Child(opts, this);
533 }
534 return this;
535 },
536 /**
537 * initialize the instance. Used internally.
538 * @private
539 * @name init(el, optons)
540 * @param {DOMElement|jQuery|String} el the element we are transforming
541 * @param {Object} options options for this instance
542 * @trigger init.jstree, loading.jstree, loaded.jstree, ready.jstree, changed.jstree
543 */
544 init : function (el, options) {
545 this._model = {
546 data : {},
547 changed : [],
548 force_full_redraw : false,
549 redraw_timeout : false,
550 default_state : {
551 loaded : true,
552 opened : false,
553 selected : false,
554 disabled : false
555 }
556 };
557 this._model.data[$.jstree.root] = {
558 id : $.jstree.root,
559 parent : null,
560 parents : [],
561 children : [],
562 children_d : [],
563 state : { loaded : false }
564 };
565
566 this.element = $(el).addClass('jstree jstree-' + this._id);
567 this.settings = options;
568
569 this._data.core.ready = false;
570 this._data.core.loaded = false;
571 this._data.core.rtl = (this.element.css("direction") === "rtl");
572 this.element[this._data.core.rtl ? 'addClass' : 'removeClass']("jstree-rtl");
573 this.element.attr('role','tree');
574 if(this.settings.core.multiple) {
575 this.element.attr('aria-multiselectable', true);
576 }
577 if(!this.element.attr('tabindex')) {
578 this.element.attr('tabindex','0');
579 }
580
581 this.bind();
582 /**
583 * triggered after all events are bound
584 * @event
585 * @name init.jstree
586 */
587 this.trigger("init");
588
589 this._data.core.original_container_html = this.element.find(" > ul > li").clone(true);
590 this._data.core.original_container_html
591 .find("li").addBack()
592 .contents().filter(function() {
593 return this.nodeType === 3 && (!this.nodeValue || /^\s+$/.test(this.nodeValue));
594 })
595 .remove();
596 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>");
597 this.element.attr('aria-activedescendant','j' + this._id + '_loading');
598 this._data.core.li_height = this.get_container_ul().children("li").first().outerHeight() || 24;
599 this._data.core.node = this._create_prototype_node();
600 /**
601 * triggered after the loading text is shown and before loading starts
602 * @event
603 * @name loading.jstree
604 */
605 this.trigger("loading");
606 this.load_node($.jstree.root);
607 },
608 /**
609 * destroy an instance
610 * @name destroy()
611 * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact
612 */
613 destroy : function (keep_html) {
614 /**
615 * triggered before the tree is destroyed
616 * @event
617 * @name destroy.jstree
618 */
619 this.trigger("destroy");
620 if(this._wrk) {
621 try {
622 window.URL.revokeObjectURL(this._wrk);
623 this._wrk = null;
624 }
625 catch (ignore) { }
626 }
627 if(!keep_html) { this.element.empty(); }
628 this.teardown();
629 },
630 /**
631 * Create a prototype node
632 * @name _create_prototype_node()
633 * @return {DOMElement}
634 */
635 _create_prototype_node : function () {
636 var _node = document.createElement('LI'), _temp1, _temp2;
637 _node.setAttribute('role', 'treeitem');
638 _temp1 = document.createElement('I');
639 _temp1.className = 'jstree-icon jstree-ocl';
640 _temp1.setAttribute('role', 'presentation');
641 _node.appendChild(_temp1);
642 _temp1 = document.createElement('A');
643 _temp1.className = 'jstree-anchor';
644 _temp1.setAttribute('href','#');
645 _temp1.setAttribute('tabindex','-1');
646 _temp2 = document.createElement('I');
647 _temp2.className = 'jstree-icon jstree-themeicon';
648 _temp2.setAttribute('role', 'presentation');
649 _temp1.appendChild(_temp2);
650 _node.appendChild(_temp1);
651 _temp1 = _temp2 = null;
652
653 return _node;
654 },
655 _kbevent_to_func : function (e) {
656 var keys = {
657 8: "Backspace", 9: "Tab", 13: "Return", 19: "Pause", 27: "Esc",
658 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home",
659 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "Print", 45: "Insert",
660 46: "Delete", 96: "Numpad0", 97: "Numpad1", 98: "Numpad2", 99 : "Numpad3",
661 100: "Numpad4", 101: "Numpad5", 102: "Numpad6", 103: "Numpad7",
662 104: "Numpad8", 105: "Numpad9", '-13': "NumpadEnter", 112: "F1",
663 113: "F2", 114: "F3", 115: "F4", 116: "F5", 117: "F6", 118: "F7",
664 119: "F8", 120: "F9", 121: "F10", 122: "F11", 123: "F12", 144: "Numlock",
665 145: "Scrolllock", 16: 'Shift', 17: 'Ctrl', 18: 'Alt',
666 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5',
667 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a',
668 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h',
669 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o',
670 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v',
671 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.',
672 186: ';', 187: '=', 188: ',', 189: '-', 190: '.', 191: '/', 192: '`',
673 219: '[', 220: '\\',221: ']', 222: "'", 111: '/', 106: '*', 173: '-'
674 };
675 var parts = [];
676 if (e.ctrlKey) { parts.push('ctrl'); }
677 if (e.altKey) { parts.push('alt'); }
678 if (e.shiftKey) { parts.push('shift'); }
679 parts.push(keys[e.which] || e.which);
680 parts = parts.sort().join('-').toLowerCase();
681
682 var kb = this.settings.core.keyboard, i, tmp;
683 for (i in kb) {
684 if (kb.hasOwnProperty(i)) {
685 tmp = i;
686 if (tmp !== '-' && tmp !== '+') {
687 tmp = tmp.replace('--', '-MINUS').replace('+-', '-MINUS').replace('++', '-PLUS').replace('-+', '-PLUS');
688 tmp = tmp.split(/-|\+/).sort().join('-').replace('MINUS', '-').replace('PLUS', '+').toLowerCase();
689 }
690 if (tmp === parts) {
691 return kb[i];
692 }
693 }
694 }
695 return null;
696 },
697 /**
698 * part of the destroying of an instance. Used internally.
699 * @private
700 * @name teardown()
701 */
702 teardown : function () {
703 this.unbind();
704 this.element
705 .removeClass('jstree')
706 .removeData('jstree')
707 .find("[class^='jstree']")
708 .addBack()
709 .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); });
710 this.element = null;
711 },
712 /**
713 * bind all events. Used internally.
714 * @private
715 * @name bind()
716 */
717 bind : function () {
718 var word = '',
719 tout = null,
720 was_click = 0;
721 this.element
722 .on("dblclick.jstree", function (e) {
723 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
724 if(document.selection && document.selection.empty) {
725 document.selection.empty();
726 }
727 else {
728 if(window.getSelection) {
729 var sel = window.getSelection();
730 try {
731 sel.removeAllRanges();
732 sel.collapse();
733 } catch (ignore) { }
734 }
735 }
736 })
737 .on("mousedown.jstree", $.proxy(function (e) {
738 if(e.target === this.element[0]) {
739 e.preventDefault(); // prevent losing focus when clicking scroll arrows (FF, Chrome)
740 was_click = +(new Date()); // ie does not allow to prevent losing focus
741 }
742 }, this))
743 .on("mousedown.jstree", ".jstree-ocl", function (e) {
744 e.preventDefault(); // prevent any node inside from losing focus when clicking the open/close icon
745 })
746 .on("click.jstree", ".jstree-ocl", $.proxy(function (e) {
747 this.toggle_node(e.target);
748 }, this))
749 .on("dblclick.jstree", ".jstree-anchor", $.proxy(function (e) {
750 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
751 if(this.settings.core.dblclick_toggle) {
752 this.toggle_node(e.target);
753 }
754 }, this))
755 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
756 e.preventDefault();
757 if(e.currentTarget !== document.activeElement) { $(e.currentTarget).focus(); }
758 this.activate_node(e.currentTarget, e);
759 }, this))
760 .on('keydown.jstree', '.jstree-anchor', $.proxy(function (e) {
761 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
762 if(this._data.core.rtl) {
763 if(e.which === 37) { e.which = 39; }
764 else if(e.which === 39) { e.which = 37; }
765 }
766 var f = this._kbevent_to_func(e);
767 if (f) {
768 var r = f.call(this, e);
769 if (r === false || r === true) {
770 return r;
771 }
772 }
773 }, this))
774 .on("load_node.jstree", $.proxy(function (e, data) {
775 if(data.status) {
776 if(data.node.id === $.jstree.root && !this._data.core.loaded) {
777 this._data.core.loaded = true;
778 if(this._firstChild(this.get_container_ul()[0])) {
779 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
780 }
781 /**
782 * triggered after the root node is loaded for the first time
783 * @event
784 * @name loaded.jstree
785 */
786 this.trigger("loaded");
787 }
788 if(!this._data.core.ready) {
789 setTimeout($.proxy(function() {
790 if(this.element && !this.get_container_ul().find('.jstree-loading').length) {
791 this._data.core.ready = true;
792 if(this._data.core.selected.length) {
793 if(this.settings.core.expand_selected_onload) {
794 var tmp = [], i, j;
795 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
796 tmp = tmp.concat(this._model.data[this._data.core.selected[i]].parents);
797 }
798 tmp = $.vakata.array_unique(tmp);
799 for(i = 0, j = tmp.length; i < j; i++) {
800 this.open_node(tmp[i], false, 0);
801 }
802 }
803 this.trigger('changed', { 'action' : 'ready', 'selected' : this._data.core.selected });
804 }
805 /**
806 * triggered after all nodes are finished loading
807 * @event
808 * @name ready.jstree
809 */
810 this.trigger("ready");
811 }
812 }, this), 0);
813 }
814 }
815 }, this))
816 // quick searching when the tree is focused
817 .on('keypress.jstree', $.proxy(function (e) {
818 if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
819 if(tout) { clearTimeout(tout); }
820 tout = setTimeout(function () {
821 word = '';
822 }, 500);
823
824 var chr = String.fromCharCode(e.which).toLowerCase(),
825 col = this.element.find('.jstree-anchor').filter(':visible'),
826 ind = col.index(document.activeElement) || 0,
827 end = false;
828 word += chr;
829
830 // match for whole word from current node down (including the current node)
831 if(word.length > 1) {
832 col.slice(ind).each($.proxy(function (i, v) {
833 if($(v).text().toLowerCase().indexOf(word) === 0) {
834 $(v).focus();
835 end = true;
836 return false;
837 }
838 }, this));
839 if(end) { return; }
840
841 // match for whole word from the beginning of the tree
842 col.slice(0, ind).each($.proxy(function (i, v) {
843 if($(v).text().toLowerCase().indexOf(word) === 0) {
844 $(v).focus();
845 end = true;
846 return false;
847 }
848 }, this));
849 if(end) { return; }
850 }
851 // list nodes that start with that letter (only if word consists of a single char)
852 if(new RegExp('^' + chr.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '+$').test(word)) {
853 // search for the next node starting with that letter
854 col.slice(ind + 1).each($.proxy(function (i, v) {
855 if($(v).text().toLowerCase().charAt(0) === chr) {
856 $(v).focus();
857 end = true;
858 return false;
859 }
860 }, this));
861 if(end) { return; }
862
863 // search from the beginning
864 col.slice(0, ind + 1).each($.proxy(function (i, v) {
865 if($(v).text().toLowerCase().charAt(0) === chr) {
866 $(v).focus();
867 end = true;
868 return false;
869 }
870 }, this));
871 if(end) { return; }
872 }
873 }, this))
874 // THEME RELATED
875 .on("init.jstree", $.proxy(function () {
876 var s = this.settings.core.themes;
877 this._data.core.themes.dots = s.dots;
878 this._data.core.themes.stripes = s.stripes;
879 this._data.core.themes.icons = s.icons;
880 this._data.core.themes.ellipsis = s.ellipsis;
881 this.set_theme(s.name || "default", s.url);
882 this.set_theme_variant(s.variant);
883 }, this))
884 .on("loading.jstree", $.proxy(function () {
885 this[ this._data.core.themes.dots ? "show_dots" : "hide_dots" ]();
886 this[ this._data.core.themes.icons ? "show_icons" : "hide_icons" ]();
887 this[ this._data.core.themes.stripes ? "show_stripes" : "hide_stripes" ]();
888 this[ this._data.core.themes.ellipsis ? "show_ellipsis" : "hide_ellipsis" ]();
889 }, this))
890 .on('blur.jstree', '.jstree-anchor', $.proxy(function (e) {
891 this._data.core.focused = null;
892 $(e.currentTarget).filter('.jstree-hovered').mouseleave();
893 this.element.attr('tabindex', '0');
894 }, this))
895 .on('focus.jstree', '.jstree-anchor', $.proxy(function (e) {
896 var tmp = this.get_node(e.currentTarget);
897 if(tmp && tmp.id) {
898 this._data.core.focused = tmp.id;
899 }
900 this.element.find('.jstree-hovered').not(e.currentTarget).mouseleave();
901 $(e.currentTarget).mouseenter();
902 this.element.attr('tabindex', '-1');
903 }, this))
904 .on('focus.jstree', $.proxy(function () {
905 if(+(new Date()) - was_click > 500 && !this._data.core.focused && this.settings.core.restore_focus) {
906 was_click = 0;
907 var act = this.get_node(this.element.attr('aria-activedescendant'), true);
908 if(act) {
909 act.find('> .jstree-anchor').focus();
910 }
911 }
912 }, this))
913 .on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e) {
914 this.hover_node(e.currentTarget);
915 }, this))
916 .on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e) {
917 this.dehover_node(e.currentTarget);
918 }, this));
919 },
920 /**
921 * part of the destroying of an instance. Used internally.
922 * @private
923 * @name unbind()
924 */
925 unbind : function () {
926 this.element.off('.jstree');
927 $(document).off('.jstree-' + this._id);
928 },
929 /**
930 * trigger an event. Used internally.
931 * @private
932 * @name trigger(ev [, data])
933 * @param {String} ev the name of the event to trigger
934 * @param {Object} data additional data to pass with the event
935 */
936 trigger : function (ev, data) {
937 if(!data) {
938 data = {};
939 }
940 data.instance = this;
941 this.element.triggerHandler(ev.replace('.jstree','') + '.jstree', data);
942 },
943 /**
944 * returns the jQuery extended instance container
945 * @name get_container()
946 * @return {jQuery}
947 */
948 get_container : function () {
949 return this.element;
950 },
951 /**
952 * returns the jQuery extended main UL node inside the instance container. Used internally.
953 * @private
954 * @name get_container_ul()
955 * @return {jQuery}
956 */
957 get_container_ul : function () {
958 return this.element.children(".jstree-children").first();
959 },
960 /**
961 * gets string replacements (localization). Used internally.
962 * @private
963 * @name get_string(key)
964 * @param {String} key
965 * @return {String}
966 */
967 get_string : function (key) {
968 var a = this.settings.core.strings;
969 if($.isFunction(a)) { return a.call(this, key); }
970 if(a && a[key]) { return a[key]; }
971 return key;
972 },
973 /**
974 * gets the first child of a DOM node. Used internally.
975 * @private
976 * @name _firstChild(dom)
977 * @param {DOMElement} dom
978 * @return {DOMElement}
979 */
980 _firstChild : function (dom) {
981 dom = dom ? dom.firstChild : null;
982 while(dom !== null && dom.nodeType !== 1) {
983 dom = dom.nextSibling;
984 }
985 return dom;
986 },
987 /**
988 * gets the next sibling of a DOM node. Used internally.
989 * @private
990 * @name _nextSibling(dom)
991 * @param {DOMElement} dom
992 * @return {DOMElement}
993 */
994 _nextSibling : function (dom) {
995 dom = dom ? dom.nextSibling : null;
996 while(dom !== null && dom.nodeType !== 1) {
997 dom = dom.nextSibling;
998 }
999 return dom;
1000 },
1001 /**
1002 * gets the previous sibling of a DOM node. Used internally.
1003 * @private
1004 * @name _previousSibling(dom)
1005 * @param {DOMElement} dom
1006 * @return {DOMElement}
1007 */
1008 _previousSibling : function (dom) {
1009 dom = dom ? dom.previousSibling : null;
1010 while(dom !== null && dom.nodeType !== 1) {
1011 dom = dom.previousSibling;
1012 }
1013 return dom;
1014 },
1015 /**
1016 * 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)
1017 * @name get_node(obj [, as_dom])
1018 * @param {mixed} obj
1019 * @param {Boolean} as_dom
1020 * @return {Object|jQuery}
1021 */
1022 get_node : function (obj, as_dom) {
1023 if(obj && obj.id) {
1024 obj = obj.id;
1025 }
1026 var dom;
1027 try {
1028 if(this._model.data[obj]) {
1029 obj = this._model.data[obj];
1030 }
1031 else if(typeof obj === "string" && this._model.data[obj.replace(/^#/, '')]) {
1032 obj = this._model.data[obj.replace(/^#/, '')];
1033 }
1034 else if(typeof obj === "string" && (dom = $('#' + obj.replace($.jstree.idregex,'\\$&'), this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) {
1035 obj = this._model.data[dom.closest('.jstree-node').attr('id')];
1036 }
1037 else if((dom = $(obj, this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) {
1038 obj = this._model.data[dom.closest('.jstree-node').attr('id')];
1039 }
1040 else if((dom = $(obj, this.element)).length && dom.hasClass('jstree')) {
1041 obj = this._model.data[$.jstree.root];
1042 }
1043 else {
1044 return false;
1045 }
1046
1047 if(as_dom) {
1048 obj = obj.id === $.jstree.root ? this.element : $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element);
1049 }
1050 return obj;
1051 } catch (ex) { return false; }
1052 },
1053 /**
1054 * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array)
1055 * @name get_path(obj [, glue, ids])
1056 * @param {mixed} obj the node
1057 * @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
1058 * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used
1059 * @return {mixed}
1060 */
1061 get_path : function (obj, glue, ids) {
1062 obj = obj.parents ? obj : this.get_node(obj);
1063 if(!obj || obj.id === $.jstree.root || !obj.parents) {
1064 return false;
1065 }
1066 var i, j, p = [];
1067 p.push(ids ? obj.id : obj.text);
1068 for(i = 0, j = obj.parents.length; i < j; i++) {
1069 p.push(ids ? obj.parents[i] : this.get_text(obj.parents[i]));
1070 }
1071 p = p.reverse().slice(1);
1072 return glue ? p.join(glue) : p;
1073 },
1074 /**
1075 * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
1076 * @name get_next_dom(obj [, strict])
1077 * @param {mixed} obj
1078 * @param {Boolean} strict
1079 * @return {jQuery}
1080 */
1081 get_next_dom : function (obj, strict) {
1082 var tmp;
1083 obj = this.get_node(obj, true);
1084 if(obj[0] === this.element[0]) {
1085 tmp = this._firstChild(this.get_container_ul()[0]);
1086 while (tmp && tmp.offsetHeight === 0) {
1087 tmp = this._nextSibling(tmp);
1088 }
1089 return tmp ? $(tmp) : false;
1090 }
1091 if(!obj || !obj.length) {
1092 return false;
1093 }
1094 if(strict) {
1095 tmp = obj[0];
1096 do {
1097 tmp = this._nextSibling(tmp);
1098 } while (tmp && tmp.offsetHeight === 0);
1099 return tmp ? $(tmp) : false;
1100 }
1101 if(obj.hasClass("jstree-open")) {
1102 tmp = this._firstChild(obj.children('.jstree-children')[0]);
1103 while (tmp && tmp.offsetHeight === 0) {
1104 tmp = this._nextSibling(tmp);
1105 }
1106 if(tmp !== null) {
1107 return $(tmp);
1108 }
1109 }
1110 tmp = obj[0];
1111 do {
1112 tmp = this._nextSibling(tmp);
1113 } while (tmp && tmp.offsetHeight === 0);
1114 if(tmp !== null) {
1115 return $(tmp);
1116 }
1117 return obj.parentsUntil(".jstree",".jstree-node").nextAll(".jstree-node:visible").first();
1118 },
1119 /**
1120 * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned.
1121 * @name get_prev_dom(obj [, strict])
1122 * @param {mixed} obj
1123 * @param {Boolean} strict
1124 * @return {jQuery}
1125 */
1126 get_prev_dom : function (obj, strict) {
1127 var tmp;
1128 obj = this.get_node(obj, true);
1129 if(obj[0] === this.element[0]) {
1130 tmp = this.get_container_ul()[0].lastChild;
1131 while (tmp && tmp.offsetHeight === 0) {
1132 tmp = this._previousSibling(tmp);
1133 }
1134 return tmp ? $(tmp) : false;
1135 }
1136 if(!obj || !obj.length) {
1137 return false;
1138 }
1139 if(strict) {
1140 tmp = obj[0];
1141 do {
1142 tmp = this._previousSibling(tmp);
1143 } while (tmp && tmp.offsetHeight === 0);
1144 return tmp ? $(tmp) : false;
1145 }
1146 tmp = obj[0];
1147 do {
1148 tmp = this._previousSibling(tmp);
1149 } while (tmp && tmp.offsetHeight === 0);
1150 if(tmp !== null) {
1151 obj = $(tmp);
1152 while(obj.hasClass("jstree-open")) {
1153 obj = obj.children(".jstree-children").first().children(".jstree-node:visible:last");
1154 }
1155 return obj;
1156 }
1157 tmp = obj[0].parentNode.parentNode;
1158 return tmp && tmp.className && tmp.className.indexOf('jstree-node') !== -1 ? $(tmp) : false;
1159 },
1160 /**
1161 * get the parent ID of a node
1162 * @name get_parent(obj)
1163 * @param {mixed} obj
1164 * @return {String}
1165 */
1166 get_parent : function (obj) {
1167 obj = this.get_node(obj);
1168 if(!obj || obj.id === $.jstree.root) {
1169 return false;
1170 }
1171 return obj.parent;
1172 },
1173 /**
1174 * get a jQuery collection of all the children of a node (node must be rendered), returns false on error
1175 * @name get_children_dom(obj)
1176 * @param {mixed} obj
1177 * @return {jQuery}
1178 */
1179 get_children_dom : function (obj) {
1180 obj = this.get_node(obj, true);
1181 if(obj[0] === this.element[0]) {
1182 return this.get_container_ul().children(".jstree-node");
1183 }
1184 if(!obj || !obj.length) {
1185 return false;
1186 }
1187 return obj.children(".jstree-children").children(".jstree-node");
1188 },
1189 /**
1190 * checks if a node has children
1191 * @name is_parent(obj)
1192 * @param {mixed} obj
1193 * @return {Boolean}
1194 */
1195 is_parent : function (obj) {
1196 obj = this.get_node(obj);
1197 return obj && (obj.state.loaded === false || obj.children.length > 0);
1198 },
1199 /**
1200 * checks if a node is loaded (its children are available)
1201 * @name is_loaded(obj)
1202 * @param {mixed} obj
1203 * @return {Boolean}
1204 */
1205 is_loaded : function (obj) {
1206 obj = this.get_node(obj);
1207 return obj && obj.state.loaded;
1208 },
1209 /**
1210 * check if a node is currently loading (fetching children)
1211 * @name is_loading(obj)
1212 * @param {mixed} obj
1213 * @return {Boolean}
1214 */
1215 is_loading : function (obj) {
1216 obj = this.get_node(obj);
1217 return obj && obj.state && obj.state.loading;
1218 },
1219 /**
1220 * check if a node is opened
1221 * @name is_open(obj)
1222 * @param {mixed} obj
1223 * @return {Boolean}
1224 */
1225 is_open : function (obj) {
1226 obj = this.get_node(obj);
1227 return obj && obj.state.opened;
1228 },
1229 /**
1230 * check if a node is in a closed state
1231 * @name is_closed(obj)
1232 * @param {mixed} obj
1233 * @return {Boolean}
1234 */
1235 is_closed : function (obj) {
1236 obj = this.get_node(obj);
1237 return obj && this.is_parent(obj) && !obj.state.opened;
1238 },
1239 /**
1240 * check if a node has no children
1241 * @name is_leaf(obj)
1242 * @param {mixed} obj
1243 * @return {Boolean}
1244 */
1245 is_leaf : function (obj) {
1246 return !this.is_parent(obj);
1247 },
1248 /**
1249 * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array.
1250 * @name load_node(obj [, callback])
1251 * @param {mixed} obj
1252 * @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
1253 * @return {Boolean}
1254 * @trigger load_node.jstree
1255 */
1256 load_node : function (obj, callback) {
1257 var k, l, i, j, c;
1258 if($.isArray(obj)) {
1259 this._load_nodes(obj.slice(), callback);
1260 return true;
1261 }
1262 obj = this.get_node(obj);
1263 if(!obj) {
1264 if(callback) { callback.call(this, obj, false); }
1265 return false;
1266 }
1267 // 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?
1268 if(obj.state.loaded) {
1269 obj.state.loaded = false;
1270 for(i = 0, j = obj.parents.length; i < j; i++) {
1271 this._model.data[obj.parents[i]].children_d = $.vakata.array_filter(this._model.data[obj.parents[i]].children_d, function (v) {
1272 return $.inArray(v, obj.children_d) === -1;
1273 });
1274 }
1275 for(k = 0, l = obj.children_d.length; k < l; k++) {
1276 if(this._model.data[obj.children_d[k]].state.selected) {
1277 c = true;
1278 }
1279 delete this._model.data[obj.children_d[k]];
1280 }
1281 if (c) {
1282 this._data.core.selected = $.vakata.array_filter(this._data.core.selected, function (v) {
1283 return $.inArray(v, obj.children_d) === -1;
1284 });
1285 }
1286 obj.children = [];
1287 obj.children_d = [];
1288 if(c) {
1289 this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected });
1290 }
1291 }
1292 obj.state.failed = false;
1293 obj.state.loading = true;
1294 this.get_node(obj, true).addClass("jstree-loading").attr('aria-busy',true);
1295 this._load_node(obj, $.proxy(function (status) {
1296 obj = this._model.data[obj.id];
1297 obj.state.loading = false;
1298 obj.state.loaded = status;
1299 obj.state.failed = !obj.state.loaded;
1300 var dom = this.get_node(obj, true), i = 0, j = 0, m = this._model.data, has_children = false;
1301 for(i = 0, j = obj.children.length; i < j; i++) {
1302 if(m[obj.children[i]] && !m[obj.children[i]].state.hidden) {
1303 has_children = true;
1304 break;
1305 }
1306 }
1307 if(obj.state.loaded && dom && dom.length) {
1308 dom.removeClass('jstree-closed jstree-open jstree-leaf');
1309 if (!has_children) {
1310 dom.addClass('jstree-leaf');
1311 }
1312 else {
1313 if (obj.id !== '#') {
1314 dom.addClass(obj.state.opened ? 'jstree-open' : 'jstree-closed');
1315 }
1316 }
1317 }
1318 dom.removeClass("jstree-loading").attr('aria-busy',false);
1319 /**
1320 * triggered after a node is loaded
1321 * @event
1322 * @name load_node.jstree
1323 * @param {Object} node the node that was loading
1324 * @param {Boolean} status was the node loaded successfully
1325 */
1326 this.trigger('load_node', { "node" : obj, "status" : status });
1327 if(callback) {
1328 callback.call(this, obj, status);
1329 }
1330 }, this));
1331 return true;
1332 },
1333 /**
1334 * load an array of nodes (will also load unavailable nodes as soon as the appear in the structure). Used internally.
1335 * @private
1336 * @name _load_nodes(nodes [, callback])
1337 * @param {array} nodes
1338 * @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
1339 */
1340 _load_nodes : function (nodes, callback, is_callback, force_reload) {
1341 var r = true,
1342 c = function () { this._load_nodes(nodes, callback, true); },
1343 m = this._model.data, i, j, tmp = [];
1344 for(i = 0, j = nodes.length; i < j; i++) {
1345 if(m[nodes[i]] && ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || (!is_callback && force_reload) )) {
1346 if(!this.is_loading(nodes[i])) {
1347 this.load_node(nodes[i], c);
1348 }
1349 r = false;
1350 }
1351 }
1352 if(r) {
1353 for(i = 0, j = nodes.length; i < j; i++) {
1354 if(m[nodes[i]] && m[nodes[i]].state.loaded) {
1355 tmp.push(nodes[i]);
1356 }
1357 }
1358 if(callback && !callback.done) {
1359 callback.call(this, tmp);
1360 callback.done = true;
1361 }
1362 }
1363 },
1364 /**
1365 * loads all unloaded nodes
1366 * @name load_all([obj, callback])
1367 * @param {mixed} obj the node to load recursively, omit to load all nodes in the tree
1368 * @param {function} callback a function to be executed once loading all the nodes is complete,
1369 * @trigger load_all.jstree
1370 */
1371 load_all : function (obj, callback) {
1372 if(!obj) { obj = $.jstree.root; }
1373 obj = this.get_node(obj);
1374 if(!obj) { return false; }
1375 var to_load = [],
1376 m = this._model.data,
1377 c = m[obj.id].children_d,
1378 i, j;
1379 if(obj.state && !obj.state.loaded) {
1380 to_load.push(obj.id);
1381 }
1382 for(i = 0, j = c.length; i < j; i++) {
1383 if(m[c[i]] && m[c[i]].state && !m[c[i]].state.loaded) {
1384 to_load.push(c[i]);
1385 }
1386 }
1387 if(to_load.length) {
1388 this._load_nodes(to_load, function () {
1389 this.load_all(obj, callback);
1390 });
1391 }
1392 else {
1393 /**
1394 * triggered after a load_all call completes
1395 * @event
1396 * @name load_all.jstree
1397 * @param {Object} node the recursively loaded node
1398 */
1399 if(callback) { callback.call(this, obj); }
1400 this.trigger('load_all', { "node" : obj });
1401 }
1402 },
1403 /**
1404 * handles the actual loading of a node. Used only internally.
1405 * @private
1406 * @name _load_node(obj [, callback])
1407 * @param {mixed} obj
1408 * @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
1409 * @return {Boolean}
1410 */
1411 _load_node : function (obj, callback) {
1412 var s = this.settings.core.data, t;
1413 var notTextOrCommentNode = function notTextOrCommentNode () {
1414 return this.nodeType !== 3 && this.nodeType !== 8;
1415 };
1416 // use original HTML
1417 if(!s) {
1418 if(obj.id === $.jstree.root) {
1419 return this._append_html_data(obj, this._data.core.original_container_html.clone(true), function (status) {
1420 callback.call(this, status);
1421 });
1422 }
1423 else {
1424 return callback.call(this, false);
1425 }
1426 // return callback.call(this, obj.id === $.jstree.root ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false);
1427 }
1428 if($.isFunction(s)) {
1429 return s.call(this, obj, $.proxy(function (d) {
1430 if(d === false) {
1431 callback.call(this, false);
1432 }
1433 else {
1434 this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(notTextOrCommentNode) : d, function (status) {
1435 callback.call(this, status);
1436 });
1437 }
1438 // 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));
1439 }, this));
1440 }
1441 if(typeof s === 'object') {
1442 if(s.url) {
1443 s = $.extend(true, {}, s);
1444 if($.isFunction(s.url)) {
1445 s.url = s.url.call(this, obj);
1446 }
1447 if($.isFunction(s.data)) {
1448 s.data = s.data.call(this, obj);
1449 }
1450 return $.ajax(s)
1451 .done($.proxy(function (d,t,x) {
1452 var type = x.getResponseHeader('Content-Type');
1453 if((type && type.indexOf('json') !== -1) || typeof d === "object") {
1454 return this._append_json_data(obj, d, function (status) { callback.call(this, status); });
1455 //return callback.call(this, this._append_json_data(obj, d));
1456 }
1457 if((type && type.indexOf('html') !== -1) || typeof d === "string") {
1458 return this._append_html_data(obj, $($.parseHTML(d)).filter(notTextOrCommentNode), function (status) { callback.call(this, status); });
1459 // return callback.call(this, this._append_html_data(obj, $(d)));
1460 }
1461 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 }) };
1462 this.settings.core.error.call(this, this._data.core.last_error);
1463 return callback.call(this, false);
1464 }, this))
1465 .fail($.proxy(function (f) {
1466 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 }) };
1467 callback.call(this, false);
1468 this.settings.core.error.call(this, this._data.core.last_error);
1469 }, this));
1470 }
1471 if ($.isArray(s)) {
1472 t = $.extend(true, [], s);
1473 } else if ($.isPlainObject(s)) {
1474 t = $.extend(true, {}, s);
1475 } else {
1476 t = s;
1477 }
1478 if(obj.id === $.jstree.root) {
1479 return this._append_json_data(obj, t, function (status) {
1480 callback.call(this, status);
1481 });
1482 }
1483 else {
1484 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
1485 this.settings.core.error.call(this, this._data.core.last_error);
1486 return callback.call(this, false);
1487 }
1488 //return callback.call(this, (obj.id === $.jstree.root ? this._append_json_data(obj, t) : false) );
1489 }
1490 if(typeof s === 'string') {
1491 if(obj.id === $.jstree.root) {
1492 return this._append_html_data(obj, $($.parseHTML(s)).filter(notTextOrCommentNode), function (status) {
1493 callback.call(this, status);
1494 });
1495 }
1496 else {
1497 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
1498 this.settings.core.error.call(this, this._data.core.last_error);
1499 return callback.call(this, false);
1500 }
1501 //return callback.call(this, (obj.id === $.jstree.root ? this._append_html_data(obj, $(s)) : false) );
1502 }
1503 return callback.call(this, false);
1504 },
1505 /**
1506 * adds a node to the list of nodes to redraw. Used only internally.
1507 * @private
1508 * @name _node_changed(obj [, callback])
1509 * @param {mixed} obj
1510 */
1511 _node_changed : function (obj) {
1512 obj = this.get_node(obj);
1513 if (obj && $.inArray(obj.id, this._model.changed) === -1) {
1514 this._model.changed.push(obj.id);
1515 }
1516 },
1517 /**
1518 * appends HTML content to the tree. Used internally.
1519 * @private
1520 * @name _append_html_data(obj, data)
1521 * @param {mixed} obj the node to append to
1522 * @param {String} data the HTML string to parse and append
1523 * @trigger model.jstree, changed.jstree
1524 */
1525 _append_html_data : function (dom, data, cb) {
1526 dom = this.get_node(dom);
1527 dom.children = [];
1528 dom.children_d = [];
1529 var dat = data.is('ul') ? data.children() : data,
1530 par = dom.id,
1531 chd = [],
1532 dpc = [],
1533 m = this._model.data,
1534 p = m[par],
1535 s = this._data.core.selected.length,
1536 tmp, i, j;
1537 dat.each($.proxy(function (i, v) {
1538 tmp = this._parse_model_from_html($(v), par, p.parents.concat());
1539 if(tmp) {
1540 chd.push(tmp);
1541 dpc.push(tmp);
1542 if(m[tmp].children_d.length) {
1543 dpc = dpc.concat(m[tmp].children_d);
1544 }
1545 }
1546 }, this));
1547 p.children = chd;
1548 p.children_d = dpc;
1549 for(i = 0, j = p.parents.length; i < j; i++) {
1550 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1551 }
1552 /**
1553 * triggered when new data is inserted to the tree model
1554 * @event
1555 * @name model.jstree
1556 * @param {Array} nodes an array of node IDs
1557 * @param {String} parent the parent ID of the nodes
1558 */
1559 this.trigger('model', { "nodes" : dpc, 'parent' : par });
1560 if(par !== $.jstree.root) {
1561 this._node_changed(par);
1562 this.redraw();
1563 }
1564 else {
1565 this.get_container_ul().children('.jstree-initial-node').remove();
1566 this.redraw(true);
1567 }
1568 if(this._data.core.selected.length !== s) {
1569 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
1570 }
1571 cb.call(this, true);
1572 },
1573 /**
1574 * appends JSON content to the tree. Used internally.
1575 * @private
1576 * @name _append_json_data(obj, data)
1577 * @param {mixed} obj the node to append to
1578 * @param {String} data the JSON object to parse and append
1579 * @param {Boolean} force_processing internal param - do not set
1580 * @trigger model.jstree, changed.jstree
1581 */
1582 _append_json_data : function (dom, data, cb, force_processing) {
1583 if(this.element === null) { return; }
1584 dom = this.get_node(dom);
1585 dom.children = [];
1586 dom.children_d = [];
1587 // *%$@!!!
1588 if(data.d) {
1589 data = data.d;
1590 if(typeof data === "string") {
1591 data = JSON.parse(data);
1592 }
1593 }
1594 if(!$.isArray(data)) { data = [data]; }
1595 var w = null,
1596 args = {
1597 'df' : this._model.default_state,
1598 'dat' : data,
1599 'par' : dom.id,
1600 'm' : this._model.data,
1601 't_id' : this._id,
1602 't_cnt' : this._cnt,
1603 'sel' : this._data.core.selected
1604 },
1605 func = function (data, undefined) {
1606 if(data.data) { data = data.data; }
1607 var dat = data.dat,
1608 par = data.par,
1609 chd = [],
1610 dpc = [],
1611 add = [],
1612 df = data.df,
1613 t_id = data.t_id,
1614 t_cnt = data.t_cnt,
1615 m = data.m,
1616 p = m[par],
1617 sel = data.sel,
1618 tmp, i, j, rslt,
1619 parse_flat = function (d, p, ps) {
1620 if(!ps) { ps = []; }
1621 else { ps = ps.concat(); }
1622 if(p) { ps.unshift(p); }
1623 var tid = d.id.toString(),
1624 i, j, c, e,
1625 tmp = {
1626 id : tid,
1627 text : d.text || '',
1628 icon : d.icon !== undefined ? d.icon : true,
1629 parent : p,
1630 parents : ps,
1631 children : d.children || [],
1632 children_d : d.children_d || [],
1633 data : d.data,
1634 state : { },
1635 li_attr : { id : false },
1636 a_attr : { href : '#' },
1637 original : false
1638 };
1639 for(i in df) {
1640 if(df.hasOwnProperty(i)) {
1641 tmp.state[i] = df[i];
1642 }
1643 }
1644 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1645 tmp.icon = d.data.jstree.icon;
1646 }
1647 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
1648 tmp.icon = true;
1649 }
1650 if(d && d.data) {
1651 tmp.data = d.data;
1652 if(d.data.jstree) {
1653 for(i in d.data.jstree) {
1654 if(d.data.jstree.hasOwnProperty(i)) {
1655 tmp.state[i] = d.data.jstree[i];
1656 }
1657 }
1658 }
1659 }
1660 if(d && typeof d.state === 'object') {
1661 for (i in d.state) {
1662 if(d.state.hasOwnProperty(i)) {
1663 tmp.state[i] = d.state[i];
1664 }
1665 }
1666 }
1667 if(d && typeof d.li_attr === 'object') {
1668 for (i in d.li_attr) {
1669 if(d.li_attr.hasOwnProperty(i)) {
1670 tmp.li_attr[i] = d.li_attr[i];
1671 }
1672 }
1673 }
1674 if(!tmp.li_attr.id) {
1675 tmp.li_attr.id = tid;
1676 }
1677 if(d && typeof d.a_attr === 'object') {
1678 for (i in d.a_attr) {
1679 if(d.a_attr.hasOwnProperty(i)) {
1680 tmp.a_attr[i] = d.a_attr[i];
1681 }
1682 }
1683 }
1684 if(d && d.children && d.children === true) {
1685 tmp.state.loaded = false;
1686 tmp.children = [];
1687 tmp.children_d = [];
1688 }
1689 m[tmp.id] = tmp;
1690 for(i = 0, j = tmp.children.length; i < j; i++) {
1691 c = parse_flat(m[tmp.children[i]], tmp.id, ps);
1692 e = m[c];
1693 tmp.children_d.push(c);
1694 if(e.children_d.length) {
1695 tmp.children_d = tmp.children_d.concat(e.children_d);
1696 }
1697 }
1698 delete d.data;
1699 delete d.children;
1700 m[tmp.id].original = d;
1701 if(tmp.state.selected) {
1702 add.push(tmp.id);
1703 }
1704 return tmp.id;
1705 },
1706 parse_nest = function (d, p, ps) {
1707 if(!ps) { ps = []; }
1708 else { ps = ps.concat(); }
1709 if(p) { ps.unshift(p); }
1710 var tid = false, i, j, c, e, tmp;
1711 do {
1712 tid = 'j' + t_id + '_' + (++t_cnt);
1713 } while(m[tid]);
1714
1715 tmp = {
1716 id : false,
1717 text : typeof d === 'string' ? d : '',
1718 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true,
1719 parent : p,
1720 parents : ps,
1721 children : [],
1722 children_d : [],
1723 data : null,
1724 state : { },
1725 li_attr : { id : false },
1726 a_attr : { href : '#' },
1727 original : false
1728 };
1729 for(i in df) {
1730 if(df.hasOwnProperty(i)) {
1731 tmp.state[i] = df[i];
1732 }
1733 }
1734 if(d && d.id) { tmp.id = d.id.toString(); }
1735 if(d && d.text) { tmp.text = d.text; }
1736 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
1737 tmp.icon = d.data.jstree.icon;
1738 }
1739 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
1740 tmp.icon = true;
1741 }
1742 if(d && d.data) {
1743 tmp.data = d.data;
1744 if(d.data.jstree) {
1745 for(i in d.data.jstree) {
1746 if(d.data.jstree.hasOwnProperty(i)) {
1747 tmp.state[i] = d.data.jstree[i];
1748 }
1749 }
1750 }
1751 }
1752 if(d && typeof d.state === 'object') {
1753 for (i in d.state) {
1754 if(d.state.hasOwnProperty(i)) {
1755 tmp.state[i] = d.state[i];
1756 }
1757 }
1758 }
1759 if(d && typeof d.li_attr === 'object') {
1760 for (i in d.li_attr) {
1761 if(d.li_attr.hasOwnProperty(i)) {
1762 tmp.li_attr[i] = d.li_attr[i];
1763 }
1764 }
1765 }
1766 if(tmp.li_attr.id && !tmp.id) {
1767 tmp.id = tmp.li_attr.id.toString();
1768 }
1769 if(!tmp.id) {
1770 tmp.id = tid;
1771 }
1772 if(!tmp.li_attr.id) {
1773 tmp.li_attr.id = tmp.id;
1774 }
1775 if(d && typeof d.a_attr === 'object') {
1776 for (i in d.a_attr) {
1777 if(d.a_attr.hasOwnProperty(i)) {
1778 tmp.a_attr[i] = d.a_attr[i];
1779 }
1780 }
1781 }
1782 if(d && d.children && d.children.length) {
1783 for(i = 0, j = d.children.length; i < j; i++) {
1784 c = parse_nest(d.children[i], tmp.id, ps);
1785 e = m[c];
1786 tmp.children.push(c);
1787 if(e.children_d.length) {
1788 tmp.children_d = tmp.children_d.concat(e.children_d);
1789 }
1790 }
1791 tmp.children_d = tmp.children_d.concat(tmp.children);
1792 }
1793 if(d && d.children && d.children === true) {
1794 tmp.state.loaded = false;
1795 tmp.children = [];
1796 tmp.children_d = [];
1797 }
1798 delete d.data;
1799 delete d.children;
1800 tmp.original = d;
1801 m[tmp.id] = tmp;
1802 if(tmp.state.selected) {
1803 add.push(tmp.id);
1804 }
1805 return tmp.id;
1806 };
1807
1808 if(dat.length && dat[0].id !== undefined && dat[0].parent !== undefined) {
1809 // Flat JSON support (for easy import from DB):
1810 // 1) convert to object (foreach)
1811 for(i = 0, j = dat.length; i < j; i++) {
1812 if(!dat[i].children) {
1813 dat[i].children = [];
1814 }
1815 if(!dat[i].state) {
1816 dat[i].state = {};
1817 }
1818 m[dat[i].id.toString()] = dat[i];
1819 }
1820 // 2) populate children (foreach)
1821 for(i = 0, j = dat.length; i < j; i++) {
1822 if (!m[dat[i].parent.toString()]) {
1823 this._data.core.last_error = { 'error' : 'parse', 'plugin' : 'core', 'id' : 'core_07', 'reason' : 'Node with invalid parent', 'data' : JSON.stringify({ 'id' : dat[i].id.toString(), 'parent' : dat[i].parent.toString() }) };
1824 this.settings.core.error.call(this, this._data.core.last_error);
1825 continue;
1826 }
1827
1828 m[dat[i].parent.toString()].children.push(dat[i].id.toString());
1829 // populate parent.children_d
1830 p.children_d.push(dat[i].id.toString());
1831 }
1832 // 3) normalize && populate parents and children_d with recursion
1833 for(i = 0, j = p.children.length; i < j; i++) {
1834 tmp = parse_flat(m[p.children[i]], par, p.parents.concat());
1835 dpc.push(tmp);
1836 if(m[tmp].children_d.length) {
1837 dpc = dpc.concat(m[tmp].children_d);
1838 }
1839 }
1840 for(i = 0, j = p.parents.length; i < j; i++) {
1841 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1842 }
1843 // ?) three_state selection - p.state.selected && t - (if three_state foreach(dat => ch) -> foreach(parents) if(parent.selected) child.selected = true;
1844 rslt = {
1845 'cnt' : t_cnt,
1846 'mod' : m,
1847 'sel' : sel,
1848 'par' : par,
1849 'dpc' : dpc,
1850 'add' : add
1851 };
1852 }
1853 else {
1854 for(i = 0, j = dat.length; i < j; i++) {
1855 tmp = parse_nest(dat[i], par, p.parents.concat());
1856 if(tmp) {
1857 chd.push(tmp);
1858 dpc.push(tmp);
1859 if(m[tmp].children_d.length) {
1860 dpc = dpc.concat(m[tmp].children_d);
1861 }
1862 }
1863 }
1864 p.children = chd;
1865 p.children_d = dpc;
1866 for(i = 0, j = p.parents.length; i < j; i++) {
1867 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
1868 }
1869 rslt = {
1870 'cnt' : t_cnt,
1871 'mod' : m,
1872 'sel' : sel,
1873 'par' : par,
1874 'dpc' : dpc,
1875 'add' : add
1876 };
1877 }
1878 if(typeof window === 'undefined' || typeof window.document === 'undefined') {
1879 postMessage(rslt);
1880 }
1881 else {
1882 return rslt;
1883 }
1884 },
1885 rslt = function (rslt, worker) {
1886 if(this.element === null) { return; }
1887 this._cnt = rslt.cnt;
1888 var i, m = this._model.data;
1889 for (i in m) {
1890 if (m.hasOwnProperty(i) && m[i].state && m[i].state.loading && rslt.mod[i]) {
1891 rslt.mod[i].state.loading = true;
1892 }
1893 }
1894 this._model.data = rslt.mod; // breaks the reference in load_node - careful
1895
1896 if(worker) {
1897 var j, a = rslt.add, r = rslt.sel, s = this._data.core.selected.slice();
1898 m = this._model.data;
1899 // if selection was changed while calculating in worker
1900 if(r.length !== s.length || $.vakata.array_unique(r.concat(s)).length !== r.length) {
1901 // deselect nodes that are no longer selected
1902 for(i = 0, j = r.length; i < j; i++) {
1903 if($.inArray(r[i], a) === -1 && $.inArray(r[i], s) === -1) {
1904 m[r[i]].state.selected = false;
1905 }
1906 }
1907 // select nodes that were selected in the mean time
1908 for(i = 0, j = s.length; i < j; i++) {
1909 if($.inArray(s[i], r) === -1) {
1910 m[s[i]].state.selected = true;
1911 }
1912 }
1913 }
1914 }
1915 if(rslt.add.length) {
1916 this._data.core.selected = this._data.core.selected.concat(rslt.add);
1917 }
1918
1919 this.trigger('model', { "nodes" : rslt.dpc, 'parent' : rslt.par });
1920
1921 if(rslt.par !== $.jstree.root) {
1922 this._node_changed(rslt.par);
1923 this.redraw();
1924 }
1925 else {
1926 // this.get_container_ul().children('.jstree-initial-node').remove();
1927 this.redraw(true);
1928 }
1929 if(rslt.add.length) {
1930 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
1931 }
1932 cb.call(this, true);
1933 };
1934 if(this.settings.core.worker && window.Blob && window.URL && window.Worker) {
1935 try {
1936 if(this._wrk === null) {
1937 this._wrk = window.URL.createObjectURL(
1938 new window.Blob(
1939 ['self.onmessage = ' + func.toString()],
1940 {type:"text/javascript"}
1941 )
1942 );
1943 }
1944 if(!this._data.core.working || force_processing) {
1945 this._data.core.working = true;
1946 w = new window.Worker(this._wrk);
1947 w.onmessage = $.proxy(function (e) {
1948 rslt.call(this, e.data, true);
1949 try { w.terminate(); w = null; } catch(ignore) { }
1950 if(this._data.core.worker_queue.length) {
1951 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1952 }
1953 else {
1954 this._data.core.working = false;
1955 }
1956 }, this);
1957 if(!args.par) {
1958 if(this._data.core.worker_queue.length) {
1959 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1960 }
1961 else {
1962 this._data.core.working = false;
1963 }
1964 }
1965 else {
1966 w.postMessage(args);
1967 }
1968 }
1969 else {
1970 this._data.core.worker_queue.push([dom, data, cb, true]);
1971 }
1972 }
1973 catch(e) {
1974 rslt.call(this, func(args), false);
1975 if(this._data.core.worker_queue.length) {
1976 this._append_json_data.apply(this, this._data.core.worker_queue.shift());
1977 }
1978 else {
1979 this._data.core.working = false;
1980 }
1981 }
1982 }
1983 else {
1984 rslt.call(this, func(args), false);
1985 }
1986 },
1987 /**
1988 * parses a node from a jQuery object and appends them to the in memory tree model. Used internally.
1989 * @private
1990 * @name _parse_model_from_html(d [, p, ps])
1991 * @param {jQuery} d the jQuery object to parse
1992 * @param {String} p the parent ID
1993 * @param {Array} ps list of all parents
1994 * @return {String} the ID of the object added to the model
1995 */
1996 _parse_model_from_html : function (d, p, ps) {
1997 if(!ps) { ps = []; }
1998 else { ps = [].concat(ps); }
1999 if(p) { ps.unshift(p); }
2000 var c, e, m = this._model.data,
2001 data = {
2002 id : false,
2003 text : false,
2004 icon : true,
2005 parent : p,
2006 parents : ps,
2007 children : [],
2008 children_d : [],
2009 data : null,
2010 state : { },
2011 li_attr : { id : false },
2012 a_attr : { href : '#' },
2013 original : false
2014 }, i, tmp, tid;
2015 for(i in this._model.default_state) {
2016 if(this._model.default_state.hasOwnProperty(i)) {
2017 data.state[i] = this._model.default_state[i];
2018 }
2019 }
2020 tmp = $.vakata.attributes(d, true);
2021 $.each(tmp, function (i, v) {
2022 v = $.trim(v);
2023 if(!v.length) { return true; }
2024 data.li_attr[i] = v;
2025 if(i === 'id') {
2026 data.id = v.toString();
2027 }
2028 });
2029 tmp = d.children('a').first();
2030 if(tmp.length) {
2031 tmp = $.vakata.attributes(tmp, true);
2032 $.each(tmp, function (i, v) {
2033 v = $.trim(v);
2034 if(v.length) {
2035 data.a_attr[i] = v;
2036 }
2037 });
2038 }
2039 tmp = d.children("a").first().length ? d.children("a").first().clone() : d.clone();
2040 tmp.children("ins, i, ul").remove();
2041 tmp = tmp.html();
2042 tmp = $('<div />').html(tmp);
2043 data.text = this.settings.core.force_text ? tmp.text() : tmp.html();
2044 tmp = d.data();
2045 data.data = tmp ? $.extend(true, {}, tmp) : null;
2046 data.state.opened = d.hasClass('jstree-open');
2047 data.state.selected = d.children('a').hasClass('jstree-clicked');
2048 data.state.disabled = d.children('a').hasClass('jstree-disabled');
2049 if(data.data && data.data.jstree) {
2050 for(i in data.data.jstree) {
2051 if(data.data.jstree.hasOwnProperty(i)) {
2052 data.state[i] = data.data.jstree[i];
2053 }
2054 }
2055 }
2056 tmp = d.children("a").children(".jstree-themeicon");
2057 if(tmp.length) {
2058 data.icon = tmp.hasClass('jstree-themeicon-hidden') ? false : tmp.attr('rel');
2059 }
2060 if(data.state.icon !== undefined) {
2061 data.icon = data.state.icon;
2062 }
2063 if(data.icon === undefined || data.icon === null || data.icon === "") {
2064 data.icon = true;
2065 }
2066 tmp = d.children("ul").children("li");
2067 do {
2068 tid = 'j' + this._id + '_' + (++this._cnt);
2069 } while(m[tid]);
2070 data.id = data.li_attr.id ? data.li_attr.id.toString() : tid;
2071 if(tmp.length) {
2072 tmp.each($.proxy(function (i, v) {
2073 c = this._parse_model_from_html($(v), data.id, ps);
2074 e = this._model.data[c];
2075 data.children.push(c);
2076 if(e.children_d.length) {
2077 data.children_d = data.children_d.concat(e.children_d);
2078 }
2079 }, this));
2080 data.children_d = data.children_d.concat(data.children);
2081 }
2082 else {
2083 if(d.hasClass('jstree-closed')) {
2084 data.state.loaded = false;
2085 }
2086 }
2087 if(data.li_attr['class']) {
2088 data.li_attr['class'] = data.li_attr['class'].replace('jstree-closed','').replace('jstree-open','');
2089 }
2090 if(data.a_attr['class']) {
2091 data.a_attr['class'] = data.a_attr['class'].replace('jstree-clicked','').replace('jstree-disabled','');
2092 }
2093 m[data.id] = data;
2094 if(data.state.selected) {
2095 this._data.core.selected.push(data.id);
2096 }
2097 return data.id;
2098 },
2099 /**
2100 * 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.
2101 * @private
2102 * @name _parse_model_from_flat_json(d [, p, ps])
2103 * @param {Object} d the JSON object to parse
2104 * @param {String} p the parent ID
2105 * @param {Array} ps list of all parents
2106 * @return {String} the ID of the object added to the model
2107 */
2108 _parse_model_from_flat_json : function (d, p, ps) {
2109 if(!ps) { ps = []; }
2110 else { ps = ps.concat(); }
2111 if(p) { ps.unshift(p); }
2112 var tid = d.id.toString(),
2113 m = this._model.data,
2114 df = this._model.default_state,
2115 i, j, c, e,
2116 tmp = {
2117 id : tid,
2118 text : d.text || '',
2119 icon : d.icon !== undefined ? d.icon : true,
2120 parent : p,
2121 parents : ps,
2122 children : d.children || [],
2123 children_d : d.children_d || [],
2124 data : d.data,
2125 state : { },
2126 li_attr : { id : false },
2127 a_attr : { href : '#' },
2128 original : false
2129 };
2130 for(i in df) {
2131 if(df.hasOwnProperty(i)) {
2132 tmp.state[i] = df[i];
2133 }
2134 }
2135 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
2136 tmp.icon = d.data.jstree.icon;
2137 }
2138 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
2139 tmp.icon = true;
2140 }
2141 if(d && d.data) {
2142 tmp.data = d.data;
2143 if(d.data.jstree) {
2144 for(i in d.data.jstree) {
2145 if(d.data.jstree.hasOwnProperty(i)) {
2146 tmp.state[i] = d.data.jstree[i];
2147 }
2148 }
2149 }
2150 }
2151 if(d && typeof d.state === 'object') {
2152 for (i in d.state) {
2153 if(d.state.hasOwnProperty(i)) {
2154 tmp.state[i] = d.state[i];
2155 }
2156 }
2157 }
2158 if(d && typeof d.li_attr === 'object') {
2159 for (i in d.li_attr) {
2160 if(d.li_attr.hasOwnProperty(i)) {
2161 tmp.li_attr[i] = d.li_attr[i];
2162 }
2163 }
2164 }
2165 if(!tmp.li_attr.id) {
2166 tmp.li_attr.id = tid;
2167 }
2168 if(d && typeof d.a_attr === 'object') {
2169 for (i in d.a_attr) {
2170 if(d.a_attr.hasOwnProperty(i)) {
2171 tmp.a_attr[i] = d.a_attr[i];
2172 }
2173 }
2174 }
2175 if(d && d.children && d.children === true) {
2176 tmp.state.loaded = false;
2177 tmp.children = [];
2178 tmp.children_d = [];
2179 }
2180 m[tmp.id] = tmp;
2181 for(i = 0, j = tmp.children.length; i < j; i++) {
2182 c = this._parse_model_from_flat_json(m[tmp.children[i]], tmp.id, ps);
2183 e = m[c];
2184 tmp.children_d.push(c);
2185 if(e.children_d.length) {
2186 tmp.children_d = tmp.children_d.concat(e.children_d);
2187 }
2188 }
2189 delete d.data;
2190 delete d.children;
2191 m[tmp.id].original = d;
2192 if(tmp.state.selected) {
2193 this._data.core.selected.push(tmp.id);
2194 }
2195 return tmp.id;
2196 },
2197 /**
2198 * parses a node from a JSON object and appends it to the in memory tree model. Used internally.
2199 * @private
2200 * @name _parse_model_from_json(d [, p, ps])
2201 * @param {Object} d the JSON object to parse
2202 * @param {String} p the parent ID
2203 * @param {Array} ps list of all parents
2204 * @return {String} the ID of the object added to the model
2205 */
2206 _parse_model_from_json : function (d, p, ps) {
2207 if(!ps) { ps = []; }
2208 else { ps = ps.concat(); }
2209 if(p) { ps.unshift(p); }
2210 var tid = false, i, j, c, e, m = this._model.data, df = this._model.default_state, tmp;
2211 do {
2212 tid = 'j' + this._id + '_' + (++this._cnt);
2213 } while(m[tid]);
2214
2215 tmp = {
2216 id : false,
2217 text : typeof d === 'string' ? d : '',
2218 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true,
2219 parent : p,
2220 parents : ps,
2221 children : [],
2222 children_d : [],
2223 data : null,
2224 state : { },
2225 li_attr : { id : false },
2226 a_attr : { href : '#' },
2227 original : false
2228 };
2229 for(i in df) {
2230 if(df.hasOwnProperty(i)) {
2231 tmp.state[i] = df[i];
2232 }
2233 }
2234 if(d && d.id) { tmp.id = d.id.toString(); }
2235 if(d && d.text) { tmp.text = d.text; }
2236 if(d && d.data && d.data.jstree && d.data.jstree.icon) {
2237 tmp.icon = d.data.jstree.icon;
2238 }
2239 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") {
2240 tmp.icon = true;
2241 }
2242 if(d && d.data) {
2243 tmp.data = d.data;
2244 if(d.data.jstree) {
2245 for(i in d.data.jstree) {
2246 if(d.data.jstree.hasOwnProperty(i)) {
2247 tmp.state[i] = d.data.jstree[i];
2248 }
2249 }
2250 }
2251 }
2252 if(d && typeof d.state === 'object') {
2253 for (i in d.state) {
2254 if(d.state.hasOwnProperty(i)) {
2255 tmp.state[i] = d.state[i];
2256 }
2257 }
2258 }
2259 if(d && typeof d.li_attr === 'object') {
2260 for (i in d.li_attr) {
2261 if(d.li_attr.hasOwnProperty(i)) {
2262 tmp.li_attr[i] = d.li_attr[i];
2263 }
2264 }
2265 }
2266 if(tmp.li_attr.id && !tmp.id) {
2267 tmp.id = tmp.li_attr.id.toString();
2268 }
2269 if(!tmp.id) {
2270 tmp.id = tid;
2271 }
2272 if(!tmp.li_attr.id) {
2273 tmp.li_attr.id = tmp.id;
2274 }
2275 if(d && typeof d.a_attr === 'object') {
2276 for (i in d.a_attr) {
2277 if(d.a_attr.hasOwnProperty(i)) {
2278 tmp.a_attr[i] = d.a_attr[i];
2279 }
2280 }
2281 }
2282 if(d && d.children && d.children.length) {
2283 for(i = 0, j = d.children.length; i < j; i++) {
2284 c = this._parse_model_from_json(d.children[i], tmp.id, ps);
2285 e = m[c];
2286 tmp.children.push(c);
2287 if(e.children_d.length) {
2288 tmp.children_d = tmp.children_d.concat(e.children_d);
2289 }
2290 }
2291 tmp.children_d = tmp.children_d.concat(tmp.children);
2292 }
2293 if(d && d.children && d.children === true) {
2294 tmp.state.loaded = false;
2295 tmp.children = [];
2296 tmp.children_d = [];
2297 }
2298 delete d.data;
2299 delete d.children;
2300 tmp.original = d;
2301 m[tmp.id] = tmp;
2302 if(tmp.state.selected) {
2303 this._data.core.selected.push(tmp.id);
2304 }
2305 return tmp.id;
2306 },
2307 /**
2308 * redraws all nodes that need to be redrawn. Used internally.
2309 * @private
2310 * @name _redraw()
2311 * @trigger redraw.jstree
2312 */
2313 _redraw : function () {
2314 var nodes = this._model.force_full_redraw ? this._model.data[$.jstree.root].children.concat([]) : this._model.changed.concat([]),
2315 f = document.createElement('UL'), tmp, i, j, fe = this._data.core.focused;
2316 for(i = 0, j = nodes.length; i < j; i++) {
2317 tmp = this.redraw_node(nodes[i], true, this._model.force_full_redraw);
2318 if(tmp && this._model.force_full_redraw) {
2319 f.appendChild(tmp);
2320 }
2321 }
2322 if(this._model.force_full_redraw) {
2323 f.className = this.get_container_ul()[0].className;
2324 f.setAttribute('role','group');
2325 this.element.empty().append(f);
2326 //this.get_container_ul()[0].appendChild(f);
2327 }
2328 if(fe !== null) {
2329 tmp = this.get_node(fe, true);
2330 if(tmp && tmp.length && tmp.children('.jstree-anchor')[0] !== document.activeElement) {
2331 tmp.children('.jstree-anchor').focus();
2332 }
2333 else {
2334 this._data.core.focused = null;
2335 }
2336 }
2337 this._model.force_full_redraw = false;
2338 this._model.changed = [];
2339 /**
2340 * triggered after nodes are redrawn
2341 * @event
2342 * @name redraw.jstree
2343 * @param {array} nodes the redrawn nodes
2344 */
2345 this.trigger('redraw', { "nodes" : nodes });
2346 },
2347 /**
2348 * redraws all nodes that need to be redrawn or optionally - the whole tree
2349 * @name redraw([full])
2350 * @param {Boolean} full if set to `true` all nodes are redrawn.
2351 */
2352 redraw : function (full) {
2353 if(full) {
2354 this._model.force_full_redraw = true;
2355 }
2356 //if(this._model.redraw_timeout) {
2357 // clearTimeout(this._model.redraw_timeout);
2358 //}
2359 //this._model.redraw_timeout = setTimeout($.proxy(this._redraw, this),0);
2360 this._redraw();
2361 },
2362 /**
2363 * redraws a single node's children. Used internally.
2364 * @private
2365 * @name draw_children(node)
2366 * @param {mixed} node the node whose children will be redrawn
2367 */
2368 draw_children : function (node) {
2369 var obj = this.get_node(node),
2370 i = false,
2371 j = false,
2372 k = false,
2373 d = document;
2374 if(!obj) { return false; }
2375 if(obj.id === $.jstree.root) { return this.redraw(true); }
2376 node = this.get_node(node, true);
2377 if(!node || !node.length) { return false; } // TODO: quick toggle
2378
2379 node.children('.jstree-children').remove();
2380 node = node[0];
2381 if(obj.children.length && obj.state.loaded) {
2382 k = d.createElement('UL');
2383 k.setAttribute('role', 'group');
2384 k.className = 'jstree-children';
2385 for(i = 0, j = obj.children.length; i < j; i++) {
2386 k.appendChild(this.redraw_node(obj.children[i], true, true));
2387 }
2388 node.appendChild(k);
2389 }
2390 },
2391 /**
2392 * redraws a single node. Used internally.
2393 * @private
2394 * @name redraw_node(node, deep, is_callback, force_render)
2395 * @param {mixed} node the node to redraw
2396 * @param {Boolean} deep should child nodes be redrawn too
2397 * @param {Boolean} is_callback is this a recursion call
2398 * @param {Boolean} force_render should children of closed parents be drawn anyway
2399 */
2400 redraw_node : function (node, deep, is_callback, force_render) {
2401 var obj = this.get_node(node),
2402 par = false,
2403 ind = false,
2404 old = false,
2405 i = false,
2406 j = false,
2407 k = false,
2408 c = '',
2409 d = document,
2410 m = this._model.data,
2411 f = false,
2412 s = false,
2413 tmp = null,
2414 t = 0,
2415 l = 0,
2416 has_children = false,
2417 last_sibling = false;
2418 if(!obj) { return false; }
2419 if(obj.id === $.jstree.root) { return this.redraw(true); }
2420 deep = deep || obj.children.length === 0;
2421 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);
2422 if(!node) {
2423 deep = true;
2424 //node = d.createElement('LI');
2425 if(!is_callback) {
2426 par = obj.parent !== $.jstree.root ? $('#' + obj.parent.replace($.jstree.idregex,'\\$&'), this.element)[0] : null;
2427 if(par !== null && (!par || !m[obj.parent].state.opened)) {
2428 return false;
2429 }
2430 ind = $.inArray(obj.id, par === null ? m[$.jstree.root].children : m[obj.parent].children);
2431 }
2432 }
2433 else {
2434 node = $(node);
2435 if(!is_callback) {
2436 par = node.parent().parent()[0];
2437 if(par === this.element[0]) {
2438 par = null;
2439 }
2440 ind = node.index();
2441 }
2442 // m[obj.id].data = node.data(); // use only node's data, no need to touch jquery storage
2443 if(!deep && obj.children.length && !node.children('.jstree-children').length) {
2444 deep = true;
2445 }
2446 if(!deep) {
2447 old = node.children('.jstree-children')[0];
2448 }
2449 f = node.children('.jstree-anchor')[0] === document.activeElement;
2450 node.remove();
2451 //node = d.createElement('LI');
2452 //node = node[0];
2453 }
2454 node = this._data.core.node.cloneNode(true);
2455 // node is DOM, deep is boolean
2456
2457 c = 'jstree-node ';
2458 for(i in obj.li_attr) {
2459 if(obj.li_attr.hasOwnProperty(i)) {
2460 if(i === 'id') { continue; }
2461 if(i !== 'class') {
2462 node.setAttribute(i, obj.li_attr[i]);
2463 }
2464 else {
2465 c += obj.li_attr[i];
2466 }
2467 }
2468 }
2469 if(!obj.a_attr.id) {
2470 obj.a_attr.id = obj.id + '_anchor';
2471 }
2472 node.setAttribute('aria-selected', !!obj.state.selected);
2473 node.setAttribute('aria-level', obj.parents.length);
2474 node.setAttribute('aria-labelledby', obj.a_attr.id);
2475 if(obj.state.disabled) {
2476 node.setAttribute('aria-disabled', true);
2477 }
2478
2479 for(i = 0, j = obj.children.length; i < j; i++) {
2480 if(!m[obj.children[i]].state.hidden) {
2481 has_children = true;
2482 break;
2483 }
2484 }
2485 if(obj.parent !== null && m[obj.parent] && !obj.state.hidden) {
2486 i = $.inArray(obj.id, m[obj.parent].children);
2487 last_sibling = obj.id;
2488 if(i !== -1) {
2489 i++;
2490 for(j = m[obj.parent].children.length; i < j; i++) {
2491 if(!m[m[obj.parent].children[i]].state.hidden) {
2492 last_sibling = m[obj.parent].children[i];
2493 }
2494 if(last_sibling !== obj.id) {
2495 break;
2496 }
2497 }
2498 }
2499 }
2500
2501 if(obj.state.hidden) {
2502 c += ' jstree-hidden';
2503 }
2504 if (obj.state.loading) {
2505 c += ' jstree-loading';
2506 }
2507 if(obj.state.loaded && !has_children) {
2508 c += ' jstree-leaf';
2509 }
2510 else {
2511 c += obj.state.opened && obj.state.loaded ? ' jstree-open' : ' jstree-closed';
2512 node.setAttribute('aria-expanded', (obj.state.opened && obj.state.loaded) );
2513 }
2514 if(last_sibling === obj.id) {
2515 c += ' jstree-last';
2516 }
2517 node.id = obj.id;
2518 node.className = c;
2519 c = ( obj.state.selected ? ' jstree-clicked' : '') + ( obj.state.disabled ? ' jstree-disabled' : '');
2520 for(j in obj.a_attr) {
2521 if(obj.a_attr.hasOwnProperty(j)) {
2522 if(j === 'href' && obj.a_attr[j] === '#') { continue; }
2523 if(j !== 'class') {
2524 node.childNodes[1].setAttribute(j, obj.a_attr[j]);
2525 }
2526 else {
2527 c += ' ' + obj.a_attr[j];
2528 }
2529 }
2530 }
2531 if(c.length) {
2532 node.childNodes[1].className = 'jstree-anchor ' + c;
2533 }
2534 if((obj.icon && obj.icon !== true) || obj.icon === false) {
2535 if(obj.icon === false) {
2536 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-hidden';
2537 }
2538 else if(obj.icon.indexOf('/') === -1 && obj.icon.indexOf('.') === -1) {
2539 node.childNodes[1].childNodes[0].className += ' ' + obj.icon + ' jstree-themeicon-custom';
2540 }
2541 else {
2542 node.childNodes[1].childNodes[0].style.backgroundImage = 'url("'+obj.icon+'")';
2543 node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center';
2544 node.childNodes[1].childNodes[0].style.backgroundSize = 'auto';
2545 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom';
2546 }
2547 }
2548
2549 if(this.settings.core.force_text) {
2550 node.childNodes[1].appendChild(d.createTextNode(obj.text));
2551 }
2552 else {
2553 node.childNodes[1].innerHTML += obj.text;
2554 }
2555
2556
2557 if(deep && obj.children.length && (obj.state.opened || force_render) && obj.state.loaded) {
2558 k = d.createElement('UL');
2559 k.setAttribute('role', 'group');
2560 k.className = 'jstree-children';
2561 for(i = 0, j = obj.children.length; i < j; i++) {
2562 k.appendChild(this.redraw_node(obj.children[i], deep, true));
2563 }
2564 node.appendChild(k);
2565 }
2566 if(old) {
2567 node.appendChild(old);
2568 }
2569 if(!is_callback) {
2570 // append back using par / ind
2571 if(!par) {
2572 par = this.element[0];
2573 }
2574 for(i = 0, j = par.childNodes.length; i < j; i++) {
2575 if(par.childNodes[i] && par.childNodes[i].className && par.childNodes[i].className.indexOf('jstree-children') !== -1) {
2576 tmp = par.childNodes[i];
2577 break;
2578 }
2579 }
2580 if(!tmp) {
2581 tmp = d.createElement('UL');
2582 tmp.setAttribute('role', 'group');
2583 tmp.className = 'jstree-children';
2584 par.appendChild(tmp);
2585 }
2586 par = tmp;
2587
2588 if(ind < par.childNodes.length) {
2589 par.insertBefore(node, par.childNodes[ind]);
2590 }
2591 else {
2592 par.appendChild(node);
2593 }
2594 if(f) {
2595 t = this.element[0].scrollTop;
2596 l = this.element[0].scrollLeft;
2597 node.childNodes[1].focus();
2598 this.element[0].scrollTop = t;
2599 this.element[0].scrollLeft = l;
2600 }
2601 }
2602 if(obj.state.opened && !obj.state.loaded) {
2603 obj.state.opened = false;
2604 setTimeout($.proxy(function () {
2605 this.open_node(obj.id, false, 0);
2606 }, this), 0);
2607 }
2608 return node;
2609 },
2610 /**
2611 * opens a node, revaling its children. If the node is not loaded it will be loaded and opened once ready.
2612 * @name open_node(obj [, callback, animation])
2613 * @param {mixed} obj the node to open
2614 * @param {Function} callback a function to execute once the node is opened
2615 * @param {Number} animation the animation duration in milliseconds when opening the node (overrides the `core.animation` setting). Use `false` for no animation.
2616 * @trigger open_node.jstree, after_open.jstree, before_open.jstree
2617 */
2618 open_node : function (obj, callback, animation) {
2619 var t1, t2, d, t;
2620 if($.isArray(obj)) {
2621 obj = obj.slice();
2622 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2623 this.open_node(obj[t1], callback, animation);
2624 }
2625 return true;
2626 }
2627 obj = this.get_node(obj);
2628 if(!obj || obj.id === $.jstree.root) {
2629 return false;
2630 }
2631 animation = animation === undefined ? this.settings.core.animation : animation;
2632 if(!this.is_closed(obj)) {
2633 if(callback) {
2634 callback.call(this, obj, false);
2635 }
2636 return false;
2637 }
2638 if(!this.is_loaded(obj)) {
2639 if(this.is_loading(obj)) {
2640 return setTimeout($.proxy(function () {
2641 this.open_node(obj, callback, animation);
2642 }, this), 500);
2643 }
2644 this.load_node(obj, function (o, ok) {
2645 return ok ? this.open_node(o, callback, animation) : (callback ? callback.call(this, o, false) : false);
2646 });
2647 }
2648 else {
2649 d = this.get_node(obj, true);
2650 t = this;
2651 if(d.length) {
2652 if(animation && d.children(".jstree-children").length) {
2653 d.children(".jstree-children").stop(true, true);
2654 }
2655 if(obj.children.length && !this._firstChild(d.children('.jstree-children')[0])) {
2656 this.draw_children(obj);
2657 //d = this.get_node(obj, true);
2658 }
2659 if(!animation) {
2660 this.trigger('before_open', { "node" : obj });
2661 d[0].className = d[0].className.replace('jstree-closed', 'jstree-open');
2662 d[0].setAttribute("aria-expanded", true);
2663 }
2664 else {
2665 this.trigger('before_open', { "node" : obj });
2666 d
2667 .children(".jstree-children").css("display","none").end()
2668 .removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true)
2669 .children(".jstree-children").stop(true, true)
2670 .slideDown(animation, function () {
2671 this.style.display = "";
2672 if (t.element) {
2673 t.trigger("after_open", { "node" : obj });
2674 }
2675 });
2676 }
2677 }
2678 obj.state.opened = true;
2679 if(callback) {
2680 callback.call(this, obj, true);
2681 }
2682 if(!d.length) {
2683 /**
2684 * 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)
2685 * @event
2686 * @name before_open.jstree
2687 * @param {Object} node the opened node
2688 */
2689 this.trigger('before_open', { "node" : obj });
2690 }
2691 /**
2692 * triggered when a node is opened (if there is an animation it will not be completed yet)
2693 * @event
2694 * @name open_node.jstree
2695 * @param {Object} node the opened node
2696 */
2697 this.trigger('open_node', { "node" : obj });
2698 if(!animation || !d.length) {
2699 /**
2700 * triggered when a node is opened and the animation is complete
2701 * @event
2702 * @name after_open.jstree
2703 * @param {Object} node the opened node
2704 */
2705 this.trigger("after_open", { "node" : obj });
2706 }
2707 return true;
2708 }
2709 },
2710 /**
2711 * opens every parent of a node (node should be loaded)
2712 * @name _open_to(obj)
2713 * @param {mixed} obj the node to reveal
2714 * @private
2715 */
2716 _open_to : function (obj) {
2717 obj = this.get_node(obj);
2718 if(!obj || obj.id === $.jstree.root) {
2719 return false;
2720 }
2721 var i, j, p = obj.parents;
2722 for(i = 0, j = p.length; i < j; i+=1) {
2723 if(i !== $.jstree.root) {
2724 this.open_node(p[i], false, 0);
2725 }
2726 }
2727 return $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element);
2728 },
2729 /**
2730 * closes a node, hiding its children
2731 * @name close_node(obj [, animation])
2732 * @param {mixed} obj the node to close
2733 * @param {Number} animation the animation duration in milliseconds when closing the node (overrides the `core.animation` setting). Use `false` for no animation.
2734 * @trigger close_node.jstree, after_close.jstree
2735 */
2736 close_node : function (obj, animation) {
2737 var t1, t2, t, d;
2738 if($.isArray(obj)) {
2739 obj = obj.slice();
2740 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2741 this.close_node(obj[t1], animation);
2742 }
2743 return true;
2744 }
2745 obj = this.get_node(obj);
2746 if(!obj || obj.id === $.jstree.root) {
2747 return false;
2748 }
2749 if(this.is_closed(obj)) {
2750 return false;
2751 }
2752 animation = animation === undefined ? this.settings.core.animation : animation;
2753 t = this;
2754 d = this.get_node(obj, true);
2755
2756 obj.state.opened = false;
2757 /**
2758 * triggered when a node is closed (if there is an animation it will not be complete yet)
2759 * @event
2760 * @name close_node.jstree
2761 * @param {Object} node the closed node
2762 */
2763 this.trigger('close_node',{ "node" : obj });
2764 if(!d.length) {
2765 /**
2766 * triggered when a node is closed and the animation is complete
2767 * @event
2768 * @name after_close.jstree
2769 * @param {Object} node the closed node
2770 */
2771 this.trigger("after_close", { "node" : obj });
2772 }
2773 else {
2774 if(!animation) {
2775 d[0].className = d[0].className.replace('jstree-open', 'jstree-closed');
2776 d.attr("aria-expanded", false).children('.jstree-children').remove();
2777 this.trigger("after_close", { "node" : obj });
2778 }
2779 else {
2780 d
2781 .children(".jstree-children").attr("style","display:block !important").end()
2782 .removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false)
2783 .children(".jstree-children").stop(true, true).slideUp(animation, function () {
2784 this.style.display = "";
2785 d.children('.jstree-children').remove();
2786 if (t.element) {
2787 t.trigger("after_close", { "node" : obj });
2788 }
2789 });
2790 }
2791 }
2792 },
2793 /**
2794 * toggles a node - closing it if it is open, opening it if it is closed
2795 * @name toggle_node(obj)
2796 * @param {mixed} obj the node to toggle
2797 */
2798 toggle_node : function (obj) {
2799 var t1, t2;
2800 if($.isArray(obj)) {
2801 obj = obj.slice();
2802 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2803 this.toggle_node(obj[t1]);
2804 }
2805 return true;
2806 }
2807 if(this.is_closed(obj)) {
2808 return this.open_node(obj);
2809 }
2810 if(this.is_open(obj)) {
2811 return this.close_node(obj);
2812 }
2813 },
2814 /**
2815 * 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.
2816 * @name open_all([obj, animation, original_obj])
2817 * @param {mixed} obj the node to open recursively, omit to open all nodes in the tree
2818 * @param {Number} animation the animation duration in milliseconds when opening the nodes, the default is no animation
2819 * @param {jQuery} reference to the node that started the process (internal use)
2820 * @trigger open_all.jstree
2821 */
2822 open_all : function (obj, animation, original_obj) {
2823 if(!obj) { obj = $.jstree.root; }
2824 obj = this.get_node(obj);
2825 if(!obj) { return false; }
2826 var dom = obj.id === $.jstree.root ? this.get_container_ul() : this.get_node(obj, true), i, j, _this;
2827 if(!dom.length) {
2828 for(i = 0, j = obj.children_d.length; i < j; i++) {
2829 if(this.is_closed(this._model.data[obj.children_d[i]])) {
2830 this._model.data[obj.children_d[i]].state.opened = true;
2831 }
2832 }
2833 return this.trigger('open_all', { "node" : obj });
2834 }
2835 original_obj = original_obj || dom;
2836 _this = this;
2837 dom = this.is_closed(obj) ? dom.find('.jstree-closed').addBack() : dom.find('.jstree-closed');
2838 dom.each(function () {
2839 _this.open_node(
2840 this,
2841 function(node, status) { if(status && this.is_parent(node)) { this.open_all(node, animation, original_obj); } },
2842 animation || 0
2843 );
2844 });
2845 if(original_obj.find('.jstree-closed').length === 0) {
2846 /**
2847 * triggered when an `open_all` call completes
2848 * @event
2849 * @name open_all.jstree
2850 * @param {Object} node the opened node
2851 */
2852 this.trigger('open_all', { "node" : this.get_node(original_obj) });
2853 }
2854 },
2855 /**
2856 * closes all nodes within a node (or the tree), revaling their children
2857 * @name close_all([obj, animation])
2858 * @param {mixed} obj the node to close recursively, omit to close all nodes in the tree
2859 * @param {Number} animation the animation duration in milliseconds when closing the nodes, the default is no animation
2860 * @trigger close_all.jstree
2861 */
2862 close_all : function (obj, animation) {
2863 if(!obj) { obj = $.jstree.root; }
2864 obj = this.get_node(obj);
2865 if(!obj) { return false; }
2866 var dom = obj.id === $.jstree.root ? this.get_container_ul() : this.get_node(obj, true),
2867 _this = this, i, j;
2868 if(dom.length) {
2869 dom = this.is_open(obj) ? dom.find('.jstree-open').addBack() : dom.find('.jstree-open');
2870 $(dom.get().reverse()).each(function () { _this.close_node(this, animation || 0); });
2871 }
2872 for(i = 0, j = obj.children_d.length; i < j; i++) {
2873 this._model.data[obj.children_d[i]].state.opened = false;
2874 }
2875 /**
2876 * triggered when an `close_all` call completes
2877 * @event
2878 * @name close_all.jstree
2879 * @param {Object} node the closed node
2880 */
2881 this.trigger('close_all', { "node" : obj });
2882 },
2883 /**
2884 * checks if a node is disabled (not selectable)
2885 * @name is_disabled(obj)
2886 * @param {mixed} obj
2887 * @return {Boolean}
2888 */
2889 is_disabled : function (obj) {
2890 obj = this.get_node(obj);
2891 return obj && obj.state && obj.state.disabled;
2892 },
2893 /**
2894 * enables a node - so that it can be selected
2895 * @name enable_node(obj)
2896 * @param {mixed} obj the node to enable
2897 * @trigger enable_node.jstree
2898 */
2899 enable_node : function (obj) {
2900 var t1, t2;
2901 if($.isArray(obj)) {
2902 obj = obj.slice();
2903 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2904 this.enable_node(obj[t1]);
2905 }
2906 return true;
2907 }
2908 obj = this.get_node(obj);
2909 if(!obj || obj.id === $.jstree.root) {
2910 return false;
2911 }
2912 obj.state.disabled = false;
2913 this.get_node(obj,true).children('.jstree-anchor').removeClass('jstree-disabled').attr('aria-disabled', false);
2914 /**
2915 * triggered when an node is enabled
2916 * @event
2917 * @name enable_node.jstree
2918 * @param {Object} node the enabled node
2919 */
2920 this.trigger('enable_node', { 'node' : obj });
2921 },
2922 /**
2923 * disables a node - so that it can not be selected
2924 * @name disable_node(obj)
2925 * @param {mixed} obj the node to disable
2926 * @trigger disable_node.jstree
2927 */
2928 disable_node : function (obj) {
2929 var t1, t2;
2930 if($.isArray(obj)) {
2931 obj = obj.slice();
2932 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2933 this.disable_node(obj[t1]);
2934 }
2935 return true;
2936 }
2937 obj = this.get_node(obj);
2938 if(!obj || obj.id === $.jstree.root) {
2939 return false;
2940 }
2941 obj.state.disabled = true;
2942 this.get_node(obj,true).children('.jstree-anchor').addClass('jstree-disabled').attr('aria-disabled', true);
2943 /**
2944 * triggered when an node is disabled
2945 * @event
2946 * @name disable_node.jstree
2947 * @param {Object} node the disabled node
2948 */
2949 this.trigger('disable_node', { 'node' : obj });
2950 },
2951 /**
2952 * determines if a node is hidden
2953 * @name is_hidden(obj)
2954 * @param {mixed} obj the node
2955 */
2956 is_hidden : function (obj) {
2957 obj = this.get_node(obj);
2958 return obj.state.hidden === true;
2959 },
2960 /**
2961 * hides a node - it is still in the structure but will not be visible
2962 * @name hide_node(obj)
2963 * @param {mixed} obj the node to hide
2964 * @param {Boolean} skip_redraw internal parameter controlling if redraw is called
2965 * @trigger hide_node.jstree
2966 */
2967 hide_node : function (obj, skip_redraw) {
2968 var t1, t2;
2969 if($.isArray(obj)) {
2970 obj = obj.slice();
2971 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
2972 this.hide_node(obj[t1], true);
2973 }
2974 if (!skip_redraw) {
2975 this.redraw();
2976 }
2977 return true;
2978 }
2979 obj = this.get_node(obj);
2980 if(!obj || obj.id === $.jstree.root) {
2981 return false;
2982 }
2983 if(!obj.state.hidden) {
2984 obj.state.hidden = true;
2985 this._node_changed(obj.parent);
2986 if(!skip_redraw) {
2987 this.redraw();
2988 }
2989 /**
2990 * triggered when an node is hidden
2991 * @event
2992 * @name hide_node.jstree
2993 * @param {Object} node the hidden node
2994 */
2995 this.trigger('hide_node', { 'node' : obj });
2996 }
2997 },
2998 /**
2999 * shows a node
3000 * @name show_node(obj)
3001 * @param {mixed} obj the node to show
3002 * @param {Boolean} skip_redraw internal parameter controlling if redraw is called
3003 * @trigger show_node.jstree
3004 */
3005 show_node : function (obj, skip_redraw) {
3006 var t1, t2;
3007 if($.isArray(obj)) {
3008 obj = obj.slice();
3009 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3010 this.show_node(obj[t1], true);
3011 }
3012 if (!skip_redraw) {
3013 this.redraw();
3014 }
3015 return true;
3016 }
3017 obj = this.get_node(obj);
3018 if(!obj || obj.id === $.jstree.root) {
3019 return false;
3020 }
3021 if(obj.state.hidden) {
3022 obj.state.hidden = false;
3023 this._node_changed(obj.parent);
3024 if(!skip_redraw) {
3025 this.redraw();
3026 }
3027 /**
3028 * triggered when an node is shown
3029 * @event
3030 * @name show_node.jstree
3031 * @param {Object} node the shown node
3032 */
3033 this.trigger('show_node', { 'node' : obj });
3034 }
3035 },
3036 /**
3037 * hides all nodes
3038 * @name hide_all()
3039 * @trigger hide_all.jstree
3040 */
3041 hide_all : function (skip_redraw) {
3042 var i, m = this._model.data, ids = [];
3043 for(i in m) {
3044 if(m.hasOwnProperty(i) && i !== $.jstree.root && !m[i].state.hidden) {
3045 m[i].state.hidden = true;
3046 ids.push(i);
3047 }
3048 }
3049 this._model.force_full_redraw = true;
3050 if(!skip_redraw) {
3051 this.redraw();
3052 }
3053 /**
3054 * triggered when all nodes are hidden
3055 * @event
3056 * @name hide_all.jstree
3057 * @param {Array} nodes the IDs of all hidden nodes
3058 */
3059 this.trigger('hide_all', { 'nodes' : ids });
3060 return ids;
3061 },
3062 /**
3063 * shows all nodes
3064 * @name show_all()
3065 * @trigger show_all.jstree
3066 */
3067 show_all : function (skip_redraw) {
3068 var i, m = this._model.data, ids = [];
3069 for(i in m) {
3070 if(m.hasOwnProperty(i) && i !== $.jstree.root && m[i].state.hidden) {
3071 m[i].state.hidden = false;
3072 ids.push(i);
3073 }
3074 }
3075 this._model.force_full_redraw = true;
3076 if(!skip_redraw) {
3077 this.redraw();
3078 }
3079 /**
3080 * triggered when all nodes are shown
3081 * @event
3082 * @name show_all.jstree
3083 * @param {Array} nodes the IDs of all shown nodes
3084 */
3085 this.trigger('show_all', { 'nodes' : ids });
3086 return ids;
3087 },
3088 /**
3089 * called when a node is selected by the user. Used internally.
3090 * @private
3091 * @name activate_node(obj, e)
3092 * @param {mixed} obj the node
3093 * @param {Object} e the related event
3094 * @trigger activate_node.jstree, changed.jstree
3095 */
3096 activate_node : function (obj, e) {
3097 if(this.is_disabled(obj)) {
3098 return false;
3099 }
3100 if(!e || typeof e !== 'object') {
3101 e = {};
3102 }
3103
3104 // 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
3105 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;
3106 if(this._data.core.last_clicked && !this._data.core.last_clicked.state.selected) { this._data.core.last_clicked = null; }
3107 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]); }
3108
3109 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 ) )) {
3110 if(!this.settings.core.multiple && (e.metaKey || e.ctrlKey || e.shiftKey) && this.is_selected(obj)) {
3111 this.deselect_node(obj, false, e);
3112 }
3113 else {
3114 this.deselect_all(true);
3115 this.select_node(obj, false, false, e);
3116 this._data.core.last_clicked = this.get_node(obj);
3117 }
3118 }
3119 else {
3120 if(e.shiftKey) {
3121 var o = this.get_node(obj).id,
3122 l = this._data.core.last_clicked.id,
3123 p = this.get_node(this._data.core.last_clicked.parent).children,
3124 c = false,
3125 i, j;
3126 for(i = 0, j = p.length; i < j; i += 1) {
3127 // separate IFs work whem o and l are the same
3128 if(p[i] === o) {
3129 c = !c;
3130 }
3131 if(p[i] === l) {
3132 c = !c;
3133 }
3134 if(!this.is_disabled(p[i]) && (c || p[i] === o || p[i] === l)) {
3135 if (!this.is_hidden(p[i])) {
3136 this.select_node(p[i], true, false, e);
3137 }
3138 }
3139 else {
3140 this.deselect_node(p[i], true, e);
3141 }
3142 }
3143 this.trigger('changed', { 'action' : 'select_node', 'node' : this.get_node(obj), 'selected' : this._data.core.selected, 'event' : e });
3144 }
3145 else {
3146 if(!this.is_selected(obj)) {
3147 this.select_node(obj, false, false, e);
3148 }
3149 else {
3150 this.deselect_node(obj, false, e);
3151 }
3152 }
3153 }
3154 /**
3155 * triggered when an node is clicked or intercated with by the user
3156 * @event
3157 * @name activate_node.jstree
3158 * @param {Object} node
3159 * @param {Object} event the ooriginal event (if any) which triggered the call (may be an empty object)
3160 */
3161 this.trigger('activate_node', { 'node' : this.get_node(obj), 'event' : e });
3162 },
3163 /**
3164 * applies the hover state on a node, called when a node is hovered by the user. Used internally.
3165 * @private
3166 * @name hover_node(obj)
3167 * @param {mixed} obj
3168 * @trigger hover_node.jstree
3169 */
3170 hover_node : function (obj) {
3171 obj = this.get_node(obj, true);
3172 if(!obj || !obj.length || obj.children('.jstree-hovered').length) {
3173 return false;
3174 }
3175 var o = this.element.find('.jstree-hovered'), t = this.element;
3176 if(o && o.length) { this.dehover_node(o); }
3177
3178 obj.children('.jstree-anchor').addClass('jstree-hovered');
3179 /**
3180 * triggered when an node is hovered
3181 * @event
3182 * @name hover_node.jstree
3183 * @param {Object} node
3184 */
3185 this.trigger('hover_node', { 'node' : this.get_node(obj) });
3186 setTimeout(function () { t.attr('aria-activedescendant', obj[0].id); }, 0);
3187 },
3188 /**
3189 * removes the hover state from a nodecalled when a node is no longer hovered by the user. Used internally.
3190 * @private
3191 * @name dehover_node(obj)
3192 * @param {mixed} obj
3193 * @trigger dehover_node.jstree
3194 */
3195 dehover_node : function (obj) {
3196 obj = this.get_node(obj, true);
3197 if(!obj || !obj.length || !obj.children('.jstree-hovered').length) {
3198 return false;
3199 }
3200 obj.children('.jstree-anchor').removeClass('jstree-hovered');
3201 /**
3202 * triggered when an node is no longer hovered
3203 * @event
3204 * @name dehover_node.jstree
3205 * @param {Object} node
3206 */
3207 this.trigger('dehover_node', { 'node' : this.get_node(obj) });
3208 },
3209 /**
3210 * select a node
3211 * @name select_node(obj [, supress_event, prevent_open])
3212 * @param {mixed} obj an array can be used to select multiple nodes
3213 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3214 * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened
3215 * @trigger select_node.jstree, changed.jstree
3216 */
3217 select_node : function (obj, supress_event, prevent_open, e) {
3218 var dom, t1, t2, th;
3219 if($.isArray(obj)) {
3220 obj = obj.slice();
3221 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3222 this.select_node(obj[t1], supress_event, prevent_open, e);
3223 }
3224 return true;
3225 }
3226 obj = this.get_node(obj);
3227 if(!obj || obj.id === $.jstree.root) {
3228 return false;
3229 }
3230 dom = this.get_node(obj, true);
3231 if(!obj.state.selected) {
3232 obj.state.selected = true;
3233 this._data.core.selected.push(obj.id);
3234 if(!prevent_open) {
3235 dom = this._open_to(obj);
3236 }
3237 if(dom && dom.length) {
3238 dom.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked');
3239 }
3240 /**
3241 * triggered when an node is selected
3242 * @event
3243 * @name select_node.jstree
3244 * @param {Object} node
3245 * @param {Array} selected the current selection
3246 * @param {Object} event the event (if any) that triggered this select_node
3247 */
3248 this.trigger('select_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3249 if(!supress_event) {
3250 /**
3251 * triggered when selection changes
3252 * @event
3253 * @name changed.jstree
3254 * @param {Object} node
3255 * @param {Object} action the action that caused the selection to change
3256 * @param {Array} selected the current selection
3257 * @param {Object} event the event (if any) that triggered this changed event
3258 */
3259 this.trigger('changed', { 'action' : 'select_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3260 }
3261 }
3262 },
3263 /**
3264 * deselect a node
3265 * @name deselect_node(obj [, supress_event])
3266 * @param {mixed} obj an array can be used to deselect multiple nodes
3267 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3268 * @trigger deselect_node.jstree, changed.jstree
3269 */
3270 deselect_node : function (obj, supress_event, e) {
3271 var t1, t2, dom;
3272 if($.isArray(obj)) {
3273 obj = obj.slice();
3274 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3275 this.deselect_node(obj[t1], supress_event, e);
3276 }
3277 return true;
3278 }
3279 obj = this.get_node(obj);
3280 if(!obj || obj.id === $.jstree.root) {
3281 return false;
3282 }
3283 dom = this.get_node(obj, true);
3284 if(obj.state.selected) {
3285 obj.state.selected = false;
3286 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.id);
3287 if(dom.length) {
3288 dom.attr('aria-selected', false).children('.jstree-anchor').removeClass('jstree-clicked');
3289 }
3290 /**
3291 * triggered when an node is deselected
3292 * @event
3293 * @name deselect_node.jstree
3294 * @param {Object} node
3295 * @param {Array} selected the current selection
3296 * @param {Object} event the event (if any) that triggered this deselect_node
3297 */
3298 this.trigger('deselect_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3299 if(!supress_event) {
3300 this.trigger('changed', { 'action' : 'deselect_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e });
3301 }
3302 }
3303 },
3304 /**
3305 * select all nodes in the tree
3306 * @name select_all([supress_event])
3307 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3308 * @trigger select_all.jstree, changed.jstree
3309 */
3310 select_all : function (supress_event) {
3311 var tmp = this._data.core.selected.concat([]), i, j;
3312 this._data.core.selected = this._model.data[$.jstree.root].children_d.concat();
3313 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
3314 if(this._model.data[this._data.core.selected[i]]) {
3315 this._model.data[this._data.core.selected[i]].state.selected = true;
3316 }
3317 }
3318 this.redraw(true);
3319 /**
3320 * triggered when all nodes are selected
3321 * @event
3322 * @name select_all.jstree
3323 * @param {Array} selected the current selection
3324 */
3325 this.trigger('select_all', { 'selected' : this._data.core.selected });
3326 if(!supress_event) {
3327 this.trigger('changed', { 'action' : 'select_all', 'selected' : this._data.core.selected, 'old_selection' : tmp });
3328 }
3329 },
3330 /**
3331 * deselect all selected nodes
3332 * @name deselect_all([supress_event])
3333 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered
3334 * @trigger deselect_all.jstree, changed.jstree
3335 */
3336 deselect_all : function (supress_event) {
3337 var tmp = this._data.core.selected.concat([]), i, j;
3338 for(i = 0, j = this._data.core.selected.length; i < j; i++) {
3339 if(this._model.data[this._data.core.selected[i]]) {
3340 this._model.data[this._data.core.selected[i]].state.selected = false;
3341 }
3342 }
3343 this._data.core.selected = [];
3344 this.element.find('.jstree-clicked').removeClass('jstree-clicked').parent().attr('aria-selected', false);
3345 /**
3346 * triggered when all nodes are deselected
3347 * @event
3348 * @name deselect_all.jstree
3349 * @param {Object} node the previous selection
3350 * @param {Array} selected the current selection
3351 */
3352 this.trigger('deselect_all', { 'selected' : this._data.core.selected, 'node' : tmp });
3353 if(!supress_event) {
3354 this.trigger('changed', { 'action' : 'deselect_all', 'selected' : this._data.core.selected, 'old_selection' : tmp });
3355 }
3356 },
3357 /**
3358 * checks if a node is selected
3359 * @name is_selected(obj)
3360 * @param {mixed} obj
3361 * @return {Boolean}
3362 */
3363 is_selected : function (obj) {
3364 obj = this.get_node(obj);
3365 if(!obj || obj.id === $.jstree.root) {
3366 return false;
3367 }
3368 return obj.state.selected;
3369 },
3370 /**
3371 * get an array of all selected nodes
3372 * @name get_selected([full])
3373 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3374 * @return {Array}
3375 */
3376 get_selected : function (full) {
3377 return full ? $.map(this._data.core.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.core.selected.slice();
3378 },
3379 /**
3380 * get an array of all top level selected nodes (ignoring children of selected nodes)
3381 * @name get_top_selected([full])
3382 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3383 * @return {Array}
3384 */
3385 get_top_selected : function (full) {
3386 var tmp = this.get_selected(true),
3387 obj = {}, i, j, k, l;
3388 for(i = 0, j = tmp.length; i < j; i++) {
3389 obj[tmp[i].id] = tmp[i];
3390 }
3391 for(i = 0, j = tmp.length; i < j; i++) {
3392 for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
3393 if(obj[tmp[i].children_d[k]]) {
3394 delete obj[tmp[i].children_d[k]];
3395 }
3396 }
3397 }
3398 tmp = [];
3399 for(i in obj) {
3400 if(obj.hasOwnProperty(i)) {
3401 tmp.push(i);
3402 }
3403 }
3404 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
3405 },
3406 /**
3407 * get an array of all bottom level selected nodes (ignoring selected parents)
3408 * @name get_bottom_selected([full])
3409 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
3410 * @return {Array}
3411 */
3412 get_bottom_selected : function (full) {
3413 var tmp = this.get_selected(true),
3414 obj = [], i, j;
3415 for(i = 0, j = tmp.length; i < j; i++) {
3416 if(!tmp[i].children.length) {
3417 obj.push(tmp[i].id);
3418 }
3419 }
3420 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
3421 },
3422 /**
3423 * gets the current state of the tree so that it can be restored later with `set_state(state)`. Used internally.
3424 * @name get_state()
3425 * @private
3426 * @return {Object}
3427 */
3428 get_state : function () {
3429 var state = {
3430 'core' : {
3431 'open' : [],
3432 'loaded' : [],
3433 'scroll' : {
3434 'left' : this.element.scrollLeft(),
3435 'top' : this.element.scrollTop()
3436 },
3437 /*!
3438 'themes' : {
3439 'name' : this.get_theme(),
3440 'icons' : this._data.core.themes.icons,
3441 'dots' : this._data.core.themes.dots
3442 },
3443 */
3444 'selected' : []
3445 }
3446 }, i;
3447 for(i in this._model.data) {
3448 if(this._model.data.hasOwnProperty(i)) {
3449 if(i !== $.jstree.root) {
3450 if(this._model.data[i].state.loaded && this.settings.core.loaded_state) {
3451 state.core.loaded.push(i);
3452 }
3453 if(this._model.data[i].state.opened) {
3454 state.core.open.push(i);
3455 }
3456 if(this._model.data[i].state.selected) {
3457 state.core.selected.push(i);
3458 }
3459 }
3460 }
3461 }
3462 return state;
3463 },
3464 /**
3465 * sets the state of the tree. Used internally.
3466 * @name set_state(state [, callback])
3467 * @private
3468 * @param {Object} state the state to restore. Keep in mind this object is passed by reference and jstree will modify it.
3469 * @param {Function} callback an optional function to execute once the state is restored.
3470 * @trigger set_state.jstree
3471 */
3472 set_state : function (state, callback) {
3473 if(state) {
3474 if(state.core && state.core.selected && state.core.initial_selection === undefined) {
3475 state.core.initial_selection = this._data.core.selected.concat([]).sort().join(',');
3476 }
3477 if(state.core) {
3478 var res, n, t, _this, i;
3479 if(state.core.loaded) {
3480 if(!this.settings.core.loaded_state || !$.isArray(state.core.loaded) || !state.core.loaded.length) {
3481 delete state.core.loaded;
3482 this.set_state(state, callback);
3483 }
3484 else {
3485 this._load_nodes(state.core.loaded, function (nodes) {
3486 delete state.core.loaded;
3487 this.set_state(state, callback);
3488 });
3489 }
3490 return false;
3491 }
3492 if(state.core.open) {
3493 if(!$.isArray(state.core.open) || !state.core.open.length) {
3494 delete state.core.open;
3495 this.set_state(state, callback);
3496 }
3497 else {
3498 this._load_nodes(state.core.open, function (nodes) {
3499 this.open_node(nodes, false, 0);
3500 delete state.core.open;
3501 this.set_state(state, callback);
3502 });
3503 }
3504 return false;
3505 }
3506 if(state.core.scroll) {
3507 if(state.core.scroll && state.core.scroll.left !== undefined) {
3508 this.element.scrollLeft(state.core.scroll.left);
3509 }
3510 if(state.core.scroll && state.core.scroll.top !== undefined) {
3511 this.element.scrollTop(state.core.scroll.top);
3512 }
3513 delete state.core.scroll;
3514 this.set_state(state, callback);
3515 return false;
3516 }
3517 if(state.core.selected) {
3518 _this = this;
3519 if (state.core.initial_selection === undefined ||
3520 state.core.initial_selection === this._data.core.selected.concat([]).sort().join(',')
3521 ) {
3522 this.deselect_all();
3523 $.each(state.core.selected, function (i, v) {
3524 _this.select_node(v, false, true);
3525 });
3526 }
3527 delete state.core.initial_selection;
3528 delete state.core.selected;
3529 this.set_state(state, callback);
3530 return false;
3531 }
3532 for(i in state) {
3533 if(state.hasOwnProperty(i) && i !== "core" && $.inArray(i, this.settings.plugins) === -1) {
3534 delete state[i];
3535 }
3536 }
3537 if($.isEmptyObject(state.core)) {
3538 delete state.core;
3539 this.set_state(state, callback);
3540 return false;
3541 }
3542 }
3543 if($.isEmptyObject(state)) {
3544 state = null;
3545 if(callback) { callback.call(this); }
3546 /**
3547 * triggered when a `set_state` call completes
3548 * @event
3549 * @name set_state.jstree
3550 */
3551 this.trigger('set_state');
3552 return false;
3553 }
3554 return true;
3555 }
3556 return false;
3557 },
3558 /**
3559 * refreshes the tree - all nodes are reloaded with calls to `load_node`.
3560 * @name refresh()
3561 * @param {Boolean} skip_loading an option to skip showing the loading indicator
3562 * @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
3563 * @trigger refresh.jstree
3564 */
3565 refresh : function (skip_loading, forget_state) {
3566 this._data.core.state = forget_state === true ? {} : this.get_state();
3567 if(forget_state && $.isFunction(forget_state)) { this._data.core.state = forget_state.call(this, this._data.core.state); }
3568 this._cnt = 0;
3569 this._model.data = {};
3570 this._model.data[$.jstree.root] = {
3571 id : $.jstree.root,
3572 parent : null,
3573 parents : [],
3574 children : [],
3575 children_d : [],
3576 state : { loaded : false }
3577 };
3578 this._data.core.selected = [];
3579 this._data.core.last_clicked = null;
3580 this._data.core.focused = null;
3581
3582 var c = this.get_container_ul()[0].className;
3583 if(!skip_loading) {
3584 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>");
3585 this.element.attr('aria-activedescendant','j'+this._id+'_loading');
3586 }
3587 this.load_node($.jstree.root, function (o, s) {
3588 if(s) {
3589 this.get_container_ul()[0].className = c;
3590 if(this._firstChild(this.get_container_ul()[0])) {
3591 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
3592 }
3593 this.set_state($.extend(true, {}, this._data.core.state), function () {
3594 /**
3595 * triggered when a `refresh` call completes
3596 * @event
3597 * @name refresh.jstree
3598 */
3599 this.trigger('refresh');
3600 });
3601 }
3602 this._data.core.state = null;
3603 });
3604 },
3605 /**
3606 * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`.
3607 * @name refresh_node(obj)
3608 * @param {mixed} obj the node
3609 * @trigger refresh_node.jstree
3610 */
3611 refresh_node : function (obj) {
3612 obj = this.get_node(obj);
3613 if(!obj || obj.id === $.jstree.root) { return false; }
3614 var opened = [], to_load = [], s = this._data.core.selected.concat([]);
3615 to_load.push(obj.id);
3616 if(obj.state.opened === true) { opened.push(obj.id); }
3617 this.get_node(obj, true).find('.jstree-open').each(function() { to_load.push(this.id); opened.push(this.id); });
3618 this._load_nodes(to_load, $.proxy(function (nodes) {
3619 this.open_node(opened, false, 0);
3620 this.select_node(s);
3621 /**
3622 * triggered when a node is refreshed
3623 * @event
3624 * @name refresh_node.jstree
3625 * @param {Object} node - the refreshed node
3626 * @param {Array} nodes - an array of the IDs of the nodes that were reloaded
3627 */
3628 this.trigger('refresh_node', { 'node' : obj, 'nodes' : nodes });
3629 }, this), false, true);
3630 },
3631 /**
3632 * set (change) the ID of a node
3633 * @name set_id(obj, id)
3634 * @param {mixed} obj the node
3635 * @param {String} id the new ID
3636 * @return {Boolean}
3637 * @trigger set_id.jstree
3638 */
3639 set_id : function (obj, id) {
3640 obj = this.get_node(obj);
3641 if(!obj || obj.id === $.jstree.root) { return false; }
3642 var i, j, m = this._model.data, old = obj.id;
3643 id = id.toString();
3644 // update parents (replace current ID with new one in children and children_d)
3645 m[obj.parent].children[$.inArray(obj.id, m[obj.parent].children)] = id;
3646 for(i = 0, j = obj.parents.length; i < j; i++) {
3647 m[obj.parents[i]].children_d[$.inArray(obj.id, m[obj.parents[i]].children_d)] = id;
3648 }
3649 // update children (replace current ID with new one in parent and parents)
3650 for(i = 0, j = obj.children.length; i < j; i++) {
3651 m[obj.children[i]].parent = id;
3652 }
3653 for(i = 0, j = obj.children_d.length; i < j; i++) {
3654 m[obj.children_d[i]].parents[$.inArray(obj.id, m[obj.children_d[i]].parents)] = id;
3655 }
3656 i = $.inArray(obj.id, this._data.core.selected);
3657 if(i !== -1) { this._data.core.selected[i] = id; }
3658 // update model and obj itself (obj.id, this._model.data[KEY])
3659 i = this.get_node(obj.id, true);
3660 if(i) {
3661 i.attr('id', id); //.children('.jstree-anchor').attr('id', id + '_anchor').end().attr('aria-labelledby', id + '_anchor');
3662 if(this.element.attr('aria-activedescendant') === obj.id) {
3663 this.element.attr('aria-activedescendant', id);
3664 }
3665 }
3666 delete m[obj.id];
3667 obj.id = id;
3668 obj.li_attr.id = id;
3669 m[id] = obj;
3670 /**
3671 * triggered when a node id value is changed
3672 * @event
3673 * @name set_id.jstree
3674 * @param {Object} node
3675 * @param {String} old the old id
3676 */
3677 this.trigger('set_id',{ "node" : obj, "new" : obj.id, "old" : old });
3678 return true;
3679 },
3680 /**
3681 * get the text value of a node
3682 * @name get_text(obj)
3683 * @param {mixed} obj the node
3684 * @return {String}
3685 */
3686 get_text : function (obj) {
3687 obj = this.get_node(obj);
3688 return (!obj || obj.id === $.jstree.root) ? false : obj.text;
3689 },
3690 /**
3691 * set the text value of a node. Used internally, please use `rename_node(obj, val)`.
3692 * @private
3693 * @name set_text(obj, val)
3694 * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes
3695 * @param {String} val the new text value
3696 * @return {Boolean}
3697 * @trigger set_text.jstree
3698 */
3699 set_text : function (obj, val) {
3700 var t1, t2;
3701 if($.isArray(obj)) {
3702 obj = obj.slice();
3703 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3704 this.set_text(obj[t1], val);
3705 }
3706 return true;
3707 }
3708 obj = this.get_node(obj);
3709 if(!obj || obj.id === $.jstree.root) { return false; }
3710 obj.text = val;
3711 if(this.get_node(obj, true).length) {
3712 this.redraw_node(obj.id);
3713 }
3714 /**
3715 * triggered when a node text value is changed
3716 * @event
3717 * @name set_text.jstree
3718 * @param {Object} obj
3719 * @param {String} text the new value
3720 */
3721 this.trigger('set_text',{ "obj" : obj, "text" : val });
3722 return true;
3723 },
3724 /**
3725 * gets a JSON representation of a node (or the whole tree)
3726 * @name get_json([obj, options])
3727 * @param {mixed} obj
3728 * @param {Object} options
3729 * @param {Boolean} options.no_state do not return state information
3730 * @param {Boolean} options.no_id do not return ID
3731 * @param {Boolean} options.no_children do not include children
3732 * @param {Boolean} options.no_data do not include node data
3733 * @param {Boolean} options.no_li_attr do not include LI attributes
3734 * @param {Boolean} options.no_a_attr do not include A attributes
3735 * @param {Boolean} options.flat return flat JSON instead of nested
3736 * @return {Object}
3737 */
3738 get_json : function (obj, options, flat) {
3739 obj = this.get_node(obj || $.jstree.root);
3740 if(!obj) { return false; }
3741 if(options && options.flat && !flat) { flat = []; }
3742 var tmp = {
3743 'id' : obj.id,
3744 'text' : obj.text,
3745 'icon' : this.get_icon(obj),
3746 'li_attr' : $.extend(true, {}, obj.li_attr),
3747 'a_attr' : $.extend(true, {}, obj.a_attr),
3748 'state' : {},
3749 'data' : options && options.no_data ? false : $.extend(true, $.isArray(obj.data)?[]:{}, obj.data)
3750 //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ),
3751 }, i, j;
3752 if(options && options.flat) {
3753 tmp.parent = obj.parent;
3754 }
3755 else {
3756 tmp.children = [];
3757 }
3758 if(!options || !options.no_state) {
3759 for(i in obj.state) {
3760 if(obj.state.hasOwnProperty(i)) {
3761 tmp.state[i] = obj.state[i];
3762 }
3763 }
3764 } else {
3765 delete tmp.state;
3766 }
3767 if(options && options.no_li_attr) {
3768 delete tmp.li_attr;
3769 }
3770 if(options && options.no_a_attr) {
3771 delete tmp.a_attr;
3772 }
3773 if(options && options.no_id) {
3774 delete tmp.id;
3775 if(tmp.li_attr && tmp.li_attr.id) {
3776 delete tmp.li_attr.id;
3777 }
3778 if(tmp.a_attr && tmp.a_attr.id) {
3779 delete tmp.a_attr.id;
3780 }
3781 }
3782 if(options && options.flat && obj.id !== $.jstree.root) {
3783 flat.push(tmp);
3784 }
3785 if(!options || !options.no_children) {
3786 for(i = 0, j = obj.children.length; i < j; i++) {
3787 if(options && options.flat) {
3788 this.get_json(obj.children[i], options, flat);
3789 }
3790 else {
3791 tmp.children.push(this.get_json(obj.children[i], options));
3792 }
3793 }
3794 }
3795 return options && options.flat ? flat : (obj.id === $.jstree.root ? tmp.children : tmp);
3796 },
3797 /**
3798 * create a new node (do not confuse with load_node)
3799 * @name create_node([par, node, pos, callback, is_loaded])
3800 * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`)
3801 * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name)
3802 * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last"
3803 * @param {Function} callback a function to be called once the node is created
3804 * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded
3805 * @return {String} the ID of the newly create node
3806 * @trigger model.jstree, create_node.jstree
3807 */
3808 create_node : function (par, node, pos, callback, is_loaded) {
3809 if(par === null) { par = $.jstree.root; }
3810 par = this.get_node(par);
3811 if(!par) { return false; }
3812 pos = pos === undefined ? "last" : pos;
3813 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
3814 return this.load_node(par, function () { this.create_node(par, node, pos, callback, true); });
3815 }
3816 if(!node) { node = { "text" : this.get_string('New node') }; }
3817 if(typeof node === "string") {
3818 node = { "text" : node };
3819 } else {
3820 node = $.extend(true, {}, node);
3821 }
3822 if(node.text === undefined) { node.text = this.get_string('New node'); }
3823 var tmp, dpc, i, j;
3824
3825 if(par.id === $.jstree.root) {
3826 if(pos === "before") { pos = "first"; }
3827 if(pos === "after") { pos = "last"; }
3828 }
3829 switch(pos) {
3830 case "before":
3831 tmp = this.get_node(par.parent);
3832 pos = $.inArray(par.id, tmp.children);
3833 par = tmp;
3834 break;
3835 case "after" :
3836 tmp = this.get_node(par.parent);
3837 pos = $.inArray(par.id, tmp.children) + 1;
3838 par = tmp;
3839 break;
3840 case "inside":
3841 case "first":
3842 pos = 0;
3843 break;
3844 case "last":
3845 pos = par.children.length;
3846 break;
3847 default:
3848 if(!pos) { pos = 0; }
3849 break;
3850 }
3851 if(pos > par.children.length) { pos = par.children.length; }
3852 if(!node.id) { node.id = true; }
3853 if(!this.check("create_node", node, par, pos)) {
3854 this.settings.core.error.call(this, this._data.core.last_error);
3855 return false;
3856 }
3857 if(node.id === true) { delete node.id; }
3858 node = this._parse_model_from_json(node, par.id, par.parents.concat());
3859 if(!node) { return false; }
3860 tmp = this.get_node(node);
3861 dpc = [];
3862 dpc.push(node);
3863 dpc = dpc.concat(tmp.children_d);
3864 this.trigger('model', { "nodes" : dpc, "parent" : par.id });
3865
3866 par.children_d = par.children_d.concat(dpc);
3867 for(i = 0, j = par.parents.length; i < j; i++) {
3868 this._model.data[par.parents[i]].children_d = this._model.data[par.parents[i]].children_d.concat(dpc);
3869 }
3870 node = tmp;
3871 tmp = [];
3872 for(i = 0, j = par.children.length; i < j; i++) {
3873 tmp[i >= pos ? i+1 : i] = par.children[i];
3874 }
3875 tmp[pos] = node.id;
3876 par.children = tmp;
3877
3878 this.redraw_node(par, true);
3879 /**
3880 * triggered when a node is created
3881 * @event
3882 * @name create_node.jstree
3883 * @param {Object} node
3884 * @param {String} parent the parent's ID
3885 * @param {Number} position the position of the new node among the parent's children
3886 */
3887 this.trigger('create_node', { "node" : this.get_node(node), "parent" : par.id, "position" : pos });
3888 if(callback) { callback.call(this, this.get_node(node)); }
3889 return node.id;
3890 },
3891 /**
3892 * set the text value of a node
3893 * @name rename_node(obj, val)
3894 * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name
3895 * @param {String} val the new text value
3896 * @return {Boolean}
3897 * @trigger rename_node.jstree
3898 */
3899 rename_node : function (obj, val) {
3900 var t1, t2, old;
3901 if($.isArray(obj)) {
3902 obj = obj.slice();
3903 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3904 this.rename_node(obj[t1], val);
3905 }
3906 return true;
3907 }
3908 obj = this.get_node(obj);
3909 if(!obj || obj.id === $.jstree.root) { return false; }
3910 old = obj.text;
3911 if(!this.check("rename_node", obj, this.get_parent(obj), val)) {
3912 this.settings.core.error.call(this, this._data.core.last_error);
3913 return false;
3914 }
3915 this.set_text(obj, val); // .apply(this, Array.prototype.slice.call(arguments))
3916 /**
3917 * triggered when a node is renamed
3918 * @event
3919 * @name rename_node.jstree
3920 * @param {Object} node
3921 * @param {String} text the new value
3922 * @param {String} old the old value
3923 */
3924 this.trigger('rename_node', { "node" : obj, "text" : val, "old" : old });
3925 return true;
3926 },
3927 /**
3928 * remove a node
3929 * @name delete_node(obj)
3930 * @param {mixed} obj the node, you can pass an array to delete multiple nodes
3931 * @return {Boolean}
3932 * @trigger delete_node.jstree, changed.jstree
3933 */
3934 delete_node : function (obj) {
3935 var t1, t2, par, pos, tmp, i, j, k, l, c, top, lft;
3936 if($.isArray(obj)) {
3937 obj = obj.slice();
3938 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
3939 this.delete_node(obj[t1]);
3940 }
3941 return true;
3942 }
3943 obj = this.get_node(obj);
3944 if(!obj || obj.id === $.jstree.root) { return false; }
3945 par = this.get_node(obj.parent);
3946 pos = $.inArray(obj.id, par.children);
3947 c = false;
3948 if(!this.check("delete_node", obj, par, pos)) {
3949 this.settings.core.error.call(this, this._data.core.last_error);
3950 return false;
3951 }
3952 if(pos !== -1) {
3953 par.children = $.vakata.array_remove(par.children, pos);
3954 }
3955 tmp = obj.children_d.concat([]);
3956 tmp.push(obj.id);
3957 for(i = 0, j = obj.parents.length; i < j; i++) {
3958 this._model.data[obj.parents[i]].children_d = $.vakata.array_filter(this._model.data[obj.parents[i]].children_d, function (v) {
3959 return $.inArray(v, tmp) === -1;
3960 });
3961 }
3962 for(k = 0, l = tmp.length; k < l; k++) {
3963 if(this._model.data[tmp[k]].state.selected) {
3964 c = true;
3965 break;
3966 }
3967 }
3968 if (c) {
3969 this._data.core.selected = $.vakata.array_filter(this._data.core.selected, function (v) {
3970 return $.inArray(v, tmp) === -1;
3971 });
3972 }
3973 /**
3974 * triggered when a node is deleted
3975 * @event
3976 * @name delete_node.jstree
3977 * @param {Object} node
3978 * @param {String} parent the parent's ID
3979 */
3980 this.trigger('delete_node', { "node" : obj, "parent" : par.id });
3981 if(c) {
3982 this.trigger('changed', { 'action' : 'delete_node', 'node' : obj, 'selected' : this._data.core.selected, 'parent' : par.id });
3983 }
3984 for(k = 0, l = tmp.length; k < l; k++) {
3985 delete this._model.data[tmp[k]];
3986 }
3987 if($.inArray(this._data.core.focused, tmp) !== -1) {
3988 this._data.core.focused = null;
3989 top = this.element[0].scrollTop;
3990 lft = this.element[0].scrollLeft;
3991 if(par.id === $.jstree.root) {
3992 if (this._model.data[$.jstree.root].children[0]) {
3993 this.get_node(this._model.data[$.jstree.root].children[0], true).children('.jstree-anchor').focus();
3994 }
3995 }
3996 else {
3997 this.get_node(par, true).children('.jstree-anchor').focus();
3998 }
3999 this.element[0].scrollTop = top;
4000 this.element[0].scrollLeft = lft;
4001 }
4002 this.redraw_node(par, true);
4003 return true;
4004 },
4005 /**
4006 * check if an operation is premitted on the tree. Used internally.
4007 * @private
4008 * @name check(chk, obj, par, pos)
4009 * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node"
4010 * @param {mixed} obj the node
4011 * @param {mixed} par the parent
4012 * @param {mixed} pos the position to insert at, or if "rename_node" - the new name
4013 * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node
4014 * @return {Boolean}
4015 */
4016 check : function (chk, obj, par, pos, more) {
4017 obj = obj && obj.id ? obj : this.get_node(obj);
4018 par = par && par.id ? par : this.get_node(par);
4019 var tmp = chk.match(/^move_node|copy_node|create_node$/i) ? par : obj,
4020 chc = this.settings.core.check_callback;
4021 if(chk === "move_node" || chk === "copy_node") {
4022 if((!more || !more.is_multi) && (obj.id === par.id || (chk === "move_node" && $.inArray(obj.id, par.children) === pos) || $.inArray(par.id, obj.children_d) !== -1)) {
4023 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 }) };
4024 return false;
4025 }
4026 }
4027 if(tmp && tmp.data) { tmp = tmp.data; }
4028 if(tmp && tmp.functions && (tmp.functions[chk] === false || tmp.functions[chk] === true)) {
4029 if(tmp.functions[chk] === false) {
4030 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 }) };
4031 }
4032 return tmp.functions[chk];
4033 }
4034 if(chc === false || ($.isFunction(chc) && chc.call(this, chk, obj, par, pos, more) === false) || (chc && chc[chk] === false)) {
4035 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 }) };
4036 return false;
4037 }
4038 return true;
4039 },
4040 /**
4041 * get the last error
4042 * @name last_error()
4043 * @return {Object}
4044 */
4045 last_error : function () {
4046 return this._data.core.last_error;
4047 },
4048 /**
4049 * move a node to a new parent
4050 * @name move_node(obj, par [, pos, callback, is_loaded])
4051 * @param {mixed} obj the node to move, pass an array to move multiple nodes
4052 * @param {mixed} par the new parent
4053 * @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`
4054 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
4055 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
4056 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
4057 * @param {Boolean} instance internal parameter indicating if the node comes from another instance
4058 * @trigger move_node.jstree
4059 */
4060 move_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) {
4061 var t1, t2, old_par, old_pos, new_par, old_ins, is_multi, dpc, tmp, i, j, k, l, p;
4062
4063 par = this.get_node(par);
4064 pos = pos === undefined ? 0 : pos;
4065 if(!par) { return false; }
4066 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
4067 return this.load_node(par, function () { this.move_node(obj, par, pos, callback, true, false, origin); });
4068 }
4069
4070 if($.isArray(obj)) {
4071 if(obj.length === 1) {
4072 obj = obj[0];
4073 }
4074 else {
4075 //obj = obj.slice();
4076 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4077 if((tmp = this.move_node(obj[t1], par, pos, callback, is_loaded, false, origin))) {
4078 par = tmp;
4079 pos = "after";
4080 }
4081 }
4082 this.redraw();
4083 return true;
4084 }
4085 }
4086 obj = obj && obj.id ? obj : this.get_node(obj);
4087
4088 if(!obj || obj.id === $.jstree.root) { return false; }
4089
4090 old_par = (obj.parent || $.jstree.root).toString();
4091 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent);
4092 old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
4093 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
4094 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;
4095 if(old_ins && old_ins._id) {
4096 obj = old_ins._model.data[obj.id];
4097 }
4098
4099 if(is_multi) {
4100 if((tmp = this.copy_node(obj, par, pos, callback, is_loaded, false, origin))) {
4101 if(old_ins) { old_ins.delete_node(obj); }
4102 return tmp;
4103 }
4104 return false;
4105 }
4106 //var m = this._model.data;
4107 if(par.id === $.jstree.root) {
4108 if(pos === "before") { pos = "first"; }
4109 if(pos === "after") { pos = "last"; }
4110 }
4111 switch(pos) {
4112 case "before":
4113 pos = $.inArray(par.id, new_par.children);
4114 break;
4115 case "after" :
4116 pos = $.inArray(par.id, new_par.children) + 1;
4117 break;
4118 case "inside":
4119 case "first":
4120 pos = 0;
4121 break;
4122 case "last":
4123 pos = new_par.children.length;
4124 break;
4125 default:
4126 if(!pos) { pos = 0; }
4127 break;
4128 }
4129 if(pos > new_par.children.length) { pos = new_par.children.length; }
4130 if(!this.check("move_node", obj, new_par, pos, { 'core' : true, 'origin' : origin, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) {
4131 this.settings.core.error.call(this, this._data.core.last_error);
4132 return false;
4133 }
4134 if(obj.parent === new_par.id) {
4135 dpc = new_par.children.concat();
4136 tmp = $.inArray(obj.id, dpc);
4137 if(tmp !== -1) {
4138 dpc = $.vakata.array_remove(dpc, tmp);
4139 if(pos > tmp) { pos--; }
4140 }
4141 tmp = [];
4142 for(i = 0, j = dpc.length; i < j; i++) {
4143 tmp[i >= pos ? i+1 : i] = dpc[i];
4144 }
4145 tmp[pos] = obj.id;
4146 new_par.children = tmp;
4147 this._node_changed(new_par.id);
4148 this.redraw(new_par.id === $.jstree.root);
4149 }
4150 else {
4151 // clean old parent and up
4152 tmp = obj.children_d.concat();
4153 tmp.push(obj.id);
4154 for(i = 0, j = obj.parents.length; i < j; i++) {
4155 dpc = [];
4156 p = old_ins._model.data[obj.parents[i]].children_d;
4157 for(k = 0, l = p.length; k < l; k++) {
4158 if($.inArray(p[k], tmp) === -1) {
4159 dpc.push(p[k]);
4160 }
4161 }
4162 old_ins._model.data[obj.parents[i]].children_d = dpc;
4163 }
4164 old_ins._model.data[old_par].children = $.vakata.array_remove_item(old_ins._model.data[old_par].children, obj.id);
4165
4166 // insert into new parent and up
4167 for(i = 0, j = new_par.parents.length; i < j; i++) {
4168 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(tmp);
4169 }
4170 dpc = [];
4171 for(i = 0, j = new_par.children.length; i < j; i++) {
4172 dpc[i >= pos ? i+1 : i] = new_par.children[i];
4173 }
4174 dpc[pos] = obj.id;
4175 new_par.children = dpc;
4176 new_par.children_d.push(obj.id);
4177 new_par.children_d = new_par.children_d.concat(obj.children_d);
4178
4179 // update object
4180 obj.parent = new_par.id;
4181 tmp = new_par.parents.concat();
4182 tmp.unshift(new_par.id);
4183 p = obj.parents.length;
4184 obj.parents = tmp;
4185
4186 // update object children
4187 tmp = tmp.concat();
4188 for(i = 0, j = obj.children_d.length; i < j; i++) {
4189 this._model.data[obj.children_d[i]].parents = this._model.data[obj.children_d[i]].parents.slice(0,p*-1);
4190 Array.prototype.push.apply(this._model.data[obj.children_d[i]].parents, tmp);
4191 }
4192
4193 if(old_par === $.jstree.root || new_par.id === $.jstree.root) {
4194 this._model.force_full_redraw = true;
4195 }
4196 if(!this._model.force_full_redraw) {
4197 this._node_changed(old_par);
4198 this._node_changed(new_par.id);
4199 }
4200 if(!skip_redraw) {
4201 this.redraw();
4202 }
4203 }
4204 if(callback) { callback.call(this, obj, new_par, pos); }
4205 /**
4206 * triggered when a node is moved
4207 * @event
4208 * @name move_node.jstree
4209 * @param {Object} node
4210 * @param {String} parent the parent's ID
4211 * @param {Number} position the position of the node among the parent's children
4212 * @param {String} old_parent the old parent of the node
4213 * @param {Number} old_position the old position of the node
4214 * @param {Boolean} is_multi do the node and new parent belong to different instances
4215 * @param {jsTree} old_instance the instance the node came from
4216 * @param {jsTree} new_instance the instance of the new parent
4217 */
4218 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 });
4219 return obj.id;
4220 },
4221 /**
4222 * copy a node to a new parent
4223 * @name copy_node(obj, par [, pos, callback, is_loaded])
4224 * @param {mixed} obj the node to copy, pass an array to copy multiple nodes
4225 * @param {mixed} par the new parent
4226 * @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`
4227 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
4228 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
4229 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
4230 * @param {Boolean} instance internal parameter indicating if the node comes from another instance
4231 * @trigger model.jstree copy_node.jstree
4232 */
4233 copy_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) {
4234 var t1, t2, dpc, tmp, i, j, node, old_par, new_par, old_ins, is_multi;
4235
4236 par = this.get_node(par);
4237 pos = pos === undefined ? 0 : pos;
4238 if(!par) { return false; }
4239 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
4240 return this.load_node(par, function () { this.copy_node(obj, par, pos, callback, true, false, origin); });
4241 }
4242
4243 if($.isArray(obj)) {
4244 if(obj.length === 1) {
4245 obj = obj[0];
4246 }
4247 else {
4248 //obj = obj.slice();
4249 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4250 if((tmp = this.copy_node(obj[t1], par, pos, callback, is_loaded, true, origin))) {
4251 par = tmp;
4252 pos = "after";
4253 }
4254 }
4255 this.redraw();
4256 return true;
4257 }
4258 }
4259 obj = obj && obj.id ? obj : this.get_node(obj);
4260 if(!obj || obj.id === $.jstree.root) { return false; }
4261
4262 old_par = (obj.parent || $.jstree.root).toString();
4263 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent);
4264 old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
4265 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
4266
4267 if(old_ins && old_ins._id) {
4268 obj = old_ins._model.data[obj.id];
4269 }
4270
4271 if(par.id === $.jstree.root) {
4272 if(pos === "before") { pos = "first"; }
4273 if(pos === "after") { pos = "last"; }
4274 }
4275 switch(pos) {
4276 case "before":
4277 pos = $.inArray(par.id, new_par.children);
4278 break;
4279 case "after" :
4280 pos = $.inArray(par.id, new_par.children) + 1;
4281 break;
4282 case "inside":
4283 case "first":
4284 pos = 0;
4285 break;
4286 case "last":
4287 pos = new_par.children.length;
4288 break;
4289 default:
4290 if(!pos) { pos = 0; }
4291 break;
4292 }
4293 if(pos > new_par.children.length) { pos = new_par.children.length; }
4294 if(!this.check("copy_node", obj, new_par, pos, { 'core' : true, 'origin' : origin, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) {
4295 this.settings.core.error.call(this, this._data.core.last_error);
4296 return false;
4297 }
4298 node = old_ins ? old_ins.get_json(obj, { no_id : true, no_data : true, no_state : true }) : obj;
4299 if(!node) { return false; }
4300 if(node.id === true) { delete node.id; }
4301 node = this._parse_model_from_json(node, new_par.id, new_par.parents.concat());
4302 if(!node) { return false; }
4303 tmp = this.get_node(node);
4304 if(obj && obj.state && obj.state.loaded === false) { tmp.state.loaded = false; }
4305 dpc = [];
4306 dpc.push(node);
4307 dpc = dpc.concat(tmp.children_d);
4308 this.trigger('model', { "nodes" : dpc, "parent" : new_par.id });
4309
4310 // insert into new parent and up
4311 for(i = 0, j = new_par.parents.length; i < j; i++) {
4312 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(dpc);
4313 }
4314 dpc = [];
4315 for(i = 0, j = new_par.children.length; i < j; i++) {
4316 dpc[i >= pos ? i+1 : i] = new_par.children[i];
4317 }
4318 dpc[pos] = tmp.id;
4319 new_par.children = dpc;
4320 new_par.children_d.push(tmp.id);
4321 new_par.children_d = new_par.children_d.concat(tmp.children_d);
4322
4323 if(new_par.id === $.jstree.root) {
4324 this._model.force_full_redraw = true;
4325 }
4326 if(!this._model.force_full_redraw) {
4327 this._node_changed(new_par.id);
4328 }
4329 if(!skip_redraw) {
4330 this.redraw(new_par.id === $.jstree.root);
4331 }
4332 if(callback) { callback.call(this, tmp, new_par, pos); }
4333 /**
4334 * triggered when a node is copied
4335 * @event
4336 * @name copy_node.jstree
4337 * @param {Object} node the copied node
4338 * @param {Object} original the original node
4339 * @param {String} parent the parent's ID
4340 * @param {Number} position the position of the node among the parent's children
4341 * @param {String} old_parent the old parent of the node
4342 * @param {Number} old_position the position of the original node
4343 * @param {Boolean} is_multi do the node and new parent belong to different instances
4344 * @param {jsTree} old_instance the instance the node came from
4345 * @param {jsTree} new_instance the instance of the new parent
4346 */
4347 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 });
4348 return tmp.id;
4349 },
4350 /**
4351 * cut a node (a later call to `paste(obj)` would move the node)
4352 * @name cut(obj)
4353 * @param {mixed} obj multiple objects can be passed using an array
4354 * @trigger cut.jstree
4355 */
4356 cut : function (obj) {
4357 if(!obj) { obj = this._data.core.selected.concat(); }
4358 if(!$.isArray(obj)) { obj = [obj]; }
4359 if(!obj.length) { return false; }
4360 var tmp = [], o, t1, t2;
4361 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4362 o = this.get_node(obj[t1]);
4363 if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); }
4364 }
4365 if(!tmp.length) { return false; }
4366 ccp_node = tmp;
4367 ccp_inst = this;
4368 ccp_mode = 'move_node';
4369 /**
4370 * triggered when nodes are added to the buffer for moving
4371 * @event
4372 * @name cut.jstree
4373 * @param {Array} node
4374 */
4375 this.trigger('cut', { "node" : obj });
4376 },
4377 /**
4378 * copy a node (a later call to `paste(obj)` would copy the node)
4379 * @name copy(obj)
4380 * @param {mixed} obj multiple objects can be passed using an array
4381 * @trigger copy.jstree
4382 */
4383 copy : function (obj) {
4384 if(!obj) { obj = this._data.core.selected.concat(); }
4385 if(!$.isArray(obj)) { obj = [obj]; }
4386 if(!obj.length) { return false; }
4387 var tmp = [], o, t1, t2;
4388 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4389 o = this.get_node(obj[t1]);
4390 if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); }
4391 }
4392 if(!tmp.length) { return false; }
4393 ccp_node = tmp;
4394 ccp_inst = this;
4395 ccp_mode = 'copy_node';
4396 /**
4397 * triggered when nodes are added to the buffer for copying
4398 * @event
4399 * @name copy.jstree
4400 * @param {Array} node
4401 */
4402 this.trigger('copy', { "node" : obj });
4403 },
4404 /**
4405 * get the current buffer (any nodes that are waiting for a paste operation)
4406 * @name get_buffer()
4407 * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance)
4408 */
4409 get_buffer : function () {
4410 return { 'mode' : ccp_mode, 'node' : ccp_node, 'inst' : ccp_inst };
4411 },
4412 /**
4413 * check if there is something in the buffer to paste
4414 * @name can_paste()
4415 * @return {Boolean}
4416 */
4417 can_paste : function () {
4418 return ccp_mode !== false && ccp_node !== false; // && ccp_inst._model.data[ccp_node];
4419 },
4420 /**
4421 * copy or move the previously cut or copied nodes to a new parent
4422 * @name paste(obj [, pos])
4423 * @param {mixed} obj the new parent
4424 * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0`
4425 * @trigger paste.jstree
4426 */
4427 paste : function (obj, pos) {
4428 obj = this.get_node(obj);
4429 if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; }
4430 if(this[ccp_mode](ccp_node, obj, pos, false, false, false, ccp_inst)) {
4431 /**
4432 * triggered when paste is invoked
4433 * @event
4434 * @name paste.jstree
4435 * @param {String} parent the ID of the receiving node
4436 * @param {Array} node the nodes in the buffer
4437 * @param {String} mode the performed operation - "copy_node" or "move_node"
4438 */
4439 this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode });
4440 }
4441 ccp_node = false;
4442 ccp_mode = false;
4443 ccp_inst = false;
4444 },
4445 /**
4446 * clear the buffer of previously copied or cut nodes
4447 * @name clear_buffer()
4448 * @trigger clear_buffer.jstree
4449 */
4450 clear_buffer : function () {
4451 ccp_node = false;
4452 ccp_mode = false;
4453 ccp_inst = false;
4454 /**
4455 * triggered when the copy / cut buffer is cleared
4456 * @event
4457 * @name clear_buffer.jstree
4458 */
4459 this.trigger('clear_buffer');
4460 },
4461 /**
4462 * put a node in edit mode (input field to rename the node)
4463 * @name edit(obj [, default_text, callback])
4464 * @param {mixed} obj
4465 * @param {String} default_text the text to populate the input with (if omitted or set to a non-string value the node's text value is used)
4466 * @param {Function} callback a function to be called once the text box is blurred, it is called in the instance's scope and receives the node, a status parameter (true if the rename is successful, false otherwise) and a boolean indicating if the user cancelled the edit. You can access the node's title using .text
4467 */
4468 edit : function (obj, default_text, callback) {
4469 var rtl, w, a, s, t, h1, h2, fn, tmp, cancel = false;
4470 obj = this.get_node(obj);
4471 if(!obj) { return false; }
4472 if(!this.check("edit", obj, this.get_parent(obj))) {
4473 this.settings.core.error.call(this, this._data.core.last_error);
4474 return false;
4475 }
4476 tmp = obj;
4477 default_text = typeof default_text === 'string' ? default_text : obj.text;
4478 this.set_text(obj, "");
4479 obj = this._open_to(obj);
4480 tmp.text = default_text;
4481
4482 rtl = this._data.core.rtl;
4483 w = this.element.width();
4484 this._data.core.focused = tmp.id;
4485 a = obj.children('.jstree-anchor').focus();
4486 s = $('<span>');
4487 /*!
4488 oi = obj.children("i:visible"),
4489 ai = a.children("i:visible"),
4490 w1 = oi.width() * oi.length,
4491 w2 = ai.width() * ai.length,
4492 */
4493 t = default_text;
4494 h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body");
4495 h2 = $("<"+"input />", {
4496 "value" : t,
4497 "class" : "jstree-rename-input",
4498 // "size" : t.length,
4499 "css" : {
4500 "padding" : "0",
4501 "border" : "1px solid silver",
4502 "box-sizing" : "border-box",
4503 "display" : "inline-block",
4504 "height" : (this._data.core.li_height) + "px",
4505 "lineHeight" : (this._data.core.li_height) + "px",
4506 "width" : "150px" // will be set a bit further down
4507 },
4508 "blur" : $.proxy(function (e) {
4509 e.stopImmediatePropagation();
4510 e.preventDefault();
4511 var i = s.children(".jstree-rename-input"),
4512 v = i.val(),
4513 f = this.settings.core.force_text,
4514 nv;
4515 if(v === "") { v = t; }
4516 h1.remove();
4517 s.replaceWith(a);
4518 s.remove();
4519 t = f ? t : $('<div></div>').append($.parseHTML(t)).html();
4520 this.set_text(obj, t);
4521 nv = !!this.rename_node(obj, f ? $('<div></div>').text(v).text() : $('<div></div>').append($.parseHTML(v)).html());
4522 if(!nv) {
4523 this.set_text(obj, t); // move this up? and fix #483
4524 }
4525 this._data.core.focused = tmp.id;
4526 setTimeout($.proxy(function () {
4527 var node = this.get_node(tmp.id, true);
4528 if(node.length) {
4529 this._data.core.focused = tmp.id;
4530 node.children('.jstree-anchor').focus();
4531 }
4532 }, this), 0);
4533 if(callback) {
4534 callback.call(this, tmp, nv, cancel);
4535 }
4536 h2 = null;
4537 }, this),
4538 "keydown" : function (e) {
4539 var key = e.which;
4540 if(key === 27) {
4541 cancel = true;
4542 this.value = t;
4543 }
4544 if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) {
4545 e.stopImmediatePropagation();
4546 }
4547 if(key === 27 || key === 13) {
4548 e.preventDefault();
4549 this.blur();
4550 }
4551 },
4552 "click" : function (e) { e.stopImmediatePropagation(); },
4553 "mousedown" : function (e) { e.stopImmediatePropagation(); },
4554 "keyup" : function (e) {
4555 h2.width(Math.min(h1.text("pW" + this.value).width(),w));
4556 },
4557 "keypress" : function(e) {
4558 if(e.which === 13) { return false; }
4559 }
4560 });
4561 fn = {
4562 fontFamily : a.css('fontFamily') || '',
4563 fontSize : a.css('fontSize') || '',
4564 fontWeight : a.css('fontWeight') || '',
4565 fontStyle : a.css('fontStyle') || '',
4566 fontStretch : a.css('fontStretch') || '',
4567 fontVariant : a.css('fontVariant') || '',
4568 letterSpacing : a.css('letterSpacing') || '',
4569 wordSpacing : a.css('wordSpacing') || ''
4570 };
4571 s.attr('class', a.attr('class')).append(a.contents().clone()).append(h2);
4572 a.replaceWith(s);
4573 h1.css(fn);
4574 h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select();
4575 $(document).one('mousedown.jstree touchstart.jstree dnd_start.vakata', function (e) {
4576 if (h2 && e.target !== h2) {
4577 $(h2).blur();
4578 }
4579 });
4580 },
4581
4582
4583 /**
4584 * changes the theme
4585 * @name set_theme(theme_name [, theme_url])
4586 * @param {String} theme_name the name of the new theme to apply
4587 * @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.
4588 * @trigger set_theme.jstree
4589 */
4590 set_theme : function (theme_name, theme_url) {
4591 if(!theme_name) { return false; }
4592 if(theme_url === true) {
4593 var dir = this.settings.core.themes.dir;
4594 if(!dir) { dir = $.jstree.path + '/themes'; }
4595 theme_url = dir + '/' + theme_name + '/style.css';
4596 }
4597 if(theme_url && $.inArray(theme_url, themes_loaded) === -1) {
4598 $('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />');
4599 themes_loaded.push(theme_url);
4600 }
4601 if(this._data.core.themes.name) {
4602 this.element.removeClass('jstree-' + this._data.core.themes.name);
4603 }
4604 this._data.core.themes.name = theme_name;
4605 this.element.addClass('jstree-' + theme_name);
4606 this.element[this.settings.core.themes.responsive ? 'addClass' : 'removeClass' ]('jstree-' + theme_name + '-responsive');
4607 /**
4608 * triggered when a theme is set
4609 * @event
4610 * @name set_theme.jstree
4611 * @param {String} theme the new theme
4612 */
4613 this.trigger('set_theme', { 'theme' : theme_name });
4614 },
4615 /**
4616 * gets the name of the currently applied theme name
4617 * @name get_theme()
4618 * @return {String}
4619 */
4620 get_theme : function () { return this._data.core.themes.name; },
4621 /**
4622 * changes the theme variant (if the theme has variants)
4623 * @name set_theme_variant(variant_name)
4624 * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed)
4625 */
4626 set_theme_variant : function (variant_name) {
4627 if(this._data.core.themes.variant) {
4628 this.element.removeClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
4629 }
4630 this._data.core.themes.variant = variant_name;
4631 if(variant_name) {
4632 this.element.addClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
4633 }
4634 },
4635 /**
4636 * gets the name of the currently applied theme variant
4637 * @name get_theme()
4638 * @return {String}
4639 */
4640 get_theme_variant : function () { return this._data.core.themes.variant; },
4641 /**
4642 * shows a striped background on the container (if the theme supports it)
4643 * @name show_stripes()
4644 */
4645 show_stripes : function () {
4646 this._data.core.themes.stripes = true;
4647 this.get_container_ul().addClass("jstree-striped");
4648 /**
4649 * triggered when stripes are shown
4650 * @event
4651 * @name show_stripes.jstree
4652 */
4653 this.trigger('show_stripes');
4654 },
4655 /**
4656 * hides the striped background on the container
4657 * @name hide_stripes()
4658 */
4659 hide_stripes : function () {
4660 this._data.core.themes.stripes = false;
4661 this.get_container_ul().removeClass("jstree-striped");
4662 /**
4663 * triggered when stripes are hidden
4664 * @event
4665 * @name hide_stripes.jstree
4666 */
4667 this.trigger('hide_stripes');
4668 },
4669 /**
4670 * toggles the striped background on the container
4671 * @name toggle_stripes()
4672 */
4673 toggle_stripes : function () { if(this._data.core.themes.stripes) { this.hide_stripes(); } else { this.show_stripes(); } },
4674 /**
4675 * shows the connecting dots (if the theme supports it)
4676 * @name show_dots()
4677 */
4678 show_dots : function () {
4679 this._data.core.themes.dots = true;
4680 this.get_container_ul().removeClass("jstree-no-dots");
4681 /**
4682 * triggered when dots are shown
4683 * @event
4684 * @name show_dots.jstree
4685 */
4686 this.trigger('show_dots');
4687 },
4688 /**
4689 * hides the connecting dots
4690 * @name hide_dots()
4691 */
4692 hide_dots : function () {
4693 this._data.core.themes.dots = false;
4694 this.get_container_ul().addClass("jstree-no-dots");
4695 /**
4696 * triggered when dots are hidden
4697 * @event
4698 * @name hide_dots.jstree
4699 */
4700 this.trigger('hide_dots');
4701 },
4702 /**
4703 * toggles the connecting dots
4704 * @name toggle_dots()
4705 */
4706 toggle_dots : function () { if(this._data.core.themes.dots) { this.hide_dots(); } else { this.show_dots(); } },
4707 /**
4708 * show the node icons
4709 * @name show_icons()
4710 */
4711 show_icons : function () {
4712 this._data.core.themes.icons = true;
4713 this.get_container_ul().removeClass("jstree-no-icons");
4714 /**
4715 * triggered when icons are shown
4716 * @event
4717 * @name show_icons.jstree
4718 */
4719 this.trigger('show_icons');
4720 },
4721 /**
4722 * hide the node icons
4723 * @name hide_icons()
4724 */
4725 hide_icons : function () {
4726 this._data.core.themes.icons = false;
4727 this.get_container_ul().addClass("jstree-no-icons");
4728 /**
4729 * triggered when icons are hidden
4730 * @event
4731 * @name hide_icons.jstree
4732 */
4733 this.trigger('hide_icons');
4734 },
4735 /**
4736 * toggle the node icons
4737 * @name toggle_icons()
4738 */
4739 toggle_icons : function () { if(this._data.core.themes.icons) { this.hide_icons(); } else { this.show_icons(); } },
4740 /**
4741 * show the node ellipsis
4742 * @name show_icons()
4743 */
4744 show_ellipsis : function () {
4745 this._data.core.themes.ellipsis = true;
4746 this.get_container_ul().addClass("jstree-ellipsis");
4747 /**
4748 * triggered when ellisis is shown
4749 * @event
4750 * @name show_ellipsis.jstree
4751 */
4752 this.trigger('show_ellipsis');
4753 },
4754 /**
4755 * hide the node ellipsis
4756 * @name hide_ellipsis()
4757 */
4758 hide_ellipsis : function () {
4759 this._data.core.themes.ellipsis = false;
4760 this.get_container_ul().removeClass("jstree-ellipsis");
4761 /**
4762 * triggered when ellisis is hidden
4763 * @event
4764 * @name hide_ellipsis.jstree
4765 */
4766 this.trigger('hide_ellipsis');
4767 },
4768 /**
4769 * toggle the node ellipsis
4770 * @name toggle_icons()
4771 */
4772 toggle_ellipsis : function () { if(this._data.core.themes.ellipsis) { this.hide_ellipsis(); } else { this.show_ellipsis(); } },
4773 /**
4774 * set the node icon for a node
4775 * @name set_icon(obj, icon)
4776 * @param {mixed} obj
4777 * @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
4778 */
4779 set_icon : function (obj, icon) {
4780 var t1, t2, dom, old;
4781 if($.isArray(obj)) {
4782 obj = obj.slice();
4783 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4784 this.set_icon(obj[t1], icon);
4785 }
4786 return true;
4787 }
4788 obj = this.get_node(obj);
4789 if(!obj || obj.id === $.jstree.root) { return false; }
4790 old = obj.icon;
4791 obj.icon = icon === true || icon === null || icon === undefined || icon === '' ? true : icon;
4792 dom = this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon");
4793 if(icon === false) {
4794 dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel");
4795 this.hide_icon(obj);
4796 }
4797 else if(icon === true || icon === null || icon === undefined || icon === '') {
4798 dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel");
4799 if(old === false) { this.show_icon(obj); }
4800 }
4801 else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) {
4802 dom.removeClass(old).css("background","");
4803 dom.addClass(icon + ' jstree-themeicon-custom').attr("rel",icon);
4804 if(old === false) { this.show_icon(obj); }
4805 }
4806 else {
4807 dom.removeClass(old).css("background","");
4808 dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon);
4809 if(old === false) { this.show_icon(obj); }
4810 }
4811 return true;
4812 },
4813 /**
4814 * get the node icon for a node
4815 * @name get_icon(obj)
4816 * @param {mixed} obj
4817 * @return {String}
4818 */
4819 get_icon : function (obj) {
4820 obj = this.get_node(obj);
4821 return (!obj || obj.id === $.jstree.root) ? false : obj.icon;
4822 },
4823 /**
4824 * hide the icon on an individual node
4825 * @name hide_icon(obj)
4826 * @param {mixed} obj
4827 */
4828 hide_icon : function (obj) {
4829 var t1, t2;
4830 if($.isArray(obj)) {
4831 obj = obj.slice();
4832 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4833 this.hide_icon(obj[t1]);
4834 }
4835 return true;
4836 }
4837 obj = this.get_node(obj);
4838 if(!obj || obj === $.jstree.root) { return false; }
4839 obj.icon = false;
4840 this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden');
4841 return true;
4842 },
4843 /**
4844 * show the icon on an individual node
4845 * @name show_icon(obj)
4846 * @param {mixed} obj
4847 */
4848 show_icon : function (obj) {
4849 var t1, t2, dom;
4850 if($.isArray(obj)) {
4851 obj = obj.slice();
4852 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
4853 this.show_icon(obj[t1]);
4854 }
4855 return true;
4856 }
4857 obj = this.get_node(obj);
4858 if(!obj || obj === $.jstree.root) { return false; }
4859 dom = this.get_node(obj, true);
4860 obj.icon = dom.length ? dom.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true;
4861 if(!obj.icon) { obj.icon = true; }
4862 dom.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden');
4863 return true;
4864 }
4865 };
4866
4867 // helpers
4868 $.vakata = {};
4869 // collect attributes
4870 $.vakata.attributes = function(node, with_values) {
4871 node = $(node)[0];
4872 var attr = with_values ? {} : [];
4873 if(node && node.attributes) {
4874 $.each(node.attributes, function (i, v) {
4875 if($.inArray(v.name.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; }
4876 if(v.value !== null && $.trim(v.value) !== '') {
4877 if(with_values) { attr[v.name] = v.value; }
4878 else { attr.push(v.name); }
4879 }
4880 });
4881 }
4882 return attr;
4883 };
4884 $.vakata.array_unique = function(array) {
4885 var a = [], i, j, l, o = {};
4886 for(i = 0, l = array.length; i < l; i++) {
4887 if(o[array[i]] === undefined) {
4888 a.push(array[i]);
4889 o[array[i]] = true;
4890 }
4891 }
4892 return a;
4893 };
4894 // remove item from array
4895 $.vakata.array_remove = function(array, from) {
4896 array.splice(from, 1);
4897 return array;
4898 //var rest = array.slice((to || from) + 1 || array.length);
4899 //array.length = from < 0 ? array.length + from : from;
4900 //array.push.apply(array, rest);
4901 //return array;
4902 };
4903 // remove item from array
4904 $.vakata.array_remove_item = function(array, item) {
4905 var tmp = $.inArray(item, array);
4906 return tmp !== -1 ? $.vakata.array_remove(array, tmp) : array;
4907 };
4908 $.vakata.array_filter = function(c,a,b,d,e) {
4909 if (c.filter) {
4910 return c.filter(a, b);
4911 }
4912 d=[];
4913 for (e in c) {
4914 if (~~e+''===e+'' && e>=0 && a.call(b,c[e],+e,c)) {
4915 d.push(c[e]);
4916 }
4917 }
4918 return d;
4919 };
4920 }));