1/*globals jQuery, define, exports, require, window, document, postMessage */ 2(function (factory) { 3 "use strict"; 4 if (typeof define === 'function' && define.amd) { 5 define(['jquery'], factory); 6 } 7 else if(typeof exports === 'object') { 8 factory(require('jquery')); 9 } 10 else { 11 factory(jQuery); 12 } 13}(function ($, undefined) { 14 "use strict"; 15/*! 16 * jsTree 3.1.0 17 * http://jstree.com/ 18 * 19 * Copyright (c) 2014 Ivan Bozhanov (http://vakata.com) 20 * 21 * Licensed same as jquery - under the terms of the MIT License 22 * http://www.opensource.org/licenses/mit-license.php 23 */ 24/*! 25 * if using jslint please allow for the jQuery global and use following options: 26 * jslint: browser: true, ass: true, bitwise: true, continue: true, nomen: true, plusplus: true, regexp: true, unparam: true, todo: true, white: true 27 */ 28 29 // prevent another load? maybe there is a better way? 30 if($.jstree) { 31 return; 32 } 33 34 /** 35 * ### jsTree core functionality 36 */ 37 38 // internal variables 39 var instance_counter = 0, 40 ccp_node = false, 41 ccp_mode = false, 42 ccp_inst = false, 43 themes_loaded = [], 44 src = $('script:last').attr('src'), 45 document = window.document, // local variable is always faster to access then a global 46 _node = document.createElement('LI'), _temp1, _temp2; 47 48 _node.setAttribute('role', 'treeitem'); 49 _temp1 = document.createElement('I'); 50 _temp1.className = 'jstree-icon jstree-ocl'; 51 _temp1.setAttribute('role', 'presentation'); 52 _node.appendChild(_temp1); 53 _temp1 = document.createElement('A'); 54 _temp1.className = 'jstree-anchor'; 55 _temp1.setAttribute('href','#'); 56 _temp1.setAttribute('tabindex','-1'); 57 _temp2 = document.createElement('I'); 58 _temp2.className = 'jstree-icon jstree-themeicon'; 59 _temp2.setAttribute('role', 'presentation'); 60 _temp1.appendChild(_temp2); 61 _node.appendChild(_temp1); 62 _temp1 = _temp2 = null; 63 64 65 /** 66 * holds all jstree related functions and variables, including the actual class and methods to create, access and manipulate instances. 67 * @name $.jstree 68 */ 69 $.jstree = { 70 /** 71 * specifies the jstree version in use 72 * @name $.jstree.version 73 */ 74 version : '3.1.0', 75 /** 76 * holds all the default options used when creating new instances 77 * @name $.jstree.defaults 78 */ 79 defaults : { 80 /** 81 * configure which plugins will be active on an instance. Should be an array of strings, where each element is a plugin name. The default is `[]` 82 * @name $.jstree.defaults.plugins 83 */ 84 plugins : [] 85 }, 86 /** 87 * stores all loaded jstree plugins (used internally) 88 * @name $.jstree.plugins 89 */ 90 plugins : {}, 91 path : src && src.indexOf('/') !== -1 ? src.replace(/\/[^\/]+$/,'') : '', 92 idregex : /[\\:&!^|()\[\]<>@*'+~#";.,=\- \/${}%?`]/g 93 }; 94 /** 95 * creates a jstree instance 96 * @name $.jstree.create(el [, options]) 97 * @param {DOMElement|jQuery|String} el the element to create the instance on, can be jQuery extended or a selector 98 * @param {Object} options options for this instance (extends `$.jstree.defaults`) 99 * @return {jsTree} the new instance 100 */ 101 $.jstree.create = function (el, options) { 102 var tmp = new $.jstree.core(++instance_counter), 103 opt = options; 104 options = $.extend(true, {}, $.jstree.defaults, options); 105 if(opt && opt.plugins) { 106 options.plugins = opt.plugins; 107 } 108 $.each(options.plugins, function (i, k) { 109 if(i !== 'core') { 110 tmp = tmp.plugin(k, options[k]); 111 } 112 }); 113 $(el).data('jstree', tmp); 114 tmp.init(el, options); 115 return tmp; 116 }; 117 /** 118 * remove all traces of jstree from the DOM and destroy all instances 119 * @name $.jstree.destroy() 120 */ 121 $.jstree.destroy = function () { 122 $('.jstree:jstree').jstree('destroy'); 123 $(document).off('.jstree'); 124 }; 125 /** 126 * the jstree class constructor, used only internally 127 * @private 128 * @name $.jstree.core(id) 129 * @param {Number} id this instance's index 130 */ 131 $.jstree.core = function (id) { 132 this._id = id; 133 this._cnt = 0; 134 this._wrk = null; 135 this._data = { 136 core : { 137 themes : { 138 name : false, 139 dots : false, 140 icons : false 141 }, 142 selected : [], 143 last_error : {}, 144 working : false, 145 worker_queue : [], 146 focused : null 147 } 148 }; 149 }; 150 /** 151 * get a reference to an existing instance 152 * 153 * __Examples__ 154 * 155 * // provided a container with an ID of "tree", and a nested node with an ID of "branch" 156 * // all of there will return the same instance 157 * $.jstree.reference('tree'); 158 * $.jstree.reference('#tree'); 159 * $.jstree.reference($('#tree')); 160 * $.jstree.reference(document.getElementByID('tree')); 161 * $.jstree.reference('branch'); 162 * $.jstree.reference('#branch'); 163 * $.jstree.reference($('#branch')); 164 * $.jstree.reference(document.getElementByID('branch')); 165 * 166 * @name $.jstree.reference(needle) 167 * @param {DOMElement|jQuery|String} needle 168 * @return {jsTree|null} the instance or `null` if not found 169 */ 170 $.jstree.reference = function (needle) { 171 var tmp = null, 172 obj = null; 173 if(needle && needle.id && (!needle.tagName || !needle.nodeType)) { needle = needle.id; } 174 175 if(!obj || !obj.length) { 176 try { obj = $(needle); } catch (ignore) { } 177 } 178 if(!obj || !obj.length) { 179 try { obj = $('#' + needle.replace($.jstree.idregex,'\\$&')); } catch (ignore) { } 180 } 181 if(obj && obj.length && (obj = obj.closest('.jstree')).length && (obj = obj.data('jstree'))) { 182 tmp = obj; 183 } 184 else { 185 $('.jstree').each(function () { 186 var inst = $(this).data('jstree'); 187 if(inst && inst._model.data[needle]) { 188 tmp = inst; 189 return false; 190 } 191 }); 192 } 193 return tmp; 194 }; 195 /** 196 * Create an instance, get an instance or invoke a command on a instance. 197 * 198 * 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). 199 * 200 * 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). 201 * 202 * If there is an existing instance and `arg` is not a string the instance itself is returned (similar to `$.jstree.reference`). 203 * 204 * In any other case - nothing is returned and chaining is not broken. 205 * 206 * __Examples__ 207 * 208 * $('#tree1').jstree(); // creates an instance 209 * $('#tree2').jstree({ plugins : [] }); // create an instance with some options 210 * $('#tree1').jstree('open_node', '#branch_1'); // call a method on an existing instance, passing additional arguments 211 * $('#tree2').jstree(); // get an existing instance (or create an instance) 212 * $('#tree2').jstree(true); // get an existing instance (will not create new instance) 213 * $('#branch_1').jstree().select_node('#branch_1'); // get an instance (using a nested element and call a method) 214 * 215 * @name $().jstree([arg]) 216 * @param {String|Object} arg 217 * @return {Mixed} 218 */ 219 $.fn.jstree = function (arg) { 220 // check for string argument 221 var is_method = (typeof arg === 'string'), 222 args = Array.prototype.slice.call(arguments, 1), 223 result = null; 224 if(arg === true && !this.length) { return false; } 225 this.each(function () { 226 // get the instance (if there is one) and method (if it exists) 227 var instance = $.jstree.reference(this), 228 method = is_method && instance ? instance[arg] : null; 229 // if calling a method, and method is available - execute on the instance 230 result = is_method && method ? 231 method.apply(instance, args) : 232 null; 233 // if there is no instance and no method is being called - create one 234 if(!instance && !is_method && (arg === undefined || $.isPlainObject(arg))) { 235 $.jstree.create(this, arg); 236 } 237 // if there is an instance and no method is called - return the instance 238 if( (instance && !is_method) || arg === true ) { 239 result = instance || false; 240 } 241 // if there was a method call which returned a result - break and return the value 242 if(result !== null && result !== undefined) { 243 return false; 244 } 245 }); 246 // if there was a method call with a valid return value - return that, otherwise continue the chain 247 return result !== null && result !== undefined ? 248 result : this; 249 }; 250 /** 251 * used to find elements containing an instance 252 * 253 * __Examples__ 254 * 255 * $('div:jstree').each(function () { 256 * $(this).jstree('destroy'); 257 * }); 258 * 259 * @name $(':jstree') 260 * @return {jQuery} 261 */ 262 $.expr[':'].jstree = $.expr.createPseudo(function(search) { 263 return function(a) { 264 return $(a).hasClass('jstree') && 265 $(a).data('jstree') !== undefined; 266 }; 267 }); 268 269 /** 270 * stores all defaults for the core 271 * @name $.jstree.defaults.core 272 */ 273 $.jstree.defaults.core = { 274 /** 275 * data configuration 276 * 277 * 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). 278 * 279 * You can also pass in a HTML string or a JSON array here. 280 * 281 * 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. 282 * 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. 283 * 284 * 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. 285 * 286 * __Examples__ 287 * 288 * // AJAX 289 * $('#tree').jstree({ 290 * 'core' : { 291 * 'data' : { 292 * 'url' : '/get/children/', 293 * 'data' : function (node) { 294 * return { 'id' : node.id }; 295 * } 296 * } 297 * }); 298 * 299 * // direct data 300 * $('#tree').jstree({ 301 * 'core' : { 302 * 'data' : [ 303 * 'Simple root node', 304 * { 305 * 'id' : 'node_2', 306 * 'text' : 'Root node with options', 307 * 'state' : { 'opened' : true, 'selected' : true }, 308 * 'children' : [ { 'text' : 'Child 1' }, 'Child 2'] 309 * } 310 * ] 311 * }); 312 * 313 * // function 314 * $('#tree').jstree({ 315 * 'core' : { 316 * 'data' : function (obj, callback) { 317 * callback.call(this, ['Root 1', 'Root 2']); 318 * } 319 * }); 320 * 321 * @name $.jstree.defaults.core.data 322 */ 323 data : false, 324 /** 325 * configure the various strings used throughout the tree 326 * 327 * You can use an object where the key is the string you need to replace and the value is your replacement. 328 * Another option is to specify a function which will be called with an argument of the needed string and should return the replacement. 329 * If left as `false` no replacement is made. 330 * 331 * __Examples__ 332 * 333 * $('#tree').jstree({ 334 * 'core' : { 335 * 'strings' : { 336 * 'Loading ...' : 'Please wait ...' 337 * } 338 * } 339 * }); 340 * 341 * @name $.jstree.defaults.core.strings 342 */ 343 strings : false, 344 /** 345 * determines what happens when a user tries to modify the structure of the tree 346 * If left as `false` all operations like create, rename, delete, move or copy are prevented. 347 * You can set this to `true` to allow all interactions or use a function to have better control. 348 * 349 * __Examples__ 350 * 351 * $('#tree').jstree({ 352 * 'core' : { 353 * 'check_callback' : function (operation, node, node_parent, node_position, more) { 354 * // operation can be 'create_node', 'rename_node', 'delete_node', 'move_node' or 'copy_node' 355 * // in case of 'rename_node' node_position is filled with the new node name 356 * return operation === 'rename_node' ? true : false; 357 * } 358 * } 359 * }); 360 * 361 * @name $.jstree.defaults.core.check_callback 362 */ 363 check_callback : false, 364 /** 365 * a callback called with a single object parameter in the instance's scope when something goes wrong (operation prevented, ajax failed, etc) 366 * @name $.jstree.defaults.core.error 367 */ 368 error : $.noop, 369 /** 370 * the open / close animation duration in milliseconds - set this to `false` to disable the animation (default is `200`) 371 * @name $.jstree.defaults.core.animation 372 */ 373 animation : 200, 374 /** 375 * a boolean indicating if multiple nodes can be selected 376 * @name $.jstree.defaults.core.multiple 377 */ 378 multiple : true, 379 /** 380 * theme configuration object 381 * @name $.jstree.defaults.core.themes 382 */ 383 themes : { 384 /** 385 * the name of the theme to use (if left as `false` the default theme is used) 386 * @name $.jstree.defaults.core.themes.name 387 */ 388 name : false, 389 /** 390 * 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. 391 * @name $.jstree.defaults.core.themes.url 392 */ 393 url : false, 394 /** 395 * the location of all jstree themes - only used if `url` is set to `true` 396 * @name $.jstree.defaults.core.themes.dir 397 */ 398 dir : false, 399 /** 400 * a boolean indicating if connecting dots are shown 401 * @name $.jstree.defaults.core.themes.dots 402 */ 403 dots : true, 404 /** 405 * a boolean indicating if node icons are shown 406 * @name $.jstree.defaults.core.themes.icons 407 */ 408 icons : true, 409 /** 410 * a boolean indicating if the tree background is striped 411 * @name $.jstree.defaults.core.themes.stripes 412 */ 413 stripes : false, 414 /** 415 * a string (or boolean `false`) specifying the theme variant to use (if the theme supports variants) 416 * @name $.jstree.defaults.core.themes.variant 417 */ 418 variant : false, 419 /** 420 * a boolean specifying if a reponsive version of the theme should kick in on smaller screens (if the theme supports it). Defaults to `false`. 421 * @name $.jstree.defaults.core.themes.responsive 422 */ 423 responsive : false 424 }, 425 /** 426 * 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) 427 * @name $.jstree.defaults.core.expand_selected_onload 428 */ 429 expand_selected_onload : true, 430 /** 431 * 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` 432 * @name $.jstree.defaults.core.worker 433 */ 434 worker : true, 435 /** 436 * Force node text to plain text (and escape HTML). Defaults to `false` 437 * @name $.jstree.defaults.core.force_text 438 */ 439 force_text : false, 440 /** 441 * Should the node should be toggled if the text is double clicked . Defaults to `true` 442 * @name $.jstree.defaults.core.dblclick_toggle 443 */ 444 dblclick_toggle : true 445 }; 446 $.jstree.core.prototype = { 447 /** 448 * used to decorate an instance with a plugin. Used internally. 449 * @private 450 * @name plugin(deco [, opts]) 451 * @param {String} deco the plugin to decorate with 452 * @param {Object} opts options for the plugin 453 * @return {jsTree} 454 */ 455 plugin : function (deco, opts) { 456 var Child = $.jstree.plugins[deco]; 457 if(Child) { 458 this._data[deco] = {}; 459 Child.prototype = this; 460 return new Child(opts, this); 461 } 462 return this; 463 }, 464 /** 465 * initialize the instance. Used internally. 466 * @private 467 * @name init(el, optons) 468 * @param {DOMElement|jQuery|String} el the element we are transforming 469 * @param {Object} options options for this instance 470 * @trigger init.jstree, loading.jstree, loaded.jstree, ready.jstree, changed.jstree 471 */ 472 init : function (el, options) { 473 this._model = { 474 data : { 475 '#' : { 476 id : '#', 477 parent : null, 478 parents : [], 479 children : [], 480 children_d : [], 481 state : { loaded : false } 482 } 483 }, 484 changed : [], 485 force_full_redraw : false, 486 redraw_timeout : false, 487 default_state : { 488 loaded : true, 489 opened : false, 490 selected : false, 491 disabled : false 492 } 493 }; 494 495 this.element = $(el).addClass('jstree jstree-' + this._id); 496 this.settings = options; 497 498 this._data.core.ready = false; 499 this._data.core.loaded = false; 500 this._data.core.rtl = (this.element.css("direction") === "rtl"); 501 this.element[this._data.core.rtl ? 'addClass' : 'removeClass']("jstree-rtl"); 502 this.element.attr('role','tree'); 503 if(this.settings.core.multiple) { 504 this.element.attr('aria-multiselectable', true); 505 } 506 if(!this.element.attr('tabindex')) { 507 this.element.attr('tabindex','0'); 508 } 509 510 this.bind(); 511 /** 512 * triggered after all events are bound 513 * @event 514 * @name init.jstree 515 */ 516 this.trigger("init"); 517 518 this._data.core.original_container_html = this.element.find(" > ul > li").clone(true); 519 this._data.core.original_container_html 520 .find("li").addBack() 521 .contents().filter(function() { 522 return this.nodeType === 3 && (!this.nodeValue || /^\s+$/.test(this.nodeValue)); 523 }) 524 .remove(); 525 this.element.html("<"+"ul class='jstree-container-ul jstree-children' role='group'><"+"li id='j"+this._id+"_loading' class='jstree-initial-node jstree-loading jstree-leaf jstree-last' role='tree-item'><i class='jstree-icon jstree-ocl'></i><"+"a class='jstree-anchor' href='#'><i class='jstree-icon jstree-themeicon-hidden'></i>" + this.get_string("Loading ...") + "</a></li></ul>"); 526 this.element.attr('aria-activedescendant','j' + this._id + '_loading'); 527 this._data.core.li_height = this.get_container_ul().children("li").first().height() || 24; 528 /** 529 * triggered after the loading text is shown and before loading starts 530 * @event 531 * @name loading.jstree 532 */ 533 this.trigger("loading"); 534 this.load_node('#'); 535 }, 536 /** 537 * destroy an instance 538 * @name destroy() 539 * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact 540 */ 541 destroy : function (keep_html) { 542 if(this._wrk) { 543 try { 544 window.URL.revokeObjectURL(this._wrk); 545 this._wrk = null; 546 } 547 catch (ignore) { } 548 } 549 if(!keep_html) { this.element.empty(); } 550 this.teardown(); 551 }, 552 /** 553 * part of the destroying of an instance. Used internally. 554 * @private 555 * @name teardown() 556 */ 557 teardown : function () { 558 this.unbind(); 559 this.element 560 .removeClass('jstree') 561 .removeData('jstree') 562 .find("[class^='jstree']") 563 .addBack() 564 .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); 565 this.element = null; 566 }, 567 /** 568 * bind all events. Used internally. 569 * @private 570 * @name bind() 571 */ 572 bind : function () { 573 var word = '', 574 tout = null, 575 was_click = 0; 576 this.element 577 .on("dblclick.jstree", function () { 578 if(document.selection && document.selection.empty) { 579 document.selection.empty(); 580 } 581 else { 582 if(window.getSelection) { 583 var sel = window.getSelection(); 584 try { 585 sel.removeAllRanges(); 586 sel.collapse(); 587 } catch (ignore) { } 588 } 589 } 590 }) 591 .on("mousedown.jstree", $.proxy(function (e) { 592 if(e.target === this.element[0]) { 593 e.preventDefault(); // prevent losing focus when clicking scroll arrows (FF, Chrome) 594 was_click = +(new Date()); // ie does not allow to prevent losing focus 595 } 596 }, this)) 597 .on("mousedown.jstree", ".jstree-ocl", function (e) { 598 e.preventDefault(); // prevent any node inside from losing focus when clicking the open/close icon 599 }) 600 .on("click.jstree", ".jstree-ocl", $.proxy(function (e) { 601 this.toggle_node(e.target); 602 }, this)) 603 .on("dblclick.jstree", ".jstree-anchor", $.proxy(function (e) { 604 if(this.settings.core.dblclick_toggle) { 605 this.toggle_node(e.target); 606 } 607 }, this)) 608 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) { 609 e.preventDefault(); 610 if(e.currentTarget !== document.activeElement) { $(e.currentTarget).focus(); } 611 this.activate_node(e.currentTarget, e); 612 }, this)) 613 .on('keydown.jstree', '.jstree-anchor', $.proxy(function (e) { 614 if(e.target.tagName === "INPUT") { return true; } 615 if(e.which !== 32 && e.which !== 13 && (e.shiftKey || e.ctrlKey || e.altKey || e.metaKey)) { return true; } 616 var o = null; 617 if(this._data.core.rtl) { 618 if(e.which === 37) { e.which = 39; } 619 else if(e.which === 39) { e.which = 37; } 620 } 621 switch(e.which) { 622 case 32: // aria defines space only with Ctrl 623 if(e.ctrlKey) { 624 e.type = "click"; 625 $(e.currentTarget).trigger(e); 626 } 627 break; 628 case 13: // enter 629 e.type = "click"; 630 $(e.currentTarget).trigger(e); 631 break; 632 case 37: // right 633 e.preventDefault(); 634 if(this.is_open(e.currentTarget)) { 635 this.close_node(e.currentTarget); 636 } 637 else { 638 o = this.get_parent(e.currentTarget); 639 if(o && o.id !== '#') { this.get_node(o, true).children('.jstree-anchor').focus(); } 640 } 641 break; 642 case 38: // up 643 e.preventDefault(); 644 o = this.get_prev_dom(e.currentTarget); 645 if(o && o.length) { o.children('.jstree-anchor').focus(); } 646 break; 647 case 39: // left 648 e.preventDefault(); 649 if(this.is_closed(e.currentTarget)) { 650 this.open_node(e.currentTarget, function (o) { this.get_node(o, true).children('.jstree-anchor').focus(); }); 651 } 652 else if (this.is_open(e.currentTarget)) { 653 o = this.get_node(e.currentTarget, true).children('.jstree-children')[0]; 654 if(o) { $(this._firstChild(o)).children('.jstree-anchor').focus(); } 655 } 656 break; 657 case 40: // down 658 e.preventDefault(); 659 o = this.get_next_dom(e.currentTarget); 660 if(o && o.length) { o.children('.jstree-anchor').focus(); } 661 break; 662 case 106: // aria defines * on numpad as open_all - not very common 663 this.open_all(); 664 break; 665 case 36: // home 666 e.preventDefault(); 667 o = this._firstChild(this.get_container_ul()[0]); 668 if(o) { $(o).children('.jstree-anchor').filter(':visible').focus(); } 669 break; 670 case 35: // end 671 e.preventDefault(); 672 this.element.find('.jstree-anchor').filter(':visible').last().focus(); 673 break; 674 /* 675 // delete 676 case 46: 677 e.preventDefault(); 678 o = this.get_node(e.currentTarget); 679 if(o && o.id && o.id !== '#') { 680 o = this.is_selected(o) ? this.get_selected() : o; 681 this.delete_node(o); 682 } 683 break; 684 // f2 685 case 113: 686 e.preventDefault(); 687 o = this.get_node(e.currentTarget); 688 if(o && o.id && o.id !== '#') { 689 // this.edit(o); 690 } 691 break; 692 default: 693 // console.log(e.which); 694 break; 695 */ 696 } 697 }, this)) 698 .on("load_node.jstree", $.proxy(function (e, data) { 699 if(data.status) { 700 if(data.node.id === '#' && !this._data.core.loaded) { 701 this._data.core.loaded = true; 702 if(this._firstChild(this.get_container_ul()[0])) { 703 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id); 704 } 705 /** 706 * triggered after the root node is loaded for the first time 707 * @event 708 * @name loaded.jstree 709 */ 710 this.trigger("loaded"); 711 } 712 if(!this._data.core.ready) { 713 setTimeout($.proxy(function() { 714 if(!this.get_container_ul().find('.jstree-loading').length) { 715 this._data.core.ready = true; 716 if(this._data.core.selected.length) { 717 if(this.settings.core.expand_selected_onload) { 718 var tmp = [], i, j; 719 for(i = 0, j = this._data.core.selected.length; i < j; i++) { 720 tmp = tmp.concat(this._model.data[this._data.core.selected[i]].parents); 721 } 722 tmp = $.vakata.array_unique(tmp); 723 for(i = 0, j = tmp.length; i < j; i++) { 724 this.open_node(tmp[i], false, 0); 725 } 726 } 727 this.trigger('changed', { 'action' : 'ready', 'selected' : this._data.core.selected }); 728 } 729 /** 730 * triggered after all nodes are finished loading 731 * @event 732 * @name ready.jstree 733 */ 734 this.trigger("ready"); 735 } 736 }, this), 0); 737 } 738 } 739 }, this)) 740 // quick searching when the tree is focused 741 .on('keypress.jstree', $.proxy(function (e) { 742 if(e.target.tagName === "INPUT") { return true; } 743 if(tout) { clearTimeout(tout); } 744 tout = setTimeout(function () { 745 word = ''; 746 }, 500); 747 748 var chr = String.fromCharCode(e.which).toLowerCase(), 749 col = this.element.find('.jstree-anchor').filter(':visible'), 750 ind = col.index(document.activeElement) || 0, 751 end = false; 752 word += chr; 753 754 // match for whole word from current node down (including the current node) 755 if(word.length > 1) { 756 col.slice(ind).each($.proxy(function (i, v) { 757 if($(v).text().toLowerCase().indexOf(word) === 0) { 758 $(v).focus(); 759 end = true; 760 return false; 761 } 762 }, this)); 763 if(end) { return; } 764 765 // match for whole word from the beginning of the tree 766 col.slice(0, ind).each($.proxy(function (i, v) { 767 if($(v).text().toLowerCase().indexOf(word) === 0) { 768 $(v).focus(); 769 end = true; 770 return false; 771 } 772 }, this)); 773 if(end) { return; } 774 } 775 // list nodes that start with that letter (only if word consists of a single char) 776 if(new RegExp('^' + chr + '+$').test(word)) { 777 // search for the next node starting with that letter 778 col.slice(ind + 1).each($.proxy(function (i, v) { 779 if($(v).text().toLowerCase().charAt(0) === chr) { 780 $(v).focus(); 781 end = true; 782 return false; 783 } 784 }, this)); 785 if(end) { return; } 786 787 // search from the beginning 788 col.slice(0, ind + 1).each($.proxy(function (i, v) { 789 if($(v).text().toLowerCase().charAt(0) === chr) { 790 $(v).focus(); 791 end = true; 792 return false; 793 } 794 }, this)); 795 if(end) { return; } 796 } 797 }, this)) 798 // THEME RELATED 799 .on("init.jstree", $.proxy(function () { 800 var s = this.settings.core.themes; 801 this._data.core.themes.dots = s.dots; 802 this._data.core.themes.stripes = s.stripes; 803 this._data.core.themes.icons = s.icons; 804 this.set_theme(s.name || "default", s.url); 805 this.set_theme_variant(s.variant); 806 }, this)) 807 .on("loading.jstree", $.proxy(function () { 808 this[ this._data.core.themes.dots ? "show_dots" : "hide_dots" ](); 809 this[ this._data.core.themes.icons ? "show_icons" : "hide_icons" ](); 810 this[ this._data.core.themes.stripes ? "show_stripes" : "hide_stripes" ](); 811 }, this)) 812 .on('blur.jstree', '.jstree-anchor', $.proxy(function (e) { 813 this._data.core.focused = null; 814 $(e.currentTarget).filter('.jstree-hovered').mouseleave(); 815 this.element.attr('tabindex', '0'); 816 }, this)) 817 .on('focus.jstree', '.jstree-anchor', $.proxy(function (e) { 818 var tmp = this.get_node(e.currentTarget); 819 if(tmp && tmp.id) { 820 this._data.core.focused = tmp.id; 821 } 822 this.element.find('.jstree-hovered').not(e.currentTarget).mouseleave(); 823 $(e.currentTarget).mouseenter(); 824 this.element.attr('tabindex', '-1'); 825 }, this)) 826 .on('focus.jstree', $.proxy(function () { 827 if(+(new Date()) - was_click > 500 && !this._data.core.focused) { 828 was_click = 0; 829 this.get_node(this.element.attr('aria-activedescendant'), true).find('> .jstree-anchor').focus(); 830 } 831 }, this)) 832 .on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e) { 833 this.hover_node(e.currentTarget); 834 }, this)) 835 .on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e) { 836 this.dehover_node(e.currentTarget); 837 }, this)); 838 }, 839 /** 840 * part of the destroying of an instance. Used internally. 841 * @private 842 * @name unbind() 843 */ 844 unbind : function () { 845 this.element.off('.jstree'); 846 $(document).off('.jstree-' + this._id); 847 }, 848 /** 849 * trigger an event. Used internally. 850 * @private 851 * @name trigger(ev [, data]) 852 * @param {String} ev the name of the event to trigger 853 * @param {Object} data additional data to pass with the event 854 */ 855 trigger : function (ev, data) { 856 if(!data) { 857 data = {}; 858 } 859 data.instance = this; 860 this.element.triggerHandler(ev.replace('.jstree','') + '.jstree', data); 861 }, 862 /** 863 * returns the jQuery extended instance container 864 * @name get_container() 865 * @return {jQuery} 866 */ 867 get_container : function () { 868 return this.element; 869 }, 870 /** 871 * returns the jQuery extended main UL node inside the instance container. Used internally. 872 * @private 873 * @name get_container_ul() 874 * @return {jQuery} 875 */ 876 get_container_ul : function () { 877 return this.element.children(".jstree-children").first(); 878 }, 879 /** 880 * gets string replacements (localization). Used internally. 881 * @private 882 * @name get_string(key) 883 * @param {String} key 884 * @return {String} 885 */ 886 get_string : function (key) { 887 var a = this.settings.core.strings; 888 if($.isFunction(a)) { return a.call(this, key); } 889 if(a && a[key]) { return a[key]; } 890 return key; 891 }, 892 /** 893 * gets the first child of a DOM node. Used internally. 894 * @private 895 * @name _firstChild(dom) 896 * @param {DOMElement} dom 897 * @return {DOMElement} 898 */ 899 _firstChild : function (dom) { 900 dom = dom ? dom.firstChild : null; 901 while(dom !== null && dom.nodeType !== 1) { 902 dom = dom.nextSibling; 903 } 904 return dom; 905 }, 906 /** 907 * gets the next sibling of a DOM node. Used internally. 908 * @private 909 * @name _nextSibling(dom) 910 * @param {DOMElement} dom 911 * @return {DOMElement} 912 */ 913 _nextSibling : function (dom) { 914 dom = dom ? dom.nextSibling : null; 915 while(dom !== null && dom.nodeType !== 1) { 916 dom = dom.nextSibling; 917 } 918 return dom; 919 }, 920 /** 921 * gets the previous sibling of a DOM node. Used internally. 922 * @private 923 * @name _previousSibling(dom) 924 * @param {DOMElement} dom 925 * @return {DOMElement} 926 */ 927 _previousSibling : function (dom) { 928 dom = dom ? dom.previousSibling : null; 929 while(dom !== null && dom.nodeType !== 1) { 930 dom = dom.previousSibling; 931 } 932 return dom; 933 }, 934 /** 935 * 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) 936 * @name get_node(obj [, as_dom]) 937 * @param {mixed} obj 938 * @param {Boolean} as_dom 939 * @return {Object|jQuery} 940 */ 941 get_node : function (obj, as_dom) { 942 if(obj && obj.id) { 943 obj = obj.id; 944 } 945 var dom; 946 try { 947 if(this._model.data[obj]) { 948 obj = this._model.data[obj]; 949 } 950 else if(typeof obj === "string" && this._model.data[obj.replace(/^#/, '')]) { 951 obj = this._model.data[obj.replace(/^#/, '')]; 952 } 953 else if(typeof obj === "string" && (dom = $('#' + obj.replace($.jstree.idregex,'\\$&'), this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) { 954 obj = this._model.data[dom.closest('.jstree-node').attr('id')]; 955 } 956 else if((dom = $(obj, this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) { 957 obj = this._model.data[dom.closest('.jstree-node').attr('id')]; 958 } 959 else if((dom = $(obj, this.element)).length && dom.hasClass('jstree')) { 960 obj = this._model.data['#']; 961 } 962 else { 963 return false; 964 } 965 966 if(as_dom) { 967 obj = obj.id === '#' ? this.element : $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element); 968 } 969 return obj; 970 } catch (ex) { return false; } 971 }, 972 /** 973 * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array) 974 * @name get_path(obj [, glue, ids]) 975 * @param {mixed} obj the node 976 * @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 977 * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used 978 * @return {mixed} 979 */ 980 get_path : function (obj, glue, ids) { 981 obj = obj.parents ? obj : this.get_node(obj); 982 if(!obj || obj.id === '#' || !obj.parents) { 983 return false; 984 } 985 var i, j, p = []; 986 p.push(ids ? obj.id : obj.text); 987 for(i = 0, j = obj.parents.length; i < j; i++) { 988 p.push(ids ? obj.parents[i] : this.get_text(obj.parents[i])); 989 } 990 p = p.reverse().slice(1); 991 return glue ? p.join(glue) : p; 992 }, 993 /** 994 * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned. 995 * @name get_next_dom(obj [, strict]) 996 * @param {mixed} obj 997 * @param {Boolean} strict 998 * @return {jQuery} 999 */ 1000 get_next_dom : function (obj, strict) { 1001 var tmp; 1002 obj = this.get_node(obj, true); 1003 if(obj[0] === this.element[0]) { 1004 tmp = this._firstChild(this.get_container_ul()[0]); 1005 while (tmp && tmp.offsetHeight === 0) { 1006 tmp = this._nextSibling(tmp); 1007 } 1008 return tmp ? $(tmp) : false; 1009 } 1010 if(!obj || !obj.length) { 1011 return false; 1012 } 1013 if(strict) { 1014 tmp = obj[0]; 1015 do { 1016 tmp = this._nextSibling(tmp); 1017 } while (tmp && tmp.offsetHeight === 0); 1018 return tmp ? $(tmp) : false; 1019 } 1020 if(obj.hasClass("jstree-open")) { 1021 tmp = this._firstChild(obj.children('.jstree-children')[0]); 1022 while (tmp && tmp.offsetHeight === 0) { 1023 tmp = this._nextSibling(tmp); 1024 } 1025 if(tmp !== null) { 1026 return $(tmp); 1027 } 1028 } 1029 tmp = obj[0]; 1030 do { 1031 tmp = this._nextSibling(tmp); 1032 } while (tmp && tmp.offsetHeight === 0); 1033 if(tmp !== null) { 1034 return $(tmp); 1035 } 1036 return obj.parentsUntil(".jstree",".jstree-node").nextAll(".jstree-node:visible").first(); 1037 }, 1038 /** 1039 * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned. 1040 * @name get_prev_dom(obj [, strict]) 1041 * @param {mixed} obj 1042 * @param {Boolean} strict 1043 * @return {jQuery} 1044 */ 1045 get_prev_dom : function (obj, strict) { 1046 var tmp; 1047 obj = this.get_node(obj, true); 1048 if(obj[0] === this.element[0]) { 1049 tmp = this.get_container_ul()[0].lastChild; 1050 while (tmp && tmp.offsetHeight === 0) { 1051 tmp = this._previousSibling(tmp); 1052 } 1053 return tmp ? $(tmp) : false; 1054 } 1055 if(!obj || !obj.length) { 1056 return false; 1057 } 1058 if(strict) { 1059 tmp = obj[0]; 1060 do { 1061 tmp = this._previousSibling(tmp); 1062 } while (tmp && tmp.offsetHeight === 0); 1063 return tmp ? $(tmp) : false; 1064 } 1065 tmp = obj[0]; 1066 do { 1067 tmp = this._previousSibling(tmp); 1068 } while (tmp && tmp.offsetHeight === 0); 1069 if(tmp !== null) { 1070 obj = $(tmp); 1071 while(obj.hasClass("jstree-open")) { 1072 obj = obj.children(".jstree-children").first().children(".jstree-node:visible:last"); 1073 } 1074 return obj; 1075 } 1076 tmp = obj[0].parentNode.parentNode; 1077 return tmp && tmp.className && tmp.className.indexOf('jstree-node') !== -1 ? $(tmp) : false; 1078 }, 1079 /** 1080 * get the parent ID of a node 1081 * @name get_parent(obj) 1082 * @param {mixed} obj 1083 * @return {String} 1084 */ 1085 get_parent : function (obj) { 1086 obj = this.get_node(obj); 1087 if(!obj || obj.id === '#') { 1088 return false; 1089 } 1090 return obj.parent; 1091 }, 1092 /** 1093 * get a jQuery collection of all the children of a node (node must be rendered) 1094 * @name get_children_dom(obj) 1095 * @param {mixed} obj 1096 * @return {jQuery} 1097 */ 1098 get_children_dom : function (obj) { 1099 obj = this.get_node(obj, true); 1100 if(obj[0] === this.element[0]) { 1101 return this.get_container_ul().children(".jstree-node"); 1102 } 1103 if(!obj || !obj.length) { 1104 return false; 1105 } 1106 return obj.children(".jstree-children").children(".jstree-node"); 1107 }, 1108 /** 1109 * checks if a node has children 1110 * @name is_parent(obj) 1111 * @param {mixed} obj 1112 * @return {Boolean} 1113 */ 1114 is_parent : function (obj) { 1115 obj = this.get_node(obj); 1116 return obj && (obj.state.loaded === false || obj.children.length > 0); 1117 }, 1118 /** 1119 * checks if a node is loaded (its children are available) 1120 * @name is_loaded(obj) 1121 * @param {mixed} obj 1122 * @return {Boolean} 1123 */ 1124 is_loaded : function (obj) { 1125 obj = this.get_node(obj); 1126 return obj && obj.state.loaded; 1127 }, 1128 /** 1129 * check if a node is currently loading (fetching children) 1130 * @name is_loading(obj) 1131 * @param {mixed} obj 1132 * @return {Boolean} 1133 */ 1134 is_loading : function (obj) { 1135 obj = this.get_node(obj); 1136 return obj && obj.state && obj.state.loading; 1137 }, 1138 /** 1139 * check if a node is opened 1140 * @name is_open(obj) 1141 * @param {mixed} obj 1142 * @return {Boolean} 1143 */ 1144 is_open : function (obj) { 1145 obj = this.get_node(obj); 1146 return obj && obj.state.opened; 1147 }, 1148 /** 1149 * check if a node is in a closed state 1150 * @name is_closed(obj) 1151 * @param {mixed} obj 1152 * @return {Boolean} 1153 */ 1154 is_closed : function (obj) { 1155 obj = this.get_node(obj); 1156 return obj && this.is_parent(obj) && !obj.state.opened; 1157 }, 1158 /** 1159 * check if a node has no children 1160 * @name is_leaf(obj) 1161 * @param {mixed} obj 1162 * @return {Boolean} 1163 */ 1164 is_leaf : function (obj) { 1165 return !this.is_parent(obj); 1166 }, 1167 /** 1168 * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array. 1169 * @name load_node(obj [, callback]) 1170 * @param {mixed} obj 1171 * @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 1172 * @return {Boolean} 1173 * @trigger load_node.jstree 1174 */ 1175 load_node : function (obj, callback) { 1176 var k, l, i, j, c; 1177 if($.isArray(obj)) { 1178 this._load_nodes(obj.slice(), callback); 1179 return true; 1180 } 1181 obj = this.get_node(obj); 1182 if(!obj) { 1183 if(callback) { callback.call(this, obj, false); } 1184 return false; 1185 } 1186 // 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? 1187 if(obj.state.loaded) { 1188 obj.state.loaded = false; 1189 for(k = 0, l = obj.children_d.length; k < l; k++) { 1190 for(i = 0, j = obj.parents.length; i < j; i++) { 1191 this._model.data[obj.parents[i]].children_d = $.vakata.array_remove_item(this._model.data[obj.parents[i]].children_d, obj.children_d[k]); 1192 } 1193 if(this._model.data[obj.children_d[k]].state.selected) { 1194 c = true; 1195 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.children_d[k]); 1196 } 1197 delete this._model.data[obj.children_d[k]]; 1198 } 1199 obj.children = []; 1200 obj.children_d = []; 1201 if(c) { 1202 this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected }); 1203 } 1204 } 1205 obj.state.failed = false; 1206 obj.state.loading = true; 1207 this.get_node(obj, true).addClass("jstree-loading").attr('aria-busy',true); 1208 this._load_node(obj, $.proxy(function (status) { 1209 obj = this._model.data[obj.id]; 1210 obj.state.loading = false; 1211 obj.state.loaded = status; 1212 obj.state.failed = !obj.state.loaded; 1213 var dom = this.get_node(obj, true); 1214 if(obj.state.loaded && !obj.children.length && dom && dom.length && !dom.hasClass('jstree-leaf')) { 1215 dom.removeClass('jstree-closed jstree-open').addClass('jstree-leaf'); 1216 } 1217 dom.removeClass("jstree-loading").attr('aria-busy',false); 1218 /** 1219 * triggered after a node is loaded 1220 * @event 1221 * @name load_node.jstree 1222 * @param {Object} node the node that was loading 1223 * @param {Boolean} status was the node loaded successfully 1224 */ 1225 this.trigger('load_node', { "node" : obj, "status" : status }); 1226 if(callback) { 1227 callback.call(this, obj, status); 1228 } 1229 }, this)); 1230 return true; 1231 }, 1232 /** 1233 * load an array of nodes (will also load unavailable nodes as soon as the appear in the structure). Used internally. 1234 * @private 1235 * @name _load_nodes(nodes [, callback]) 1236 * @param {array} nodes 1237 * @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 1238 */ 1239 _load_nodes : function (nodes, callback, is_callback) { 1240 var r = true, 1241 c = function () { this._load_nodes(nodes, callback, true); }, 1242 m = this._model.data, i, j, tmp = []; 1243 for(i = 0, j = nodes.length; i < j; i++) { 1244 if(m[nodes[i]] && ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || !is_callback)) { 1245 if(!this.is_loading(nodes[i])) { 1246 this.load_node(nodes[i], c); 1247 } 1248 r = false; 1249 } 1250 } 1251 if(r) { 1252 for(i = 0, j = nodes.length; i < j; i++) { 1253 if(m[nodes[i]] && m[nodes[i]].state.loaded) { 1254 tmp.push(nodes[i]); 1255 } 1256 } 1257 if(callback && !callback.done) { 1258 callback.call(this, tmp); 1259 callback.done = true; 1260 } 1261 } 1262 }, 1263 /** 1264 * loads all unloaded nodes 1265 * @name load_all([obj, callback]) 1266 * @param {mixed} obj the node to load recursively, omit to load all nodes in the tree 1267 * @param {function} callback a function to be executed once loading all the nodes is complete, 1268 * @trigger load_all.jstree 1269 */ 1270 load_all : function (obj, callback) { 1271 if(!obj) { obj = '#'; } 1272 obj = this.get_node(obj); 1273 if(!obj) { return false; } 1274 var to_load = [], 1275 m = this._model.data, 1276 c = m[obj.id].children_d, 1277 i, j; 1278 if(obj.state && !obj.state.loaded) { 1279 to_load.push(obj.id); 1280 } 1281 for(i = 0, j = c.length; i < j; i++) { 1282 if(m[c[i]] && m[c[i]].state && !m[c[i]].state.loaded) { 1283 to_load.push(c[i]); 1284 } 1285 } 1286 if(to_load.length) { 1287 this._load_nodes(to_load, function () { 1288 this.load_all(obj, callback); 1289 }); 1290 } 1291 else { 1292 /** 1293 * triggered after a load_all call completes 1294 * @event 1295 * @name load_all.jstree 1296 * @param {Object} node the recursively loaded node 1297 */ 1298 if(callback) { callback.call(this, obj); } 1299 this.trigger('load_all', { "node" : obj }); 1300 } 1301 }, 1302 /** 1303 * handles the actual loading of a node. Used only internally. 1304 * @private 1305 * @name _load_node(obj [, callback]) 1306 * @param {mixed} obj 1307 * @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 1308 * @return {Boolean} 1309 */ 1310 _load_node : function (obj, callback) { 1311 var s = this.settings.core.data, t; 1312 // use original HTML 1313 if(!s) { 1314 if(obj.id === '#') { 1315 return this._append_html_data(obj, this._data.core.original_container_html.clone(true), function (status) { 1316 callback.call(this, status); 1317 }); 1318 } 1319 else { 1320 return callback.call(this, false); 1321 } 1322 // return callback.call(this, obj.id === '#' ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false); 1323 } 1324 if($.isFunction(s)) { 1325 return s.call(this, obj, $.proxy(function (d) { 1326 if(d === false) { 1327 callback.call(this, false); 1328 } 1329 this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }) : d, function (status) { 1330 callback.call(this, status); 1331 }); 1332 // 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)); 1333 }, this)); 1334 } 1335 if(typeof s === 'object') { 1336 if(s.url) { 1337 s = $.extend(true, {}, s); 1338 if($.isFunction(s.url)) { 1339 s.url = s.url.call(this, obj); 1340 } 1341 if($.isFunction(s.data)) { 1342 s.data = s.data.call(this, obj); 1343 } 1344 return $.ajax(s) 1345 .done($.proxy(function (d,t,x) { 1346 var type = x.getResponseHeader('Content-Type'); 1347 if((type && type.indexOf('json') !== -1) || typeof d === "object") { 1348 return this._append_json_data(obj, d, function (status) { callback.call(this, status); }); 1349 //return callback.call(this, this._append_json_data(obj, d)); 1350 } 1351 if((type && type.indexOf('html') !== -1) || typeof d === "string") { 1352 return this._append_html_data(obj, $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }), function (status) { callback.call(this, status); }); 1353 // return callback.call(this, this._append_html_data(obj, $(d))); 1354 } 1355 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 }) }; 1356 this.settings.core.error.call(this, this._data.core.last_error); 1357 return callback.call(this, false); 1358 }, this)) 1359 .fail($.proxy(function (f) { 1360 callback.call(this, false); 1361 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 }) }; 1362 this.settings.core.error.call(this, this._data.core.last_error); 1363 }, this)); 1364 } 1365 t = ($.isArray(s) || $.isPlainObject(s)) ? JSON.parse(JSON.stringify(s)) : s; 1366 if(obj.id === '#') { 1367 return this._append_json_data(obj, t, function (status) { 1368 callback.call(this, status); 1369 }); 1370 } 1371 else { 1372 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) }; 1373 this.settings.core.error.call(this, this._data.core.last_error); 1374 return callback.call(this, false); 1375 } 1376 //return callback.call(this, (obj.id === "#" ? this._append_json_data(obj, t) : false) ); 1377 } 1378 if(typeof s === 'string') { 1379 if(obj.id === '#') { 1380 return this._append_html_data(obj, $($.parseHTML(s)).filter(function () { return this.nodeType !== 3; }), function (status) { 1381 callback.call(this, status); 1382 }); 1383 } 1384 else { 1385 this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) }; 1386 this.settings.core.error.call(this, this._data.core.last_error); 1387 return callback.call(this, false); 1388 } 1389 //return callback.call(this, (obj.id === "#" ? this._append_html_data(obj, $(s)) : false) ); 1390 } 1391 return callback.call(this, false); 1392 }, 1393 /** 1394 * adds a node to the list of nodes to redraw. Used only internally. 1395 * @private 1396 * @name _node_changed(obj [, callback]) 1397 * @param {mixed} obj 1398 */ 1399 _node_changed : function (obj) { 1400 obj = this.get_node(obj); 1401 if(obj) { 1402 this._model.changed.push(obj.id); 1403 } 1404 }, 1405 /** 1406 * appends HTML content to the tree. Used internally. 1407 * @private 1408 * @name _append_html_data(obj, data) 1409 * @param {mixed} obj the node to append to 1410 * @param {String} data the HTML string to parse and append 1411 * @trigger model.jstree, changed.jstree 1412 */ 1413 _append_html_data : function (dom, data, cb) { 1414 dom = this.get_node(dom); 1415 dom.children = []; 1416 dom.children_d = []; 1417 var dat = data.is('ul') ? data.children() : data, 1418 par = dom.id, 1419 chd = [], 1420 dpc = [], 1421 m = this._model.data, 1422 p = m[par], 1423 s = this._data.core.selected.length, 1424 tmp, i, j; 1425 dat.each($.proxy(function (i, v) { 1426 tmp = this._parse_model_from_html($(v), par, p.parents.concat()); 1427 if(tmp) { 1428 chd.push(tmp); 1429 dpc.push(tmp); 1430 if(m[tmp].children_d.length) { 1431 dpc = dpc.concat(m[tmp].children_d); 1432 } 1433 } 1434 }, this)); 1435 p.children = chd; 1436 p.children_d = dpc; 1437 for(i = 0, j = p.parents.length; i < j; i++) { 1438 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc); 1439 } 1440 /** 1441 * triggered when new data is inserted to the tree model 1442 * @event 1443 * @name model.jstree 1444 * @param {Array} nodes an array of node IDs 1445 * @param {String} parent the parent ID of the nodes 1446 */ 1447 this.trigger('model', { "nodes" : dpc, 'parent' : par }); 1448 if(par !== '#') { 1449 this._node_changed(par); 1450 this.redraw(); 1451 } 1452 else { 1453 this.get_container_ul().children('.jstree-initial-node').remove(); 1454 this.redraw(true); 1455 } 1456 if(this._data.core.selected.length !== s) { 1457 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected }); 1458 } 1459 cb.call(this, true); 1460 }, 1461 /** 1462 * appends JSON content to the tree. Used internally. 1463 * @private 1464 * @name _append_json_data(obj, data) 1465 * @param {mixed} obj the node to append to 1466 * @param {String} data the JSON object to parse and append 1467 * @param {Boolean} force_processing internal param - do not set 1468 * @trigger model.jstree, changed.jstree 1469 */ 1470 _append_json_data : function (dom, data, cb, force_processing) { 1471 dom = this.get_node(dom); 1472 dom.children = []; 1473 dom.children_d = []; 1474 // *%$@!!! 1475 if(data.d) { 1476 data = data.d; 1477 if(typeof data === "string") { 1478 data = JSON.parse(data); 1479 } 1480 } 1481 if(!$.isArray(data)) { data = [data]; } 1482 var w = null, 1483 args = { 1484 'df' : this._model.default_state, 1485 'dat' : data, 1486 'par' : dom.id, 1487 'm' : this._model.data, 1488 't_id' : this._id, 1489 't_cnt' : this._cnt, 1490 'sel' : this._data.core.selected 1491 }, 1492 func = function (data, undefined) { 1493 if(data.data) { data = data.data; } 1494 var dat = data.dat, 1495 par = data.par, 1496 chd = [], 1497 dpc = [], 1498 add = [], 1499 df = data.df, 1500 t_id = data.t_id, 1501 t_cnt = data.t_cnt, 1502 m = data.m, 1503 p = m[par], 1504 sel = data.sel, 1505 tmp, i, j, rslt, 1506 parse_flat = function (d, p, ps) { 1507 if(!ps) { ps = []; } 1508 else { ps = ps.concat(); } 1509 if(p) { ps.unshift(p); } 1510 var tid = d.id.toString(), 1511 i, j, c, e, 1512 tmp = { 1513 id : tid, 1514 text : d.text || '', 1515 icon : d.icon !== undefined ? d.icon : true, 1516 parent : p, 1517 parents : ps, 1518 children : d.children || [], 1519 children_d : d.children_d || [], 1520 data : d.data, 1521 state : { }, 1522 li_attr : { id : false }, 1523 a_attr : { href : '#' }, 1524 original : false 1525 }; 1526 for(i in df) { 1527 if(df.hasOwnProperty(i)) { 1528 tmp.state[i] = df[i]; 1529 } 1530 } 1531 if(d && d.data && d.data.jstree && d.data.jstree.icon) { 1532 tmp.icon = d.data.jstree.icon; 1533 } 1534 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { 1535 tmp.icon = true; 1536 } 1537 if(d && d.data) { 1538 tmp.data = d.data; 1539 if(d.data.jstree) { 1540 for(i in d.data.jstree) { 1541 if(d.data.jstree.hasOwnProperty(i)) { 1542 tmp.state[i] = d.data.jstree[i]; 1543 } 1544 } 1545 } 1546 } 1547 if(d && typeof d.state === 'object') { 1548 for (i in d.state) { 1549 if(d.state.hasOwnProperty(i)) { 1550 tmp.state[i] = d.state[i]; 1551 } 1552 } 1553 } 1554 if(d && typeof d.li_attr === 'object') { 1555 for (i in d.li_attr) { 1556 if(d.li_attr.hasOwnProperty(i)) { 1557 tmp.li_attr[i] = d.li_attr[i]; 1558 } 1559 } 1560 } 1561 if(!tmp.li_attr.id) { 1562 tmp.li_attr.id = tid; 1563 } 1564 if(d && typeof d.a_attr === 'object') { 1565 for (i in d.a_attr) { 1566 if(d.a_attr.hasOwnProperty(i)) { 1567 tmp.a_attr[i] = d.a_attr[i]; 1568 } 1569 } 1570 } 1571 if(d && d.children && d.children === true) { 1572 tmp.state.loaded = false; 1573 tmp.children = []; 1574 tmp.children_d = []; 1575 } 1576 m[tmp.id] = tmp; 1577 for(i = 0, j = tmp.children.length; i < j; i++) { 1578 c = parse_flat(m[tmp.children[i]], tmp.id, ps); 1579 e = m[c]; 1580 tmp.children_d.push(c); 1581 if(e.children_d.length) { 1582 tmp.children_d = tmp.children_d.concat(e.children_d); 1583 } 1584 } 1585 delete d.data; 1586 delete d.children; 1587 m[tmp.id].original = d; 1588 if(tmp.state.selected) { 1589 add.push(tmp.id); 1590 } 1591 return tmp.id; 1592 }, 1593 parse_nest = function (d, p, ps) { 1594 if(!ps) { ps = []; } 1595 else { ps = ps.concat(); } 1596 if(p) { ps.unshift(p); } 1597 var tid = false, i, j, c, e, tmp; 1598 do { 1599 tid = 'j' + t_id + '_' + (++t_cnt); 1600 } while(m[tid]); 1601 1602 tmp = { 1603 id : false, 1604 text : typeof d === 'string' ? d : '', 1605 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true, 1606 parent : p, 1607 parents : ps, 1608 children : [], 1609 children_d : [], 1610 data : null, 1611 state : { }, 1612 li_attr : { id : false }, 1613 a_attr : { href : '#' }, 1614 original : false 1615 }; 1616 for(i in df) { 1617 if(df.hasOwnProperty(i)) { 1618 tmp.state[i] = df[i]; 1619 } 1620 } 1621 if(d && d.id) { tmp.id = d.id.toString(); } 1622 if(d && d.text) { tmp.text = d.text; } 1623 if(d && d.data && d.data.jstree && d.data.jstree.icon) { 1624 tmp.icon = d.data.jstree.icon; 1625 } 1626 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { 1627 tmp.icon = true; 1628 } 1629 if(d && d.data) { 1630 tmp.data = d.data; 1631 if(d.data.jstree) { 1632 for(i in d.data.jstree) { 1633 if(d.data.jstree.hasOwnProperty(i)) { 1634 tmp.state[i] = d.data.jstree[i]; 1635 } 1636 } 1637 } 1638 } 1639 if(d && typeof d.state === 'object') { 1640 for (i in d.state) { 1641 if(d.state.hasOwnProperty(i)) { 1642 tmp.state[i] = d.state[i]; 1643 } 1644 } 1645 } 1646 if(d && typeof d.li_attr === 'object') { 1647 for (i in d.li_attr) { 1648 if(d.li_attr.hasOwnProperty(i)) { 1649 tmp.li_attr[i] = d.li_attr[i]; 1650 } 1651 } 1652 } 1653 if(tmp.li_attr.id && !tmp.id) { 1654 tmp.id = tmp.li_attr.id.toString(); 1655 } 1656 if(!tmp.id) { 1657 tmp.id = tid; 1658 } 1659 if(!tmp.li_attr.id) { 1660 tmp.li_attr.id = tmp.id; 1661 } 1662 if(d && typeof d.a_attr === 'object') { 1663 for (i in d.a_attr) { 1664 if(d.a_attr.hasOwnProperty(i)) { 1665 tmp.a_attr[i] = d.a_attr[i]; 1666 } 1667 } 1668 } 1669 if(d && d.children && d.children.length) { 1670 for(i = 0, j = d.children.length; i < j; i++) { 1671 c = parse_nest(d.children[i], tmp.id, ps); 1672 e = m[c]; 1673 tmp.children.push(c); 1674 if(e.children_d.length) { 1675 tmp.children_d = tmp.children_d.concat(e.children_d); 1676 } 1677 } 1678 tmp.children_d = tmp.children_d.concat(tmp.children); 1679 } 1680 if(d && d.children && d.children === true) { 1681 tmp.state.loaded = false; 1682 tmp.children = []; 1683 tmp.children_d = []; 1684 } 1685 delete d.data; 1686 delete d.children; 1687 tmp.original = d; 1688 m[tmp.id] = tmp; 1689 if(tmp.state.selected) { 1690 add.push(tmp.id); 1691 } 1692 return tmp.id; 1693 }; 1694 1695 if(dat.length && dat[0].id !== undefined && dat[0].parent !== undefined) { 1696 // Flat JSON support (for easy import from DB): 1697 // 1) convert to object (foreach) 1698 for(i = 0, j = dat.length; i < j; i++) { 1699 if(!dat[i].children) { 1700 dat[i].children = []; 1701 } 1702 m[dat[i].id.toString()] = dat[i]; 1703 } 1704 // 2) populate children (foreach) 1705 for(i = 0, j = dat.length; i < j; i++) { 1706 m[dat[i].parent.toString()].children.push(dat[i].id.toString()); 1707 // populate parent.children_d 1708 p.children_d.push(dat[i].id.toString()); 1709 } 1710 // 3) normalize && populate parents and children_d with recursion 1711 for(i = 0, j = p.children.length; i < j; i++) { 1712 tmp = parse_flat(m[p.children[i]], par, p.parents.concat()); 1713 dpc.push(tmp); 1714 if(m[tmp].children_d.length) { 1715 dpc = dpc.concat(m[tmp].children_d); 1716 } 1717 } 1718 for(i = 0, j = p.parents.length; i < j; i++) { 1719 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc); 1720 } 1721 // ?) three_state selection - p.state.selected && t - (if three_state foreach(dat => ch) -> foreach(parents) if(parent.selected) child.selected = true; 1722 rslt = { 1723 'cnt' : t_cnt, 1724 'mod' : m, 1725 'sel' : sel, 1726 'par' : par, 1727 'dpc' : dpc, 1728 'add' : add 1729 }; 1730 } 1731 else { 1732 for(i = 0, j = dat.length; i < j; i++) { 1733 tmp = parse_nest(dat[i], par, p.parents.concat()); 1734 if(tmp) { 1735 chd.push(tmp); 1736 dpc.push(tmp); 1737 if(m[tmp].children_d.length) { 1738 dpc = dpc.concat(m[tmp].children_d); 1739 } 1740 } 1741 } 1742 p.children = chd; 1743 p.children_d = dpc; 1744 for(i = 0, j = p.parents.length; i < j; i++) { 1745 m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc); 1746 } 1747 rslt = { 1748 'cnt' : t_cnt, 1749 'mod' : m, 1750 'sel' : sel, 1751 'par' : par, 1752 'dpc' : dpc, 1753 'add' : add 1754 }; 1755 } 1756 if(typeof window === 'undefined' || typeof window.document === 'undefined') { 1757 postMessage(rslt); 1758 } 1759 else { 1760 return rslt; 1761 } 1762 }, 1763 rslt = function (rslt, worker) { 1764 this._cnt = rslt.cnt; 1765 this._model.data = rslt.mod; // breaks the reference in load_node - careful 1766 1767 if(worker) { 1768 var i, j, a = rslt.add, r = rslt.sel, s = this._data.core.selected.slice(), m = this._model.data; 1769 // if selection was changed while calculating in worker 1770 if(r.length !== s.length || $.vakata.array_unique(r.concat(s)).length !== r.length) { 1771 // deselect nodes that are no longer selected 1772 for(i = 0, j = r.length; i < j; i++) { 1773 if($.inArray(r[i], a) === -1 && $.inArray(r[i], s) === -1) { 1774 m[r[i]].state.selected = false; 1775 } 1776 } 1777 // select nodes that were selected in the mean time 1778 for(i = 0, j = s.length; i < j; i++) { 1779 if($.inArray(s[i], r) === -1) { 1780 m[s[i]].state.selected = true; 1781 } 1782 } 1783 } 1784 } 1785 if(rslt.add.length) { 1786 this._data.core.selected = this._data.core.selected.concat(rslt.add); 1787 } 1788 1789 this.trigger('model', { "nodes" : rslt.dpc, 'parent' : rslt.par }); 1790 1791 if(rslt.par !== '#') { 1792 this._node_changed(rslt.par); 1793 this.redraw(); 1794 } 1795 else { 1796 // this.get_container_ul().children('.jstree-initial-node').remove(); 1797 this.redraw(true); 1798 } 1799 if(rslt.add.length) { 1800 this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected }); 1801 } 1802 cb.call(this, true); 1803 }; 1804 if(this.settings.core.worker && window.Blob && window.URL && window.Worker) { 1805 try { 1806 if(this._wrk === null) { 1807 this._wrk = window.URL.createObjectURL( 1808 new window.Blob( 1809 ['self.onmessage = ' + func.toString()], 1810 {type:"text/javascript"} 1811 ) 1812 ); 1813 } 1814 if(!this._data.core.working || force_processing) { 1815 this._data.core.working = true; 1816 w = new window.Worker(this._wrk); 1817 w.onmessage = $.proxy(function (e) { 1818 rslt.call(this, e.data, true); 1819 try { w.terminate(); w = null; } catch(ignore) { } 1820 if(this._data.core.worker_queue.length) { 1821 this._append_json_data.apply(this, this._data.core.worker_queue.shift()); 1822 } 1823 else { 1824 this._data.core.working = false; 1825 } 1826 }, this); 1827 if(!args.par) { 1828 if(this._data.core.worker_queue.length) { 1829 this._append_json_data.apply(this, this._data.core.worker_queue.shift()); 1830 } 1831 else { 1832 this._data.core.working = false; 1833 } 1834 } 1835 else { 1836 w.postMessage(args); 1837 } 1838 } 1839 else { 1840 this._data.core.worker_queue.push([dom, data, cb, true]); 1841 } 1842 } 1843 catch(e) { 1844 rslt.call(this, func(args), false); 1845 if(this._data.core.worker_queue.length) { 1846 this._append_json_data.apply(this, this._data.core.worker_queue.shift()); 1847 } 1848 else { 1849 this._data.core.working = false; 1850 } 1851 } 1852 } 1853 else { 1854 rslt.call(this, func(args), false); 1855 } 1856 }, 1857 /** 1858 * parses a node from a jQuery object and appends them to the in memory tree model. Used internally. 1859 * @private 1860 * @name _parse_model_from_html(d [, p, ps]) 1861 * @param {jQuery} d the jQuery object to parse 1862 * @param {String} p the parent ID 1863 * @param {Array} ps list of all parents 1864 * @return {String} the ID of the object added to the model 1865 */ 1866 _parse_model_from_html : function (d, p, ps) { 1867 if(!ps) { ps = []; } 1868 else { ps = [].concat(ps); } 1869 if(p) { ps.unshift(p); } 1870 var c, e, m = this._model.data, 1871 data = { 1872 id : false, 1873 text : false, 1874 icon : true, 1875 parent : p, 1876 parents : ps, 1877 children : [], 1878 children_d : [], 1879 data : null, 1880 state : { }, 1881 li_attr : { id : false }, 1882 a_attr : { href : '#' }, 1883 original : false 1884 }, i, tmp, tid; 1885 for(i in this._model.default_state) { 1886 if(this._model.default_state.hasOwnProperty(i)) { 1887 data.state[i] = this._model.default_state[i]; 1888 } 1889 } 1890 tmp = $.vakata.attributes(d, true); 1891 $.each(tmp, function (i, v) { 1892 v = $.trim(v); 1893 if(!v.length) { return true; } 1894 data.li_attr[i] = v; 1895 if(i === 'id') { 1896 data.id = v.toString(); 1897 } 1898 }); 1899 tmp = d.children('a').first(); 1900 if(tmp.length) { 1901 tmp = $.vakata.attributes(tmp, true); 1902 $.each(tmp, function (i, v) { 1903 v = $.trim(v); 1904 if(v.length) { 1905 data.a_attr[i] = v; 1906 } 1907 }); 1908 } 1909 tmp = d.children("a").first().length ? d.children("a").first().clone() : d.clone(); 1910 tmp.children("ins, i, ul").remove(); 1911 tmp = tmp.html(); 1912 tmp = $('<div />').html(tmp); 1913 data.text = this.settings.core.force_text ? tmp.text() : tmp.html(); 1914 tmp = d.data(); 1915 data.data = tmp ? $.extend(true, {}, tmp) : null; 1916 data.state.opened = d.hasClass('jstree-open'); 1917 data.state.selected = d.children('a').hasClass('jstree-clicked'); 1918 data.state.disabled = d.children('a').hasClass('jstree-disabled'); 1919 if(data.data && data.data.jstree) { 1920 for(i in data.data.jstree) { 1921 if(data.data.jstree.hasOwnProperty(i)) { 1922 data.state[i] = data.data.jstree[i]; 1923 } 1924 } 1925 } 1926 tmp = d.children("a").children(".jstree-themeicon"); 1927 if(tmp.length) { 1928 data.icon = tmp.hasClass('jstree-themeicon-hidden') ? false : tmp.attr('rel'); 1929 } 1930 if(data.state.icon) { 1931 data.icon = data.state.icon; 1932 } 1933 if(data.icon === undefined || data.icon === null || data.icon === "") { 1934 data.icon = true; 1935 } 1936 tmp = d.children("ul").children("li"); 1937 do { 1938 tid = 'j' + this._id + '_' + (++this._cnt); 1939 } while(m[tid]); 1940 data.id = data.li_attr.id ? data.li_attr.id.toString() : tid; 1941 if(tmp.length) { 1942 tmp.each($.proxy(function (i, v) { 1943 c = this._parse_model_from_html($(v), data.id, ps); 1944 e = this._model.data[c]; 1945 data.children.push(c); 1946 if(e.children_d.length) { 1947 data.children_d = data.children_d.concat(e.children_d); 1948 } 1949 }, this)); 1950 data.children_d = data.children_d.concat(data.children); 1951 } 1952 else { 1953 if(d.hasClass('jstree-closed')) { 1954 data.state.loaded = false; 1955 } 1956 } 1957 if(data.li_attr['class']) { 1958 data.li_attr['class'] = data.li_attr['class'].replace('jstree-closed','').replace('jstree-open',''); 1959 } 1960 if(data.a_attr['class']) { 1961 data.a_attr['class'] = data.a_attr['class'].replace('jstree-clicked','').replace('jstree-disabled',''); 1962 } 1963 m[data.id] = data; 1964 if(data.state.selected) { 1965 this._data.core.selected.push(data.id); 1966 } 1967 return data.id; 1968 }, 1969 /** 1970 * 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. 1971 * @private 1972 * @name _parse_model_from_flat_json(d [, p, ps]) 1973 * @param {Object} d the JSON object to parse 1974 * @param {String} p the parent ID 1975 * @param {Array} ps list of all parents 1976 * @return {String} the ID of the object added to the model 1977 */ 1978 _parse_model_from_flat_json : function (d, p, ps) { 1979 if(!ps) { ps = []; } 1980 else { ps = ps.concat(); } 1981 if(p) { ps.unshift(p); } 1982 var tid = d.id.toString(), 1983 m = this._model.data, 1984 df = this._model.default_state, 1985 i, j, c, e, 1986 tmp = { 1987 id : tid, 1988 text : d.text || '', 1989 icon : d.icon !== undefined ? d.icon : true, 1990 parent : p, 1991 parents : ps, 1992 children : d.children || [], 1993 children_d : d.children_d || [], 1994 data : d.data, 1995 state : { }, 1996 li_attr : { id : false }, 1997 a_attr : { href : '#' }, 1998 original : false 1999 }; 2000 for(i in df) { 2001 if(df.hasOwnProperty(i)) { 2002 tmp.state[i] = df[i]; 2003 } 2004 } 2005 if(d && d.data && d.data.jstree && d.data.jstree.icon) { 2006 tmp.icon = d.data.jstree.icon; 2007 } 2008 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { 2009 tmp.icon = true; 2010 } 2011 if(d && d.data) { 2012 tmp.data = d.data; 2013 if(d.data.jstree) { 2014 for(i in d.data.jstree) { 2015 if(d.data.jstree.hasOwnProperty(i)) { 2016 tmp.state[i] = d.data.jstree[i]; 2017 } 2018 } 2019 } 2020 } 2021 if(d && typeof d.state === 'object') { 2022 for (i in d.state) { 2023 if(d.state.hasOwnProperty(i)) { 2024 tmp.state[i] = d.state[i]; 2025 } 2026 } 2027 } 2028 if(d && typeof d.li_attr === 'object') { 2029 for (i in d.li_attr) { 2030 if(d.li_attr.hasOwnProperty(i)) { 2031 tmp.li_attr[i] = d.li_attr[i]; 2032 } 2033 } 2034 } 2035 if(!tmp.li_attr.id) { 2036 tmp.li_attr.id = tid; 2037 } 2038 if(d && typeof d.a_attr === 'object') { 2039 for (i in d.a_attr) { 2040 if(d.a_attr.hasOwnProperty(i)) { 2041 tmp.a_attr[i] = d.a_attr[i]; 2042 } 2043 } 2044 } 2045 if(d && d.children && d.children === true) { 2046 tmp.state.loaded = false; 2047 tmp.children = []; 2048 tmp.children_d = []; 2049 } 2050 m[tmp.id] = tmp; 2051 for(i = 0, j = tmp.children.length; i < j; i++) { 2052 c = this._parse_model_from_flat_json(m[tmp.children[i]], tmp.id, ps); 2053 e = m[c]; 2054 tmp.children_d.push(c); 2055 if(e.children_d.length) { 2056 tmp.children_d = tmp.children_d.concat(e.children_d); 2057 } 2058 } 2059 delete d.data; 2060 delete d.children; 2061 m[tmp.id].original = d; 2062 if(tmp.state.selected) { 2063 this._data.core.selected.push(tmp.id); 2064 } 2065 return tmp.id; 2066 }, 2067 /** 2068 * parses a node from a JSON object and appends it to the in memory tree model. Used internally. 2069 * @private 2070 * @name _parse_model_from_json(d [, p, ps]) 2071 * @param {Object} d the JSON object to parse 2072 * @param {String} p the parent ID 2073 * @param {Array} ps list of all parents 2074 * @return {String} the ID of the object added to the model 2075 */ 2076 _parse_model_from_json : function (d, p, ps) { 2077 if(!ps) { ps = []; } 2078 else { ps = ps.concat(); } 2079 if(p) { ps.unshift(p); } 2080 var tid = false, i, j, c, e, m = this._model.data, df = this._model.default_state, tmp; 2081 do { 2082 tid = 'j' + this._id + '_' + (++this._cnt); 2083 } while(m[tid]); 2084 2085 tmp = { 2086 id : false, 2087 text : typeof d === 'string' ? d : '', 2088 icon : typeof d === 'object' && d.icon !== undefined ? d.icon : true, 2089 parent : p, 2090 parents : ps, 2091 children : [], 2092 children_d : [], 2093 data : null, 2094 state : { }, 2095 li_attr : { id : false }, 2096 a_attr : { href : '#' }, 2097 original : false 2098 }; 2099 for(i in df) { 2100 if(df.hasOwnProperty(i)) { 2101 tmp.state[i] = df[i]; 2102 } 2103 } 2104 if(d && d.id) { tmp.id = d.id.toString(); } 2105 if(d && d.text) { tmp.text = d.text; } 2106 if(d && d.data && d.data.jstree && d.data.jstree.icon) { 2107 tmp.icon = d.data.jstree.icon; 2108 } 2109 if(tmp.icon === undefined || tmp.icon === null || tmp.icon === "") { 2110 tmp.icon = true; 2111 } 2112 if(d && d.data) { 2113 tmp.data = d.data; 2114 if(d.data.jstree) { 2115 for(i in d.data.jstree) { 2116 if(d.data.jstree.hasOwnProperty(i)) { 2117 tmp.state[i] = d.data.jstree[i]; 2118 } 2119 } 2120 } 2121 } 2122 if(d && typeof d.state === 'object') { 2123 for (i in d.state) { 2124 if(d.state.hasOwnProperty(i)) { 2125 tmp.state[i] = d.state[i]; 2126 } 2127 } 2128 } 2129 if(d && typeof d.li_attr === 'object') { 2130 for (i in d.li_attr) { 2131 if(d.li_attr.hasOwnProperty(i)) { 2132 tmp.li_attr[i] = d.li_attr[i]; 2133 } 2134 } 2135 } 2136 if(tmp.li_attr.id && !tmp.id) { 2137 tmp.id = tmp.li_attr.id.toString(); 2138 } 2139 if(!tmp.id) { 2140 tmp.id = tid; 2141 } 2142 if(!tmp.li_attr.id) { 2143 tmp.li_attr.id = tmp.id; 2144 } 2145 if(d && typeof d.a_attr === 'object') { 2146 for (i in d.a_attr) { 2147 if(d.a_attr.hasOwnProperty(i)) { 2148 tmp.a_attr[i] = d.a_attr[i]; 2149 } 2150 } 2151 } 2152 if(d && d.children && d.children.length) { 2153 for(i = 0, j = d.children.length; i < j; i++) { 2154 c = this._parse_model_from_json(d.children[i], tmp.id, ps); 2155 e = m[c]; 2156 tmp.children.push(c); 2157 if(e.children_d.length) { 2158 tmp.children_d = tmp.children_d.concat(e.children_d); 2159 } 2160 } 2161 tmp.children_d = tmp.children_d.concat(tmp.children); 2162 } 2163 if(d && d.children && d.children === true) { 2164 tmp.state.loaded = false; 2165 tmp.children = []; 2166 tmp.children_d = []; 2167 } 2168 delete d.data; 2169 delete d.children; 2170 tmp.original = d; 2171 m[tmp.id] = tmp; 2172 if(tmp.state.selected) { 2173 this._data.core.selected.push(tmp.id); 2174 } 2175 return tmp.id; 2176 }, 2177 /** 2178 * redraws all nodes that need to be redrawn. Used internally. 2179 * @private 2180 * @name _redraw() 2181 * @trigger redraw.jstree 2182 */ 2183 _redraw : function () { 2184 var nodes = this._model.force_full_redraw ? this._model.data['#'].children.concat([]) : this._model.changed.concat([]), 2185 f = document.createElement('UL'), tmp, i, j, fe = this._data.core.focused; 2186 for(i = 0, j = nodes.length; i < j; i++) { 2187 tmp = this.redraw_node(nodes[i], true, this._model.force_full_redraw); 2188 if(tmp && this._model.force_full_redraw) { 2189 f.appendChild(tmp); 2190 } 2191 } 2192 if(this._model.force_full_redraw) { 2193 f.className = this.get_container_ul()[0].className; 2194 f.setAttribute('role','group'); 2195 this.element.empty().append(f); 2196 //this.get_container_ul()[0].appendChild(f); 2197 } 2198 if(fe !== null) { 2199 tmp = this.get_node(fe, true); 2200 if(tmp && tmp.length && tmp.children('.jstree-anchor')[0] !== document.activeElement) { 2201 tmp.children('.jstree-anchor').focus(); 2202 } 2203 else { 2204 this._data.core.focused = null; 2205 } 2206 } 2207 this._model.force_full_redraw = false; 2208 this._model.changed = []; 2209 /** 2210 * triggered after nodes are redrawn 2211 * @event 2212 * @name redraw.jstree 2213 * @param {array} nodes the redrawn nodes 2214 */ 2215 this.trigger('redraw', { "nodes" : nodes }); 2216 }, 2217 /** 2218 * redraws all nodes that need to be redrawn or optionally - the whole tree 2219 * @name redraw([full]) 2220 * @param {Boolean} full if set to `true` all nodes are redrawn. 2221 */ 2222 redraw : function (full) { 2223 if(full) { 2224 this._model.force_full_redraw = true; 2225 } 2226 //if(this._model.redraw_timeout) { 2227 // clearTimeout(this._model.redraw_timeout); 2228 //} 2229 //this._model.redraw_timeout = setTimeout($.proxy(this._redraw, this),0); 2230 this._redraw(); 2231 }, 2232 /** 2233 * redraws a single node's children. Used internally. 2234 * @private 2235 * @name draw_children(node) 2236 * @param {mixed} node the node whose children will be redrawn 2237 */ 2238 draw_children : function (node) { 2239 var obj = this.get_node(node), 2240 i = false, 2241 j = false, 2242 k = false, 2243 d = document; 2244 if(!obj) { return false; } 2245 if(obj.id === '#') { return this.redraw(true); } 2246 node = this.get_node(node, true); 2247 if(!node || !node.length) { return false; } // TODO: quick toggle 2248 2249 node.children('.jstree-children').remove(); 2250 node = node[0]; 2251 if(obj.children.length && obj.state.loaded) { 2252 k = d.createElement('UL'); 2253 k.setAttribute('role', 'group'); 2254 k.className = 'jstree-children'; 2255 for(i = 0, j = obj.children.length; i < j; i++) { 2256 k.appendChild(this.redraw_node(obj.children[i], true, true)); 2257 } 2258 node.appendChild(k); 2259 } 2260 }, 2261 /** 2262 * redraws a single node. Used internally. 2263 * @private 2264 * @name redraw_node(node, deep, is_callback, force_render) 2265 * @param {mixed} node the node to redraw 2266 * @param {Boolean} deep should child nodes be redrawn too 2267 * @param {Boolean} is_callback is this a recursion call 2268 * @param {Boolean} force_render should children of closed parents be drawn anyway 2269 */ 2270 redraw_node : function (node, deep, is_callback, force_render) { 2271 var obj = this.get_node(node), 2272 par = false, 2273 ind = false, 2274 old = false, 2275 i = false, 2276 j = false, 2277 k = false, 2278 c = '', 2279 d = document, 2280 m = this._model.data, 2281 f = false, 2282 s = false, 2283 tmp = null, 2284 t = 0, 2285 l = 0; 2286 if(!obj) { return false; } 2287 if(obj.id === '#') { return this.redraw(true); } 2288 deep = deep || obj.children.length === 0; 2289 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); 2290 if(!node) { 2291 deep = true; 2292 //node = d.createElement('LI'); 2293 if(!is_callback) { 2294 par = obj.parent !== '#' ? $('#' + obj.parent.replace($.jstree.idregex,'\\$&'), this.element)[0] : null; 2295 if(par !== null && (!par || !m[obj.parent].state.opened)) { 2296 return false; 2297 } 2298 ind = $.inArray(obj.id, par === null ? m['#'].children : m[obj.parent].children); 2299 } 2300 } 2301 else { 2302 node = $(node); 2303 if(!is_callback) { 2304 par = node.parent().parent()[0]; 2305 if(par === this.element[0]) { 2306 par = null; 2307 } 2308 ind = node.index(); 2309 } 2310 // m[obj.id].data = node.data(); // use only node's data, no need to touch jquery storage 2311 if(!deep && obj.children.length && !node.children('.jstree-children').length) { 2312 deep = true; 2313 } 2314 if(!deep) { 2315 old = node.children('.jstree-children')[0]; 2316 } 2317 f = node.children('.jstree-anchor')[0] === document.activeElement; 2318 node.remove(); 2319 //node = d.createElement('LI'); 2320 //node = node[0]; 2321 } 2322 node = _node.cloneNode(true); 2323 // node is DOM, deep is boolean 2324 2325 c = 'jstree-node '; 2326 for(i in obj.li_attr) { 2327 if(obj.li_attr.hasOwnProperty(i)) { 2328 if(i === 'id') { continue; } 2329 if(i !== 'class') { 2330 node.setAttribute(i, obj.li_attr[i]); 2331 } 2332 else { 2333 c += obj.li_attr[i]; 2334 } 2335 } 2336 } 2337 if(!obj.a_attr.id) { 2338 obj.a_attr.id = obj.id + '_anchor'; 2339 } 2340 node.setAttribute('aria-selected', !!obj.state.selected); 2341 node.setAttribute('aria-level', obj.parents.length); 2342 node.setAttribute('aria-labelledby', obj.a_attr.id); 2343 if(obj.state.disabled) { 2344 node.setAttribute('aria-disabled', true); 2345 } 2346 2347 if(obj.state.loaded && !obj.children.length) { 2348 c += ' jstree-leaf'; 2349 } 2350 else { 2351 c += obj.state.opened && obj.state.loaded ? ' jstree-open' : ' jstree-closed'; 2352 node.setAttribute('aria-expanded', (obj.state.opened && obj.state.loaded) ); 2353 } 2354 if(obj.parent !== null && m[obj.parent].children[m[obj.parent].children.length - 1] === obj.id) { 2355 c += ' jstree-last'; 2356 } 2357 node.id = obj.id; 2358 node.className = c; 2359 c = ( obj.state.selected ? ' jstree-clicked' : '') + ( obj.state.disabled ? ' jstree-disabled' : ''); 2360 for(j in obj.a_attr) { 2361 if(obj.a_attr.hasOwnProperty(j)) { 2362 if(j === 'href' && obj.a_attr[j] === '#') { continue; } 2363 if(j !== 'class') { 2364 node.childNodes[1].setAttribute(j, obj.a_attr[j]); 2365 } 2366 else { 2367 c += ' ' + obj.a_attr[j]; 2368 } 2369 } 2370 } 2371 if(c.length) { 2372 node.childNodes[1].className = 'jstree-anchor ' + c; 2373 } 2374 if((obj.icon && obj.icon !== true) || obj.icon === false) { 2375 if(obj.icon === false) { 2376 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-hidden'; 2377 } 2378 else if(obj.icon.indexOf('/') === -1 && obj.icon.indexOf('.') === -1) { 2379 node.childNodes[1].childNodes[0].className += ' ' + obj.icon + ' jstree-themeicon-custom'; 2380 } 2381 else { 2382 node.childNodes[1].childNodes[0].style.backgroundImage = 'url('+obj.icon+')'; 2383 node.childNodes[1].childNodes[0].style.backgroundPosition = 'center center'; 2384 node.childNodes[1].childNodes[0].style.backgroundSize = 'auto'; 2385 node.childNodes[1].childNodes[0].className += ' jstree-themeicon-custom'; 2386 } 2387 } 2388 2389 if(this.settings.core.force_text) { 2390 node.childNodes[1].appendChild(d.createTextNode(obj.text)); 2391 } 2392 else { 2393 node.childNodes[1].innerHTML += obj.text; 2394 } 2395 2396 2397 if(deep && obj.children.length && (obj.state.opened || force_render) && obj.state.loaded) { 2398 k = d.createElement('UL'); 2399 k.setAttribute('role', 'group'); 2400 k.className = 'jstree-children'; 2401 for(i = 0, j = obj.children.length; i < j; i++) { 2402 k.appendChild(this.redraw_node(obj.children[i], deep, true)); 2403 } 2404 node.appendChild(k); 2405 } 2406 if(old) { 2407 node.appendChild(old); 2408 } 2409 if(!is_callback) { 2410 // append back using par / ind 2411 if(!par) { 2412 par = this.element[0]; 2413 } 2414 for(i = 0, j = par.childNodes.length; i < j; i++) { 2415 if(par.childNodes[i] && par.childNodes[i].className && par.childNodes[i].className.indexOf('jstree-children') !== -1) { 2416 tmp = par.childNodes[i]; 2417 break; 2418 } 2419 } 2420 if(!tmp) { 2421 tmp = d.createElement('UL'); 2422 tmp.setAttribute('role', 'group'); 2423 tmp.className = 'jstree-children'; 2424 par.appendChild(tmp); 2425 } 2426 par = tmp; 2427 2428 if(ind < par.childNodes.length) { 2429 par.insertBefore(node, par.childNodes[ind]); 2430 } 2431 else { 2432 par.appendChild(node); 2433 } 2434 if(f) { 2435 t = this.element[0].scrollTop; 2436 l = this.element[0].scrollLeft; 2437 node.childNodes[1].focus(); 2438 this.element[0].scrollTop = t; 2439 this.element[0].scrollLeft = l; 2440 } 2441 } 2442 if(obj.state.opened && !obj.state.loaded) { 2443 obj.state.opened = false; 2444 setTimeout($.proxy(function () { 2445 this.open_node(obj.id, false, 0); 2446 }, this), 0); 2447 } 2448 return node; 2449 }, 2450 /** 2451 * opens a node, revaling its children. If the node is not loaded it will be loaded and opened once ready. 2452 * @name open_node(obj [, callback, animation]) 2453 * @param {mixed} obj the node to open 2454 * @param {Function} callback a function to execute once the node is opened 2455 * @param {Number} animation the animation duration in milliseconds when opening the node (overrides the `core.animation` setting). Use `false` for no animation. 2456 * @trigger open_node.jstree, after_open.jstree, before_open.jstree 2457 */ 2458 open_node : function (obj, callback, animation) { 2459 var t1, t2, d, t; 2460 if($.isArray(obj)) { 2461 obj = obj.slice(); 2462 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2463 this.open_node(obj[t1], callback, animation); 2464 } 2465 return true; 2466 } 2467 obj = this.get_node(obj); 2468 if(!obj || obj.id === '#') { 2469 return false; 2470 } 2471 animation = animation === undefined ? this.settings.core.animation : animation; 2472 if(!this.is_closed(obj)) { 2473 if(callback) { 2474 callback.call(this, obj, false); 2475 } 2476 return false; 2477 } 2478 if(!this.is_loaded(obj)) { 2479 if(this.is_loading(obj)) { 2480 return setTimeout($.proxy(function () { 2481 this.open_node(obj, callback, animation); 2482 }, this), 500); 2483 } 2484 this.load_node(obj, function (o, ok) { 2485 return ok ? this.open_node(o, callback, animation) : (callback ? callback.call(this, o, false) : false); 2486 }); 2487 } 2488 else { 2489 d = this.get_node(obj, true); 2490 t = this; 2491 if(d.length) { 2492 if(animation && d.children(".jstree-children").length) { 2493 d.children(".jstree-children").stop(true, true); 2494 } 2495 if(obj.children.length && !this._firstChild(d.children('.jstree-children')[0])) { 2496 this.draw_children(obj); 2497 //d = this.get_node(obj, true); 2498 } 2499 if(!animation) { 2500 this.trigger('before_open', { "node" : obj }); 2501 d[0].className = d[0].className.replace('jstree-closed', 'jstree-open'); 2502 d[0].setAttribute("aria-expanded", true); 2503 } 2504 else { 2505 this.trigger('before_open', { "node" : obj }); 2506 d 2507 .children(".jstree-children").css("display","none").end() 2508 .removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true) 2509 .children(".jstree-children").stop(true, true) 2510 .slideDown(animation, function () { 2511 this.style.display = ""; 2512 t.trigger("after_open", { "node" : obj }); 2513 }); 2514 } 2515 } 2516 obj.state.opened = true; 2517 if(callback) { 2518 callback.call(this, obj, true); 2519 } 2520 if(!d.length) { 2521 /** 2522 * 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) 2523 * @event 2524 * @name before_open.jstree 2525 * @param {Object} node the opened node 2526 */ 2527 this.trigger('before_open', { "node" : obj }); 2528 } 2529 /** 2530 * triggered when a node is opened (if there is an animation it will not be completed yet) 2531 * @event 2532 * @name open_node.jstree 2533 * @param {Object} node the opened node 2534 */ 2535 this.trigger('open_node', { "node" : obj }); 2536 if(!animation || !d.length) { 2537 /** 2538 * triggered when a node is opened and the animation is complete 2539 * @event 2540 * @name after_open.jstree 2541 * @param {Object} node the opened node 2542 */ 2543 this.trigger("after_open", { "node" : obj }); 2544 } 2545 } 2546 }, 2547 /** 2548 * opens every parent of a node (node should be loaded) 2549 * @name _open_to(obj) 2550 * @param {mixed} obj the node to reveal 2551 * @private 2552 */ 2553 _open_to : function (obj) { 2554 obj = this.get_node(obj); 2555 if(!obj || obj.id === '#') { 2556 return false; 2557 } 2558 var i, j, p = obj.parents; 2559 for(i = 0, j = p.length; i < j; i+=1) { 2560 if(i !== '#') { 2561 this.open_node(p[i], false, 0); 2562 } 2563 } 2564 return $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element); 2565 }, 2566 /** 2567 * closes a node, hiding its children 2568 * @name close_node(obj [, animation]) 2569 * @param {mixed} obj the node to close 2570 * @param {Number} animation the animation duration in milliseconds when closing the node (overrides the `core.animation` setting). Use `false` for no animation. 2571 * @trigger close_node.jstree, after_close.jstree 2572 */ 2573 close_node : function (obj, animation) { 2574 var t1, t2, t, d; 2575 if($.isArray(obj)) { 2576 obj = obj.slice(); 2577 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2578 this.close_node(obj[t1], animation); 2579 } 2580 return true; 2581 } 2582 obj = this.get_node(obj); 2583 if(!obj || obj.id === '#') { 2584 return false; 2585 } 2586 if(this.is_closed(obj)) { 2587 return false; 2588 } 2589 animation = animation === undefined ? this.settings.core.animation : animation; 2590 t = this; 2591 d = this.get_node(obj, true); 2592 if(d.length) { 2593 if(!animation) { 2594 d[0].className = d[0].className.replace('jstree-open', 'jstree-closed'); 2595 d.attr("aria-expanded", false).children('.jstree-children').remove(); 2596 } 2597 else { 2598 d 2599 .children(".jstree-children").attr("style","display:block !important").end() 2600 .removeClass("jstree-open").addClass("jstree-closed").attr("aria-expanded", false) 2601 .children(".jstree-children").stop(true, true).slideUp(animation, function () { 2602 this.style.display = ""; 2603 d.children('.jstree-children').remove(); 2604 t.trigger("after_close", { "node" : obj }); 2605 }); 2606 } 2607 } 2608 obj.state.opened = false; 2609 /** 2610 * triggered when a node is closed (if there is an animation it will not be complete yet) 2611 * @event 2612 * @name close_node.jstree 2613 * @param {Object} node the closed node 2614 */ 2615 this.trigger('close_node',{ "node" : obj }); 2616 if(!animation || !d.length) { 2617 /** 2618 * triggered when a node is closed and the animation is complete 2619 * @event 2620 * @name after_close.jstree 2621 * @param {Object} node the closed node 2622 */ 2623 this.trigger("after_close", { "node" : obj }); 2624 } 2625 }, 2626 /** 2627 * toggles a node - closing it if it is open, opening it if it is closed 2628 * @name toggle_node(obj) 2629 * @param {mixed} obj the node to toggle 2630 */ 2631 toggle_node : function (obj) { 2632 var t1, t2; 2633 if($.isArray(obj)) { 2634 obj = obj.slice(); 2635 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2636 this.toggle_node(obj[t1]); 2637 } 2638 return true; 2639 } 2640 if(this.is_closed(obj)) { 2641 return this.open_node(obj); 2642 } 2643 if(this.is_open(obj)) { 2644 return this.close_node(obj); 2645 } 2646 }, 2647 /** 2648 * 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. 2649 * @name open_all([obj, animation, original_obj]) 2650 * @param {mixed} obj the node to open recursively, omit to open all nodes in the tree 2651 * @param {Number} animation the animation duration in milliseconds when opening the nodes, the default is no animation 2652 * @param {jQuery} reference to the node that started the process (internal use) 2653 * @trigger open_all.jstree 2654 */ 2655 open_all : function (obj, animation, original_obj) { 2656 if(!obj) { obj = '#'; } 2657 obj = this.get_node(obj); 2658 if(!obj) { return false; } 2659 var dom = obj.id === '#' ? this.get_container_ul() : this.get_node(obj, true), i, j, _this; 2660 if(!dom.length) { 2661 for(i = 0, j = obj.children_d.length; i < j; i++) { 2662 if(this.is_closed(this._model.data[obj.children_d[i]])) { 2663 this._model.data[obj.children_d[i]].state.opened = true; 2664 } 2665 } 2666 return this.trigger('open_all', { "node" : obj }); 2667 } 2668 original_obj = original_obj || dom; 2669 _this = this; 2670 dom = this.is_closed(obj) ? dom.find('.jstree-closed').addBack() : dom.find('.jstree-closed'); 2671 dom.each(function () { 2672 _this.open_node( 2673 this, 2674 function(node, status) { if(status && this.is_parent(node)) { this.open_all(node, animation, original_obj); } }, 2675 animation || 0 2676 ); 2677 }); 2678 if(original_obj.find('.jstree-closed').length === 0) { 2679 /** 2680 * triggered when an `open_all` call completes 2681 * @event 2682 * @name open_all.jstree 2683 * @param {Object} node the opened node 2684 */ 2685 this.trigger('open_all', { "node" : this.get_node(original_obj) }); 2686 } 2687 }, 2688 /** 2689 * closes all nodes within a node (or the tree), revaling their children 2690 * @name close_all([obj, animation]) 2691 * @param {mixed} obj the node to close recursively, omit to close all nodes in the tree 2692 * @param {Number} animation the animation duration in milliseconds when closing the nodes, the default is no animation 2693 * @trigger close_all.jstree 2694 */ 2695 close_all : function (obj, animation) { 2696 if(!obj) { obj = '#'; } 2697 obj = this.get_node(obj); 2698 if(!obj) { return false; } 2699 var dom = obj.id === '#' ? this.get_container_ul() : this.get_node(obj, true), 2700 _this = this, i, j; 2701 if(!dom.length) { 2702 for(i = 0, j = obj.children_d.length; i < j; i++) { 2703 this._model.data[obj.children_d[i]].state.opened = false; 2704 } 2705 return this.trigger('close_all', { "node" : obj }); 2706 } 2707 dom = this.is_open(obj) ? dom.find('.jstree-open').addBack() : dom.find('.jstree-open'); 2708 $(dom.get().reverse()).each(function () { _this.close_node(this, animation || 0); }); 2709 /** 2710 * triggered when an `close_all` call completes 2711 * @event 2712 * @name close_all.jstree 2713 * @param {Object} node the closed node 2714 */ 2715 this.trigger('close_all', { "node" : obj }); 2716 }, 2717 /** 2718 * checks if a node is disabled (not selectable) 2719 * @name is_disabled(obj) 2720 * @param {mixed} obj 2721 * @return {Boolean} 2722 */ 2723 is_disabled : function (obj) { 2724 obj = this.get_node(obj); 2725 return obj && obj.state && obj.state.disabled; 2726 }, 2727 /** 2728 * enables a node - so that it can be selected 2729 * @name enable_node(obj) 2730 * @param {mixed} obj the node to enable 2731 * @trigger enable_node.jstree 2732 */ 2733 enable_node : function (obj) { 2734 var t1, t2; 2735 if($.isArray(obj)) { 2736 obj = obj.slice(); 2737 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2738 this.enable_node(obj[t1]); 2739 } 2740 return true; 2741 } 2742 obj = this.get_node(obj); 2743 if(!obj || obj.id === '#') { 2744 return false; 2745 } 2746 obj.state.disabled = false; 2747 this.get_node(obj,true).children('.jstree-anchor').removeClass('jstree-disabled').attr('aria-disabled', false); 2748 /** 2749 * triggered when an node is enabled 2750 * @event 2751 * @name enable_node.jstree 2752 * @param {Object} node the enabled node 2753 */ 2754 this.trigger('enable_node', { 'node' : obj }); 2755 }, 2756 /** 2757 * disables a node - so that it can not be selected 2758 * @name disable_node(obj) 2759 * @param {mixed} obj the node to disable 2760 * @trigger disable_node.jstree 2761 */ 2762 disable_node : function (obj) { 2763 var t1, t2; 2764 if($.isArray(obj)) { 2765 obj = obj.slice(); 2766 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2767 this.disable_node(obj[t1]); 2768 } 2769 return true; 2770 } 2771 obj = this.get_node(obj); 2772 if(!obj || obj.id === '#') { 2773 return false; 2774 } 2775 obj.state.disabled = true; 2776 this.get_node(obj,true).children('.jstree-anchor').addClass('jstree-disabled').attr('aria-disabled', true); 2777 /** 2778 * triggered when an node is disabled 2779 * @event 2780 * @name disable_node.jstree 2781 * @param {Object} node the disabled node 2782 */ 2783 this.trigger('disable_node', { 'node' : obj }); 2784 }, 2785 /** 2786 * called when a node is selected by the user. Used internally. 2787 * @private 2788 * @name activate_node(obj, e) 2789 * @param {mixed} obj the node 2790 * @param {Object} e the related event 2791 * @trigger activate_node.jstree, changed.jstree 2792 */ 2793 activate_node : function (obj, e) { 2794 if(this.is_disabled(obj)) { 2795 return false; 2796 } 2797 2798 // 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 2799 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; 2800 if(this._data.core.last_clicked && !this._data.core.last_clicked.state.selected) { this._data.core.last_clicked = null; } 2801 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]); } 2802 2803 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 ) )) { 2804 if(!this.settings.core.multiple && (e.metaKey || e.ctrlKey || e.shiftKey) && this.is_selected(obj)) { 2805 this.deselect_node(obj, false, e); 2806 } 2807 else { 2808 this.deselect_all(true); 2809 this.select_node(obj, false, false, e); 2810 this._data.core.last_clicked = this.get_node(obj); 2811 } 2812 } 2813 else { 2814 if(e.shiftKey) { 2815 var o = this.get_node(obj).id, 2816 l = this._data.core.last_clicked.id, 2817 p = this.get_node(this._data.core.last_clicked.parent).children, 2818 c = false, 2819 i, j; 2820 for(i = 0, j = p.length; i < j; i += 1) { 2821 // separate IFs work whem o and l are the same 2822 if(p[i] === o) { 2823 c = !c; 2824 } 2825 if(p[i] === l) { 2826 c = !c; 2827 } 2828 if(c || p[i] === o || p[i] === l) { 2829 this.select_node(p[i], true, false, e); 2830 } 2831 else { 2832 this.deselect_node(p[i], true, e); 2833 } 2834 } 2835 this.trigger('changed', { 'action' : 'select_node', 'node' : this.get_node(obj), 'selected' : this._data.core.selected, 'event' : e }); 2836 } 2837 else { 2838 if(!this.is_selected(obj)) { 2839 this.select_node(obj, false, false, e); 2840 } 2841 else { 2842 this.deselect_node(obj, false, e); 2843 } 2844 } 2845 } 2846 /** 2847 * triggered when an node is clicked or intercated with by the user 2848 * @event 2849 * @name activate_node.jstree 2850 * @param {Object} node 2851 */ 2852 this.trigger('activate_node', { 'node' : this.get_node(obj) }); 2853 }, 2854 /** 2855 * applies the hover state on a node, called when a node is hovered by the user. Used internally. 2856 * @private 2857 * @name hover_node(obj) 2858 * @param {mixed} obj 2859 * @trigger hover_node.jstree 2860 */ 2861 hover_node : function (obj) { 2862 obj = this.get_node(obj, true); 2863 if(!obj || !obj.length || obj.children('.jstree-hovered').length) { 2864 return false; 2865 } 2866 var o = this.element.find('.jstree-hovered'), t = this.element; 2867 if(o && o.length) { this.dehover_node(o); } 2868 2869 obj.children('.jstree-anchor').addClass('jstree-hovered'); 2870 /** 2871 * triggered when an node is hovered 2872 * @event 2873 * @name hover_node.jstree 2874 * @param {Object} node 2875 */ 2876 this.trigger('hover_node', { 'node' : this.get_node(obj) }); 2877 setTimeout(function () { t.attr('aria-activedescendant', obj[0].id); }, 0); 2878 }, 2879 /** 2880 * removes the hover state from a nodecalled when a node is no longer hovered by the user. Used internally. 2881 * @private 2882 * @name dehover_node(obj) 2883 * @param {mixed} obj 2884 * @trigger dehover_node.jstree 2885 */ 2886 dehover_node : function (obj) { 2887 obj = this.get_node(obj, true); 2888 if(!obj || !obj.length || !obj.children('.jstree-hovered').length) { 2889 return false; 2890 } 2891 obj.children('.jstree-anchor').removeClass('jstree-hovered'); 2892 /** 2893 * triggered when an node is no longer hovered 2894 * @event 2895 * @name dehover_node.jstree 2896 * @param {Object} node 2897 */ 2898 this.trigger('dehover_node', { 'node' : this.get_node(obj) }); 2899 }, 2900 /** 2901 * select a node 2902 * @name select_node(obj [, supress_event, prevent_open]) 2903 * @param {mixed} obj an array can be used to select multiple nodes 2904 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered 2905 * @param {Boolean} prevent_open if set to `true` parents of the selected node won't be opened 2906 * @trigger select_node.jstree, changed.jstree 2907 */ 2908 select_node : function (obj, supress_event, prevent_open, e) { 2909 var dom, t1, t2, th; 2910 if($.isArray(obj)) { 2911 obj = obj.slice(); 2912 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2913 this.select_node(obj[t1], supress_event, prevent_open, e); 2914 } 2915 return true; 2916 } 2917 obj = this.get_node(obj); 2918 if(!obj || obj.id === '#') { 2919 return false; 2920 } 2921 dom = this.get_node(obj, true); 2922 if(!obj.state.selected) { 2923 obj.state.selected = true; 2924 this._data.core.selected.push(obj.id); 2925 if(!prevent_open) { 2926 dom = this._open_to(obj); 2927 } 2928 if(dom && dom.length) { 2929 dom.attr('aria-selected', true).children('.jstree-anchor').addClass('jstree-clicked'); 2930 } 2931 /** 2932 * triggered when an node is selected 2933 * @event 2934 * @name select_node.jstree 2935 * @param {Object} node 2936 * @param {Array} selected the current selection 2937 * @param {Object} event the event (if any) that triggered this select_node 2938 */ 2939 this.trigger('select_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); 2940 if(!supress_event) { 2941 /** 2942 * triggered when selection changes 2943 * @event 2944 * @name changed.jstree 2945 * @param {Object} node 2946 * @param {Object} action the action that caused the selection to change 2947 * @param {Array} selected the current selection 2948 * @param {Object} event the event (if any) that triggered this changed event 2949 */ 2950 this.trigger('changed', { 'action' : 'select_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); 2951 } 2952 } 2953 }, 2954 /** 2955 * deselect a node 2956 * @name deselect_node(obj [, supress_event]) 2957 * @param {mixed} obj an array can be used to deselect multiple nodes 2958 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered 2959 * @trigger deselect_node.jstree, changed.jstree 2960 */ 2961 deselect_node : function (obj, supress_event, e) { 2962 var t1, t2, dom; 2963 if($.isArray(obj)) { 2964 obj = obj.slice(); 2965 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 2966 this.deselect_node(obj[t1], supress_event, e); 2967 } 2968 return true; 2969 } 2970 obj = this.get_node(obj); 2971 if(!obj || obj.id === '#') { 2972 return false; 2973 } 2974 dom = this.get_node(obj, true); 2975 if(obj.state.selected) { 2976 obj.state.selected = false; 2977 this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.id); 2978 if(dom.length) { 2979 dom.attr('aria-selected', false).children('.jstree-anchor').removeClass('jstree-clicked'); 2980 } 2981 /** 2982 * triggered when an node is deselected 2983 * @event 2984 * @name deselect_node.jstree 2985 * @param {Object} node 2986 * @param {Array} selected the current selection 2987 * @param {Object} event the event (if any) that triggered this deselect_node 2988 */ 2989 this.trigger('deselect_node', { 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); 2990 if(!supress_event) { 2991 this.trigger('changed', { 'action' : 'deselect_node', 'node' : obj, 'selected' : this._data.core.selected, 'event' : e }); 2992 } 2993 } 2994 }, 2995 /** 2996 * select all nodes in the tree 2997 * @name select_all([supress_event]) 2998 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered 2999 * @trigger select_all.jstree, changed.jstree 3000 */ 3001 select_all : function (supress_event) { 3002 var tmp = this._data.core.selected.concat([]), i, j; 3003 this._data.core.selected = this._model.data['#'].children_d.concat(); 3004 for(i = 0, j = this._data.core.selected.length; i < j; i++) { 3005 if(this._model.data[this._data.core.selected[i]]) { 3006 this._model.data[this._data.core.selected[i]].state.selected = true; 3007 } 3008 } 3009 this.redraw(true); 3010 /** 3011 * triggered when all nodes are selected 3012 * @event 3013 * @name select_all.jstree 3014 * @param {Array} selected the current selection 3015 */ 3016 this.trigger('select_all', { 'selected' : this._data.core.selected }); 3017 if(!supress_event) { 3018 this.trigger('changed', { 'action' : 'select_all', 'selected' : this._data.core.selected, 'old_selection' : tmp }); 3019 } 3020 }, 3021 /** 3022 * deselect all selected nodes 3023 * @name deselect_all([supress_event]) 3024 * @param {Boolean} supress_event if set to `true` the `changed.jstree` event won't be triggered 3025 * @trigger deselect_all.jstree, changed.jstree 3026 */ 3027 deselect_all : function (supress_event) { 3028 var tmp = this._data.core.selected.concat([]), i, j; 3029 for(i = 0, j = this._data.core.selected.length; i < j; i++) { 3030 if(this._model.data[this._data.core.selected[i]]) { 3031 this._model.data[this._data.core.selected[i]].state.selected = false; 3032 } 3033 } 3034 this._data.core.selected = []; 3035 this.element.find('.jstree-clicked').removeClass('jstree-clicked').parent().attr('aria-selected', false); 3036 /** 3037 * triggered when all nodes are deselected 3038 * @event 3039 * @name deselect_all.jstree 3040 * @param {Object} node the previous selection 3041 * @param {Array} selected the current selection 3042 */ 3043 this.trigger('deselect_all', { 'selected' : this._data.core.selected, 'node' : tmp }); 3044 if(!supress_event) { 3045 this.trigger('changed', { 'action' : 'deselect_all', 'selected' : this._data.core.selected, 'old_selection' : tmp }); 3046 } 3047 }, 3048 /** 3049 * checks if a node is selected 3050 * @name is_selected(obj) 3051 * @param {mixed} obj 3052 * @return {Boolean} 3053 */ 3054 is_selected : function (obj) { 3055 obj = this.get_node(obj); 3056 if(!obj || obj.id === '#') { 3057 return false; 3058 } 3059 return obj.state.selected; 3060 }, 3061 /** 3062 * get an array of all selected nodes 3063 * @name get_selected([full]) 3064 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned 3065 * @return {Array} 3066 */ 3067 get_selected : function (full) { 3068 return full ? $.map(this._data.core.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.core.selected.slice(); 3069 }, 3070 /** 3071 * get an array of all top level selected nodes (ignoring children of selected nodes) 3072 * @name get_top_selected([full]) 3073 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned 3074 * @return {Array} 3075 */ 3076 get_top_selected : function (full) { 3077 var tmp = this.get_selected(true), 3078 obj = {}, i, j, k, l; 3079 for(i = 0, j = tmp.length; i < j; i++) { 3080 obj[tmp[i].id] = tmp[i]; 3081 } 3082 for(i = 0, j = tmp.length; i < j; i++) { 3083 for(k = 0, l = tmp[i].children_d.length; k < l; k++) { 3084 if(obj[tmp[i].children_d[k]]) { 3085 delete obj[tmp[i].children_d[k]]; 3086 } 3087 } 3088 } 3089 tmp = []; 3090 for(i in obj) { 3091 if(obj.hasOwnProperty(i)) { 3092 tmp.push(i); 3093 } 3094 } 3095 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp; 3096 }, 3097 /** 3098 * get an array of all bottom level selected nodes (ignoring selected parents) 3099 * @name get_bottom_selected([full]) 3100 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned 3101 * @return {Array} 3102 */ 3103 get_bottom_selected : function (full) { 3104 var tmp = this.get_selected(true), 3105 obj = [], i, j; 3106 for(i = 0, j = tmp.length; i < j; i++) { 3107 if(!tmp[i].children.length) { 3108 obj.push(tmp[i].id); 3109 } 3110 } 3111 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj; 3112 }, 3113 /** 3114 * gets the current state of the tree so that it can be restored later with `set_state(state)`. Used internally. 3115 * @name get_state() 3116 * @private 3117 * @return {Object} 3118 */ 3119 get_state : function () { 3120 var state = { 3121 'core' : { 3122 'open' : [], 3123 'scroll' : { 3124 'left' : this.element.scrollLeft(), 3125 'top' : this.element.scrollTop() 3126 }, 3127 /*! 3128 'themes' : { 3129 'name' : this.get_theme(), 3130 'icons' : this._data.core.themes.icons, 3131 'dots' : this._data.core.themes.dots 3132 }, 3133 */ 3134 'selected' : [] 3135 } 3136 }, i; 3137 for(i in this._model.data) { 3138 if(this._model.data.hasOwnProperty(i)) { 3139 if(i !== '#') { 3140 if(this._model.data[i].state.opened) { 3141 state.core.open.push(i); 3142 } 3143 if(this._model.data[i].state.selected) { 3144 state.core.selected.push(i); 3145 } 3146 } 3147 } 3148 } 3149 return state; 3150 }, 3151 /** 3152 * sets the state of the tree. Used internally. 3153 * @name set_state(state [, callback]) 3154 * @private 3155 * @param {Object} state the state to restore 3156 * @param {Function} callback an optional function to execute once the state is restored. 3157 * @trigger set_state.jstree 3158 */ 3159 set_state : function (state, callback) { 3160 if(state) { 3161 if(state.core) { 3162 var res, n, t, _this, i; 3163 if(state.core.open) { 3164 if(!$.isArray(state.core.open) || !state.core.open.length) { 3165 delete state.core.open; 3166 this.set_state(state, callback); 3167 } 3168 else { 3169 this._load_nodes(state.core.open, function (nodes) { 3170 this.open_node(nodes, false, 0); 3171 delete state.core.open; 3172 this.set_state(state, callback); 3173 }, true); 3174 } 3175 return false; 3176 } 3177 if(state.core.scroll) { 3178 if(state.core.scroll && state.core.scroll.left !== undefined) { 3179 this.element.scrollLeft(state.core.scroll.left); 3180 } 3181 if(state.core.scroll && state.core.scroll.top !== undefined) { 3182 this.element.scrollTop(state.core.scroll.top); 3183 } 3184 delete state.core.scroll; 3185 this.set_state(state, callback); 3186 return false; 3187 } 3188 if(state.core.selected) { 3189 _this = this; 3190 this.deselect_all(); 3191 $.each(state.core.selected, function (i, v) { 3192 _this.select_node(v, false, true); 3193 }); 3194 delete state.core.selected; 3195 this.set_state(state, callback); 3196 return false; 3197 } 3198 for(i in state) { 3199 if(state.hasOwnProperty(i) && i !== "core" && $.inArray(i, this.settings.plugins) === -1) { 3200 delete state[i]; 3201 } 3202 } 3203 if($.isEmptyObject(state.core)) { 3204 delete state.core; 3205 this.set_state(state, callback); 3206 return false; 3207 } 3208 } 3209 if($.isEmptyObject(state)) { 3210 state = null; 3211 if(callback) { callback.call(this); } 3212 /** 3213 * triggered when a `set_state` call completes 3214 * @event 3215 * @name set_state.jstree 3216 */ 3217 this.trigger('set_state'); 3218 return false; 3219 } 3220 return true; 3221 } 3222 return false; 3223 }, 3224 /** 3225 * refreshes the tree - all nodes are reloaded with calls to `load_node`. 3226 * @name refresh() 3227 * @param {Boolean} skip_loading an option to skip showing the loading indicator 3228 * @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 3229 * @trigger refresh.jstree 3230 */ 3231 refresh : function (skip_loading, forget_state) { 3232 this._data.core.state = forget_state === true ? {} : this.get_state(); 3233 if(forget_state && $.isFunction(forget_state)) { this._data.core.state = forget_state.call(this, this._data.core.state); } 3234 this._cnt = 0; 3235 this._model.data = { 3236 '#' : { 3237 id : '#', 3238 parent : null, 3239 parents : [], 3240 children : [], 3241 children_d : [], 3242 state : { loaded : false } 3243 } 3244 }; 3245 var c = this.get_container_ul()[0].className; 3246 if(!skip_loading) { 3247 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>"); 3248 this.element.attr('aria-activedescendant','j'+this._id+'_loading'); 3249 } 3250 this.load_node('#', function (o, s) { 3251 if(s) { 3252 this.get_container_ul()[0].className = c; 3253 if(this._firstChild(this.get_container_ul()[0])) { 3254 this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id); 3255 } 3256 this.set_state($.extend(true, {}, this._data.core.state), function () { 3257 /** 3258 * triggered when a `refresh` call completes 3259 * @event 3260 * @name refresh.jstree 3261 */ 3262 this.trigger('refresh'); 3263 }); 3264 } 3265 this._data.core.state = null; 3266 }); 3267 }, 3268 /** 3269 * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`. 3270 * @name refresh_node(obj) 3271 * @param {mixed} obj the node 3272 * @trigger refresh_node.jstree 3273 */ 3274 refresh_node : function (obj) { 3275 obj = this.get_node(obj); 3276 if(!obj || obj.id === '#') { return false; } 3277 var opened = [], to_load = [], s = this._data.core.selected.concat([]); 3278 to_load.push(obj.id); 3279 if(obj.state.opened === true) { opened.push(obj.id); } 3280 this.get_node(obj, true).find('.jstree-open').each(function() { opened.push(this.id); }); 3281 this._load_nodes(to_load, $.proxy(function (nodes) { 3282 this.open_node(opened, false, 0); 3283 this.select_node(this._data.core.selected); 3284 /** 3285 * triggered when a node is refreshed 3286 * @event 3287 * @name refresh_node.jstree 3288 * @param {Object} node - the refreshed node 3289 * @param {Array} nodes - an array of the IDs of the nodes that were reloaded 3290 */ 3291 this.trigger('refresh_node', { 'node' : obj, 'nodes' : nodes }); 3292 }, this)); 3293 }, 3294 /** 3295 * set (change) the ID of a node 3296 * @name set_id(obj, id) 3297 * @param {mixed} obj the node 3298 * @param {String} id the new ID 3299 * @return {Boolean} 3300 */ 3301 set_id : function (obj, id) { 3302 obj = this.get_node(obj); 3303 if(!obj || obj.id === '#') { return false; } 3304 var i, j, m = this._model.data; 3305 id = id.toString(); 3306 // update parents (replace current ID with new one in children and children_d) 3307 m[obj.parent].children[$.inArray(obj.id, m[obj.parent].children)] = id; 3308 for(i = 0, j = obj.parents.length; i < j; i++) { 3309 m[obj.parents[i]].children_d[$.inArray(obj.id, m[obj.parents[i]].children_d)] = id; 3310 } 3311 // update children (replace current ID with new one in parent and parents) 3312 for(i = 0, j = obj.children.length; i < j; i++) { 3313 m[obj.children[i]].parent = id; 3314 } 3315 for(i = 0, j = obj.children_d.length; i < j; i++) { 3316 m[obj.children_d[i]].parents[$.inArray(obj.id, m[obj.children_d[i]].parents)] = id; 3317 } 3318 i = $.inArray(obj.id, this._data.core.selected); 3319 if(i !== -1) { this._data.core.selected[i] = id; } 3320 // update model and obj itself (obj.id, this._model.data[KEY]) 3321 i = this.get_node(obj.id, true); 3322 if(i) { 3323 i.attr('id', id).children('.jstree-anchor').attr('id', id + '_anchor').end().attr('aria-labelledby', id + '_anchor'); 3324 if(this.element.attr('aria-activedescendant') === obj.id) { 3325 this.element.attr('aria-activedescendant', id); 3326 } 3327 } 3328 delete m[obj.id]; 3329 obj.id = id; 3330 obj.li_attr.id = id; 3331 m[id] = obj; 3332 return true; 3333 }, 3334 /** 3335 * get the text value of a node 3336 * @name get_text(obj) 3337 * @param {mixed} obj the node 3338 * @return {String} 3339 */ 3340 get_text : function (obj) { 3341 obj = this.get_node(obj); 3342 return (!obj || obj.id === '#') ? false : obj.text; 3343 }, 3344 /** 3345 * set the text value of a node. Used internally, please use `rename_node(obj, val)`. 3346 * @private 3347 * @name set_text(obj, val) 3348 * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes 3349 * @param {String} val the new text value 3350 * @return {Boolean} 3351 * @trigger set_text.jstree 3352 */ 3353 set_text : function (obj, val) { 3354 var t1, t2; 3355 if($.isArray(obj)) { 3356 obj = obj.slice(); 3357 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 3358 this.set_text(obj[t1], val); 3359 } 3360 return true; 3361 } 3362 obj = this.get_node(obj); 3363 if(!obj || obj.id === '#') { return false; } 3364 obj.text = val; 3365 if(this.get_node(obj, true).length) { 3366 this.redraw_node(obj.id); 3367 } 3368 /** 3369 * triggered when a node text value is changed 3370 * @event 3371 * @name set_text.jstree 3372 * @param {Object} obj 3373 * @param {String} text the new value 3374 */ 3375 this.trigger('set_text',{ "obj" : obj, "text" : val }); 3376 return true; 3377 }, 3378 /** 3379 * gets a JSON representation of a node (or the whole tree) 3380 * @name get_json([obj, options]) 3381 * @param {mixed} obj 3382 * @param {Object} options 3383 * @param {Boolean} options.no_state do not return state information 3384 * @param {Boolean} options.no_id do not return ID 3385 * @param {Boolean} options.no_children do not include children 3386 * @param {Boolean} options.no_data do not include node data 3387 * @param {Boolean} options.flat return flat JSON instead of nested 3388 * @return {Object} 3389 */ 3390 get_json : function (obj, options, flat) { 3391 obj = this.get_node(obj || '#'); 3392 if(!obj) { return false; } 3393 if(options && options.flat && !flat) { flat = []; } 3394 var tmp = { 3395 'id' : obj.id, 3396 'text' : obj.text, 3397 'icon' : this.get_icon(obj), 3398 'li_attr' : $.extend(true, {}, obj.li_attr), 3399 'a_attr' : $.extend(true, {}, obj.a_attr), 3400 'state' : {}, 3401 'data' : options && options.no_data ? false : $.extend(true, {}, obj.data) 3402 //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ), 3403 }, i, j; 3404 if(options && options.flat) { 3405 tmp.parent = obj.parent; 3406 } 3407 else { 3408 tmp.children = []; 3409 } 3410 if(!options || !options.no_state) { 3411 for(i in obj.state) { 3412 if(obj.state.hasOwnProperty(i)) { 3413 tmp.state[i] = obj.state[i]; 3414 } 3415 } 3416 } 3417 if(options && options.no_id) { 3418 delete tmp.id; 3419 if(tmp.li_attr && tmp.li_attr.id) { 3420 delete tmp.li_attr.id; 3421 } 3422 if(tmp.a_attr && tmp.a_attr.id) { 3423 delete tmp.a_attr.id; 3424 } 3425 } 3426 if(options && options.flat && obj.id !== '#') { 3427 flat.push(tmp); 3428 } 3429 if(!options || !options.no_children) { 3430 for(i = 0, j = obj.children.length; i < j; i++) { 3431 if(options && options.flat) { 3432 this.get_json(obj.children[i], options, flat); 3433 } 3434 else { 3435 tmp.children.push(this.get_json(obj.children[i], options)); 3436 } 3437 } 3438 } 3439 return options && options.flat ? flat : (obj.id === '#' ? tmp.children : tmp); 3440 }, 3441 /** 3442 * create a new node (do not confuse with load_node) 3443 * @name create_node([obj, node, pos, callback, is_loaded]) 3444 * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`) 3445 * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name) 3446 * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last" 3447 * @param {Function} callback a function to be called once the node is created 3448 * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded 3449 * @return {String} the ID of the newly create node 3450 * @trigger model.jstree, create_node.jstree 3451 */ 3452 create_node : function (par, node, pos, callback, is_loaded) { 3453 if(par === null) { par = "#"; } 3454 par = this.get_node(par); 3455 if(!par) { return false; } 3456 pos = pos === undefined ? "last" : pos; 3457 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { 3458 return this.load_node(par, function () { this.create_node(par, node, pos, callback, true); }); 3459 } 3460 if(!node) { node = { "text" : this.get_string('New node') }; } 3461 if(typeof node === "string") { node = { "text" : node }; } 3462 if(node.text === undefined) { node.text = this.get_string('New node'); } 3463 var tmp, dpc, i, j; 3464 3465 if(par.id === '#') { 3466 if(pos === "before") { pos = "first"; } 3467 if(pos === "after") { pos = "last"; } 3468 } 3469 switch(pos) { 3470 case "before": 3471 tmp = this.get_node(par.parent); 3472 pos = $.inArray(par.id, tmp.children); 3473 par = tmp; 3474 break; 3475 case "after" : 3476 tmp = this.get_node(par.parent); 3477 pos = $.inArray(par.id, tmp.children) + 1; 3478 par = tmp; 3479 break; 3480 case "inside": 3481 case "first": 3482 pos = 0; 3483 break; 3484 case "last": 3485 pos = par.children.length; 3486 break; 3487 default: 3488 if(!pos) { pos = 0; } 3489 break; 3490 } 3491 if(pos > par.children.length) { pos = par.children.length; } 3492 if(!node.id) { node.id = true; } 3493 if(!this.check("create_node", node, par, pos)) { 3494 this.settings.core.error.call(this, this._data.core.last_error); 3495 return false; 3496 } 3497 if(node.id === true) { delete node.id; } 3498 node = this._parse_model_from_json(node, par.id, par.parents.concat()); 3499 if(!node) { return false; } 3500 tmp = this.get_node(node); 3501 dpc = []; 3502 dpc.push(node); 3503 dpc = dpc.concat(tmp.children_d); 3504 this.trigger('model', { "nodes" : dpc, "parent" : par.id }); 3505 3506 par.children_d = par.children_d.concat(dpc); 3507 for(i = 0, j = par.parents.length; i < j; i++) { 3508 this._model.data[par.parents[i]].children_d = this._model.data[par.parents[i]].children_d.concat(dpc); 3509 } 3510 node = tmp; 3511 tmp = []; 3512 for(i = 0, j = par.children.length; i < j; i++) { 3513 tmp[i >= pos ? i+1 : i] = par.children[i]; 3514 } 3515 tmp[pos] = node.id; 3516 par.children = tmp; 3517 3518 this.redraw_node(par, true); 3519 if(callback) { callback.call(this, this.get_node(node)); } 3520 /** 3521 * triggered when a node is created 3522 * @event 3523 * @name create_node.jstree 3524 * @param {Object} node 3525 * @param {String} parent the parent's ID 3526 * @param {Number} position the position of the new node among the parent's children 3527 */ 3528 this.trigger('create_node', { "node" : this.get_node(node), "parent" : par.id, "position" : pos }); 3529 return node.id; 3530 }, 3531 /** 3532 * set the text value of a node 3533 * @name rename_node(obj, val) 3534 * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name 3535 * @param {String} val the new text value 3536 * @return {Boolean} 3537 * @trigger rename_node.jstree 3538 */ 3539 rename_node : function (obj, val) { 3540 var t1, t2, old; 3541 if($.isArray(obj)) { 3542 obj = obj.slice(); 3543 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 3544 this.rename_node(obj[t1], val); 3545 } 3546 return true; 3547 } 3548 obj = this.get_node(obj); 3549 if(!obj || obj.id === '#') { return false; } 3550 old = obj.text; 3551 if(!this.check("rename_node", obj, this.get_parent(obj), val)) { 3552 this.settings.core.error.call(this, this._data.core.last_error); 3553 return false; 3554 } 3555 this.set_text(obj, val); // .apply(this, Array.prototype.slice.call(arguments)) 3556 /** 3557 * triggered when a node is renamed 3558 * @event 3559 * @name rename_node.jstree 3560 * @param {Object} node 3561 * @param {String} text the new value 3562 * @param {String} old the old value 3563 */ 3564 this.trigger('rename_node', { "node" : obj, "text" : val, "old" : old }); 3565 return true; 3566 }, 3567 /** 3568 * remove a node 3569 * @name delete_node(obj) 3570 * @param {mixed} obj the node, you can pass an array to delete multiple nodes 3571 * @return {Boolean} 3572 * @trigger delete_node.jstree, changed.jstree 3573 */ 3574 delete_node : function (obj) { 3575 var t1, t2, par, pos, tmp, i, j, k, l, c; 3576 if($.isArray(obj)) { 3577 obj = obj.slice(); 3578 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 3579 this.delete_node(obj[t1]); 3580 } 3581 return true; 3582 } 3583 obj = this.get_node(obj); 3584 if(!obj || obj.id === '#') { return false; } 3585 par = this.get_node(obj.parent); 3586 pos = $.inArray(obj.id, par.children); 3587 c = false; 3588 if(!this.check("delete_node", obj, par, pos)) { 3589 this.settings.core.error.call(this, this._data.core.last_error); 3590 return false; 3591 } 3592 if(pos !== -1) { 3593 par.children = $.vakata.array_remove(par.children, pos); 3594 } 3595 tmp = obj.children_d.concat([]); 3596 tmp.push(obj.id); 3597 for(k = 0, l = tmp.length; k < l; k++) { 3598 for(i = 0, j = obj.parents.length; i < j; i++) { 3599 pos = $.inArray(tmp[k], this._model.data[obj.parents[i]].children_d); 3600 if(pos !== -1) { 3601 this._model.data[obj.parents[i]].children_d = $.vakata.array_remove(this._model.data[obj.parents[i]].children_d, pos); 3602 } 3603 } 3604 if(this._model.data[tmp[k]].state.selected) { 3605 c = true; 3606 pos = $.inArray(tmp[k], this._data.core.selected); 3607 if(pos !== -1) { 3608 this._data.core.selected = $.vakata.array_remove(this._data.core.selected, pos); 3609 } 3610 } 3611 } 3612 /** 3613 * triggered when a node is deleted 3614 * @event 3615 * @name delete_node.jstree 3616 * @param {Object} node 3617 * @param {String} parent the parent's ID 3618 */ 3619 this.trigger('delete_node', { "node" : obj, "parent" : par.id }); 3620 if(c) { 3621 this.trigger('changed', { 'action' : 'delete_node', 'node' : obj, 'selected' : this._data.core.selected, 'parent' : par.id }); 3622 } 3623 for(k = 0, l = tmp.length; k < l; k++) { 3624 delete this._model.data[tmp[k]]; 3625 } 3626 this.redraw_node(par, true); 3627 return true; 3628 }, 3629 /** 3630 * check if an operation is premitted on the tree. Used internally. 3631 * @private 3632 * @name check(chk, obj, par, pos) 3633 * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node" 3634 * @param {mixed} obj the node 3635 * @param {mixed} par the parent 3636 * @param {mixed} pos the position to insert at, or if "rename_node" - the new name 3637 * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node 3638 * @return {Boolean} 3639 */ 3640 check : function (chk, obj, par, pos, more) { 3641 obj = obj && obj.id ? obj : this.get_node(obj); 3642 par = par && par.id ? par : this.get_node(par); 3643 var tmp = chk.match(/^move_node|copy_node|create_node$/i) ? par : obj, 3644 chc = this.settings.core.check_callback; 3645 if(chk === "move_node" || chk === "copy_node") { 3646 if((!more || !more.is_multi) && (obj.id === par.id || $.inArray(obj.id, par.children) === pos || $.inArray(par.id, obj.children_d) !== -1)) { 3647 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 }) }; 3648 return false; 3649 } 3650 } 3651 if(tmp && tmp.data) { tmp = tmp.data; } 3652 if(tmp && tmp.functions && (tmp.functions[chk] === false || tmp.functions[chk] === true)) { 3653 if(tmp.functions[chk] === false) { 3654 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 }) }; 3655 } 3656 return tmp.functions[chk]; 3657 } 3658 if(chc === false || ($.isFunction(chc) && chc.call(this, chk, obj, par, pos, more) === false) || (chc && chc[chk] === false)) { 3659 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 }) }; 3660 return false; 3661 } 3662 return true; 3663 }, 3664 /** 3665 * get the last error 3666 * @name last_error() 3667 * @return {Object} 3668 */ 3669 last_error : function () { 3670 return this._data.core.last_error; 3671 }, 3672 /** 3673 * move a node to a new parent 3674 * @name move_node(obj, par [, pos, callback, is_loaded]) 3675 * @param {mixed} obj the node to move, pass an array to move multiple nodes 3676 * @param {mixed} par the new parent 3677 * @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` 3678 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position 3679 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded 3680 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn 3681 * @param {Boolean} instance internal parameter indicating if the node comes from another instance 3682 * @trigger move_node.jstree 3683 */ 3684 move_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) { 3685 var t1, t2, old_par, old_pos, new_par, old_ins, is_multi, dpc, tmp, i, j, k, l, p; 3686 3687 par = this.get_node(par); 3688 pos = pos === undefined ? 0 : pos; 3689 if(!par) { return false; } 3690 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { 3691 return this.load_node(par, function () { this.move_node(obj, par, pos, callback, true, false, origin); }); 3692 } 3693 3694 if($.isArray(obj)) { 3695 if(obj.length === 1) { 3696 obj = obj[0]; 3697 } 3698 else { 3699 //obj = obj.slice(); 3700 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 3701 if((tmp = this.move_node(obj[t1], par, pos, callback, is_loaded, false, origin))) { 3702 par = tmp; 3703 pos = "after"; 3704 } 3705 } 3706 this.redraw(); 3707 return true; 3708 } 3709 } 3710 obj = obj && obj.id ? obj : this.get_node(obj); 3711 3712 if(!obj || obj.id === '#') { return false; } 3713 3714 old_par = (obj.parent || '#').toString(); 3715 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === '#') ? par : this.get_node(par.parent); 3716 old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id)); 3717 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id); 3718 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; 3719 if(old_ins || old_ins._id) { 3720 obj = old_ins._model.data[obj.id]; 3721 } 3722 3723 if(is_multi) { 3724 if((tmp = this.copy_node(obj, par, pos, callback, is_loaded, false, origin))) { 3725 if(old_ins) { old_ins.delete_node(obj); } 3726 return tmp; 3727 } 3728 return false; 3729 } 3730 //var m = this._model.data; 3731 if(par.id === '#') { 3732 if(pos === "before") { pos = "first"; } 3733 if(pos === "after") { pos = "last"; } 3734 } 3735 switch(pos) { 3736 case "before": 3737 pos = $.inArray(par.id, new_par.children); 3738 break; 3739 case "after" : 3740 pos = $.inArray(par.id, new_par.children) + 1; 3741 break; 3742 case "inside": 3743 case "first": 3744 pos = 0; 3745 break; 3746 case "last": 3747 pos = new_par.children.length; 3748 break; 3749 default: 3750 if(!pos) { pos = 0; } 3751 break; 3752 } 3753 if(pos > new_par.children.length) { pos = new_par.children.length; } 3754 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) })) { 3755 this.settings.core.error.call(this, this._data.core.last_error); 3756 return false; 3757 } 3758 if(obj.parent === new_par.id) { 3759 dpc = new_par.children.concat(); 3760 tmp = $.inArray(obj.id, dpc); 3761 if(tmp !== -1) { 3762 dpc = $.vakata.array_remove(dpc, tmp); 3763 if(pos > tmp) { pos--; } 3764 } 3765 tmp = []; 3766 for(i = 0, j = dpc.length; i < j; i++) { 3767 tmp[i >= pos ? i+1 : i] = dpc[i]; 3768 } 3769 tmp[pos] = obj.id; 3770 new_par.children = tmp; 3771 this._node_changed(new_par.id); 3772 this.redraw(new_par.id === '#'); 3773 } 3774 else { 3775 // clean old parent and up 3776 tmp = obj.children_d.concat(); 3777 tmp.push(obj.id); 3778 for(i = 0, j = obj.parents.length; i < j; i++) { 3779 dpc = []; 3780 p = old_ins._model.data[obj.parents[i]].children_d; 3781 for(k = 0, l = p.length; k < l; k++) { 3782 if($.inArray(p[k], tmp) === -1) { 3783 dpc.push(p[k]); 3784 } 3785 } 3786 old_ins._model.data[obj.parents[i]].children_d = dpc; 3787 } 3788 old_ins._model.data[old_par].children = $.vakata.array_remove_item(old_ins._model.data[old_par].children, obj.id); 3789 3790 // insert into new parent and up 3791 for(i = 0, j = new_par.parents.length; i < j; i++) { 3792 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(tmp); 3793 } 3794 dpc = []; 3795 for(i = 0, j = new_par.children.length; i < j; i++) { 3796 dpc[i >= pos ? i+1 : i] = new_par.children[i]; 3797 } 3798 dpc[pos] = obj.id; 3799 new_par.children = dpc; 3800 new_par.children_d.push(obj.id); 3801 new_par.children_d = new_par.children_d.concat(obj.children_d); 3802 3803 // update object 3804 obj.parent = new_par.id; 3805 tmp = new_par.parents.concat(); 3806 tmp.unshift(new_par.id); 3807 p = obj.parents.length; 3808 obj.parents = tmp; 3809 3810 // update object children 3811 tmp = tmp.concat(); 3812 for(i = 0, j = obj.children_d.length; i < j; i++) { 3813 this._model.data[obj.children_d[i]].parents = this._model.data[obj.children_d[i]].parents.slice(0,p*-1); 3814 Array.prototype.push.apply(this._model.data[obj.children_d[i]].parents, tmp); 3815 } 3816 3817 if(old_par === '#' || new_par.id === '#') { 3818 this._model.force_full_redraw = true; 3819 } 3820 if(!this._model.force_full_redraw) { 3821 this._node_changed(old_par); 3822 this._node_changed(new_par.id); 3823 } 3824 if(!skip_redraw) { 3825 this.redraw(); 3826 } 3827 } 3828 if(callback) { callback.call(this, obj, new_par, pos); } 3829 /** 3830 * triggered when a node is moved 3831 * @event 3832 * @name move_node.jstree 3833 * @param {Object} node 3834 * @param {String} parent the parent's ID 3835 * @param {Number} position the position of the node among the parent's children 3836 * @param {String} old_parent the old parent of the node 3837 * @param {Number} old_position the old position of the node 3838 * @param {Boolean} is_multi do the node and new parent belong to different instances 3839 * @param {jsTree} old_instance the instance the node came from 3840 * @param {jsTree} new_instance the instance of the new parent 3841 */ 3842 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 }); 3843 return obj.id; 3844 }, 3845 /** 3846 * copy a node to a new parent 3847 * @name copy_node(obj, par [, pos, callback, is_loaded]) 3848 * @param {mixed} obj the node to copy, pass an array to copy multiple nodes 3849 * @param {mixed} par the new parent 3850 * @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` 3851 * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position 3852 * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded 3853 * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn 3854 * @param {Boolean} instance internal parameter indicating if the node comes from another instance 3855 * @trigger model.jstree copy_node.jstree 3856 */ 3857 copy_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) { 3858 var t1, t2, dpc, tmp, i, j, node, old_par, new_par, old_ins, is_multi; 3859 3860 par = this.get_node(par); 3861 pos = pos === undefined ? 0 : pos; 3862 if(!par) { return false; } 3863 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { 3864 return this.load_node(par, function () { this.copy_node(obj, par, pos, callback, true, false, origin); }); 3865 } 3866 3867 if($.isArray(obj)) { 3868 if(obj.length === 1) { 3869 obj = obj[0]; 3870 } 3871 else { 3872 //obj = obj.slice(); 3873 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 3874 if((tmp = this.copy_node(obj[t1], par, pos, callback, is_loaded, true, origin))) { 3875 par = tmp; 3876 pos = "after"; 3877 } 3878 } 3879 this.redraw(); 3880 return true; 3881 } 3882 } 3883 obj = obj && obj.id ? obj : this.get_node(obj); 3884 if(!obj || obj.id === '#') { return false; } 3885 3886 old_par = (obj.parent || '#').toString(); 3887 new_par = (!pos.toString().match(/^(before|after)$/) || par.id === '#') ? par : this.get_node(par.parent); 3888 old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id)); 3889 is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id); 3890 3891 if(old_ins || old_ins._id) { 3892 obj = old_ins._model.data[obj.id]; 3893 } 3894 3895 if(par.id === '#') { 3896 if(pos === "before") { pos = "first"; } 3897 if(pos === "after") { pos = "last"; } 3898 } 3899 switch(pos) { 3900 case "before": 3901 pos = $.inArray(par.id, new_par.children); 3902 break; 3903 case "after" : 3904 pos = $.inArray(par.id, new_par.children) + 1; 3905 break; 3906 case "inside": 3907 case "first": 3908 pos = 0; 3909 break; 3910 case "last": 3911 pos = new_par.children.length; 3912 break; 3913 default: 3914 if(!pos) { pos = 0; } 3915 break; 3916 } 3917 if(pos > new_par.children.length) { pos = new_par.children.length; } 3918 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) })) { 3919 this.settings.core.error.call(this, this._data.core.last_error); 3920 return false; 3921 } 3922 node = old_ins ? old_ins.get_json(obj, { no_id : true, no_data : true, no_state : true }) : obj; 3923 if(!node) { return false; } 3924 if(node.id === true) { delete node.id; } 3925 node = this._parse_model_from_json(node, new_par.id, new_par.parents.concat()); 3926 if(!node) { return false; } 3927 tmp = this.get_node(node); 3928 if(obj && obj.state && obj.state.loaded === false) { tmp.state.loaded = false; } 3929 dpc = []; 3930 dpc.push(node); 3931 dpc = dpc.concat(tmp.children_d); 3932 this.trigger('model', { "nodes" : dpc, "parent" : new_par.id }); 3933 3934 // insert into new parent and up 3935 for(i = 0, j = new_par.parents.length; i < j; i++) { 3936 this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(dpc); 3937 } 3938 dpc = []; 3939 for(i = 0, j = new_par.children.length; i < j; i++) { 3940 dpc[i >= pos ? i+1 : i] = new_par.children[i]; 3941 } 3942 dpc[pos] = tmp.id; 3943 new_par.children = dpc; 3944 new_par.children_d.push(tmp.id); 3945 new_par.children_d = new_par.children_d.concat(tmp.children_d); 3946 3947 if(new_par.id === '#') { 3948 this._model.force_full_redraw = true; 3949 } 3950 if(!this._model.force_full_redraw) { 3951 this._node_changed(new_par.id); 3952 } 3953 if(!skip_redraw) { 3954 this.redraw(new_par.id === '#'); 3955 } 3956 if(callback) { callback.call(this, tmp, new_par, pos); } 3957 /** 3958 * triggered when a node is copied 3959 * @event 3960 * @name copy_node.jstree 3961 * @param {Object} node the copied node 3962 * @param {Object} original the original node 3963 * @param {String} parent the parent's ID 3964 * @param {Number} position the position of the node among the parent's children 3965 * @param {String} old_parent the old parent of the node 3966 * @param {Number} old_position the position of the original node 3967 * @param {Boolean} is_multi do the node and new parent belong to different instances 3968 * @param {jsTree} old_instance the instance the node came from 3969 * @param {jsTree} new_instance the instance of the new parent 3970 */ 3971 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 }); 3972 return tmp.id; 3973 }, 3974 /** 3975 * cut a node (a later call to `paste(obj)` would move the node) 3976 * @name cut(obj) 3977 * @param {mixed} obj multiple objects can be passed using an array 3978 * @trigger cut.jstree 3979 */ 3980 cut : function (obj) { 3981 if(!obj) { obj = this._data.core.selected.concat(); } 3982 if(!$.isArray(obj)) { obj = [obj]; } 3983 if(!obj.length) { return false; } 3984 var tmp = [], o, t1, t2; 3985 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 3986 o = this.get_node(obj[t1]); 3987 if(o && o.id && o.id !== '#') { tmp.push(o); } 3988 } 3989 if(!tmp.length) { return false; } 3990 ccp_node = tmp; 3991 ccp_inst = this; 3992 ccp_mode = 'move_node'; 3993 /** 3994 * triggered when nodes are added to the buffer for moving 3995 * @event 3996 * @name cut.jstree 3997 * @param {Array} node 3998 */ 3999 this.trigger('cut', { "node" : obj }); 4000 }, 4001 /** 4002 * copy a node (a later call to `paste(obj)` would copy the node) 4003 * @name copy(obj) 4004 * @param {mixed} obj multiple objects can be passed using an array 4005 * @trigger copy.jstree 4006 */ 4007 copy : function (obj) { 4008 if(!obj) { obj = this._data.core.selected.concat(); } 4009 if(!$.isArray(obj)) { obj = [obj]; } 4010 if(!obj.length) { return false; } 4011 var tmp = [], o, t1, t2; 4012 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 4013 o = this.get_node(obj[t1]); 4014 if(o && o.id && o.id !== '#') { tmp.push(o); } 4015 } 4016 if(!tmp.length) { return false; } 4017 ccp_node = tmp; 4018 ccp_inst = this; 4019 ccp_mode = 'copy_node'; 4020 /** 4021 * triggered when nodes are added to the buffer for copying 4022 * @event 4023 * @name copy.jstree 4024 * @param {Array} node 4025 */ 4026 this.trigger('copy', { "node" : obj }); 4027 }, 4028 /** 4029 * get the current buffer (any nodes that are waiting for a paste operation) 4030 * @name get_buffer() 4031 * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance) 4032 */ 4033 get_buffer : function () { 4034 return { 'mode' : ccp_mode, 'node' : ccp_node, 'inst' : ccp_inst }; 4035 }, 4036 /** 4037 * check if there is something in the buffer to paste 4038 * @name can_paste() 4039 * @return {Boolean} 4040 */ 4041 can_paste : function () { 4042 return ccp_mode !== false && ccp_node !== false; // && ccp_inst._model.data[ccp_node]; 4043 }, 4044 /** 4045 * copy or move the previously cut or copied nodes to a new parent 4046 * @name paste(obj [, pos]) 4047 * @param {mixed} obj the new parent 4048 * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0` 4049 * @trigger paste.jstree 4050 */ 4051 paste : function (obj, pos) { 4052 obj = this.get_node(obj); 4053 if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; } 4054 if(this[ccp_mode](ccp_node, obj, pos, false, false, false, ccp_inst)) { 4055 /** 4056 * triggered when paste is invoked 4057 * @event 4058 * @name paste.jstree 4059 * @param {String} parent the ID of the receiving node 4060 * @param {Array} node the nodes in the buffer 4061 * @param {String} mode the performed operation - "copy_node" or "move_node" 4062 */ 4063 this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode }); 4064 } 4065 ccp_node = false; 4066 ccp_mode = false; 4067 ccp_inst = false; 4068 }, 4069 /** 4070 * clear the buffer of previously copied or cut nodes 4071 * @name clear_buffer() 4072 * @trigger clear_buffer.jstree 4073 */ 4074 clear_buffer : function () { 4075 ccp_node = false; 4076 ccp_mode = false; 4077 ccp_inst = false; 4078 /** 4079 * triggered when the copy / cut buffer is cleared 4080 * @event 4081 * @name clear_buffer.jstree 4082 */ 4083 this.trigger('clear_buffer'); 4084 }, 4085 /** 4086 * put a node in edit mode (input field to rename the node) 4087 * @name edit(obj [, default_text]) 4088 * @param {mixed} obj 4089 * @param {String} default_text the text to populate the input with (if omitted the node text value is used) 4090 */ 4091 edit : function (obj, default_text) { 4092 var rtl, w, a, s, t, h1, h2, fn, tmp; 4093 obj = this.get_node(obj); 4094 if(!obj) { return false; } 4095 if(this.settings.core.check_callback === false) { 4096 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_07', 'reason' : 'Could not edit node because of check_callback' }; 4097 this.settings.core.error.call(this, this._data.core.last_error); 4098 return false; 4099 } 4100 tmp = obj; 4101 default_text = typeof default_text === 'string' ? default_text : obj.text; 4102 this.set_text(obj, ""); 4103 obj = this._open_to(obj); 4104 tmp.text = default_text; 4105 4106 rtl = this._data.core.rtl; 4107 w = this.element.width(); 4108 a = obj.children('.jstree-anchor'); 4109 s = $('<span>'); 4110 /*! 4111 oi = obj.children("i:visible"), 4112 ai = a.children("i:visible"), 4113 w1 = oi.width() * oi.length, 4114 w2 = ai.width() * ai.length, 4115 */ 4116 t = default_text; 4117 h1 = $("<"+"div />", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo("body"); 4118 h2 = $("<"+"input />", { 4119 "value" : t, 4120 "class" : "jstree-rename-input", 4121 // "size" : t.length, 4122 "css" : { 4123 "padding" : "0", 4124 "border" : "1px solid silver", 4125 "box-sizing" : "border-box", 4126 "display" : "inline-block", 4127 "height" : (this._data.core.li_height) + "px", 4128 "lineHeight" : (this._data.core.li_height) + "px", 4129 "width" : "150px" // will be set a bit further down 4130 }, 4131 "blur" : $.proxy(function () { 4132 var i = s.children(".jstree-rename-input"), 4133 v = i.val(); 4134 if(v === "") { v = t; } 4135 h1.remove(); 4136 s.replaceWith(a); 4137 s.remove(); 4138 this.set_text(obj, t); 4139 if(this.rename_node(obj, $('<div></div>').text(v)[this.settings.core.force_text ? 'text' : 'html']()) === false) { 4140 this.set_text(obj, t); // move this up? and fix #483 4141 } 4142 }, this), 4143 "keydown" : function (event) { 4144 var key = event.which; 4145 if(key === 27) { 4146 this.value = t; 4147 } 4148 if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) { 4149 event.stopImmediatePropagation(); 4150 } 4151 if(key === 27 || key === 13) { 4152 event.preventDefault(); 4153 this.blur(); 4154 } 4155 }, 4156 "click" : function (e) { e.stopImmediatePropagation(); }, 4157 "mousedown" : function (e) { e.stopImmediatePropagation(); }, 4158 "keyup" : function (event) { 4159 h2.width(Math.min(h1.text("pW" + this.value).width(),w)); 4160 }, 4161 "keypress" : function(event) { 4162 if(event.which === 13) { return false; } 4163 } 4164 }); 4165 fn = { 4166 fontFamily : a.css('fontFamily') || '', 4167 fontSize : a.css('fontSize') || '', 4168 fontWeight : a.css('fontWeight') || '', 4169 fontStyle : a.css('fontStyle') || '', 4170 fontStretch : a.css('fontStretch') || '', 4171 fontVariant : a.css('fontVariant') || '', 4172 letterSpacing : a.css('letterSpacing') || '', 4173 wordSpacing : a.css('wordSpacing') || '' 4174 }; 4175 s.attr('class', a.attr('class')).append(a.contents().clone()).append(h2); 4176 a.replaceWith(s); 4177 h1.css(fn); 4178 h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select(); 4179 }, 4180 4181 4182 /** 4183 * changes the theme 4184 * @name set_theme(theme_name [, theme_url]) 4185 * @param {String} theme_name the name of the new theme to apply 4186 * @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. 4187 * @trigger set_theme.jstree 4188 */ 4189 set_theme : function (theme_name, theme_url) { 4190 if(!theme_name) { return false; } 4191 if(theme_url === true) { 4192 var dir = this.settings.core.themes.dir; 4193 if(!dir) { dir = $.jstree.path + '/themes'; } 4194 theme_url = dir + '/' + theme_name + '/style.css'; 4195 } 4196 if(theme_url && $.inArray(theme_url, themes_loaded) === -1) { 4197 $('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />'); 4198 themes_loaded.push(theme_url); 4199 } 4200 if(this._data.core.themes.name) { 4201 this.element.removeClass('jstree-' + this._data.core.themes.name); 4202 } 4203 this._data.core.themes.name = theme_name; 4204 this.element.addClass('jstree-' + theme_name); 4205 this.element[this.settings.core.themes.responsive ? 'addClass' : 'removeClass' ]('jstree-' + theme_name + '-responsive'); 4206 /** 4207 * triggered when a theme is set 4208 * @event 4209 * @name set_theme.jstree 4210 * @param {String} theme the new theme 4211 */ 4212 this.trigger('set_theme', { 'theme' : theme_name }); 4213 }, 4214 /** 4215 * gets the name of the currently applied theme name 4216 * @name get_theme() 4217 * @return {String} 4218 */ 4219 get_theme : function () { return this._data.core.themes.name; }, 4220 /** 4221 * changes the theme variant (if the theme has variants) 4222 * @name set_theme_variant(variant_name) 4223 * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed) 4224 */ 4225 set_theme_variant : function (variant_name) { 4226 if(this._data.core.themes.variant) { 4227 this.element.removeClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant); 4228 } 4229 this._data.core.themes.variant = variant_name; 4230 if(variant_name) { 4231 this.element.addClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant); 4232 } 4233 }, 4234 /** 4235 * gets the name of the currently applied theme variant 4236 * @name get_theme() 4237 * @return {String} 4238 */ 4239 get_theme_variant : function () { return this._data.core.themes.variant; }, 4240 /** 4241 * shows a striped background on the container (if the theme supports it) 4242 * @name show_stripes() 4243 */ 4244 show_stripes : function () { this._data.core.themes.stripes = true; this.get_container_ul().addClass("jstree-striped"); }, 4245 /** 4246 * hides the striped background on the container 4247 * @name hide_stripes() 4248 */ 4249 hide_stripes : function () { this._data.core.themes.stripes = false; this.get_container_ul().removeClass("jstree-striped"); }, 4250 /** 4251 * toggles the striped background on the container 4252 * @name toggle_stripes() 4253 */ 4254 toggle_stripes : function () { if(this._data.core.themes.stripes) { this.hide_stripes(); } else { this.show_stripes(); } }, 4255 /** 4256 * shows the connecting dots (if the theme supports it) 4257 * @name show_dots() 4258 */ 4259 show_dots : function () { this._data.core.themes.dots = true; this.get_container_ul().removeClass("jstree-no-dots"); }, 4260 /** 4261 * hides the connecting dots 4262 * @name hide_dots() 4263 */ 4264 hide_dots : function () { this._data.core.themes.dots = false; this.get_container_ul().addClass("jstree-no-dots"); }, 4265 /** 4266 * toggles the connecting dots 4267 * @name toggle_dots() 4268 */ 4269 toggle_dots : function () { if(this._data.core.themes.dots) { this.hide_dots(); } else { this.show_dots(); } }, 4270 /** 4271 * show the node icons 4272 * @name show_icons() 4273 */ 4274 show_icons : function () { this._data.core.themes.icons = true; this.get_container_ul().removeClass("jstree-no-icons"); }, 4275 /** 4276 * hide the node icons 4277 * @name hide_icons() 4278 */ 4279 hide_icons : function () { this._data.core.themes.icons = false; this.get_container_ul().addClass("jstree-no-icons"); }, 4280 /** 4281 * toggle the node icons 4282 * @name toggle_icons() 4283 */ 4284 toggle_icons : function () { if(this._data.core.themes.icons) { this.hide_icons(); } else { this.show_icons(); } }, 4285 /** 4286 * set the node icon for a node 4287 * @name set_icon(obj, icon) 4288 * @param {mixed} obj 4289 * @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 4290 */ 4291 set_icon : function (obj, icon) { 4292 var t1, t2, dom, old; 4293 if($.isArray(obj)) { 4294 obj = obj.slice(); 4295 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 4296 this.set_icon(obj[t1], icon); 4297 } 4298 return true; 4299 } 4300 obj = this.get_node(obj); 4301 if(!obj || obj.id === '#') { return false; } 4302 old = obj.icon; 4303 obj.icon = icon === true || icon === null || icon === undefined || icon === '' ? true : icon; 4304 dom = this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon"); 4305 if(icon === false) { 4306 this.hide_icon(obj); 4307 } 4308 else if(icon === true || icon === null || icon === undefined || icon === '') { 4309 dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel"); 4310 if(old === false) { this.show_icon(obj); } 4311 } 4312 else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) { 4313 dom.removeClass(old).css("background",""); 4314 dom.addClass(icon + ' jstree-themeicon-custom').attr("rel",icon); 4315 if(old === false) { this.show_icon(obj); } 4316 } 4317 else { 4318 dom.removeClass(old).css("background",""); 4319 dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon); 4320 if(old === false) { this.show_icon(obj); } 4321 } 4322 return true; 4323 }, 4324 /** 4325 * get the node icon for a node 4326 * @name get_icon(obj) 4327 * @param {mixed} obj 4328 * @return {String} 4329 */ 4330 get_icon : function (obj) { 4331 obj = this.get_node(obj); 4332 return (!obj || obj.id === '#') ? false : obj.icon; 4333 }, 4334 /** 4335 * hide the icon on an individual node 4336 * @name hide_icon(obj) 4337 * @param {mixed} obj 4338 */ 4339 hide_icon : function (obj) { 4340 var t1, t2; 4341 if($.isArray(obj)) { 4342 obj = obj.slice(); 4343 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 4344 this.hide_icon(obj[t1]); 4345 } 4346 return true; 4347 } 4348 obj = this.get_node(obj); 4349 if(!obj || obj === '#') { return false; } 4350 obj.icon = false; 4351 this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden'); 4352 return true; 4353 }, 4354 /** 4355 * show the icon on an individual node 4356 * @name show_icon(obj) 4357 * @param {mixed} obj 4358 */ 4359 show_icon : function (obj) { 4360 var t1, t2, dom; 4361 if($.isArray(obj)) { 4362 obj = obj.slice(); 4363 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 4364 this.show_icon(obj[t1]); 4365 } 4366 return true; 4367 } 4368 obj = this.get_node(obj); 4369 if(!obj || obj === '#') { return false; } 4370 dom = this.get_node(obj, true); 4371 obj.icon = dom.length ? dom.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true; 4372 if(!obj.icon) { obj.icon = true; } 4373 dom.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden'); 4374 return true; 4375 } 4376 }; 4377 4378 // helpers 4379 $.vakata = {}; 4380 // collect attributes 4381 $.vakata.attributes = function(node, with_values) { 4382 node = $(node)[0]; 4383 var attr = with_values ? {} : []; 4384 if(node && node.attributes) { 4385 $.each(node.attributes, function (i, v) { 4386 if($.inArray(v.name.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; } 4387 if(v.value !== null && $.trim(v.value) !== '') { 4388 if(with_values) { attr[v.name] = v.value; } 4389 else { attr.push(v.name); } 4390 } 4391 }); 4392 } 4393 return attr; 4394 }; 4395 $.vakata.array_unique = function(array) { 4396 var a = [], i, j, l, o = {}; 4397 for(i = 0, l = array.length; i < l; i++) { 4398 if(o[array[i]] === undefined) { 4399 a.push(array[i]); 4400 o[array[i]] = true; 4401 } 4402 } 4403 return a; 4404 }; 4405 // remove item from array 4406 $.vakata.array_remove = function(array, from, to) { 4407 var rest = array.slice((to || from) + 1 || array.length); 4408 array.length = from < 0 ? array.length + from : from; 4409 array.push.apply(array, rest); 4410 return array; 4411 }; 4412 // remove item from array 4413 $.vakata.array_remove_item = function(array, item) { 4414 var tmp = $.inArray(item, array); 4415 return tmp !== -1 ? $.vakata.array_remove(array, tmp) : array; 4416 }; 4417 4418 4419/** 4420 * ### Checkbox plugin 4421 * 4422 * This plugin renders checkbox icons in front of each node, making multiple selection much easier. 4423 * It also supports tri-state behavior, meaning that if a node has a few of its children checked it will be rendered as undetermined, and state will be propagated up. 4424 */ 4425 4426 var _i = document.createElement('I'); 4427 _i.className = 'jstree-icon jstree-checkbox'; 4428 _i.setAttribute('role', 'presentation'); 4429 /** 4430 * stores all defaults for the checkbox plugin 4431 * @name $.jstree.defaults.checkbox 4432 * @plugin checkbox 4433 */ 4434 $.jstree.defaults.checkbox = { 4435 /** 4436 * a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`. 4437 * @name $.jstree.defaults.checkbox.visible 4438 * @plugin checkbox 4439 */ 4440 visible : true, 4441 /** 4442 * a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`. 4443 * @name $.jstree.defaults.checkbox.three_state 4444 * @plugin checkbox 4445 */ 4446 three_state : true, 4447 /** 4448 * a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`. 4449 * @name $.jstree.defaults.checkbox.whole_node 4450 * @plugin checkbox 4451 */ 4452 whole_node : true, 4453 /** 4454 * a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`. 4455 * @name $.jstree.defaults.checkbox.keep_selected_style 4456 * @plugin checkbox 4457 */ 4458 keep_selected_style : true, 4459 /** 4460 * This setting controls how cascading and undetermined nodes are applied. 4461 * If 'up' is in the string - cascading up is enabled, if 'down' is in the string - cascading down is enabled, if 'undetermined' is in the string - undetermined nodes will be used. 4462 * If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''. 4463 * @name $.jstree.defaults.checkbox.cascade 4464 * @plugin checkbox 4465 */ 4466 cascade : '', 4467 /** 4468 * This setting controls if checkbox are bound to the general tree selection or to an internal array maintained by the checkbox plugin. Defaults to `true`, only set to `false` if you know exactly what you are doing. 4469 * @name $.jstree.defaults.checkbox.tie_selection 4470 * @plugin checkbox 4471 */ 4472 tie_selection : true 4473 }; 4474 $.jstree.plugins.checkbox = function (options, parent) { 4475 this.bind = function () { 4476 parent.bind.call(this); 4477 this._data.checkbox.uto = false; 4478 this._data.checkbox.selected = []; 4479 if(this.settings.checkbox.three_state) { 4480 this.settings.checkbox.cascade = 'up+down+undetermined'; 4481 } 4482 this.element 4483 .on("init.jstree", $.proxy(function () { 4484 this._data.checkbox.visible = this.settings.checkbox.visible; 4485 if(!this.settings.checkbox.keep_selected_style) { 4486 this.element.addClass('jstree-checkbox-no-clicked'); 4487 } 4488 if(this.settings.checkbox.tie_selection) { 4489 this.element.addClass('jstree-checkbox-selection'); 4490 } 4491 }, this)) 4492 .on("loading.jstree", $.proxy(function () { 4493 this[ this._data.checkbox.visible ? 'show_checkboxes' : 'hide_checkboxes' ](); 4494 }, this)); 4495 if(this.settings.checkbox.cascade.indexOf('undetermined') !== -1) { 4496 this.element 4497 .on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree', $.proxy(function () { 4498 // only if undetermined is in setting 4499 if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); } 4500 this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50); 4501 }, this)); 4502 } 4503 if(!this.settings.checkbox.tie_selection) { 4504 this.element 4505 .on('model.jstree', $.proxy(function (e, data) { 4506 var m = this._model.data, 4507 p = m[data.parent], 4508 dpc = data.nodes, 4509 i, j; 4510 for(i = 0, j = dpc.length; i < j; i++) { 4511 m[dpc[i]].state.checked = (m[dpc[i]].original && m[dpc[i]].original.state && m[dpc[i]].original.state.checked); 4512 if(m[dpc[i]].state.checked) { 4513 this._data.checkbox.selected.push(dpc[i]); 4514 } 4515 } 4516 }, this)); 4517 } 4518 if(this.settings.checkbox.cascade.indexOf('up') !== -1 || this.settings.checkbox.cascade.indexOf('down') !== -1) { 4519 this.element 4520 .on('model.jstree', $.proxy(function (e, data) { 4521 var m = this._model.data, 4522 p = m[data.parent], 4523 dpc = data.nodes, 4524 chd = [], 4525 c, i, j, k, l, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection; 4526 4527 if(s.indexOf('down') !== -1) { 4528 // apply down 4529 if(p.state[ t ? 'selected' : 'checked' ]) { 4530 for(i = 0, j = dpc.length; i < j; i++) { 4531 m[dpc[i]].state[ t ? 'selected' : 'checked' ] = true; 4532 } 4533 this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(dpc); 4534 } 4535 else { 4536 for(i = 0, j = dpc.length; i < j; i++) { 4537 if(m[dpc[i]].state[ t ? 'selected' : 'checked' ]) { 4538 for(k = 0, l = m[dpc[i]].children_d.length; k < l; k++) { 4539 m[m[dpc[i]].children_d[k]].state[ t ? 'selected' : 'checked' ] = true; 4540 } 4541 this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(m[dpc[i]].children_d); 4542 } 4543 } 4544 } 4545 } 4546 4547 if(s.indexOf('up') !== -1) { 4548 // apply up 4549 for(i = 0, j = p.children_d.length; i < j; i++) { 4550 if(!m[p.children_d[i]].children.length) { 4551 chd.push(m[p.children_d[i]].parent); 4552 } 4553 } 4554 chd = $.vakata.array_unique(chd); 4555 for(k = 0, l = chd.length; k < l; k++) { 4556 p = m[chd[k]]; 4557 while(p && p.id !== '#') { 4558 c = 0; 4559 for(i = 0, j = p.children.length; i < j; i++) { 4560 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; 4561 } 4562 if(c === j) { 4563 p.state[ t ? 'selected' : 'checked' ] = true; 4564 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); 4565 tmp = this.get_node(p, true); 4566 if(tmp && tmp.length) { 4567 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass( t ? 'jstree-clicked' : 'jstree-checked'); 4568 } 4569 } 4570 else { 4571 break; 4572 } 4573 p = this.get_node(p.parent); 4574 } 4575 } 4576 } 4577 4578 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected); 4579 }, this)) 4580 .on(this.settings.checkbox.tie_selection ? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e, data) { 4581 var obj = data.node, 4582 m = this._model.data, 4583 par = this.get_node(obj.parent), 4584 dom = this.get_node(obj, true), 4585 i, j, c, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection; 4586 4587 // apply down 4588 if(s.indexOf('down') !== -1) { 4589 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d)); 4590 for(i = 0, j = obj.children_d.length; i < j; i++) { 4591 tmp = m[obj.children_d[i]]; 4592 tmp.state[ t ? 'selected' : 'checked' ] = true; 4593 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { 4594 tmp.original.state.undetermined = false; 4595 } 4596 } 4597 } 4598 4599 // apply up 4600 if(s.indexOf('up') !== -1) { 4601 while(par && par.id !== '#') { 4602 c = 0; 4603 for(i = 0, j = par.children.length; i < j; i++) { 4604 c += m[par.children[i]].state[ t ? 'selected' : 'checked' ]; 4605 } 4606 if(c === j) { 4607 par.state[ t ? 'selected' : 'checked' ] = true; 4608 this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id); 4609 tmp = this.get_node(par, true); 4610 if(tmp && tmp.length) { 4611 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); 4612 } 4613 } 4614 else { 4615 break; 4616 } 4617 par = this.get_node(par.parent); 4618 } 4619 } 4620 4621 // apply down (process .children separately?) 4622 if(s.indexOf('down') !== -1 && dom.length) { 4623 dom.find('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', true); 4624 } 4625 }, this)) 4626 .on(this.settings.checkbox.tie_selection ? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e, data) { 4627 var obj = this.get_node('#'), 4628 m = this._model.data, 4629 i, j, tmp; 4630 for(i = 0, j = obj.children_d.length; i < j; i++) { 4631 tmp = m[obj.children_d[i]]; 4632 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { 4633 tmp.original.state.undetermined = false; 4634 } 4635 } 4636 }, this)) 4637 .on(this.settings.checkbox.tie_selection ? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e, data) { 4638 var obj = data.node, 4639 dom = this.get_node(obj, true), 4640 i, j, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection; 4641 if(obj && obj.original && obj.original.state && obj.original.state.undetermined) { 4642 obj.original.state.undetermined = false; 4643 } 4644 4645 // apply down 4646 if(s.indexOf('down') !== -1) { 4647 for(i = 0, j = obj.children_d.length; i < j; i++) { 4648 tmp = this._model.data[obj.children_d[i]]; 4649 tmp.state[ t ? 'selected' : 'checked' ] = false; 4650 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { 4651 tmp.original.state.undetermined = false; 4652 } 4653 } 4654 } 4655 4656 // apply up 4657 if(s.indexOf('up') !== -1) { 4658 for(i = 0, j = obj.parents.length; i < j; i++) { 4659 tmp = this._model.data[obj.parents[i]]; 4660 tmp.state[ t ? 'selected' : 'checked' ] = false; 4661 if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) { 4662 tmp.original.state.undetermined = false; 4663 } 4664 tmp = this.get_node(obj.parents[i], true); 4665 if(tmp && tmp.length) { 4666 tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked'); 4667 } 4668 } 4669 } 4670 tmp = []; 4671 for(i = 0, j = this._data[ t ? 'core' : 'checkbox' ].selected.length; i < j; i++) { 4672 // apply down + apply up 4673 if( 4674 (s.indexOf('down') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.children_d) === -1) && 4675 (s.indexOf('up') === -1 || $.inArray(this._data[ t ? 'core' : 'checkbox' ].selected[i], obj.parents) === -1) 4676 ) { 4677 tmp.push(this._data[ t ? 'core' : 'checkbox' ].selected[i]); 4678 } 4679 } 4680 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(tmp); 4681 4682 // apply down (process .children separately?) 4683 if(s.indexOf('down') !== -1 && dom.length) { 4684 dom.find('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked').parent().attr('aria-selected', false); 4685 } 4686 }, this)); 4687 } 4688 if(this.settings.checkbox.cascade.indexOf('up') !== -1) { 4689 this.element 4690 .on('delete_node.jstree', $.proxy(function (e, data) { 4691 // apply up (whole handler) 4692 var p = this.get_node(data.parent), 4693 m = this._model.data, 4694 i, j, c, tmp, t = this.settings.checkbox.tie_selection; 4695 while(p && p.id !== '#') { 4696 c = 0; 4697 for(i = 0, j = p.children.length; i < j; i++) { 4698 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; 4699 } 4700 if(c === j) { 4701 p.state[ t ? 'selected' : 'checked' ] = true; 4702 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); 4703 tmp = this.get_node(p, true); 4704 if(tmp && tmp.length) { 4705 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); 4706 } 4707 } 4708 else { 4709 break; 4710 } 4711 p = this.get_node(p.parent); 4712 } 4713 }, this)) 4714 .on('move_node.jstree', $.proxy(function (e, data) { 4715 // apply up (whole handler) 4716 var is_multi = data.is_multi, 4717 old_par = data.old_parent, 4718 new_par = this.get_node(data.parent), 4719 m = this._model.data, 4720 p, c, i, j, tmp, t = this.settings.checkbox.tie_selection; 4721 if(!is_multi) { 4722 p = this.get_node(old_par); 4723 while(p && p.id !== '#') { 4724 c = 0; 4725 for(i = 0, j = p.children.length; i < j; i++) { 4726 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; 4727 } 4728 if(c === j) { 4729 p.state[ t ? 'selected' : 'checked' ] = true; 4730 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); 4731 tmp = this.get_node(p, true); 4732 if(tmp && tmp.length) { 4733 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); 4734 } 4735 } 4736 else { 4737 break; 4738 } 4739 p = this.get_node(p.parent); 4740 } 4741 } 4742 p = new_par; 4743 while(p && p.id !== '#') { 4744 c = 0; 4745 for(i = 0, j = p.children.length; i < j; i++) { 4746 c += m[p.children[i]].state[ t ? 'selected' : 'checked' ]; 4747 } 4748 if(c === j) { 4749 if(!p.state[ t ? 'selected' : 'checked' ]) { 4750 p.state[ t ? 'selected' : 'checked' ] = true; 4751 this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id); 4752 tmp = this.get_node(p, true); 4753 if(tmp && tmp.length) { 4754 tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked'); 4755 } 4756 } 4757 } 4758 else { 4759 if(p.state[ t ? 'selected' : 'checked' ]) { 4760 p.state[ t ? 'selected' : 'checked' ] = false; 4761 this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_remove_item(this._data[ t ? 'core' : 'checkbox' ].selected, p.id); 4762 tmp = this.get_node(p, true); 4763 if(tmp && tmp.length) { 4764 tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked'); 4765 } 4766 } 4767 else { 4768 break; 4769 } 4770 } 4771 p = this.get_node(p.parent); 4772 } 4773 }, this)); 4774 } 4775 }; 4776 /** 4777 * set the undetermined state where and if necessary. Used internally. 4778 * @private 4779 * @name _undetermined() 4780 * @plugin checkbox 4781 */ 4782 this._undetermined = function () { 4783 var i, j, k, l, o = {}, m = this._model.data, t = this.settings.checkbox.tie_selection, s = this._data[ t ? 'core' : 'checkbox' ].selected, p = [], tt = this; 4784 for(i = 0, j = s.length; i < j; i++) { 4785 if(m[s[i]] && m[s[i]].parents) { 4786 for(k = 0, l = m[s[i]].parents.length; k < l; k++) { 4787 if(o[m[s[i]].parents[k]] === undefined && m[s[i]].parents[k] !== '#') { 4788 o[m[s[i]].parents[k]] = true; 4789 p.push(m[s[i]].parents[k]); 4790 } 4791 } 4792 } 4793 } 4794 // attempt for server side undetermined state 4795 this.element.find('.jstree-closed').not(':has(.jstree-children)') 4796 .each(function () { 4797 var tmp = tt.get_node(this), tmp2; 4798 if(!tmp.state.loaded) { 4799 if(tmp.original && tmp.original.state && tmp.original.state.undetermined && tmp.original.state.undetermined === true) { 4800 if(o[tmp.id] === undefined && tmp.id !== '#') { 4801 o[tmp.id] = true; 4802 p.push(tmp.id); 4803 } 4804 for(k = 0, l = tmp.parents.length; k < l; k++) { 4805 if(o[tmp.parents[k]] === undefined && tmp.parents[k] !== '#') { 4806 o[tmp.parents[k]] = true; 4807 p.push(tmp.parents[k]); 4808 } 4809 } 4810 } 4811 } 4812 else { 4813 for(i = 0, j = tmp.children_d.length; i < j; i++) { 4814 tmp2 = m[tmp.children_d[i]]; 4815 if(!tmp2.state.loaded && tmp2.original && tmp2.original.state && tmp2.original.state.undetermined && tmp2.original.state.undetermined === true) { 4816 if(o[tmp2.id] === undefined && tmp2.id !== '#') { 4817 o[tmp2.id] = true; 4818 p.push(tmp2.id); 4819 } 4820 for(k = 0, l = tmp2.parents.length; k < l; k++) { 4821 if(o[tmp2.parents[k]] === undefined && tmp2.parents[k] !== '#') { 4822 o[tmp2.parents[k]] = true; 4823 p.push(tmp2.parents[k]); 4824 } 4825 } 4826 } 4827 } 4828 } 4829 }); 4830 4831 this.element.find('.jstree-undetermined').removeClass('jstree-undetermined'); 4832 for(i = 0, j = p.length; i < j; i++) { 4833 if(!m[p[i]].state[ t ? 'selected' : 'checked' ]) { 4834 s = this.get_node(p[i], true); 4835 if(s && s.length) { 4836 s.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined'); 4837 } 4838 } 4839 } 4840 }; 4841 this.redraw_node = function(obj, deep, is_callback, force_render) { 4842 obj = parent.redraw_node.apply(this, arguments); 4843 if(obj) { 4844 var i, j, tmp = null; 4845 for(i = 0, j = obj.childNodes.length; i < j; i++) { 4846 if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) { 4847 tmp = obj.childNodes[i]; 4848 break; 4849 } 4850 } 4851 if(tmp) { 4852 if(!this.settings.checkbox.tie_selection && this._model.data[obj.id].state.checked) { tmp.className += ' jstree-checked'; } 4853 tmp.insertBefore(_i.cloneNode(false), tmp.childNodes[0]); 4854 } 4855 } 4856 if(!is_callback && this.settings.checkbox.cascade.indexOf('undetermined') !== -1) { 4857 if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); } 4858 this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50); 4859 } 4860 return obj; 4861 }; 4862 /** 4863 * show the node checkbox icons 4864 * @name show_checkboxes() 4865 * @plugin checkbox 4866 */ 4867 this.show_checkboxes = function () { this._data.core.themes.checkboxes = true; this.get_container_ul().removeClass("jstree-no-checkboxes"); }; 4868 /** 4869 * hide the node checkbox icons 4870 * @name hide_checkboxes() 4871 * @plugin checkbox 4872 */ 4873 this.hide_checkboxes = function () { this._data.core.themes.checkboxes = false; this.get_container_ul().addClass("jstree-no-checkboxes"); }; 4874 /** 4875 * toggle the node icons 4876 * @name toggle_checkboxes() 4877 * @plugin checkbox 4878 */ 4879 this.toggle_checkboxes = function () { if(this._data.core.themes.checkboxes) { this.hide_checkboxes(); } else { this.show_checkboxes(); } }; 4880 /** 4881 * checks if a node is in an undetermined state 4882 * @name is_undetermined(obj) 4883 * @param {mixed} obj 4884 * @return {Boolean} 4885 */ 4886 this.is_undetermined = function (obj) { 4887 obj = this.get_node(obj); 4888 var s = this.settings.checkbox.cascade, i, j, t = this.settings.checkbox.tie_selection, d = this._data[ t ? 'core' : 'checkbox' ].selected, m = this._model.data; 4889 if(!obj || obj.state[ t ? 'selected' : 'checked' ] === true || s.indexOf('undetermined') === -1 || (s.indexOf('down') === -1 && s.indexOf('up') === -1)) { 4890 return false; 4891 } 4892 if(!obj.state.loaded && obj.original.state.undetermined === true) { 4893 return true; 4894 } 4895 for(i = 0, j = obj.children_d.length; i < j; i++) { 4896 if($.inArray(obj.children_d[i], d) !== -1 || (!m[obj.children_d[i]].state.loaded && m[obj.children_d[i]].original.state.undetermined)) { 4897 return true; 4898 } 4899 } 4900 return false; 4901 }; 4902 4903 this.activate_node = function (obj, e) { 4904 if(this.settings.checkbox.tie_selection && (this.settings.checkbox.whole_node || $(e.target).hasClass('jstree-checkbox'))) { 4905 e.ctrlKey = true; 4906 } 4907 if(this.settings.checkbox.tie_selection || (!this.settings.checkbox.whole_node && !$(e.target).hasClass('jstree-checkbox'))) { 4908 return parent.activate_node.call(this, obj, e); 4909 } 4910 if(this.is_disabled(obj)) { 4911 return false; 4912 } 4913 if(this.is_checked(obj)) { 4914 this.uncheck_node(obj, e); 4915 } 4916 else { 4917 this.check_node(obj, e); 4918 } 4919 this.trigger('activate_node', { 'node' : this.get_node(obj) }); 4920 }; 4921 4922 /** 4923 * check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally) 4924 * @name check_node(obj) 4925 * @param {mixed} obj an array can be used to check multiple nodes 4926 * @trigger check_node.jstree 4927 * @plugin checkbox 4928 */ 4929 this.check_node = function (obj, e) { 4930 if(this.settings.checkbox.tie_selection) { return this.select_node(obj, false, true, e); } 4931 var dom, t1, t2, th; 4932 if($.isArray(obj)) { 4933 obj = obj.slice(); 4934 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 4935 this.check_node(obj[t1], e); 4936 } 4937 return true; 4938 } 4939 obj = this.get_node(obj); 4940 if(!obj || obj.id === '#') { 4941 return false; 4942 } 4943 dom = this.get_node(obj, true); 4944 if(!obj.state.checked) { 4945 obj.state.checked = true; 4946 this._data.checkbox.selected.push(obj.id); 4947 if(dom && dom.length) { 4948 dom.children('.jstree-anchor').addClass('jstree-checked'); 4949 } 4950 /** 4951 * triggered when an node is checked (only if tie_selection in checkbox settings is false) 4952 * @event 4953 * @name check_node.jstree 4954 * @param {Object} node 4955 * @param {Array} selected the current selection 4956 * @param {Object} event the event (if any) that triggered this check_node 4957 * @plugin checkbox 4958 */ 4959 this.trigger('check_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e }); 4960 } 4961 }; 4962 /** 4963 * uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally) 4964 * @name uncheck_node(obj) 4965 * @param {mixed} obj an array can be used to uncheck multiple nodes 4966 * @trigger uncheck_node.jstree 4967 * @plugin checkbox 4968 */ 4969 this.uncheck_node = function (obj, e) { 4970 if(this.settings.checkbox.tie_selection) { return this.deselect_node(obj, false, e); } 4971 var t1, t2, dom; 4972 if($.isArray(obj)) { 4973 obj = obj.slice(); 4974 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 4975 this.uncheck_node(obj[t1], e); 4976 } 4977 return true; 4978 } 4979 obj = this.get_node(obj); 4980 if(!obj || obj.id === '#') { 4981 return false; 4982 } 4983 dom = this.get_node(obj, true); 4984 if(obj.state.checked) { 4985 obj.state.checked = false; 4986 this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, obj.id); 4987 if(dom.length) { 4988 dom.children('.jstree-anchor').removeClass('jstree-checked'); 4989 } 4990 /** 4991 * triggered when an node is unchecked (only if tie_selection in checkbox settings is false) 4992 * @event 4993 * @name uncheck_node.jstree 4994 * @param {Object} node 4995 * @param {Array} selected the current selection 4996 * @param {Object} event the event (if any) that triggered this uncheck_node 4997 * @plugin checkbox 4998 */ 4999 this.trigger('uncheck_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e }); 5000 } 5001 }; 5002 /** 5003 * checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally) 5004 * @name check_all() 5005 * @trigger check_all.jstree, changed.jstree 5006 * @plugin checkbox 5007 */ 5008 this.check_all = function () { 5009 if(this.settings.checkbox.tie_selection) { return this.select_all(); } 5010 var tmp = this._data.checkbox.selected.concat([]), i, j; 5011 this._data.checkbox.selected = this._model.data['#'].children_d.concat(); 5012 for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) { 5013 if(this._model.data[this._data.checkbox.selected[i]]) { 5014 this._model.data[this._data.checkbox.selected[i]].state.checked = true; 5015 } 5016 } 5017 this.redraw(true); 5018 /** 5019 * triggered when all nodes are checked (only if tie_selection in checkbox settings is false) 5020 * @event 5021 * @name check_all.jstree 5022 * @param {Array} selected the current selection 5023 * @plugin checkbox 5024 */ 5025 this.trigger('check_all', { 'selected' : this._data.checkbox.selected }); 5026 }; 5027 /** 5028 * uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally) 5029 * @name uncheck_all() 5030 * @trigger uncheck_all.jstree 5031 * @plugin checkbox 5032 */ 5033 this.uncheck_all = function () { 5034 if(this.settings.checkbox.tie_selection) { return this.deselect_all(); } 5035 var tmp = this._data.checkbox.selected.concat([]), i, j; 5036 for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) { 5037 if(this._model.data[this._data.checkbox.selected[i]]) { 5038 this._model.data[this._data.checkbox.selected[i]].state.checked = false; 5039 } 5040 } 5041 this._data.checkbox.selected = []; 5042 this.element.find('.jstree-checked').removeClass('jstree-checked'); 5043 /** 5044 * triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false) 5045 * @event 5046 * @name uncheck_all.jstree 5047 * @param {Object} node the previous selection 5048 * @param {Array} selected the current selection 5049 * @plugin checkbox 5050 */ 5051 this.trigger('uncheck_all', { 'selected' : this._data.checkbox.selected, 'node' : tmp }); 5052 }; 5053 /** 5054 * checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected) 5055 * @name is_checked(obj) 5056 * @param {mixed} obj 5057 * @return {Boolean} 5058 * @plugin checkbox 5059 */ 5060 this.is_checked = function (obj) { 5061 if(this.settings.checkbox.tie_selection) { return this.is_selected(obj); } 5062 obj = this.get_node(obj); 5063 if(!obj || obj.id === '#') { return false; } 5064 return obj.state.checked; 5065 }; 5066 /** 5067 * get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected) 5068 * @name get_checked([full]) 5069 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned 5070 * @return {Array} 5071 * @plugin checkbox 5072 */ 5073 this.get_checked = function (full) { 5074 if(this.settings.checkbox.tie_selection) { return this.get_selected(full); } 5075 return full ? $.map(this._data.checkbox.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.checkbox.selected; 5076 }; 5077 /** 5078 * get an array of all top level checked nodes (ignoring children of checked nodes) (if tie_selection is on in the settings this function will return the same as get_top_selected) 5079 * @name get_top_checked([full]) 5080 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned 5081 * @return {Array} 5082 * @plugin checkbox 5083 */ 5084 this.get_top_checked = function (full) { 5085 if(this.settings.checkbox.tie_selection) { return this.get_top_selected(full); } 5086 var tmp = this.get_checked(true), 5087 obj = {}, i, j, k, l; 5088 for(i = 0, j = tmp.length; i < j; i++) { 5089 obj[tmp[i].id] = tmp[i]; 5090 } 5091 for(i = 0, j = tmp.length; i < j; i++) { 5092 for(k = 0, l = tmp[i].children_d.length; k < l; k++) { 5093 if(obj[tmp[i].children_d[k]]) { 5094 delete obj[tmp[i].children_d[k]]; 5095 } 5096 } 5097 } 5098 tmp = []; 5099 for(i in obj) { 5100 if(obj.hasOwnProperty(i)) { 5101 tmp.push(i); 5102 } 5103 } 5104 return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp; 5105 }; 5106 /** 5107 * get an array of all bottom level checked nodes (ignoring selected parents) (if tie_selection is on in the settings this function will return the same as get_bottom_selected) 5108 * @name get_bottom_checked([full]) 5109 * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned 5110 * @return {Array} 5111 * @plugin checkbox 5112 */ 5113 this.get_bottom_checked = function (full) { 5114 if(this.settings.checkbox.tie_selection) { return this.get_bottom_selected(full); } 5115 var tmp = this.get_checked(true), 5116 obj = [], i, j; 5117 for(i = 0, j = tmp.length; i < j; i++) { 5118 if(!tmp[i].children.length) { 5119 obj.push(tmp[i].id); 5120 } 5121 } 5122 return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj; 5123 }; 5124 this.load_node = function (obj, callback) { 5125 var k, l, i, j, c, tmp; 5126 if(!$.isArray(obj) && !this.settings.checkbox.tie_selection) { 5127 tmp = this.get_node(obj); 5128 if(tmp && tmp.state.loaded) { 5129 for(k = 0, l = tmp.children_d.length; k < l; k++) { 5130 if(this._model.data[tmp.children_d[k]].state.checked) { 5131 c = true; 5132 this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, tmp.children_d[k]); 5133 } 5134 } 5135 } 5136 } 5137 return parent.load_node.apply(this, arguments); 5138 }; 5139 this.get_state = function () { 5140 var state = parent.get_state.apply(this, arguments); 5141 if(this.settings.checkbox.tie_selection) { return state; } 5142 state.checkbox = this._data.checkbox.selected.slice(); 5143 return state; 5144 }; 5145 this.set_state = function (state, callback) { 5146 var res = parent.set_state.apply(this, arguments); 5147 if(res && state.checkbox) { 5148 if(!this.settings.checkbox.tie_selection) { 5149 this.uncheck_all(); 5150 var _this = this; 5151 $.each(state.checkbox, function (i, v) { 5152 _this.check_node(v); 5153 }); 5154 } 5155 delete state.checkbox; 5156 this.set_state(state, callback); 5157 return false; 5158 } 5159 return res; 5160 }; 5161 }; 5162 5163 // include the checkbox plugin by default 5164 // $.jstree.defaults.plugins.push("checkbox"); 5165 5166/** 5167 * ### Contextmenu plugin 5168 * 5169 * Shows a context menu when a node is right-clicked. 5170 */ 5171 5172 /** 5173 * stores all defaults for the contextmenu plugin 5174 * @name $.jstree.defaults.contextmenu 5175 * @plugin contextmenu 5176 */ 5177 $.jstree.defaults.contextmenu = { 5178 /** 5179 * a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`. 5180 * @name $.jstree.defaults.contextmenu.select_node 5181 * @plugin contextmenu 5182 */ 5183 select_node : true, 5184 /** 5185 * a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used. 5186 * @name $.jstree.defaults.contextmenu.show_at_node 5187 * @plugin contextmenu 5188 */ 5189 show_at_node : true, 5190 /** 5191 * an object of actions, or a function that accepts a node and a callback function and calls the callback function with an object of actions available for that node (you can also return the items too). 5192 * 5193 * Each action consists of a key (a unique name) and a value which is an object with the following properties (only label and action are required): 5194 * 5195 * * `separator_before` - a boolean indicating if there should be a separator before this item 5196 * * `separator_after` - a boolean indicating if there should be a separator after this item 5197 * * `_disabled` - a boolean indicating if this action should be disabled 5198 * * `label` - a string - the name of the action (could be a function returning a string) 5199 * * `action` - a function to be executed if this item is chosen 5200 * * `icon` - a string, can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class 5201 * * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2) 5202 * * `shortcut_label` - shortcut label (like for example `F2` for rename) 5203 * 5204 * @name $.jstree.defaults.contextmenu.items 5205 * @plugin contextmenu 5206 */ 5207 items : function (o, cb) { // Could be an object directly 5208 return { 5209 "create" : { 5210 "separator_before" : false, 5211 "separator_after" : true, 5212 "_disabled" : false, //(this.check("create_node", data.reference, {}, "last")), 5213 "label" : "Create", 5214 "action" : function (data) { 5215 var inst = $.jstree.reference(data.reference), 5216 obj = inst.get_node(data.reference); 5217 inst.create_node(obj, {}, "last", function (new_node) { 5218 setTimeout(function () { inst.edit(new_node); },0); 5219 }); 5220 } 5221 }, 5222 "rename" : { 5223 "separator_before" : false, 5224 "separator_after" : false, 5225 "_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")), 5226 "label" : "Rename", 5227 /* 5228 "shortcut" : 113, 5229 "shortcut_label" : 'F2', 5230 "icon" : "glyphicon glyphicon-leaf", 5231 */ 5232 "action" : function (data) { 5233 var inst = $.jstree.reference(data.reference), 5234 obj = inst.get_node(data.reference); 5235 inst.edit(obj); 5236 } 5237 }, 5238 "remove" : { 5239 "separator_before" : false, 5240 "icon" : false, 5241 "separator_after" : false, 5242 "_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")), 5243 "label" : "Delete", 5244 "action" : function (data) { 5245 var inst = $.jstree.reference(data.reference), 5246 obj = inst.get_node(data.reference); 5247 if(inst.is_selected(obj)) { 5248 inst.delete_node(inst.get_selected()); 5249 } 5250 else { 5251 inst.delete_node(obj); 5252 } 5253 } 5254 }, 5255 "ccp" : { 5256 "separator_before" : true, 5257 "icon" : false, 5258 "separator_after" : false, 5259 "label" : "Edit", 5260 "action" : false, 5261 "submenu" : { 5262 "cut" : { 5263 "separator_before" : false, 5264 "separator_after" : false, 5265 "label" : "Cut", 5266 "action" : function (data) { 5267 var inst = $.jstree.reference(data.reference), 5268 obj = inst.get_node(data.reference); 5269 if(inst.is_selected(obj)) { 5270 inst.cut(inst.get_top_selected()); 5271 } 5272 else { 5273 inst.cut(obj); 5274 } 5275 } 5276 }, 5277 "copy" : { 5278 "separator_before" : false, 5279 "icon" : false, 5280 "separator_after" : false, 5281 "label" : "Copy", 5282 "action" : function (data) { 5283 var inst = $.jstree.reference(data.reference), 5284 obj = inst.get_node(data.reference); 5285 if(inst.is_selected(obj)) { 5286 inst.copy(inst.get_top_selected()); 5287 } 5288 else { 5289 inst.copy(obj); 5290 } 5291 } 5292 }, 5293 "paste" : { 5294 "separator_before" : false, 5295 "icon" : false, 5296 "_disabled" : function (data) { 5297 return !$.jstree.reference(data.reference).can_paste(); 5298 }, 5299 "separator_after" : false, 5300 "label" : "Paste", 5301 "action" : function (data) { 5302 var inst = $.jstree.reference(data.reference), 5303 obj = inst.get_node(data.reference); 5304 inst.paste(obj); 5305 } 5306 } 5307 } 5308 } 5309 }; 5310 } 5311 }; 5312 5313 $.jstree.plugins.contextmenu = function (options, parent) { 5314 this.bind = function () { 5315 parent.bind.call(this); 5316 5317 var last_ts = 0, cto = null, ex, ey; 5318 this.element 5319 .on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e, data) { 5320 e.preventDefault(); 5321 last_ts = e.ctrlKey ? +new Date() : 0; 5322 if(data || cto) { 5323 last_ts = (+new Date()) + 10000; 5324 } 5325 if(cto) { 5326 clearTimeout(cto); 5327 } 5328 if(!this.is_loading(e.currentTarget)) { 5329 this.show_contextmenu(e.currentTarget, e.pageX, e.pageY, e); 5330 } 5331 }, this)) 5332 .on("click.jstree", ".jstree-anchor", $.proxy(function (e) { 5333 if(this._data.contextmenu.visible && (!last_ts || (+new Date()) - last_ts > 250)) { // work around safari & macOS ctrl+click 5334 $.vakata.context.hide(); 5335 } 5336 last_ts = 0; 5337 }, this)) 5338 .on("touchstart.jstree", ".jstree-anchor", function (e) { 5339 if(!e.originalEvent || !e.originalEvent.changedTouches || !e.originalEvent.changedTouches[0]) { 5340 return; 5341 } 5342 ex = e.pageX; 5343 ey = e.pageY; 5344 cto = setTimeout(function () { 5345 $(e.currentTarget).trigger('contextmenu', true); 5346 }, 750); 5347 }) 5348 .on('touchmove.vakata.jstree', function (e) { 5349 if(cto && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0] && (Math.abs(ex - e.pageX) > 50 || Math.abs(ey - e.pageY) > 50)) { 5350 clearTimeout(cto); 5351 } 5352 }) 5353 .on('touchend.vakata.jstree', function (e) { 5354 if(cto) { 5355 clearTimeout(cto); 5356 } 5357 }); 5358 5359 /* 5360 if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) { 5361 var el = null, tm = null; 5362 this.element 5363 .on("touchstart", ".jstree-anchor", function (e) { 5364 el = e.currentTarget; 5365 tm = +new Date(); 5366 $(document).one("touchend", function (e) { 5367 e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset); 5368 e.currentTarget = e.target; 5369 tm = ((+(new Date())) - tm); 5370 if(e.target === el && tm > 600 && tm < 1000) { 5371 e.preventDefault(); 5372 $(el).trigger('contextmenu', e); 5373 } 5374 el = null; 5375 tm = null; 5376 }); 5377 }); 5378 } 5379 */ 5380 $(document).on("context_hide.vakata.jstree", $.proxy(function () { this._data.contextmenu.visible = false; }, this)); 5381 }; 5382 this.teardown = function () { 5383 if(this._data.contextmenu.visible) { 5384 $.vakata.context.hide(); 5385 } 5386 parent.teardown.call(this); 5387 }; 5388 5389 /** 5390 * prepare and show the context menu for a node 5391 * @name show_contextmenu(obj [, x, y]) 5392 * @param {mixed} obj the node 5393 * @param {Number} x the x-coordinate relative to the document to show the menu at 5394 * @param {Number} y the y-coordinate relative to the document to show the menu at 5395 * @param {Object} e the event if available that triggered the contextmenu 5396 * @plugin contextmenu 5397 * @trigger show_contextmenu.jstree 5398 */ 5399 this.show_contextmenu = function (obj, x, y, e) { 5400 obj = this.get_node(obj); 5401 if(!obj || obj.id === '#') { return false; } 5402 var s = this.settings.contextmenu, 5403 d = this.get_node(obj, true), 5404 a = d.children(".jstree-anchor"), 5405 o = false, 5406 i = false; 5407 if(s.show_at_node || x === undefined || y === undefined) { 5408 o = a.offset(); 5409 x = o.left; 5410 y = o.top + this._data.core.li_height; 5411 } 5412 if(this.settings.contextmenu.select_node && !this.is_selected(obj)) { 5413 this.activate_node(obj, e); 5414 } 5415 5416 i = s.items; 5417 if($.isFunction(i)) { 5418 i = i.call(this, obj, $.proxy(function (i) { 5419 this._show_contextmenu(obj, x, y, i); 5420 }, this)); 5421 } 5422 if($.isPlainObject(i)) { 5423 this._show_contextmenu(obj, x, y, i); 5424 } 5425 }; 5426 /** 5427 * show the prepared context menu for a node 5428 * @name _show_contextmenu(obj, x, y, i) 5429 * @param {mixed} obj the node 5430 * @param {Number} x the x-coordinate relative to the document to show the menu at 5431 * @param {Number} y the y-coordinate relative to the document to show the menu at 5432 * @param {Number} i the object of items to show 5433 * @plugin contextmenu 5434 * @trigger show_contextmenu.jstree 5435 * @private 5436 */ 5437 this._show_contextmenu = function (obj, x, y, i) { 5438 var d = this.get_node(obj, true), 5439 a = d.children(".jstree-anchor"); 5440 $(document).one("context_show.vakata.jstree", $.proxy(function (e, data) { 5441 var cls = 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu'; 5442 $(data.element).addClass(cls); 5443 }, this)); 5444 this._data.contextmenu.visible = true; 5445 $.vakata.context.show(a, { 'x' : x, 'y' : y }, i); 5446 /** 5447 * triggered when the contextmenu is shown for a node 5448 * @event 5449 * @name show_contextmenu.jstree 5450 * @param {Object} node the node 5451 * @param {Number} x the x-coordinate of the menu relative to the document 5452 * @param {Number} y the y-coordinate of the menu relative to the document 5453 * @plugin contextmenu 5454 */ 5455 this.trigger('show_contextmenu', { "node" : obj, "x" : x, "y" : y }); 5456 }; 5457 }; 5458 5459 // contextmenu helper 5460 (function ($) { 5461 var right_to_left = false, 5462 vakata_context = { 5463 element : false, 5464 reference : false, 5465 position_x : 0, 5466 position_y : 0, 5467 items : [], 5468 html : "", 5469 is_visible : false 5470 }; 5471 5472 $.vakata.context = { 5473 settings : { 5474 hide_onmouseleave : 0, 5475 icons : true 5476 }, 5477 _trigger : function (event_name) { 5478 $(document).triggerHandler("context_" + event_name + ".vakata", { 5479 "reference" : vakata_context.reference, 5480 "element" : vakata_context.element, 5481 "position" : { 5482 "x" : vakata_context.position_x, 5483 "y" : vakata_context.position_y 5484 } 5485 }); 5486 }, 5487 _execute : function (i) { 5488 i = vakata_context.items[i]; 5489 return i && (!i._disabled || ($.isFunction(i._disabled) && !i._disabled({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }))) && i.action ? i.action.call(null, { 5490 "item" : i, 5491 "reference" : vakata_context.reference, 5492 "element" : vakata_context.element, 5493 "position" : { 5494 "x" : vakata_context.position_x, 5495 "y" : vakata_context.position_y 5496 } 5497 }) : false; 5498 }, 5499 _parse : function (o, is_callback) { 5500 if(!o) { return false; } 5501 if(!is_callback) { 5502 vakata_context.html = ""; 5503 vakata_context.items = []; 5504 } 5505 var str = "", 5506 sep = false, 5507 tmp; 5508 5509 if(is_callback) { str += "<"+"ul>"; } 5510 $.each(o, function (i, val) { 5511 if(!val) { return true; } 5512 vakata_context.items.push(val); 5513 if(!sep && val.separator_before) { 5514 str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>"; 5515 } 5516 sep = false; 5517 str += "<"+"li class='" + (val._class || "") + (val._disabled === true || ($.isFunction(val._disabled) && val._disabled({ "item" : val, "reference" : vakata_context.reference, "element" : vakata_context.element })) ? " vakata-contextmenu-disabled " : "") + "' "+(val.shortcut?" data-shortcut='"+val.shortcut+"' ":'')+">"; 5518 str += "<"+"a href='#' rel='" + (vakata_context.items.length - 1) + "'>"; 5519 if($.vakata.context.settings.icons) { 5520 str += "<"+"i "; 5521 if(val.icon) { 5522 if(val.icon.indexOf("/") !== -1 || val.icon.indexOf(".") !== -1) { str += " style='background:url(\"" + val.icon + "\") center center no-repeat' "; } 5523 else { str += " class='" + val.icon + "' "; } 5524 } 5525 str += "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>"; 5526 } 5527 str += ($.isFunction(val.label) ? val.label({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }) : val.label) + (val.shortcut?' <span class="vakata-contextmenu-shortcut vakata-contextmenu-shortcut-'+val.shortcut+'">'+ (val.shortcut_label || '') +'</span>':'') + "<"+"/a>"; 5528 if(val.submenu) { 5529 tmp = $.vakata.context._parse(val.submenu, true); 5530 if(tmp) { str += tmp; } 5531 } 5532 str += "<"+"/li>"; 5533 if(val.separator_after) { 5534 str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>"; 5535 sep = true; 5536 } 5537 }); 5538 str = str.replace(/<li class\='vakata-context-separator'\><\/li\>$/,""); 5539 if(is_callback) { str += "</ul>"; } 5540 /** 5541 * triggered on the document when the contextmenu is parsed (HTML is built) 5542 * @event 5543 * @plugin contextmenu 5544 * @name context_parse.vakata 5545 * @param {jQuery} reference the element that was right clicked 5546 * @param {jQuery} element the DOM element of the menu itself 5547 * @param {Object} position the x & y coordinates of the menu 5548 */ 5549 if(!is_callback) { vakata_context.html = str; $.vakata.context._trigger("parse"); } 5550 return str.length > 10 ? str : false; 5551 }, 5552 _show_submenu : function (o) { 5553 o = $(o); 5554 if(!o.length || !o.children("ul").length) { return; } 5555 var e = o.children("ul"), 5556 x = o.offset().left + o.outerWidth(), 5557 y = o.offset().top, 5558 w = e.width(), 5559 h = e.height(), 5560 dw = $(window).width() + $(window).scrollLeft(), 5561 dh = $(window).height() + $(window).scrollTop(); 5562 // може да се спести е една проверка - дали няма някой от класовете вече нагоре 5563 if(right_to_left) { 5564 o[x - (w + 10 + o.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left"); 5565 } 5566 else { 5567 o[x + w + 10 > dw ? "addClass" : "removeClass"]("vakata-context-right"); 5568 } 5569 if(y + h + 10 > dh) { 5570 e.css("bottom","-1px"); 5571 } 5572 e.show(); 5573 }, 5574 show : function (reference, position, data) { 5575 var o, e, x, y, w, h, dw, dh, cond = true; 5576 if(vakata_context.element && vakata_context.element.length) { 5577 vakata_context.element.width(''); 5578 } 5579 switch(cond) { 5580 case (!position && !reference): 5581 return false; 5582 case (!!position && !!reference): 5583 vakata_context.reference = reference; 5584 vakata_context.position_x = position.x; 5585 vakata_context.position_y = position.y; 5586 break; 5587 case (!position && !!reference): 5588 vakata_context.reference = reference; 5589 o = reference.offset(); 5590 vakata_context.position_x = o.left + reference.outerHeight(); 5591 vakata_context.position_y = o.top; 5592 break; 5593 case (!!position && !reference): 5594 vakata_context.position_x = position.x; 5595 vakata_context.position_y = position.y; 5596 break; 5597 } 5598 if(!!reference && !data && $(reference).data('vakata_contextmenu')) { 5599 data = $(reference).data('vakata_contextmenu'); 5600 } 5601 if($.vakata.context._parse(data)) { 5602 vakata_context.element.html(vakata_context.html); 5603 } 5604 if(vakata_context.items.length) { 5605 vakata_context.element.appendTo("body"); 5606 e = vakata_context.element; 5607 x = vakata_context.position_x; 5608 y = vakata_context.position_y; 5609 w = e.width(); 5610 h = e.height(); 5611 dw = $(window).width() + $(window).scrollLeft(); 5612 dh = $(window).height() + $(window).scrollTop(); 5613 if(right_to_left) { 5614 x -= (e.outerWidth() - $(reference).outerWidth()); 5615 if(x < $(window).scrollLeft() + 20) { 5616 x = $(window).scrollLeft() + 20; 5617 } 5618 } 5619 if(x + w + 20 > dw) { 5620 x = dw - (w + 20); 5621 } 5622 if(y + h + 20 > dh) { 5623 y = dh - (h + 20); 5624 } 5625 5626 vakata_context.element 5627 .css({ "left" : x, "top" : y }) 5628 .show() 5629 .find('a').first().focus().parent().addClass("vakata-context-hover"); 5630 vakata_context.is_visible = true; 5631 /** 5632 * triggered on the document when the contextmenu is shown 5633 * @event 5634 * @plugin contextmenu 5635 * @name context_show.vakata 5636 * @param {jQuery} reference the element that was right clicked 5637 * @param {jQuery} element the DOM element of the menu itself 5638 * @param {Object} position the x & y coordinates of the menu 5639 */ 5640 $.vakata.context._trigger("show"); 5641 } 5642 }, 5643 hide : function () { 5644 if(vakata_context.is_visible) { 5645 vakata_context.element.hide().find("ul").hide().end().find(':focus').blur().end().detach(); 5646 vakata_context.is_visible = false; 5647 /** 5648 * triggered on the document when the contextmenu is hidden 5649 * @event 5650 * @plugin contextmenu 5651 * @name context_hide.vakata 5652 * @param {jQuery} reference the element that was right clicked 5653 * @param {jQuery} element the DOM element of the menu itself 5654 * @param {Object} position the x & y coordinates of the menu 5655 */ 5656 $.vakata.context._trigger("hide"); 5657 } 5658 } 5659 }; 5660 $(function () { 5661 right_to_left = $("body").css("direction") === "rtl"; 5662 var to = false; 5663 5664 vakata_context.element = $("<ul class='vakata-context'></ul>"); 5665 vakata_context.element 5666 .on("mouseenter", "li", function (e) { 5667 e.stopImmediatePropagation(); 5668 5669 if($.contains(this, e.relatedTarget)) { 5670 // премахнато заради delegate mouseleave по-долу 5671 // $(this).find(".vakata-context-hover").removeClass("vakata-context-hover"); 5672 return; 5673 } 5674 5675 if(to) { clearTimeout(to); } 5676 vakata_context.element.find(".vakata-context-hover").removeClass("vakata-context-hover").end(); 5677 5678 $(this) 5679 .siblings().find("ul").hide().end().end() 5680 .parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover"); 5681 $.vakata.context._show_submenu(this); 5682 }) 5683 // тестово - дали не натоварва? 5684 .on("mouseleave", "li", function (e) { 5685 if($.contains(this, e.relatedTarget)) { return; } 5686 $(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover"); 5687 }) 5688 .on("mouseleave", function (e) { 5689 $(this).find(".vakata-context-hover").removeClass("vakata-context-hover"); 5690 if($.vakata.context.settings.hide_onmouseleave) { 5691 to = setTimeout( 5692 (function (t) { 5693 return function () { $.vakata.context.hide(); }; 5694 }(this)), $.vakata.context.settings.hide_onmouseleave); 5695 } 5696 }) 5697 .on("click", "a", function (e) { 5698 e.preventDefault(); 5699 //}) 5700 //.on("mouseup", "a", function (e) { 5701 if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata.context._execute($(this).attr("rel")) !== false) { 5702 $.vakata.context.hide(); 5703 } 5704 }) 5705 .on('keydown', 'a', function (e) { 5706 var o = null; 5707 switch(e.which) { 5708 case 13: 5709 case 32: 5710 e.type = "mouseup"; 5711 e.preventDefault(); 5712 $(e.currentTarget).trigger(e); 5713 break; 5714 case 37: 5715 if(vakata_context.is_visible) { 5716 vakata_context.element.find(".vakata-context-hover").last().closest("li").first().find("ul").hide().find(".vakata-context-hover").removeClass("vakata-context-hover").end().end().children('a').focus(); 5717 e.stopImmediatePropagation(); 5718 e.preventDefault(); 5719 } 5720 break; 5721 case 38: 5722 if(vakata_context.is_visible) { 5723 o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first(); 5724 if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); } 5725 o.addClass("vakata-context-hover").children('a').focus(); 5726 e.stopImmediatePropagation(); 5727 e.preventDefault(); 5728 } 5729 break; 5730 case 39: 5731 if(vakata_context.is_visible) { 5732 vakata_context.element.find(".vakata-context-hover").last().children("ul").show().children("li:not(.vakata-context-separator)").removeClass("vakata-context-hover").first().addClass("vakata-context-hover").children('a').focus(); 5733 e.stopImmediatePropagation(); 5734 e.preventDefault(); 5735 } 5736 break; 5737 case 40: 5738 if(vakata_context.is_visible) { 5739 o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first(); 5740 if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); } 5741 o.addClass("vakata-context-hover").children('a').focus(); 5742 e.stopImmediatePropagation(); 5743 e.preventDefault(); 5744 } 5745 break; 5746 case 27: 5747 $.vakata.context.hide(); 5748 e.preventDefault(); 5749 break; 5750 default: 5751 //console.log(e.which); 5752 break; 5753 } 5754 }) 5755 .on('keydown', function (e) { 5756 e.preventDefault(); 5757 var a = vakata_context.element.find('.vakata-contextmenu-shortcut-' + e.which).parent(); 5758 if(a.parent().not('.vakata-context-disabled')) { 5759 a.click(); 5760 } 5761 }); 5762 5763 $(document) 5764 .on("mousedown.vakata.jstree", function (e) { 5765 if(vakata_context.is_visible && !$.contains(vakata_context.element[0], e.target)) { 5766 $.vakata.context.hide(); 5767 } 5768 }) 5769 .on("context_show.vakata.jstree", function (e, data) { 5770 vakata_context.element.find("li:has(ul)").children("a").addClass("vakata-context-parent"); 5771 if(right_to_left) { 5772 vakata_context.element.addClass("vakata-context-rtl").css("direction", "rtl"); 5773 } 5774 // also apply a RTL class? 5775 vakata_context.element.find("ul").hide().end(); 5776 }); 5777 }); 5778 }($)); 5779 // $.jstree.defaults.plugins.push("contextmenu"); 5780 5781/** 5782 * ### Drag'n'drop plugin 5783 * 5784 * Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations. 5785 */ 5786 5787 /** 5788 * stores all defaults for the drag'n'drop plugin 5789 * @name $.jstree.defaults.dnd 5790 * @plugin dnd 5791 */ 5792 $.jstree.defaults.dnd = { 5793 /** 5794 * a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`. 5795 * @name $.jstree.defaults.dnd.copy 5796 * @plugin dnd 5797 */ 5798 copy : true, 5799 /** 5800 * a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`. 5801 * @name $.jstree.defaults.dnd.open_timeout 5802 * @plugin dnd 5803 */ 5804 open_timeout : 500, 5805 /** 5806 * a function invoked each time a node is about to be dragged, invoked in the tree's scope and receives the nodes about to be dragged as an argument (array) - return `false` to prevent dragging 5807 * @name $.jstree.defaults.dnd.is_draggable 5808 * @plugin dnd 5809 */ 5810 is_draggable : true, 5811 /** 5812 * a boolean indicating if checks should constantly be made while the user is dragging the node (as opposed to checking only on drop), default is `true` 5813 * @name $.jstree.defaults.dnd.check_while_dragging 5814 * @plugin dnd 5815 */ 5816 check_while_dragging : true, 5817 /** 5818 * a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false` 5819 * @name $.jstree.defaults.dnd.always_copy 5820 * @plugin dnd 5821 */ 5822 always_copy : false, 5823 /** 5824 * when dropping a node "inside", this setting indicates the position the node should go to - it can be an integer or a string: "first" (same as 0) or "last", default is `0` 5825 * @name $.jstree.defaults.dnd.inside_pos 5826 * @plugin dnd 5827 */ 5828 inside_pos : 0, 5829 /** 5830 * when starting the drag on a node that is selected this setting controls if all selected nodes are dragged or only the single node, default is `true`, which means all selected nodes are dragged when the drag is started on a selected node 5831 * @name $.jstree.defaults.dnd.drag_selection 5832 * @plugin dnd 5833 */ 5834 drag_selection : true, 5835 /** 5836 * controls whether dnd works on touch devices. If left as boolean true dnd will work the same as in desktop browsers, which in some cases may impair scrolling. If set to boolean false dnd will not work on touch devices. There is a special third option - string "selected" which means only selected nodes can be dragged on touch devices. 5837 * @name $.jstree.defaults.dnd.touch 5838 * @plugin dnd 5839 */ 5840 touch : true, 5841 /** 5842 * controls whether items can be dropped anywhere on the node, not just on the anchor, by default only the node anchor is a valid drop target. Works best with the wholerow plugin. If enabled on mobile depending on the interface it might be hard for the user to cancel the drop, since the whole tree container will be a valid drop target. 5843 * @name $.jstree.defaults.dnd.large_drop_target 5844 * @plugin dnd 5845 */ 5846 large_drop_target : false, 5847 /** 5848 * controls whether a drag can be initiated from any part of the node and not just the text/icon part, works best with the wholerow plugin. Keep in mind it can cause problems with tree scrolling on mobile depending on the interface - in that case set the touch option to "selected". 5849 * @name $.jstree.defaults.dnd.large_drag_target 5850 * @plugin dnd 5851 */ 5852 large_drag_target : false 5853 }; 5854 // TODO: now check works by checking for each node individually, how about max_children, unique, etc? 5855 $.jstree.plugins.dnd = function (options, parent) { 5856 this.bind = function () { 5857 parent.bind.call(this); 5858 5859 this.element 5860 .on('mousedown.jstree touchstart.jstree', this.settings.dnd.large_drag_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) { 5861 if(this.settings.dnd.large_drag_target && $(e.target).closest('.jstree-node')[0] !== e.currentTarget) { 5862 return true; 5863 } 5864 if(e.type === "touchstart" && (!this.settings.dnd.touch || (this.settings.dnd.touch === 'selected' && !$(e.currentTarget).closest('.jstree-node').children('.jstree-anchor').hasClass('jstree-clicked')))) { 5865 return true; 5866 } 5867 var obj = this.get_node(e.target), 5868 mlt = this.is_selected(obj) && this.settings.dnd.drag_selection ? this.get_top_selected().length : 1, 5869 txt = (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget)); 5870 if(this.settings.core.force_text) { 5871 txt = $.vakata.html.escape(txt); 5872 } 5873 if(obj && obj.id && obj.id !== "#" && (e.which === 1 || e.type === "touchstart") && 5874 (this.settings.dnd.is_draggable === true || ($.isFunction(this.settings.dnd.is_draggable) && this.settings.dnd.is_draggable.call(this, (mlt > 1 ? this.get_top_selected(true) : [obj])))) 5875 ) { 5876 this.element.trigger('mousedown.jstree'); 5877 return $.vakata.dnd.start(e, { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_top_selected() : [obj.id] }, '<div id="jstree-dnd" class="jstree-' + this.get_theme() + ' jstree-' + this.get_theme() + '-' + this.get_theme_variant() + ' ' + ( this.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' ) + '"><i class="jstree-icon jstree-er"></i>' + txt + '<ins class="jstree-copy" style="display:none;">+</ins></div>'); 5878 } 5879 }, this)); 5880 }; 5881 }; 5882 5883 $(function() { 5884 // bind only once for all instances 5885 var lastmv = false, 5886 laster = false, 5887 opento = false, 5888 marker = $('<div id="jstree-marker"> </div>').hide(); //.appendTo('body'); 5889 5890 $(document) 5891 .on('dnd_start.vakata.jstree', function (e, data) { 5892 lastmv = false; 5893 if(!data || !data.data || !data.data.jstree) { return; } 5894 marker.appendTo('body'); //.show(); 5895 }) 5896 .on('dnd_move.vakata.jstree', function (e, data) { 5897 if(opento) { clearTimeout(opento); } 5898 if(!data || !data.data || !data.data.jstree) { return; } 5899 5900 // if we are hovering the marker image do nothing (can happen on "inside" drags) 5901 if(data.event.target.id && data.event.target.id === 'jstree-marker') { 5902 return; 5903 } 5904 5905 var ins = $.jstree.reference(data.event.target), 5906 ref = false, 5907 off = false, 5908 rel = false, 5909 tmp, l, t, h, p, i, o, ok, t1, t2, op, ps, pr, ip, tm; 5910 // if we are over an instance 5911 if(ins && ins._data && ins._data.dnd) { 5912 marker.attr('class', 'jstree-' + ins.get_theme() + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' )); 5913 data.helper 5914 .children().attr('class', 'jstree-' + ins.get_theme() + ' jstree-' + ins.get_theme() + '-' + ins.get_theme_variant() + ' ' + ( ins.settings.core.themes.responsive ? ' jstree-dnd-responsive' : '' )) 5915 .find('.jstree-copy').first()[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'show' : 'hide' ](); 5916 5917 5918 // if are hovering the container itself add a new root node 5919 if( (data.event.target === ins.element[0] || data.event.target === ins.get_container_ul()[0]) && ins.get_container_ul().children().length === 0) { 5920 ok = true; 5921 for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) { 5922 ok = ok && ins.check( (data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey)) ) ? "copy_node" : "move_node"), (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), '#', 'last', { 'dnd' : true, 'ref' : ins.get_node('#'), 'pos' : 'i', 'origin' : data.data.origin, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }); 5923 if(!ok) { break; } 5924 } 5925 if(ok) { 5926 lastmv = { 'ins' : ins, 'par' : '#', 'pos' : 'last' }; 5927 marker.hide(); 5928 data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok'); 5929 return; 5930 } 5931 } 5932 else { 5933 // if we are hovering a tree node 5934 ref = ins.settings.dnd.large_drop_target ? $(data.event.target).closest('.jstree-node').children('.jstree-anchor') : $(data.event.target).closest('.jstree-anchor'); 5935 if(ref && ref.length && ref.parent().is('.jstree-closed, .jstree-open, .jstree-leaf')) { 5936 off = ref.offset(); 5937 rel = data.event.pageY - off.top; 5938 h = ref.outerHeight(); 5939 if(rel < h / 3) { 5940 o = ['b', 'i', 'a']; 5941 } 5942 else if(rel > h - h / 3) { 5943 o = ['a', 'i', 'b']; 5944 } 5945 else { 5946 o = rel > h / 2 ? ['i', 'a', 'b'] : ['i', 'b', 'a']; 5947 } 5948 $.each(o, function (j, v) { 5949 switch(v) { 5950 case 'b': 5951 l = off.left - 6; 5952 t = off.top; 5953 p = ins.get_parent(ref); 5954 i = ref.parent().index(); 5955 break; 5956 case 'i': 5957 ip = ins.settings.dnd.inside_pos; 5958 tm = ins.get_node(ref.parent()); 5959 l = off.left - 2; 5960 t = off.top + h / 2 + 1; 5961 p = tm.id; 5962 i = ip === 'first' ? 0 : (ip === 'last' ? tm.children.length : Math.min(ip, tm.children.length)); 5963 break; 5964 case 'a': 5965 l = off.left - 6; 5966 t = off.top + h; 5967 p = ins.get_parent(ref); 5968 i = ref.parent().index() + 1; 5969 break; 5970 } 5971 ok = true; 5972 for(t1 = 0, t2 = data.data.nodes.length; t1 < t2; t1++) { 5973 op = data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? "copy_node" : "move_node"; 5974 ps = i; 5975 if(op === "move_node" && v === 'a' && (data.data.origin && data.data.origin === ins) && p === ins.get_parent(data.data.nodes[t1])) { 5976 pr = ins.get_node(p); 5977 if(ps > $.inArray(data.data.nodes[t1], pr.children)) { 5978 ps -= 1; 5979 } 5980 } 5981 ok = ok && ( (ins && ins.settings && ins.settings.dnd && ins.settings.dnd.check_while_dragging === false) || ins.check(op, (data.data.origin && data.data.origin !== ins ? data.data.origin.get_node(data.data.nodes[t1]) : data.data.nodes[t1]), p, ps, { 'dnd' : true, 'ref' : ins.get_node(ref.parent()), 'pos' : v, 'origin' : data.data.origin, 'is_multi' : (data.data.origin && data.data.origin !== ins), 'is_foreign' : (!data.data.origin) }) ); 5982 if(!ok) { 5983 if(ins && ins.last_error) { laster = ins.last_error(); } 5984 break; 5985 } 5986 } 5987 if(v === 'i' && ref.parent().is('.jstree-closed') && ins.settings.dnd.open_timeout) { 5988 opento = setTimeout((function (x, z) { return function () { x.open_node(z); }; }(ins, ref)), ins.settings.dnd.open_timeout); 5989 } 5990 if(ok) { 5991 lastmv = { 'ins' : ins, 'par' : p, 'pos' : v === 'i' && ip === 'last' && i === 0 && !ins.is_loaded(tm) ? 'last' : i }; 5992 marker.css({ 'left' : l + 'px', 'top' : t + 'px' }).show(); 5993 data.helper.find('.jstree-icon').first().removeClass('jstree-er').addClass('jstree-ok'); 5994 laster = {}; 5995 o = true; 5996 return false; 5997 } 5998 }); 5999 if(o === true) { return; } 6000 } 6001 } 6002 } 6003 lastmv = false; 6004 data.helper.find('.jstree-icon').removeClass('jstree-ok').addClass('jstree-er'); 6005 marker.hide(); 6006 }) 6007 .on('dnd_scroll.vakata.jstree', function (e, data) { 6008 if(!data || !data.data || !data.data.jstree) { return; } 6009 marker.hide(); 6010 lastmv = false; 6011 data.helper.find('.jstree-icon').first().removeClass('jstree-ok').addClass('jstree-er'); 6012 }) 6013 .on('dnd_stop.vakata.jstree', function (e, data) { 6014 if(opento) { clearTimeout(opento); } 6015 if(!data || !data.data || !data.data.jstree) { return; } 6016 marker.hide().detach(); 6017 var i, j, nodes = []; 6018 if(lastmv) { 6019 for(i = 0, j = data.data.nodes.length; i < j; i++) { 6020 nodes[i] = data.data.origin ? data.data.origin.get_node(data.data.nodes[i]) : data.data.nodes[i]; 6021 } 6022 lastmv.ins[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (data.event.metaKey || data.event.ctrlKey))) ? 'copy_node' : 'move_node' ](nodes, lastmv.par, lastmv.pos, false, false, false, data.data.origin); 6023 } 6024 else { 6025 i = $(data.event.target).closest('.jstree'); 6026 if(i.length && laster && laster.error && laster.error === 'check') { 6027 i = i.jstree(true); 6028 if(i) { 6029 i.settings.core.error.call(this, laster); 6030 } 6031 } 6032 } 6033 }) 6034 .on('keyup.jstree keydown.jstree', function (e, data) { 6035 data = $.vakata.dnd._get(); 6036 if(data && data.data && data.data.jstree) { 6037 data.helper.find('.jstree-copy').first()[ data.data.origin && (data.data.origin.settings.dnd.always_copy || (data.data.origin.settings.dnd.copy && (e.metaKey || e.ctrlKey))) ? 'show' : 'hide' ](); 6038 } 6039 }); 6040 }); 6041 6042 // helpers 6043 (function ($) { 6044 $.vakata.html = { 6045 div : $('<div />'), 6046 escape : function (str) { 6047 return $.vakata.html.div.text(str).html(); 6048 }, 6049 strip : function (str) { 6050 return $.vakata.html.div.empty().append($.parseHTML(str)).text(); 6051 } 6052 }; 6053 // private variable 6054 var vakata_dnd = { 6055 element : false, 6056 target : false, 6057 is_down : false, 6058 is_drag : false, 6059 helper : false, 6060 helper_w: 0, 6061 data : false, 6062 init_x : 0, 6063 init_y : 0, 6064 scroll_l: 0, 6065 scroll_t: 0, 6066 scroll_e: false, 6067 scroll_i: false, 6068 is_touch: false 6069 }; 6070 $.vakata.dnd = { 6071 settings : { 6072 scroll_speed : 10, 6073 scroll_proximity : 20, 6074 helper_left : 5, 6075 helper_top : 10, 6076 threshold : 5, 6077 threshold_touch : 50 6078 }, 6079 _trigger : function (event_name, e) { 6080 var data = $.vakata.dnd._get(); 6081 data.event = e; 6082 $(document).triggerHandler("dnd_" + event_name + ".vakata", data); 6083 }, 6084 _get : function () { 6085 return { 6086 "data" : vakata_dnd.data, 6087 "element" : vakata_dnd.element, 6088 "helper" : vakata_dnd.helper 6089 }; 6090 }, 6091 _clean : function () { 6092 if(vakata_dnd.helper) { vakata_dnd.helper.remove(); } 6093 if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; } 6094 vakata_dnd = { 6095 element : false, 6096 target : false, 6097 is_down : false, 6098 is_drag : false, 6099 helper : false, 6100 helper_w: 0, 6101 data : false, 6102 init_x : 0, 6103 init_y : 0, 6104 scroll_l: 0, 6105 scroll_t: 0, 6106 scroll_e: false, 6107 scroll_i: false, 6108 is_touch: false 6109 }; 6110 $(document).off("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag); 6111 $(document).off("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop); 6112 }, 6113 _scroll : function (init_only) { 6114 if(!vakata_dnd.scroll_e || (!vakata_dnd.scroll_l && !vakata_dnd.scroll_t)) { 6115 if(vakata_dnd.scroll_i) { clearInterval(vakata_dnd.scroll_i); vakata_dnd.scroll_i = false; } 6116 return false; 6117 } 6118 if(!vakata_dnd.scroll_i) { 6119 vakata_dnd.scroll_i = setInterval($.vakata.dnd._scroll, 100); 6120 return false; 6121 } 6122 if(init_only === true) { return false; } 6123 6124 var i = vakata_dnd.scroll_e.scrollTop(), 6125 j = vakata_dnd.scroll_e.scrollLeft(); 6126 vakata_dnd.scroll_e.scrollTop(i + vakata_dnd.scroll_t * $.vakata.dnd.settings.scroll_speed); 6127 vakata_dnd.scroll_e.scrollLeft(j + vakata_dnd.scroll_l * $.vakata.dnd.settings.scroll_speed); 6128 if(i !== vakata_dnd.scroll_e.scrollTop() || j !== vakata_dnd.scroll_e.scrollLeft()) { 6129 /** 6130 * triggered on the document when a drag causes an element to scroll 6131 * @event 6132 * @plugin dnd 6133 * @name dnd_scroll.vakata 6134 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start 6135 * @param {DOM} element the DOM element being dragged 6136 * @param {jQuery} helper the helper shown next to the mouse 6137 * @param {jQuery} event the element that is scrolling 6138 */ 6139 $.vakata.dnd._trigger("scroll", vakata_dnd.scroll_e); 6140 } 6141 }, 6142 start : function (e, data, html) { 6143 if(e.type === "touchstart" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) { 6144 e.pageX = e.originalEvent.changedTouches[0].pageX; 6145 e.pageY = e.originalEvent.changedTouches[0].pageY; 6146 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset); 6147 } 6148 if(vakata_dnd.is_drag) { $.vakata.dnd.stop({}); } 6149 try { 6150 e.currentTarget.unselectable = "on"; 6151 e.currentTarget.onselectstart = function() { return false; }; 6152 if(e.currentTarget.style) { e.currentTarget.style.MozUserSelect = "none"; } 6153 } catch(ignore) { } 6154 vakata_dnd.init_x = e.pageX; 6155 vakata_dnd.init_y = e.pageY; 6156 vakata_dnd.data = data; 6157 vakata_dnd.is_down = true; 6158 vakata_dnd.element = e.currentTarget; 6159 vakata_dnd.target = e.target; 6160 vakata_dnd.is_touch = e.type === "touchstart"; 6161 if(html !== false) { 6162 vakata_dnd.helper = $("<div id='vakata-dnd'></div>").html(html).css({ 6163 "display" : "block", 6164 "margin" : "0", 6165 "padding" : "0", 6166 "position" : "absolute", 6167 "top" : "-2000px", 6168 "lineHeight" : "16px", 6169 "zIndex" : "10000" 6170 }); 6171 } 6172 $(document).on("mousemove.vakata.jstree touchmove.vakata.jstree", $.vakata.dnd.drag); 6173 $(document).on("mouseup.vakata.jstree touchend.vakata.jstree", $.vakata.dnd.stop); 6174 return false; 6175 }, 6176 drag : function (e) { 6177 if(e.type === "touchmove" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) { 6178 e.pageX = e.originalEvent.changedTouches[0].pageX; 6179 e.pageY = e.originalEvent.changedTouches[0].pageY; 6180 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset); 6181 } 6182 if(!vakata_dnd.is_down) { return; } 6183 if(!vakata_dnd.is_drag) { 6184 if( 6185 Math.abs(e.pageX - vakata_dnd.init_x) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) || 6186 Math.abs(e.pageY - vakata_dnd.init_y) > (vakata_dnd.is_touch ? $.vakata.dnd.settings.threshold_touch : $.vakata.dnd.settings.threshold) 6187 ) { 6188 if(vakata_dnd.helper) { 6189 vakata_dnd.helper.appendTo("body"); 6190 vakata_dnd.helper_w = vakata_dnd.helper.outerWidth(); 6191 } 6192 vakata_dnd.is_drag = true; 6193 /** 6194 * triggered on the document when a drag starts 6195 * @event 6196 * @plugin dnd 6197 * @name dnd_start.vakata 6198 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start 6199 * @param {DOM} element the DOM element being dragged 6200 * @param {jQuery} helper the helper shown next to the mouse 6201 * @param {Object} event the event that caused the start (probably mousemove) 6202 */ 6203 $.vakata.dnd._trigger("start", e); 6204 } 6205 else { return; } 6206 } 6207 6208 var d = false, w = false, 6209 dh = false, wh = false, 6210 dw = false, ww = false, 6211 dt = false, dl = false, 6212 ht = false, hl = false; 6213 6214 vakata_dnd.scroll_t = 0; 6215 vakata_dnd.scroll_l = 0; 6216 vakata_dnd.scroll_e = false; 6217 $($(e.target).parentsUntil("body").addBack().get().reverse()) 6218 .filter(function () { 6219 return (/^auto|scroll$/).test($(this).css("overflow")) && 6220 (this.scrollHeight > this.offsetHeight || this.scrollWidth > this.offsetWidth); 6221 }) 6222 .each(function () { 6223 var t = $(this), o = t.offset(); 6224 if(this.scrollHeight > this.offsetHeight) { 6225 if(o.top + t.height() - e.pageY < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; } 6226 if(e.pageY - o.top < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; } 6227 } 6228 if(this.scrollWidth > this.offsetWidth) { 6229 if(o.left + t.width() - e.pageX < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; } 6230 if(e.pageX - o.left < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; } 6231 } 6232 if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) { 6233 vakata_dnd.scroll_e = $(this); 6234 return false; 6235 } 6236 }); 6237 6238 if(!vakata_dnd.scroll_e) { 6239 d = $(document); w = $(window); 6240 dh = d.height(); wh = w.height(); 6241 dw = d.width(); ww = w.width(); 6242 dt = d.scrollTop(); dl = d.scrollLeft(); 6243 if(dh > wh && e.pageY - dt < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = -1; } 6244 if(dh > wh && wh - (e.pageY - dt) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_t = 1; } 6245 if(dw > ww && e.pageX - dl < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = -1; } 6246 if(dw > ww && ww - (e.pageX - dl) < $.vakata.dnd.settings.scroll_proximity) { vakata_dnd.scroll_l = 1; } 6247 if(vakata_dnd.scroll_t || vakata_dnd.scroll_l) { 6248 vakata_dnd.scroll_e = d; 6249 } 6250 } 6251 if(vakata_dnd.scroll_e) { $.vakata.dnd._scroll(true); } 6252 6253 if(vakata_dnd.helper) { 6254 ht = parseInt(e.pageY + $.vakata.dnd.settings.helper_top, 10); 6255 hl = parseInt(e.pageX + $.vakata.dnd.settings.helper_left, 10); 6256 if(dh && ht + 25 > dh) { ht = dh - 50; } 6257 if(dw && hl + vakata_dnd.helper_w > dw) { hl = dw - (vakata_dnd.helper_w + 2); } 6258 vakata_dnd.helper.css({ 6259 left : hl + "px", 6260 top : ht + "px" 6261 }); 6262 } 6263 /** 6264 * triggered on the document when a drag is in progress 6265 * @event 6266 * @plugin dnd 6267 * @name dnd_move.vakata 6268 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start 6269 * @param {DOM} element the DOM element being dragged 6270 * @param {jQuery} helper the helper shown next to the mouse 6271 * @param {Object} event the event that caused this to trigger (most likely mousemove) 6272 */ 6273 $.vakata.dnd._trigger("move", e); 6274 return false; 6275 }, 6276 stop : function (e) { 6277 if(e.type === "touchend" && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0]) { 6278 e.pageX = e.originalEvent.changedTouches[0].pageX; 6279 e.pageY = e.originalEvent.changedTouches[0].pageY; 6280 e.target = document.elementFromPoint(e.originalEvent.changedTouches[0].pageX - window.pageXOffset, e.originalEvent.changedTouches[0].pageY - window.pageYOffset); 6281 } 6282 if(vakata_dnd.is_drag) { 6283 /** 6284 * triggered on the document when a drag stops (the dragged element is dropped) 6285 * @event 6286 * @plugin dnd 6287 * @name dnd_stop.vakata 6288 * @param {Mixed} data any data supplied with the call to $.vakata.dnd.start 6289 * @param {DOM} element the DOM element being dragged 6290 * @param {jQuery} helper the helper shown next to the mouse 6291 * @param {Object} event the event that caused the stop 6292 */ 6293 $.vakata.dnd._trigger("stop", e); 6294 } 6295 else { 6296 if(e.type === "touchend" && e.target === vakata_dnd.target) { 6297 var to = setTimeout(function () { $(e.target).click(); }, 100); 6298 $(e.target).one('click', function() { if(to) { clearTimeout(to); } }); 6299 } 6300 } 6301 $.vakata.dnd._clean(); 6302 return false; 6303 } 6304 }; 6305 }($)); 6306 6307 // include the dnd plugin by default 6308 // $.jstree.defaults.plugins.push("dnd"); 6309 6310 6311/** 6312 * ### Massload plugin 6313 * 6314 * Adds massload functionality to jsTree, so that multiple nodes can be loaded in a single request (only useful with lazy loading). 6315 */ 6316 6317 /** 6318 * massload configuration 6319 * 6320 * It is possible to set this to a standard jQuery-like AJAX config. 6321 * In addition to the standard jQuery ajax options here you can supply functions for `data` and `url`, the functions will be run in the current instance's scope and a param will be passed indicating which node IDs need to be loaded, the return value of those functions will be used. 6322 * 6323 * You can also set this to a function, that function will receive the node IDs being loaded as argument and a second param which is a function (callback) which should be called with the result. 6324 * 6325 * Both the AJAX and the function approach rely on the same return value - an object where the keys are the node IDs, and the value is the children of that node as an array. 6326 * 6327 * { 6328 * "id1" : [{ "text" : "Child of ID1", "id" : "c1" }, { "text" : "Another child of ID1", "id" : "c2" }], 6329 * "id2" : [{ "text" : "Child of ID2", "id" : "c3" }] 6330 * } 6331 * 6332 * @name $.jstree.defaults.massload 6333 * @plugin massload 6334 */ 6335 $.jstree.defaults.massload = null; 6336 $.jstree.plugins.massload = function (options, parent) { 6337 this.init = function (el, options) { 6338 parent.init.call(this, el, options); 6339 this._data.massload = {}; 6340 }; 6341 this._load_nodes = function (nodes, callback, is_callback) { 6342 var s = this.settings.massload; 6343 if(is_callback && !$.isEmptyObject(this._data.massload)) { 6344 return parent._load_nodes.call(this, nodes, callback, is_callback); 6345 } 6346 if($.isFunction(s)) { 6347 return s.call(this, nodes, $.proxy(function (data) { 6348 if(data) { 6349 for(var i in data) { 6350 if(data.hasOwnProperty(i)) { 6351 this._data.massload[i] = data[i]; 6352 } 6353 } 6354 } 6355 parent._load_nodes.call(this, nodes, callback, is_callback); 6356 }, this)); 6357 } 6358 if(typeof s === 'object' && s && s.url) { 6359 s = $.extend(true, {}, s); 6360 if($.isFunction(s.url)) { 6361 s.url = s.url.call(this, nodes); 6362 } 6363 if($.isFunction(s.data)) { 6364 s.data = s.data.call(this, nodes); 6365 } 6366 return $.ajax(s) 6367 .done($.proxy(function (data,t,x) { 6368 if(data) { 6369 for(var i in data) { 6370 if(data.hasOwnProperty(i)) { 6371 this._data.massload[i] = data[i]; 6372 } 6373 } 6374 } 6375 parent._load_nodes.call(this, nodes, callback, is_callback); 6376 }, this)) 6377 .fail($.proxy(function (f) { 6378 parent._load_nodes.call(this, nodes, callback, is_callback); 6379 }, this)); 6380 } 6381 return parent._load_nodes.call(this, nodes, callback, is_callback); 6382 }; 6383 this._load_node = function (obj, callback) { 6384 var d = this._data.massload[obj.id]; 6385 if(d) { 6386 return this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(function () { return this.nodeType !== 3; }) : d, function (status) { 6387 callback.call(this, status); 6388 delete this._data.massload[obj.id]; 6389 }); 6390 } 6391 return parent._load_node.call(this, obj, callback); 6392 }; 6393 }; 6394 6395/** 6396 * ### Search plugin 6397 * 6398 * Adds search functionality to jsTree. 6399 */ 6400 6401 /** 6402 * stores all defaults for the search plugin 6403 * @name $.jstree.defaults.search 6404 * @plugin search 6405 */ 6406 $.jstree.defaults.search = { 6407 /** 6408 * a jQuery-like AJAX config, which jstree uses if a server should be queried for results. 6409 * 6410 * A `str` (which is the search string) parameter will be added with the request, an optional `inside` parameter will be added if the search is limited to a node id. The expected result is a JSON array with nodes that need to be opened so that matching nodes will be revealed. 6411 * Leave this setting as `false` to not query the server. You can also set this to a function, which will be invoked in the instance's scope and receive 3 parameters - the search string, the callback to call with the array of nodes to load, and the optional node ID to limit the search to 6412 * @name $.jstree.defaults.search.ajax 6413 * @plugin search 6414 */ 6415 ajax : false, 6416 /** 6417 * Indicates if the search should be fuzzy or not (should `chnd3` match `child node 3`). Default is `false`. 6418 * @name $.jstree.defaults.search.fuzzy 6419 * @plugin search 6420 */ 6421 fuzzy : false, 6422 /** 6423 * Indicates if the search should be case sensitive. Default is `false`. 6424 * @name $.jstree.defaults.search.case_sensitive 6425 * @plugin search 6426 */ 6427 case_sensitive : false, 6428 /** 6429 * Indicates if the tree should be filtered (by default) to show only matching nodes (keep in mind this can be a heavy on large trees in old browsers). 6430 * This setting can be changed at runtime when calling the search method. Default is `false`. 6431 * @name $.jstree.defaults.search.show_only_matches 6432 * @plugin search 6433 */ 6434 show_only_matches : false, 6435 /** 6436 * Indicates if all nodes opened to reveal the search result, should be closed when the search is cleared or a new search is performed. Default is `true`. 6437 * @name $.jstree.defaults.search.close_opened_onclear 6438 * @plugin search 6439 */ 6440 close_opened_onclear : true, 6441 /** 6442 * Indicates if only leaf nodes should be included in search results. Default is `false`. 6443 * @name $.jstree.defaults.search.search_leaves_only 6444 * @plugin search 6445 */ 6446 search_leaves_only : false, 6447 /** 6448 * If set to a function it wil be called in the instance's scope with two arguments - search string and node (where node will be every node in the structure, so use with caution). 6449 * If the function returns a truthy value the node will be considered a match (it might not be displayed if search_only_leaves is set to true and the node is not a leaf). Default is `false`. 6450 * @name $.jstree.defaults.search.search_callback 6451 * @plugin search 6452 */ 6453 search_callback : false 6454 }; 6455 6456 $.jstree.plugins.search = function (options, parent) { 6457 this.bind = function () { 6458 parent.bind.call(this); 6459 6460 this._data.search.str = ""; 6461 this._data.search.dom = $(); 6462 this._data.search.res = []; 6463 this._data.search.opn = []; 6464 this._data.search.som = false; 6465 6466 this.element 6467 .on('before_open.jstree', $.proxy(function (e, data) { 6468 var i, j, f, r = this._data.search.res, s = [], o = $(); 6469 if(r && r.length) { 6470 this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #'))); 6471 this._data.search.dom.children(".jstree-anchor").addClass('jstree-search'); 6472 if(this._data.search.som && this._data.search.res.length) { 6473 for(i = 0, j = r.length; i < j; i++) { 6474 s = s.concat(this.get_node(r[i]).parents); 6475 } 6476 s = $.vakata.array_remove_item($.vakata.array_unique(s),'#'); 6477 o = s.length ? $(this.element[0].querySelectorAll('#' + $.map(s, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #'))) : $(); 6478 6479 this.element.find(".jstree-node").hide().filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last'); 6480 o = o.add(this._data.search.dom); 6481 o.parentsUntil(".jstree").addBack().show() 6482 .filter(".jstree-children").each(function () { $(this).children(".jstree-node:visible").eq(-1).addClass("jstree-last"); }); 6483 } 6484 } 6485 }, this)) 6486 .on("search.jstree", $.proxy(function (e, data) { 6487 if(this._data.search.som) { 6488 if(data.nodes.length) { 6489 this.element.find(".jstree-node").hide().filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last'); 6490 data.nodes.parentsUntil(".jstree").addBack().show() 6491 .filter(".jstree-children").each(function () { $(this).children(".jstree-node:visible").eq(-1).addClass("jstree-last"); }); 6492 } 6493 } 6494 }, this)) 6495 .on("clear_search.jstree", $.proxy(function (e, data) { 6496 if(this._data.search.som && data.nodes.length) { 6497 this.element.find(".jstree-node").css("display","").filter('.jstree-last').filter(function() { return this.nextSibling; }).removeClass('jstree-last'); 6498 } 6499 }, this)); 6500 }; 6501 /** 6502 * used to search the tree nodes for a given string 6503 * @name search(str [, skip_async]) 6504 * @param {String} str the search string 6505 * @param {Boolean} skip_async if set to true server will not be queried even if configured 6506 * @param {Boolean} show_only_matches if set to true only matching nodes will be shown (keep in mind this can be very slow on large trees or old browsers) 6507 * @param {mixed} inside an optional node to whose children to limit the search 6508 * @param {Boolean} append if set to true the results of this search are appended to the previous search 6509 * @plugin search 6510 * @trigger search.jstree 6511 */ 6512 this.search = function (str, skip_async, show_only_matches, inside, append) { 6513 if(str === false || $.trim(str.toString()) === "") { 6514 return this.clear_search(); 6515 } 6516 inside = this.get_node(inside); 6517 inside = inside && inside.id ? inside.id : null; 6518 str = str.toString(); 6519 var s = this.settings.search, 6520 a = s.ajax ? s.ajax : false, 6521 m = this._model.data, 6522 f = null, 6523 r = [], 6524 p = [], i, j; 6525 if(this._data.search.res.length && !append) { 6526 this.clear_search(); 6527 } 6528 if(show_only_matches === undefined) { 6529 show_only_matches = s.show_only_matches; 6530 } 6531 if(!skip_async && a !== false) { 6532 if($.isFunction(a)) { 6533 return a.call(this, str, $.proxy(function (d) { 6534 if(d && d.d) { d = d.d; } 6535 this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () { 6536 this.search(str, true, show_only_matches, inside, append); 6537 }, true); 6538 }, this), inside); 6539 } 6540 else { 6541 a = $.extend({}, a); 6542 if(!a.data) { a.data = {}; } 6543 a.data.str = str; 6544 if(inside) { 6545 a.data.inside = inside; 6546 } 6547 return $.ajax(a) 6548 .fail($.proxy(function () { 6549 this._data.core.last_error = { 'error' : 'ajax', 'plugin' : 'search', 'id' : 'search_01', 'reason' : 'Could not load search parents', 'data' : JSON.stringify(a) }; 6550 this.settings.core.error.call(this, this._data.core.last_error); 6551 }, this)) 6552 .done($.proxy(function (d) { 6553 if(d && d.d) { d = d.d; } 6554 this._load_nodes(!$.isArray(d) ? [] : $.vakata.array_unique(d), function () { 6555 this.search(str, true, show_only_matches, inside, append); 6556 }, true); 6557 }, this)); 6558 } 6559 } 6560 if(!append) { 6561 this._data.search.str = str; 6562 this._data.search.dom = $(); 6563 this._data.search.res = []; 6564 this._data.search.opn = []; 6565 this._data.search.som = show_only_matches; 6566 } 6567 6568 f = new $.vakata.search(str, true, { caseSensitive : s.case_sensitive, fuzzy : s.fuzzy }); 6569 $.each(m[inside ? inside : '#'].children_d, function (ii, i) { 6570 var v = m[i]; 6571 if(v.text && ( (s.search_callback && s.search_callback.call(this, str, v)) || (!s.search_callback && f.search(v.text).isMatch) ) && (!s.search_leaves_only || (v.state.loaded && v.children.length === 0)) ) { 6572 r.push(i); 6573 p = p.concat(v.parents); 6574 } 6575 }); 6576 if(r.length) { 6577 p = $.vakata.array_unique(p); 6578 this._search_open(p); 6579 if(!append) { 6580 this._data.search.dom = $(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #'))); 6581 this._data.search.res = r; 6582 } 6583 else { 6584 this._data.search.dom = this._data.search.dom.add($(this.element[0].querySelectorAll('#' + $.map(r, function (v) { return "0123456789".indexOf(v[0]) !== -1 ? '\\3' + v[0] + ' ' + v.substr(1).replace($.jstree.idregex,'\\$&') : v.replace($.jstree.idregex,'\\$&'); }).join(', #')))); 6585 this._data.search.res = $.vakata.array_unique(this._data.search.res.concat(r)); 6586 } 6587 this._data.search.dom.children(".jstree-anchor").addClass('jstree-search'); 6588 } 6589 /** 6590 * triggered after search is complete 6591 * @event 6592 * @name search.jstree 6593 * @param {jQuery} nodes a jQuery collection of matching nodes 6594 * @param {String} str the search string 6595 * @param {Array} res a collection of objects represeing the matching nodes 6596 * @plugin search 6597 */ 6598 this.trigger('search', { nodes : this._data.search.dom, str : str, res : this._data.search.res, show_only_matches : show_only_matches }); 6599 }; 6600 /** 6601 * used to clear the last search (removes classes and shows all nodes if filtering is on) 6602 * @name clear_search() 6603 * @plugin search 6604 * @trigger clear_search.jstree 6605 */ 6606 this.clear_search = function () { 6607 this._data.search.dom.children(".jstree-anchor").removeClass("jstree-search"); 6608 if(this.settings.search.close_opened_onclear) { 6609 this.close_node(this._data.search.opn, 0); 6610 } 6611 /** 6612 * triggered after search is complete 6613 * @event 6614 * @name clear_search.jstree 6615 * @param {jQuery} nodes a jQuery collection of matching nodes (the result from the last search) 6616 * @param {String} str the search string (the last search string) 6617 * @param {Array} res a collection of objects represeing the matching nodes (the result from the last search) 6618 * @plugin search 6619 */ 6620 this.trigger('clear_search', { 'nodes' : this._data.search.dom, str : this._data.search.str, res : this._data.search.res }); 6621 this._data.search.str = ""; 6622 this._data.search.res = []; 6623 this._data.search.opn = []; 6624 this._data.search.dom = $(); 6625 }; 6626 /** 6627 * opens nodes that need to be opened to reveal the search results. Used only internally. 6628 * @private 6629 * @name _search_open(d) 6630 * @param {Array} d an array of node IDs 6631 * @plugin search 6632 */ 6633 this._search_open = function (d) { 6634 var t = this; 6635 $.each(d.concat([]), function (i, v) { 6636 if(v === "#") { return true; } 6637 try { v = $('#' + v.replace($.jstree.idregex,'\\$&'), t.element); } catch(ignore) { } 6638 if(v && v.length) { 6639 if(t.is_closed(v)) { 6640 t._data.search.opn.push(v[0].id); 6641 t.open_node(v, function () { t._search_open(d); }, 0); 6642 } 6643 } 6644 }); 6645 }; 6646 }; 6647 6648 // helpers 6649 (function ($) { 6650 // from http://kiro.me/projects/fuse.html 6651 $.vakata.search = function(pattern, txt, options) { 6652 options = options || {}; 6653 options = $.extend({}, $.vakata.search.defaults, options); 6654 if(options.fuzzy !== false) { 6655 options.fuzzy = true; 6656 } 6657 pattern = options.caseSensitive ? pattern : pattern.toLowerCase(); 6658 var MATCH_LOCATION = options.location, 6659 MATCH_DISTANCE = options.distance, 6660 MATCH_THRESHOLD = options.threshold, 6661 patternLen = pattern.length, 6662 matchmask, pattern_alphabet, match_bitapScore, search; 6663 if(patternLen > 32) { 6664 options.fuzzy = false; 6665 } 6666 if(options.fuzzy) { 6667 matchmask = 1 << (patternLen - 1); 6668 pattern_alphabet = (function () { 6669 var mask = {}, 6670 i = 0; 6671 for (i = 0; i < patternLen; i++) { 6672 mask[pattern.charAt(i)] = 0; 6673 } 6674 for (i = 0; i < patternLen; i++) { 6675 mask[pattern.charAt(i)] |= 1 << (patternLen - i - 1); 6676 } 6677 return mask; 6678 }()); 6679 match_bitapScore = function (e, x) { 6680 var accuracy = e / patternLen, 6681 proximity = Math.abs(MATCH_LOCATION - x); 6682 if(!MATCH_DISTANCE) { 6683 return proximity ? 1.0 : accuracy; 6684 } 6685 return accuracy + (proximity / MATCH_DISTANCE); 6686 }; 6687 } 6688 search = function (text) { 6689 text = options.caseSensitive ? text : text.toLowerCase(); 6690 if(pattern === text || text.indexOf(pattern) !== -1) { 6691 return { 6692 isMatch: true, 6693 score: 0 6694 }; 6695 } 6696 if(!options.fuzzy) { 6697 return { 6698 isMatch: false, 6699 score: 1 6700 }; 6701 } 6702 var i, j, 6703 textLen = text.length, 6704 scoreThreshold = MATCH_THRESHOLD, 6705 bestLoc = text.indexOf(pattern, MATCH_LOCATION), 6706 binMin, binMid, 6707 binMax = patternLen + textLen, 6708 lastRd, start, finish, rd, charMatch, 6709 score = 1, 6710 locations = []; 6711 if (bestLoc !== -1) { 6712 scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold); 6713 bestLoc = text.lastIndexOf(pattern, MATCH_LOCATION + patternLen); 6714 if (bestLoc !== -1) { 6715 scoreThreshold = Math.min(match_bitapScore(0, bestLoc), scoreThreshold); 6716 } 6717 } 6718 bestLoc = -1; 6719 for (i = 0; i < patternLen; i++) { 6720 binMin = 0; 6721 binMid = binMax; 6722 while (binMin < binMid) { 6723 if (match_bitapScore(i, MATCH_LOCATION + binMid) <= scoreThreshold) { 6724 binMin = binMid; 6725 } else { 6726 binMax = binMid; 6727 } 6728 binMid = Math.floor((binMax - binMin) / 2 + binMin); 6729 } 6730 binMax = binMid; 6731 start = Math.max(1, MATCH_LOCATION - binMid + 1); 6732 finish = Math.min(MATCH_LOCATION + binMid, textLen) + patternLen; 6733 rd = new Array(finish + 2); 6734 rd[finish + 1] = (1 << i) - 1; 6735 for (j = finish; j >= start; j--) { 6736 charMatch = pattern_alphabet[text.charAt(j - 1)]; 6737 if (i === 0) { 6738 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch; 6739 } else { 6740 rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | (((lastRd[j + 1] | lastRd[j]) << 1) | 1) | lastRd[j + 1]; 6741 } 6742 if (rd[j] & matchmask) { 6743 score = match_bitapScore(i, j - 1); 6744 if (score <= scoreThreshold) { 6745 scoreThreshold = score; 6746 bestLoc = j - 1; 6747 locations.push(bestLoc); 6748 if (bestLoc > MATCH_LOCATION) { 6749 start = Math.max(1, 2 * MATCH_LOCATION - bestLoc); 6750 } else { 6751 break; 6752 } 6753 } 6754 } 6755 } 6756 if (match_bitapScore(i + 1, MATCH_LOCATION) > scoreThreshold) { 6757 break; 6758 } 6759 lastRd = rd; 6760 } 6761 return { 6762 isMatch: bestLoc >= 0, 6763 score: score 6764 }; 6765 }; 6766 return txt === true ? { 'search' : search } : search(txt); 6767 }; 6768 $.vakata.search.defaults = { 6769 location : 0, 6770 distance : 100, 6771 threshold : 0.6, 6772 fuzzy : false, 6773 caseSensitive : false 6774 }; 6775 }($)); 6776 6777 // include the search plugin by default 6778 // $.jstree.defaults.plugins.push("search"); 6779 6780/** 6781 * ### Sort plugin 6782 * 6783 * Automatically sorts all siblings in the tree according to a sorting function. 6784 */ 6785 6786 /** 6787 * the settings function used to sort the nodes. 6788 * It is executed in the tree's context, accepts two nodes as arguments and should return `1` or `-1`. 6789 * @name $.jstree.defaults.sort 6790 * @plugin sort 6791 */ 6792 $.jstree.defaults.sort = function (a, b) { 6793 //return this.get_type(a) === this.get_type(b) ? (this.get_text(a) > this.get_text(b) ? 1 : -1) : this.get_type(a) >= this.get_type(b); 6794 return this.get_text(a) > this.get_text(b) ? 1 : -1; 6795 }; 6796 $.jstree.plugins.sort = function (options, parent) { 6797 this.bind = function () { 6798 parent.bind.call(this); 6799 this.element 6800 .on("model.jstree", $.proxy(function (e, data) { 6801 this.sort(data.parent, true); 6802 }, this)) 6803 .on("rename_node.jstree create_node.jstree", $.proxy(function (e, data) { 6804 this.sort(data.parent || data.node.parent, false); 6805 this.redraw_node(data.parent || data.node.parent, true); 6806 }, this)) 6807 .on("move_node.jstree copy_node.jstree", $.proxy(function (e, data) { 6808 this.sort(data.parent, false); 6809 this.redraw_node(data.parent, true); 6810 }, this)); 6811 }; 6812 /** 6813 * used to sort a node's children 6814 * @private 6815 * @name sort(obj [, deep]) 6816 * @param {mixed} obj the node 6817 * @param {Boolean} deep if set to `true` nodes are sorted recursively. 6818 * @plugin sort 6819 * @trigger search.jstree 6820 */ 6821 this.sort = function (obj, deep) { 6822 var i, j; 6823 obj = this.get_node(obj); 6824 if(obj && obj.children && obj.children.length) { 6825 obj.children.sort($.proxy(this.settings.sort, this)); 6826 if(deep) { 6827 for(i = 0, j = obj.children_d.length; i < j; i++) { 6828 this.sort(obj.children_d[i], false); 6829 } 6830 } 6831 } 6832 }; 6833 }; 6834 6835 // include the sort plugin by default 6836 // $.jstree.defaults.plugins.push("sort"); 6837 6838/** 6839 * ### State plugin 6840 * 6841 * Saves the state of the tree (selected nodes, opened nodes) on the user's computer using available options (localStorage, cookies, etc) 6842 */ 6843 6844 var to = false; 6845 /** 6846 * stores all defaults for the state plugin 6847 * @name $.jstree.defaults.state 6848 * @plugin state 6849 */ 6850 $.jstree.defaults.state = { 6851 /** 6852 * A string for the key to use when saving the current tree (change if using multiple trees in your project). Defaults to `jstree`. 6853 * @name $.jstree.defaults.state.key 6854 * @plugin state 6855 */ 6856 key : 'jstree', 6857 /** 6858 * A space separated list of events that trigger a state save. Defaults to `changed.jstree open_node.jstree close_node.jstree`. 6859 * @name $.jstree.defaults.state.events 6860 * @plugin state 6861 */ 6862 events : 'changed.jstree open_node.jstree close_node.jstree check_node.jstree uncheck_node.jstree', 6863 /** 6864 * Time in milliseconds after which the state will expire. Defaults to 'false' meaning - no expire. 6865 * @name $.jstree.defaults.state.ttl 6866 * @plugin state 6867 */ 6868 ttl : false, 6869 /** 6870 * A function that will be executed prior to restoring state with one argument - the state object. Can be used to clear unwanted parts of the state. 6871 * @name $.jstree.defaults.state.filter 6872 * @plugin state 6873 */ 6874 filter : false 6875 }; 6876 $.jstree.plugins.state = function (options, parent) { 6877 this.bind = function () { 6878 parent.bind.call(this); 6879 var bind = $.proxy(function () { 6880 this.element.on(this.settings.state.events, $.proxy(function () { 6881 if(to) { clearTimeout(to); } 6882 to = setTimeout($.proxy(function () { this.save_state(); }, this), 100); 6883 }, this)); 6884 /** 6885 * triggered when the state plugin is finished restoring the state (and immediately after ready if there is no state to restore). 6886 * @event 6887 * @name state_ready.jstree 6888 * @plugin state 6889 */ 6890 this.trigger('state_ready'); 6891 }, this); 6892 this.element 6893 .on("ready.jstree", $.proxy(function (e, data) { 6894 this.element.one("restore_state.jstree", bind); 6895 if(!this.restore_state()) { bind(); } 6896 }, this)); 6897 }; 6898 /** 6899 * save the state 6900 * @name save_state() 6901 * @plugin state 6902 */ 6903 this.save_state = function () { 6904 var st = { 'state' : this.get_state(), 'ttl' : this.settings.state.ttl, 'sec' : +(new Date()) }; 6905 $.vakata.storage.set(this.settings.state.key, JSON.stringify(st)); 6906 }; 6907 /** 6908 * restore the state from the user's computer 6909 * @name restore_state() 6910 * @plugin state 6911 */ 6912 this.restore_state = function () { 6913 var k = $.vakata.storage.get(this.settings.state.key); 6914 if(!!k) { try { k = JSON.parse(k); } catch(ex) { return false; } } 6915 if(!!k && k.ttl && k.sec && +(new Date()) - k.sec > k.ttl) { return false; } 6916 if(!!k && k.state) { k = k.state; } 6917 if(!!k && $.isFunction(this.settings.state.filter)) { k = this.settings.state.filter.call(this, k); } 6918 if(!!k) { 6919 this.element.one("set_state.jstree", function (e, data) { data.instance.trigger('restore_state', { 'state' : $.extend(true, {}, k) }); }); 6920 this.set_state(k); 6921 return true; 6922 } 6923 return false; 6924 }; 6925 /** 6926 * clear the state on the user's computer 6927 * @name clear_state() 6928 * @plugin state 6929 */ 6930 this.clear_state = function () { 6931 return $.vakata.storage.del(this.settings.state.key); 6932 }; 6933 }; 6934 6935 (function ($, undefined) { 6936 $.vakata.storage = { 6937 // simply specifying the functions in FF throws an error 6938 set : function (key, val) { return window.localStorage.setItem(key, val); }, 6939 get : function (key) { return window.localStorage.getItem(key); }, 6940 del : function (key) { return window.localStorage.removeItem(key); } 6941 }; 6942 }($)); 6943 6944 // include the state plugin by default 6945 // $.jstree.defaults.plugins.push("state"); 6946 6947/** 6948 * ### Types plugin 6949 * 6950 * Makes it possible to add predefined types for groups of nodes, which make it possible to easily control nesting rules and icon for each group. 6951 */ 6952 6953 /** 6954 * An object storing all types as key value pairs, where the key is the type name and the value is an object that could contain following keys (all optional). 6955 * 6956 * * `max_children` the maximum number of immediate children this node type can have. Do not specify or set to `-1` for unlimited. 6957 * * `max_depth` the maximum number of nesting this node type can have. A value of `1` would mean that the node can have children, but no grandchildren. Do not specify or set to `-1` for unlimited. 6958 * * `valid_children` an array of node type strings, that nodes of this type can have as children. Do not specify or set to `-1` for no limits. 6959 * * `icon` a string - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class. Omit to use the default icon from your theme. 6960 * 6961 * There are two predefined types: 6962 * 6963 * * `#` represents the root of the tree, for example `max_children` would control the maximum number of root nodes. 6964 * * `default` represents the default node - any settings here will be applied to all nodes that do not have a type specified. 6965 * 6966 * @name $.jstree.defaults.types 6967 * @plugin types 6968 */ 6969 $.jstree.defaults.types = { 6970 '#' : {}, 6971 'default' : {} 6972 }; 6973 6974 $.jstree.plugins.types = function (options, parent) { 6975 this.init = function (el, options) { 6976 var i, j; 6977 if(options && options.types && options.types['default']) { 6978 for(i in options.types) { 6979 if(i !== "default" && i !== "#" && options.types.hasOwnProperty(i)) { 6980 for(j in options.types['default']) { 6981 if(options.types['default'].hasOwnProperty(j) && options.types[i][j] === undefined) { 6982 options.types[i][j] = options.types['default'][j]; 6983 } 6984 } 6985 } 6986 } 6987 } 6988 parent.init.call(this, el, options); 6989 this._model.data['#'].type = '#'; 6990 }; 6991 this.refresh = function (skip_loading, forget_state) { 6992 parent.refresh.call(this, skip_loading, forget_state); 6993 this._model.data['#'].type = '#'; 6994 }; 6995 this.bind = function () { 6996 this.element 6997 .on('model.jstree', $.proxy(function (e, data) { 6998 var m = this._model.data, 6999 dpc = data.nodes, 7000 t = this.settings.types, 7001 i, j, c = 'default'; 7002 for(i = 0, j = dpc.length; i < j; i++) { 7003 c = 'default'; 7004 if(m[dpc[i]].original && m[dpc[i]].original.type && t[m[dpc[i]].original.type]) { 7005 c = m[dpc[i]].original.type; 7006 } 7007 if(m[dpc[i]].data && m[dpc[i]].data.jstree && m[dpc[i]].data.jstree.type && t[m[dpc[i]].data.jstree.type]) { 7008 c = m[dpc[i]].data.jstree.type; 7009 } 7010 m[dpc[i]].type = c; 7011 if(m[dpc[i]].icon === true && t[c].icon !== undefined) { 7012 m[dpc[i]].icon = t[c].icon; 7013 } 7014 } 7015 m['#'].type = '#'; 7016 }, this)); 7017 parent.bind.call(this); 7018 }; 7019 this.get_json = function (obj, options, flat) { 7020 var i, j, 7021 m = this._model.data, 7022 opt = options ? $.extend(true, {}, options, {no_id:false}) : {}, 7023 tmp = parent.get_json.call(this, obj, opt, flat); 7024 if(tmp === false) { return false; } 7025 if($.isArray(tmp)) { 7026 for(i = 0, j = tmp.length; i < j; i++) { 7027 tmp[i].type = tmp[i].id && m[tmp[i].id] && m[tmp[i].id].type ? m[tmp[i].id].type : "default"; 7028 if(options && options.no_id) { 7029 delete tmp[i].id; 7030 if(tmp[i].li_attr && tmp[i].li_attr.id) { 7031 delete tmp[i].li_attr.id; 7032 } 7033 if(tmp[i].a_attr && tmp[i].a_attr.id) { 7034 delete tmp[i].a_attr.id; 7035 } 7036 } 7037 } 7038 } 7039 else { 7040 tmp.type = tmp.id && m[tmp.id] && m[tmp.id].type ? m[tmp.id].type : "default"; 7041 if(options && options.no_id) { 7042 tmp = this._delete_ids(tmp); 7043 } 7044 } 7045 return tmp; 7046 }; 7047 this._delete_ids = function (tmp) { 7048 if($.isArray(tmp)) { 7049 for(var i = 0, j = tmp.length; i < j; i++) { 7050 tmp[i] = this._delete_ids(tmp[i]); 7051 } 7052 return tmp; 7053 } 7054 delete tmp.id; 7055 if(tmp.li_attr && tmp.li_attr.id) { 7056 delete tmp.li_attr.id; 7057 } 7058 if(tmp.a_attr && tmp.a_attr.id) { 7059 delete tmp.a_attr.id; 7060 } 7061 if(tmp.children && $.isArray(tmp.children)) { 7062 tmp.children = this._delete_ids(tmp.children); 7063 } 7064 return tmp; 7065 }; 7066 this.check = function (chk, obj, par, pos, more) { 7067 if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; } 7068 obj = obj && obj.id ? obj : this.get_node(obj); 7069 par = par && par.id ? par : this.get_node(par); 7070 var m = obj && obj.id ? (more && more.origin ? more.origin : $.jstree.reference(obj.id)) : null, tmp, d, i, j; 7071 m = m && m._model && m._model.data ? m._model.data : null; 7072 switch(chk) { 7073 case "create_node": 7074 case "move_node": 7075 case "copy_node": 7076 if(chk !== 'move_node' || $.inArray(obj.id, par.children) === -1) { 7077 tmp = this.get_rules(par); 7078 if(tmp.max_children !== undefined && tmp.max_children !== -1 && tmp.max_children === par.children.length) { 7079 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_01', 'reason' : 'max_children prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7080 return false; 7081 } 7082 if(tmp.valid_children !== undefined && tmp.valid_children !== -1 && $.inArray((obj.type || 'default'), tmp.valid_children) === -1) { 7083 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_02', 'reason' : 'valid_children prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7084 return false; 7085 } 7086 if(m && obj.children_d && obj.parents) { 7087 d = 0; 7088 for(i = 0, j = obj.children_d.length; i < j; i++) { 7089 d = Math.max(d, m[obj.children_d[i]].parents.length); 7090 } 7091 d = d - obj.parents.length + 1; 7092 } 7093 if(d <= 0 || d === undefined) { d = 1; } 7094 do { 7095 if(tmp.max_depth !== undefined && tmp.max_depth !== -1 && tmp.max_depth < d) { 7096 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'types', 'id' : 'types_03', 'reason' : 'max_depth prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7097 return false; 7098 } 7099 par = this.get_node(par.parent); 7100 tmp = this.get_rules(par); 7101 d++; 7102 } while(par); 7103 } 7104 break; 7105 } 7106 return true; 7107 }; 7108 /** 7109 * used to retrieve the type settings object for a node 7110 * @name get_rules(obj) 7111 * @param {mixed} obj the node to find the rules for 7112 * @return {Object} 7113 * @plugin types 7114 */ 7115 this.get_rules = function (obj) { 7116 obj = this.get_node(obj); 7117 if(!obj) { return false; } 7118 var tmp = this.get_type(obj, true); 7119 if(tmp.max_depth === undefined) { tmp.max_depth = -1; } 7120 if(tmp.max_children === undefined) { tmp.max_children = -1; } 7121 if(tmp.valid_children === undefined) { tmp.valid_children = -1; } 7122 return tmp; 7123 }; 7124 /** 7125 * used to retrieve the type string or settings object for a node 7126 * @name get_type(obj [, rules]) 7127 * @param {mixed} obj the node to find the rules for 7128 * @param {Boolean} rules if set to `true` instead of a string the settings object will be returned 7129 * @return {String|Object} 7130 * @plugin types 7131 */ 7132 this.get_type = function (obj, rules) { 7133 obj = this.get_node(obj); 7134 return (!obj) ? false : ( rules ? $.extend({ 'type' : obj.type }, this.settings.types[obj.type]) : obj.type); 7135 }; 7136 /** 7137 * used to change a node's type 7138 * @name set_type(obj, type) 7139 * @param {mixed} obj the node to change 7140 * @param {String} type the new type 7141 * @plugin types 7142 */ 7143 this.set_type = function (obj, type) { 7144 var t, t1, t2, old_type, old_icon; 7145 if($.isArray(obj)) { 7146 obj = obj.slice(); 7147 for(t1 = 0, t2 = obj.length; t1 < t2; t1++) { 7148 this.set_type(obj[t1], type); 7149 } 7150 return true; 7151 } 7152 t = this.settings.types; 7153 obj = this.get_node(obj); 7154 if(!t[type] || !obj) { return false; } 7155 old_type = obj.type; 7156 old_icon = this.get_icon(obj); 7157 obj.type = type; 7158 if(old_icon === true || (t[old_type] && t[old_type].icon !== undefined && old_icon === t[old_type].icon)) { 7159 this.set_icon(obj, t[type].icon !== undefined ? t[type].icon : true); 7160 } 7161 return true; 7162 }; 7163 }; 7164 // include the types plugin by default 7165 // $.jstree.defaults.plugins.push("types"); 7166 7167/** 7168 * ### Unique plugin 7169 * 7170 * Enforces that no nodes with the same name can coexist as siblings. 7171 */ 7172 7173 /** 7174 * stores all defaults for the unique plugin 7175 * @name $.jstree.defaults.unique 7176 * @plugin unique 7177 */ 7178 $.jstree.defaults.unique = { 7179 /** 7180 * Indicates if the comparison should be case sensitive. Default is `false`. 7181 * @name $.jstree.defaults.unique.case_sensitive 7182 * @plugin unique 7183 */ 7184 case_sensitive : false, 7185 /** 7186 * A callback executed in the instance's scope when a new node is created and the name is already taken, the two arguments are the conflicting name and the counter. The default will produce results like `New node (2)`. 7187 * @name $.jstree.defaults.unique.duplicate 7188 * @plugin unique 7189 */ 7190 duplicate : function (name, counter) { 7191 return name + ' (' + counter + ')'; 7192 } 7193 }; 7194 7195 $.jstree.plugins.unique = function (options, parent) { 7196 this.check = function (chk, obj, par, pos, more) { 7197 if(parent.check.call(this, chk, obj, par, pos, more) === false) { return false; } 7198 obj = obj && obj.id ? obj : this.get_node(obj); 7199 par = par && par.id ? par : this.get_node(par); 7200 if(!par || !par.children) { return true; } 7201 var n = chk === "rename_node" ? pos : obj.text, 7202 c = [], 7203 s = this.settings.unique.case_sensitive, 7204 m = this._model.data, i, j; 7205 for(i = 0, j = par.children.length; i < j; i++) { 7206 c.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase()); 7207 } 7208 if(!s) { n = n.toLowerCase(); } 7209 switch(chk) { 7210 case "delete_node": 7211 return true; 7212 case "rename_node": 7213 i = ($.inArray(n, c) === -1 || (obj.text && obj.text[ s ? 'toString' : 'toLowerCase']() === n)); 7214 if(!i) { 7215 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_01', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7216 } 7217 return i; 7218 case "create_node": 7219 i = ($.inArray(n, c) === -1); 7220 if(!i) { 7221 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_04', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7222 } 7223 return i; 7224 case "copy_node": 7225 i = ($.inArray(n, c) === -1); 7226 if(!i) { 7227 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_02', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7228 } 7229 return i; 7230 case "move_node": 7231 i = ( (obj.parent === par.id && (!more || !more.is_multi)) || $.inArray(n, c) === -1); 7232 if(!i) { 7233 this._data.core.last_error = { 'error' : 'check', 'plugin' : 'unique', 'id' : 'unique_03', 'reason' : 'Child with name ' + n + ' already exists. Preventing: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) }; 7234 } 7235 return i; 7236 } 7237 return true; 7238 }; 7239 this.create_node = function (par, node, pos, callback, is_loaded) { 7240 if(!node || node.text === undefined) { 7241 if(par === null) { 7242 par = "#"; 7243 } 7244 par = this.get_node(par); 7245 if(!par) { 7246 return parent.create_node.call(this, par, node, pos, callback, is_loaded); 7247 } 7248 pos = pos === undefined ? "last" : pos; 7249 if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) { 7250 return parent.create_node.call(this, par, node, pos, callback, is_loaded); 7251 } 7252 if(!node) { node = {}; } 7253 var tmp, n, dpc, i, j, m = this._model.data, s = this.settings.unique.case_sensitive, cb = this.settings.unique.duplicate; 7254 n = tmp = this.get_string('New node'); 7255 dpc = []; 7256 for(i = 0, j = par.children.length; i < j; i++) { 7257 dpc.push(s ? m[par.children[i]].text : m[par.children[i]].text.toLowerCase()); 7258 } 7259 i = 1; 7260 while($.inArray(s ? n : n.toLowerCase(), dpc) !== -1) { 7261 n = cb.call(this, tmp, (++i)).toString(); 7262 } 7263 node.text = n; 7264 } 7265 return parent.create_node.call(this, par, node, pos, callback, is_loaded); 7266 }; 7267 }; 7268 7269 // include the unique plugin by default 7270 // $.jstree.defaults.plugins.push("unique"); 7271 7272 7273/** 7274 * ### Wholerow plugin 7275 * 7276 * Makes each node appear block level. Making selection easier. May cause slow down for large trees in old browsers. 7277 */ 7278 7279 var div = document.createElement('DIV'); 7280 div.setAttribute('unselectable','on'); 7281 div.setAttribute('role','presentation'); 7282 div.className = 'jstree-wholerow'; 7283 div.innerHTML = ' '; 7284 $.jstree.plugins.wholerow = function (options, parent) { 7285 this.bind = function () { 7286 parent.bind.call(this); 7287 7288 this.element 7289 .on('ready.jstree set_state.jstree', $.proxy(function () { 7290 this.hide_dots(); 7291 }, this)) 7292 .on("init.jstree loading.jstree ready.jstree", $.proxy(function () { 7293 //div.style.height = this._data.core.li_height + 'px'; 7294 this.get_container_ul().addClass('jstree-wholerow-ul'); 7295 }, this)) 7296 .on("deselect_all.jstree", $.proxy(function (e, data) { 7297 this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked'); 7298 }, this)) 7299 .on("changed.jstree", $.proxy(function (e, data) { 7300 this.element.find('.jstree-wholerow-clicked').removeClass('jstree-wholerow-clicked'); 7301 var tmp = false, i, j; 7302 for(i = 0, j = data.selected.length; i < j; i++) { 7303 tmp = this.get_node(data.selected[i], true); 7304 if(tmp && tmp.length) { 7305 tmp.children('.jstree-wholerow').addClass('jstree-wholerow-clicked'); 7306 } 7307 } 7308 }, this)) 7309 .on("open_node.jstree", $.proxy(function (e, data) { 7310 this.get_node(data.node, true).find('.jstree-clicked').parent().children('.jstree-wholerow').addClass('jstree-wholerow-clicked'); 7311 }, this)) 7312 .on("hover_node.jstree dehover_node.jstree", $.proxy(function (e, data) { 7313 if(e.type === "hover_node" && this.is_disabled(data.node)) { return; } 7314 this.get_node(data.node, true).children('.jstree-wholerow')[e.type === "hover_node"?"addClass":"removeClass"]('jstree-wholerow-hovered'); 7315 }, this)) 7316 .on("contextmenu.jstree", ".jstree-wholerow", $.proxy(function (e) { 7317 e.preventDefault(); 7318 var tmp = $.Event('contextmenu', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey, pageX : e.pageX, pageY : e.pageY }); 7319 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp); 7320 }, this)) 7321 /*! 7322 .on("mousedown.jstree touchstart.jstree", ".jstree-wholerow", function (e) { 7323 if(e.target === e.currentTarget) { 7324 var a = $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor"); 7325 e.target = a[0]; 7326 a.trigger(e); 7327 } 7328 }) 7329 */ 7330 .on("click.jstree", ".jstree-wholerow", function (e) { 7331 e.stopImmediatePropagation(); 7332 var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey }); 7333 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus(); 7334 }) 7335 .on("click.jstree", ".jstree-leaf > .jstree-ocl", $.proxy(function (e) { 7336 e.stopImmediatePropagation(); 7337 var tmp = $.Event('click', { metaKey : e.metaKey, ctrlKey : e.ctrlKey, altKey : e.altKey, shiftKey : e.shiftKey }); 7338 $(e.currentTarget).closest(".jstree-node").children(".jstree-anchor").first().trigger(tmp).focus(); 7339 }, this)) 7340 .on("mouseover.jstree", ".jstree-wholerow, .jstree-icon", $.proxy(function (e) { 7341 e.stopImmediatePropagation(); 7342 if(!this.is_disabled(e.currentTarget)) { 7343 this.hover_node(e.currentTarget); 7344 } 7345 return false; 7346 }, this)) 7347 .on("mouseleave.jstree", ".jstree-node", $.proxy(function (e) { 7348 this.dehover_node(e.currentTarget); 7349 }, this)); 7350 }; 7351 this.teardown = function () { 7352 if(this.settings.wholerow) { 7353 this.element.find(".jstree-wholerow").remove(); 7354 } 7355 parent.teardown.call(this); 7356 }; 7357 this.redraw_node = function(obj, deep, callback, force_render) { 7358 obj = parent.redraw_node.apply(this, arguments); 7359 if(obj) { 7360 var tmp = div.cloneNode(true); 7361 //tmp.style.height = this._data.core.li_height + 'px'; 7362 if($.inArray(obj.id, this._data.core.selected) !== -1) { tmp.className += ' jstree-wholerow-clicked'; } 7363 if(this._data.core.focused && this._data.core.focused === obj.id) { tmp.className += ' jstree-wholerow-hovered'; } 7364 obj.insertBefore(tmp, obj.childNodes[0]); 7365 } 7366 return obj; 7367 }; 7368 }; 7369 // include the wholerow plugin by default 7370 // $.jstree.defaults.plugins.push("wholerow"); 7371 if(document.registerElement && Object && Object.create) { 7372 var proto = Object.create(HTMLElement.prototype); 7373 proto.createdCallback = function () { 7374 var c = { core : {}, plugins : [] }, i; 7375 for(i in $.jstree.plugins) { 7376 if($.jstree.plugins.hasOwnProperty(i) && this.attributes[i]) { 7377 c.plugins.push(i); 7378 if(this.getAttribute(i) && JSON.parse(this.getAttribute(i))) { 7379 c[i] = JSON.parse(this.getAttribute(i)); 7380 } 7381 } 7382 } 7383 for(i in $.jstree.defaults.core) { 7384 if($.jstree.defaults.core.hasOwnProperty(i) && this.attributes[i]) { 7385 c.core[i] = JSON.parse(this.getAttribute(i)) || this.getAttribute(i); 7386 } 7387 } 7388 $(this).jstree(c); 7389 }; 7390 // proto.attributeChangedCallback = function (name, previous, value) { }; 7391 try { 7392 document.registerElement("vakata-jstree", { prototype: proto }); 7393 } catch(ignore) { } 7394 } 7395 7396}));