UNPKG

jstree

Version:

jQuery tree plugin

1,493 lines (1,474 loc) 311 kB
/*globals jQuery, define, module, exports, require, window, document, postMessage */ (function (factory) { "use strict"; if (typeof define === 'function' && define.amd) { define(['jquery'], factory); } else if(typeof module !== 'undefined' && module.exports) { module.exports = factory(require('jquery')); } else { factory(jQuery); } }(function ($, undefined) { "use strict"; /*! * jsTree 3.3.17 * http://jstree.com/ * * Copyright (c) 2014 Ivan Bozhanov (http://vakata.com) * * Licensed same as jquery - under the terms of the MIT License * http://www.opensource.org/licenses/mit-license.php */ /*! * if using jslint please allow for the jQuery global and use following options: * jslint: loopfunc: true, browser: true, ass: true, bitwise: true, continue: true, nomen: true, plusplus: true, regexp: true, unparam: true, todo: true, white: true */ /*jshint -W083 */ // prevent another load? maybe there is a better way? if($.jstree) { return; } /** * ### jsTree core functionality */ // internal variables var instance_counter = 0, ccp_node = false, ccp_mode = false, ccp_inst = false, themes_loaded = [], src = $('script:last').attr('src'), document = window.document; // local variable is always faster to access then a global var setImmediate = window.setImmediate; var Promise = window.Promise; if (!setImmediate && Promise) { // Good enough approximation of setImmediate setImmediate = function (cb, arg) { Promise.resolve(arg).then(cb); }; } /** * holds all jstree related functions and variables, including the actual class and methods to create, access and manipulate instances. * @name $.jstree */ $.jstree = { /** * specifies the jstree version in use * @name $.jstree.version */ version : '3.3.17', /** * holds all the default options used when creating new instances * @name $.jstree.defaults */ defaults : { /** * 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 `[]` * @name $.jstree.defaults.plugins */ plugins : [] }, /** * stores all loaded jstree plugins (used internally) * @name $.jstree.plugins */ plugins : {}, path : src && src.indexOf('/') !== -1 ? src.replace(/\/[^\/]+$/,'') : '', idregex : /[\\:&!^|()\[\]<>@*'+~#";.,=\- \/${}%?`]/g, root : '#' }; /** * creates a jstree instance * @name $.jstree.create(el [, options]) * @param {DOMElement|jQuery|String} el the element to create the instance on, can be jQuery extended or a selector * @param {Object} options options for this instance (extends `$.jstree.defaults`) * @return {jsTree} the new instance */ $.jstree.create = function (el, options) { var tmp = new $.jstree.core(++instance_counter), opt = options; options = $.extend(true, {}, $.jstree.defaults, options); if(opt && opt.plugins) { options.plugins = opt.plugins; } $.each(options.plugins, function (i, k) { if(i !== 'core') { tmp = tmp.plugin(k, options[k]); } }); $(el).data('jstree', tmp); tmp.init(el, options); return tmp; }; /** * remove all traces of jstree from the DOM and destroy all instances * @name $.jstree.destroy() */ $.jstree.destroy = function () { $('.jstree:jstree').jstree('destroy'); $(document).off('.jstree'); }; /** * the jstree class constructor, used only internally * @private * @name $.jstree.core(id) * @param {Number} id this instance's index */ $.jstree.core = function (id) { this._id = id; this._cnt = 0; this._wrk = null; this._data = { core : { themes : { name : false, dots : false, icons : false, ellipsis : false }, selected : [], last_error : {}, working : false, worker_queue : [], focused : null } }; }; /** * get a reference to an existing instance * * __Examples__ * * // provided a container with an ID of "tree", and a nested node with an ID of "branch" * // all of there will return the same instance * $.jstree.reference('tree'); * $.jstree.reference('#tree'); * $.jstree.reference($('#tree')); * $.jstree.reference(document.getElementByID('tree')); * $.jstree.reference('branch'); * $.jstree.reference('#branch'); * $.jstree.reference($('#branch')); * $.jstree.reference(document.getElementByID('branch')); * * @name $.jstree.reference(needle) * @param {DOMElement|jQuery|String} needle * @return {jsTree|null} the instance or `null` if not found */ $.jstree.reference = function (needle) { var tmp = null, obj = null; if(needle && needle.id && (!needle.tagName || !needle.nodeType)) { needle = needle.id; } if(!obj || !obj.length) { try { obj = $(needle); } catch (ignore) { } } if(!obj || !obj.length) { try { obj = $('#' + needle.replace($.jstree.idregex,'\\$&')); } catch (ignore) { } } if(obj && obj.length && (obj = obj.closest('.jstree')).length && (obj = obj.data('jstree'))) { tmp = obj; } else { $('.jstree').each(function () { var inst = $(this).data('jstree'); if(inst && inst._model.data[needle]) { tmp = inst; return false; } }); } return tmp; }; /** * Create an instance, get an instance or invoke a command on a instance. * * 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). * * 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). * * If there is an existing instance and `arg` is not a string the instance itself is returned (similar to `$.jstree.reference`). * * In any other case - nothing is returned and chaining is not broken. * * __Examples__ * * $('#tree1').jstree(); // creates an instance * $('#tree2').jstree({ plugins : [] }); // create an instance with some options * $('#tree1').jstree('open_node', '#branch_1'); // call a method on an existing instance, passing additional arguments * $('#tree2').jstree(); // get an existing instance (or create an instance) * $('#tree2').jstree(true); // get an existing instance (will not create new instance) * $('#branch_1').jstree().select_node('#branch_1'); // get an instance (using a nested element and call a method) * * @name $().jstree([arg]) * @param {String|Object} arg * @return {Mixed} */ $.fn.jstree = function (arg) { // check for string argument var is_method = (typeof arg === 'string'), args = Array.prototype.slice.call(arguments, 1), result = null; if(arg === true && !this.length) { return false; } this.each(function () { // get the instance (if there is one) and method (if it exists) var instance = $.jstree.reference(this), method = is_method && instance ? instance[arg] : null; // if calling a method, and method is available - execute on the instance result = is_method && method ? method.apply(instance, args) : null; // if there is no instance and no method is being called - create one if(!instance && !is_method && (arg === undefined || $.isPlainObject(arg))) { $.jstree.create(this, arg); } // if there is an instance and no method is called - return the instance if( (instance && !is_method) || arg === true ) { result = instance || false; } // if there was a method call which returned a result - break and return the value if(result !== null && result !== undefined) { return false; } }); // if there was a method call with a valid return value - return that, otherwise continue the chain return result !== null && result !== undefined ? result : this; }; /** * used to find elements containing an instance * * __Examples__ * * $('div:jstree').each(function () { * $(this).jstree('destroy'); * }); * * @name $(':jstree') * @return {jQuery} */ $.expr.pseudos.jstree = $.expr.createPseudo(function(search) { return function(a) { return $(a).hasClass('jstree') && $(a).data('jstree') !== undefined; }; }); /** * stores all defaults for the core * @name $.jstree.defaults.core */ $.jstree.defaults.core = { /** * data configuration * * 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). * * You can also pass in a HTML string or a JSON array here. * * 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. * 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 is being loaded, the return value of those functions will be used. * * 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. * * __Examples__ * * // AJAX * $('#tree').jstree({ * 'core' : { * 'data' : { * 'url' : '/get/children/', * 'data' : function (node) { * return { 'id' : node.id }; * } * } * }); * * // direct data * $('#tree').jstree({ * 'core' : { * 'data' : [ * 'Simple root node', * { * 'id' : 'node_2', * 'text' : 'Root node with options', * 'state' : { 'opened' : true, 'selected' : true }, * 'children' : [ { 'text' : 'Child 1' }, 'Child 2'] * } * ] * } * }); * * // function * $('#tree').jstree({ * 'core' : { * 'data' : function (obj, callback) { * callback.call(this, ['Root 1', 'Root 2']); * } * }); * * @name $.jstree.defaults.core.data */ data : false, /** * configure the various strings used throughout the tree * * You can use an object where the key is the string you need to replace and the value is your replacement. * Another option is to specify a function which will be called with an argument of the needed string and should return the replacement. * If left as `false` no replacement is made. * * __Examples__ * * $('#tree').jstree({ * 'core' : { * 'strings' : { * 'Loading ...' : 'Please wait ...' * } * } * }); * * @name $.jstree.defaults.core.strings */ strings : false, /** * determines what happens when a user tries to modify the structure of the tree * If left as `false` all operations like create, rename, delete, move or copy are prevented. * You can set this to `true` to allow all interactions or use a function to have better control. * * __Examples__ * * $('#tree').jstree({ * 'core' : { * 'check_callback' : function (operation, node, node_parent, node_position, more) { * // operation can be 'create_node', 'rename_node', 'delete_node', 'move_node', 'copy_node' or 'edit' * // in case of 'rename_node' node_position is filled with the new node name * return operation === 'rename_node' ? true : false; * } * } * }); * * @name $.jstree.defaults.core.check_callback */ check_callback : false, /** * a callback called with a single object parameter in the instance's scope when something goes wrong (operation prevented, ajax failed, etc) * @name $.jstree.defaults.core.error */ error : $.noop, /** * the open / close animation duration in milliseconds - set this to `false` to disable the animation (default is `200`) * @name $.jstree.defaults.core.animation */ animation : 200, /** * a boolean indicating if multiple nodes can be selected * @name $.jstree.defaults.core.multiple */ multiple : true, /** * theme configuration object * @name $.jstree.defaults.core.themes */ themes : { /** * the name of the theme to use (if left as `false` the default theme is used) * @name $.jstree.defaults.core.themes.name */ name : false, /** * 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. * @name $.jstree.defaults.core.themes.url */ url : false, /** * the location of all jstree themes - only used if `url` is set to `true` * @name $.jstree.defaults.core.themes.dir */ dir : false, /** * a boolean indicating if connecting dots are shown * @name $.jstree.defaults.core.themes.dots */ dots : true, /** * a boolean indicating if node icons are shown * @name $.jstree.defaults.core.themes.icons */ icons : true, /** * a boolean indicating if node ellipsis should be shown - this only works with a fixed with on the container * @name $.jstree.defaults.core.themes.ellipsis */ ellipsis : false, /** * a boolean indicating if the tree background is striped * @name $.jstree.defaults.core.themes.stripes */ stripes : false, /** * a string (or boolean `false`) specifying the theme variant to use (if the theme supports variants) * @name $.jstree.defaults.core.themes.variant */ variant : false, /** * a boolean specifying if a reponsive version of the theme should kick in on smaller screens (if the theme supports it). Defaults to `false`. * @name $.jstree.defaults.core.themes.responsive */ responsive : false }, /** * 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) * @name $.jstree.defaults.core.expand_selected_onload */ expand_selected_onload : true, /** * 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` * @name $.jstree.defaults.core.worker */ worker : true, /** * Force node text to plain text (and escape HTML). Defaults to `false` * @name $.jstree.defaults.core.force_text */ force_text : false, /** * Should the node be toggled if the text is double clicked. Defaults to `true` * @name $.jstree.defaults.core.dblclick_toggle */ dblclick_toggle : true, /** * Should the loaded nodes be part of the state. Defaults to `false` * @name $.jstree.defaults.core.loaded_state */ loaded_state : false, /** * Should the last active node be focused when the tree container is blurred and the focused again. This helps working with screen readers. Defaults to `true` * @name $.jstree.defaults.core.restore_focus */ restore_focus : true, /** * Force to compute and set "aria-setsize" and "aria-posinset" explicitly for each treeitem. * Some browsers may compute incorrect elements position and produce wrong announcements for screen readers. Defaults to `false` * @name $.jstree.defaults.core.compute_elements_positions */ compute_elements_positions : false, /** * Default keyboard shortcuts (an object where each key is the button name or combo - like 'enter', 'ctrl-space', 'p', etc and the value is the function to execute in the instance's scope) * @name $.jstree.defaults.core.keyboard */ keyboard : { 'ctrl-space': function (e) { // aria defines space only with Ctrl e.type = "click"; $(e.currentTarget).trigger(e); }, 'enter': function (e) { // enter e.type = "click"; $(e.currentTarget).trigger(e); }, 'left': function (e) { // left e.preventDefault(); if(this.is_open(e.currentTarget)) { this.close_node(e.currentTarget); } else { var o = this.get_parent(e.currentTarget); if(o && o.id !== $.jstree.root) { this.get_node(o, true).children('.jstree-anchor').trigger('focus'); } } }, 'up': function (e) { // up e.preventDefault(); var o = this.get_prev_dom(e.currentTarget); if(o && o.length) { o.children('.jstree-anchor').trigger('focus'); } }, 'right': function (e) { // right e.preventDefault(); if(this.is_closed(e.currentTarget)) { this.open_node(e.currentTarget, function (o) { this.get_node(o, true).children('.jstree-anchor').trigger('focus'); }); } else if (this.is_open(e.currentTarget)) { var o = this.get_node(e.currentTarget, true).children('.jstree-children')[0]; if(o) { $(this._firstChild(o)).children('.jstree-anchor').trigger('focus'); } } }, 'down': function (e) { // down e.preventDefault(); var o = this.get_next_dom(e.currentTarget); if(o && o.length) { o.children('.jstree-anchor').trigger('focus'); } }, '*': function (e) { // aria defines * on numpad as open_all - not very common this.open_all(); }, 'home': function (e) { // home e.preventDefault(); var o = this._firstChild(this.get_container_ul()[0]); if(o) { $(o).children('.jstree-anchor').filter(':visible').trigger('focus'); } }, 'end': function (e) { // end e.preventDefault(); this.element.find('.jstree-anchor').filter(':visible').last().trigger('focus'); }, 'f2': function (e) { // f2 - safe to include - if check_callback is false it will fail e.preventDefault(); this.edit(e.currentTarget); } }, /** * Should reselecting an already selected node trigger the select and changed callbacks * @name $.jstree.defaults.core.allow_reselect */ allow_reselect : false }; $.jstree.core.prototype = { /** * used to decorate an instance with a plugin. Used internally. * @private * @name plugin(deco [, opts]) * @param {String} deco the plugin to decorate with * @param {Object} opts options for the plugin * @return {jsTree} */ plugin : function (deco, opts) { var Child = $.jstree.plugins[deco]; if(Child) { this._data[deco] = {}; Child.prototype = this; return new Child(opts, this); } return this; }, /** * initialize the instance. Used internally. * @private * @name init(el, optons) * @param {DOMElement|jQuery|String} el the element we are transforming * @param {Object} options options for this instance * @trigger init.jstree, loading.jstree, loaded.jstree, ready.jstree, changed.jstree */ init : function (el, options) { this._model = { data : {}, changed : [], force_full_redraw : false, redraw_timeout : false, default_state : { loaded : true, opened : false, selected : false, disabled : false } }; this._model.data[$.jstree.root] = { id : $.jstree.root, parent : null, parents : [], children : [], children_d : [], state : { loaded : false } }; this.element = $(el).addClass('jstree jstree-' + this._id); this.settings = options; this._data.core.ready = false; this._data.core.loaded = false; this._data.core.rtl = (this.element.css("direction") === "rtl"); this.element[this._data.core.rtl ? 'addClass' : 'removeClass']("jstree-rtl"); this.element.attr('role','tree'); if(this.settings.core.multiple) { this.element.attr('aria-multiselectable', true); } if(!this.element.attr('tabindex')) { this.element.attr('tabindex','0'); } this.bind(); /** * triggered after all events are bound * @event * @name init.jstree */ this.trigger("init"); this._data.core.original_container_html = this.element.find(" > ul > li").clone(true); this._data.core.original_container_html .find("li").addBack() .contents().filter(function() { return this.nodeType === 3 && (!this.nodeValue || /^\s+$/.test(this.nodeValue)); }) .remove(); 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='none'><i class='jstree-icon jstree-ocl'></i><"+"a class='jstree-anchor' role='treeitem' href='#'><i class='jstree-icon jstree-themeicon-hidden'></i>" + this.get_string("Loading ...") + "</a></li></ul>"); this.element.attr('aria-activedescendant','j' + this._id + '_loading'); this._data.core.li_height = this.get_container_ul().children("li").first().outerHeight() || 24; this._data.core.node = this._create_prototype_node(); /** * triggered after the loading text is shown and before loading starts * @event * @name loading.jstree */ this.trigger("loading"); this.load_node($.jstree.root); }, /** * destroy an instance * @name destroy() * @param {Boolean} keep_html if not set to `true` the container will be emptied, otherwise the current DOM elements will be kept intact */ destroy : function (keep_html) { /** * triggered before the tree is destroyed * @event * @name destroy.jstree */ this.trigger("destroy"); if(this._wrk) { try { window.URL.revokeObjectURL(this._wrk); this._wrk = null; } catch (ignore) { } } if(!keep_html) { this.element.empty(); } this.teardown(); }, /** * Create a prototype node * @name _create_prototype_node() * @return {DOMElement} */ _create_prototype_node : function () { var _node = document.createElement('LI'), _temp1, _temp2; _node.setAttribute('role', 'none'); _temp1 = document.createElement('I'); _temp1.className = 'jstree-icon jstree-ocl'; _temp1.setAttribute('role', 'presentation'); _node.appendChild(_temp1); _temp1 = document.createElement('A'); _temp1.className = 'jstree-anchor'; _temp1.setAttribute('href','#'); _temp1.setAttribute('tabindex','-1'); _temp1.setAttribute('role', 'treeitem'); _temp2 = document.createElement('I'); _temp2.className = 'jstree-icon jstree-themeicon'; _temp2.setAttribute('role', 'presentation'); _temp1.appendChild(_temp2); _node.appendChild(_temp1); _temp1 = _temp2 = null; return _node; }, _kbevent_to_func : function (e) { var keys = { 8: "Backspace", 9: "Tab", 13: "Enter", 19: "Pause", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "Print", 45: "Insert", 46: "Delete", 96: "Numpad0", 97: "Numpad1", 98: "Numpad2", 99 : "Numpad3", 100: "Numpad4", 101: "Numpad5", 102: "Numpad6", 103: "Numpad7", 104: "Numpad8", 105: "Numpad9", '-13': "NumpadEnter", 112: "F1", 113: "F2", 114: "F3", 115: "F4", 116: "F5", 117: "F6", 118: "F7", 119: "F8", 120: "F9", 121: "F10", 122: "F11", 123: "F12", 144: "Numlock", 145: "Scrolllock", 16: 'Shift', 17: 'Ctrl', 18: 'Alt', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5', 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a', 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o', 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.', 186: ';', 187: '=', 188: ',', 189: '-', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\',221: ']', 222: "'", 111: '/', 106: '*', 173: '-' }; var parts = []; if (e.ctrlKey) { parts.push('ctrl'); } if (e.altKey) { parts.push('alt'); } if (e.shiftKey) { parts.push('shift'); } parts.push(keys[e.which] ? keys[e.which].toLowerCase() : e.which); parts = parts.sort().join('-').toLowerCase(); if (parts === 'shift-shift' || parts === 'ctrl-ctrl' || parts === 'alt-alt') { return null; } var kb = this.settings.core.keyboard, i, tmp; for (i in kb) { if (kb.hasOwnProperty(i)) { tmp = i; if (tmp !== '-' && tmp !== '+') { tmp = tmp.replace('--', '-MINUS').replace('+-', '-MINUS').replace('++', '-PLUS').replace('-+', '-PLUS'); tmp = tmp.split(/-|\+/).sort().join('-').replace('MINUS', '-').replace('PLUS', '+').toLowerCase(); } if (tmp === parts) { return kb[i]; } } } return null; }, /** * part of the destroying of an instance. Used internally. * @private * @name teardown() */ teardown : function () { this.unbind(); this.element .removeClass('jstree') .removeData('jstree') .find("[class^='jstree']") .addBack() .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); this.element = null; }, /** * bind all events. Used internally. * @private * @name bind() */ bind : function () { var word = '', tout = null, was_click = 0; this.element .on("dblclick.jstree", function (e) { if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } if(document.selection && document.selection.empty) { document.selection.empty(); } else { if(window.getSelection) { var sel = window.getSelection(); try { sel.removeAllRanges(); sel.collapse(); } catch (ignore) { } } } }) .on("mousedown.jstree", function (e) { if(e.target === this.element[0]) { e.preventDefault(); // prevent losing focus when clicking scroll arrows (FF, Chrome) was_click = +(new Date()); // ie does not allow to prevent losing focus } }.bind(this)) .on("mousedown.jstree", ".jstree-ocl", function (e) { e.preventDefault(); // prevent any node inside from losing focus when clicking the open/close icon }) .on("click.jstree", ".jstree-ocl", function (e) { this.toggle_node(e.target); }.bind(this)) .on("dblclick.jstree", ".jstree-anchor", function (e) { if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } if(this.settings.core.dblclick_toggle) { this.toggle_node(e.target); } }.bind(this)) .on("click.jstree", ".jstree-anchor", function (e) { e.preventDefault(); if(e.currentTarget !== document.activeElement) { $(e.currentTarget).trigger('focus'); } this.activate_node(e.currentTarget, e); }.bind(this)) .on('keydown.jstree', '.jstree-anchor', function (e) { if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } if(this._data.core.rtl) { if(e.which === 37) { e.which = 39; } else if(e.which === 39) { e.which = 37; } } var f = this._kbevent_to_func(e); if (f) { var r = f.call(this, e); if (r === false || r === true) { return r; } } }.bind(this)) .on("load_node.jstree", function (e, data) { if(data.status) { if(data.node.id === $.jstree.root && !this._data.core.loaded) { this._data.core.loaded = true; if(this._firstChild(this.get_container_ul()[0])) { this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id); } /** * triggered after the root node is loaded for the first time * @event * @name loaded.jstree */ this.trigger("loaded"); } if(!this._data.core.ready) { setTimeout(function() { if(this.element && !this.get_container_ul().find('.jstree-loading').length) { this._data.core.ready = true; if(this._data.core.selected.length) { if(this.settings.core.expand_selected_onload) { var tmp = [], i, j; for(i = 0, j = this._data.core.selected.length; i < j; i++) { tmp = tmp.concat(this._model.data[this._data.core.selected[i]].parents); } tmp = $.vakata.array_unique(tmp); for(i = 0, j = tmp.length; i < j; i++) { this.open_node(tmp[i], false, 0); } } this.trigger('changed', { 'action' : 'ready', 'selected' : this._data.core.selected }); } /** * triggered after all nodes are finished loading * @event * @name ready.jstree */ this.trigger("ready"); } }.bind(this), 0); } } }.bind(this)) // quick searching when the tree is focused .on('keypress.jstree', function (e) { if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; } if(tout) { clearTimeout(tout); } tout = setTimeout(function () { word = ''; }, 500); var chr = String.fromCharCode(e.which).toLowerCase(), col = this.element.find('.jstree-anchor').filter(':visible'), ind = col.index(document.activeElement) || 0, end = false; word += chr; // match for whole word from current node down (including the current node) if(word.length > 1) { col.slice(ind).each(function (i, v) { if($(v).text().toLowerCase().indexOf(word) === 0) { $(v).trigger('focus'); end = true; return false; } }.bind(this)); if(end) { return; } // match for whole word from the beginning of the tree col.slice(0, ind).each(function (i, v) { if($(v).text().toLowerCase().indexOf(word) === 0) { $(v).trigger('focus'); end = true; return false; } }.bind(this)); if(end) { return; } } // list nodes that start with that letter (only if word consists of a single char) if(new RegExp('^' + chr.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '+$').test(word)) { // search for the next node starting with that letter col.slice(ind + 1).each(function (i, v) { if($(v).text().toLowerCase().charAt(0) === chr) { $(v).trigger('focus'); end = true; return false; } }.bind(this)); if(end) { return; } // search from the beginning col.slice(0, ind + 1).each(function (i, v) { if($(v).text().toLowerCase().charAt(0) === chr) { $(v).trigger('focus'); end = true; return false; } }.bind(this)); if(end) { return; } } }.bind(this)) // THEME RELATED .on("init.jstree", function () { var s = this.settings.core.themes; this._data.core.themes.dots = s.dots; this._data.core.themes.stripes = s.stripes; this._data.core.themes.icons = s.icons; this._data.core.themes.ellipsis = s.ellipsis; this.set_theme(s.name || "default", s.url); this.set_theme_variant(s.variant); }.bind(this)) .on("loading.jstree", function () { this[ this._data.core.themes.dots ? "show_dots" : "hide_dots" ](); this[ this._data.core.themes.icons ? "show_icons" : "hide_icons" ](); this[ this._data.core.themes.stripes ? "show_stripes" : "hide_stripes" ](); this[ this._data.core.themes.ellipsis ? "show_ellipsis" : "hide_ellipsis" ](); }.bind(this)) .on('blur.jstree', '.jstree-anchor', function (e) { this._data.core.focused = null; $(e.currentTarget).filter('.jstree-hovered').trigger('mouseleave'); this.element.attr('tabindex', '0'); $(e.currentTarget).attr('tabindex', '-1'); }.bind(this)) .on('focus.jstree', '.jstree-anchor', function (e) { var tmp = this.get_node(e.currentTarget); if(tmp && (tmp.id || tmp.id === 0)) { this._data.core.focused = tmp.id; } this.element.find('.jstree-hovered').not(e.currentTarget).trigger('mouseleave'); $(e.currentTarget).trigger('mouseenter'); this.element.attr('tabindex', '-1'); $(e.currentTarget).attr('tabindex', '0'); }.bind(this)) .on('focus.jstree', function () { if(+(new Date()) - was_click > 500 && !this._data.core.focused && this.settings.core.restore_focus) { was_click = 0; var act = this.get_node(this.element.attr('aria-activedescendant'), true); if(act) { act.find('> .jstree-anchor').trigger('focus'); } } }.bind(this)) .on('mouseenter.jstree', '.jstree-anchor', function (e) { this.hover_node(e.currentTarget); }.bind(this)) .on('mouseleave.jstree', '.jstree-anchor', function (e) { this.dehover_node(e.currentTarget); }.bind(this)); }, /** * part of the destroying of an instance. Used internally. * @private * @name unbind() */ unbind : function () { this.element.off('.jstree'); $(document).off('.jstree-' + this._id); }, /** * trigger an event. Used internally. * @private * @name trigger(ev [, data]) * @param {String} ev the name of the event to trigger * @param {Object} data additional data to pass with the event */ trigger : function (ev, data) { if(!data) { data = {}; } data.instance = this; this.element.triggerHandler(ev.replace('.jstree','') + '.jstree', data); }, /** * returns the jQuery extended instance container * @name get_container() * @return {jQuery} */ get_container : function () { return this.element; }, /** * returns the jQuery extended main UL node inside the instance container. Used internally. * @private * @name get_container_ul() * @return {jQuery} */ get_container_ul : function () { return this.element.children(".jstree-children").first(); }, /** * gets string replacements (localization). Used internally. * @private * @name get_string(key) * @param {String} key * @return {String} */ get_string : function (key) { var a = this.settings.core.strings; if($.vakata.is_function(a)) { return a.call(this, key); } if(a && a[key]) { return a[key]; } return key; }, /** * gets the first child of a DOM node. Used internally. * @private * @name _firstChild(dom) * @param {DOMElement} dom * @return {DOMElement} */ _firstChild : function (dom) { dom = dom ? dom.firstChild : null; while(dom !== null && dom.nodeType !== 1) { dom = dom.nextSibling; } return dom; }, /** * gets the next sibling of a DOM node. Used internally. * @private * @name _nextSibling(dom) * @param {DOMElement} dom * @return {DOMElement} */ _nextSibling : function (dom) { dom = dom ? dom.nextSibling : null; while(dom !== null && dom.nodeType !== 1) { dom = dom.nextSibling; } return dom; }, /** * gets the previous sibling of a DOM node. Used internally. * @private * @name _previousSibling(dom) * @param {DOMElement} dom * @return {DOMElement} */ _previousSibling : function (dom) { dom = dom ? dom.previousSibling : null; while(dom !== null && dom.nodeType !== 1) { dom = dom.previousSibling; } return dom; }, /** * 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) * @name get_node(obj [, as_dom]) * @param {mixed} obj * @param {Boolean} as_dom * @return {Object|jQuery} */ get_node : function (obj, as_dom) { if(obj && (obj.id || obj.id === 0)) { obj = obj.id; } if (obj instanceof $ && obj.length && obj[0].id) { obj = obj[0].id; } var dom; try { if(this._model.data[obj]) { obj = this._model.data[obj]; } else if(typeof obj === "string" && this._model.data[obj.replace(/^#/, '')]) { obj = this._model.data[obj.replace(/^#/, '')]; } else if(typeof obj === "string" && (dom = $('#' + obj.replace($.jstree.idregex,'\\$&'), this.element)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) { obj = this._model.data[dom.closest('.jstree-node').attr('id')]; } else if((dom = this.element.find(obj)).length && this._model.data[dom.closest('.jstree-node').attr('id')]) { obj = this._model.data[dom.closest('.jstree-node').attr('id')]; } else if((dom = this.element.find(obj)).length && dom.hasClass('jstree')) { obj = this._model.data[$.jstree.root]; } else { return false; } if(as_dom) { obj = obj.id === $.jstree.root ? this.element : $('#' + obj.id.replace($.jstree.idregex,'\\$&'), this.element); } return obj; } catch (ex) { return false; } }, /** * get the path to a node, either consisting of node texts, or of node IDs, optionally glued together (otherwise an array) * @name get_path(obj [, glue, ids]) * @param {mixed} obj the node * @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 * @param {Boolean} ids if set to true build the path using ID, otherwise node text is used * @return {mixed} */ get_path : function (obj, glue, ids) { obj = obj.parents ? obj : this.get_node(obj); if(!obj || obj.id === $.jstree.root || !obj.parents) { return false; } var i, j, p = []; p.push(ids ? obj.id : obj.text); for(i = 0, j = obj.parents.length; i < j; i++) { p.push(ids ? obj.parents[i] : this.get_text(obj.parents[i])); } p = p.reverse().slice(1); return glue ? p.join(glue) : p; }, /** * get the next visible node that is below the `obj` node. If `strict` is set to `true` only sibling nodes are returned. * @name get_next_dom(obj [, strict]) * @param {mixed} obj * @param {Boolean} strict * @return {jQuery} */ get_next_dom : function (obj, strict) { var tmp; obj = this.get_node(obj, true); if(obj[0] === this.element[0]) { tmp = this._firstChild(this.get_container_ul()[0]); while (tmp && tmp.offsetHeight === 0) { tmp = this._nextSibling(tmp); } return tmp ? $(tmp) : false; } if(!obj || !obj.length) { return false; } if(strict) { tmp = obj[0]; do { tmp = this._nextSibling(tmp); } while (tmp && tmp.offsetHeight === 0); return tmp ? $(tmp) : false; } if(obj.hasClass("jstree-open")) { tmp = this._firstChild(obj.children('.jstree-children')[0]); while (tmp && tmp.offsetHeight === 0) { tmp = this._nextSibling(tmp); } if(tmp !== null) { return $(tmp); } } tmp = obj[0]; do { tmp = this._nextSibling(tmp); } while (tmp && tmp.offsetHeight === 0); if(tmp !== null) { return $(tmp); } return obj.parentsUntil(".jstree",".jstree-node").nextAll(".jstree-node:visible").first(); }, /** * get the previous visible node that is above the `obj` node. If `strict` is set to `true` only sibling nodes are returned. * @name get_prev_dom(obj [, strict]) * @param {mixed} obj * @param {Boolean} strict * @return {jQuery} */ get_prev_dom : function (obj, strict) { var tmp; obj = this.get_node(obj, true); if(obj[0] === this.element[0]) { tmp = this.get_container_ul()[0].lastChild; while (tmp && tmp.offsetHeight === 0) { tmp = this._previousSibling(tmp); } return tmp ? $(tmp) : false; } if(!obj || !obj.length) { return false; } if(strict) { tmp = obj[0]; do { tmp = this._previousSibling(tmp); } while (tmp && tmp.offsetHeight === 0); return tmp ? $(tmp) : false; } tmp = obj[0]; do { tmp = this._previousSibling(tmp); } while (tmp && tmp.offsetHeight === 0); if(tmp !== null) { obj = $(tmp); while(obj.hasClass("jstree-open")) { obj = obj.children(".jstree-children").first().children(".jstree-node:visible:last"); } return obj; } tmp = obj[0].parentNode.parentNode; return tmp && tmp.className && tmp.className.indexOf('jstree-node') !== -1 ? $(tmp) : false; }, /** * get the parent ID of a node * @name get_parent(obj) * @param {mixed} obj * @return {String} */ get_parent : function (obj) { obj = this.get_node(obj); if(!obj || obj.id === $.jstree.root) { return false; } return obj.parent; }, /** * get a jQuery collection of all the children of a node (node must be rendered), returns false on error * @name get_children_dom(obj) * @param {mixed} obj * @return {jQuery} */ get_children_dom : function (obj) { obj = this.get_node(obj, true); if(obj[0] === this.element[0]) { return this.get_container_ul().children(".jstree-node"); } if(!obj || !obj.length) { return false; } return obj.children(".jstree-children").children(".jstree-node"); }, /** * checks if a node has children * @name is_parent(obj) * @param {mixed} obj * @return {Boolean} */ is_parent : function (obj) { obj = this.get_node(obj); return obj && (obj.state.loaded === false || obj.children.length > 0); }, /** * checks if a node is loaded (its children are available) * @name is_loaded(obj) * @param {mixed} obj * @return {Boolean} */ is_loaded : function (obj) { obj = this.get_node(obj); return obj && obj.state.loaded; }, /** * check if a node is currently loading (fetching children) * @name is_loading(obj) * @param {mixed} obj * @return {Boolean} */ is_loading : function (obj) { obj = this.get_node(obj); return obj && obj.state && obj.state.loading; }, /** * check if a node is opened * @name is_open(obj) * @param {mixed} obj * @return {Boolean} */ is_open : function (obj) { obj = this.get_node(obj); return obj && obj.state.opened; }, /** * check if a node is in a closed state * @name is_closed(obj) * @param {mixed} obj * @return {Boolean} */ is_closed : function (obj) { obj = this.get_node(obj); return obj && this.is_parent(obj) && !obj.state.opened; }, /** * check if a node has no children * @name is_leaf(obj) * @param {mixed} obj * @return {Boolean} */ is_leaf : function (obj) { return !this.is_parent(obj); }, /** * loads a node (fetches its children using the `core.data` setting). Multiple nodes can be passed to by using an array. * @name load_node(obj [, callback]) * @param {mixed} obj * @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 * @return {Boolean} * @trigger load_node.jstree */ load_node : function (obj, callback) { var dom = this.get_node(obj, true), k, l, i, j, c; if($.vakata.is_array(obj)) { this._load_nodes(obj.slice(), callback); return true; } obj = this.get_node(obj); if(!obj) { if(callback) { callback.call(this, obj, false); } return false; } // 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? if(obj.state.loaded) { obj.state.loaded = false; for(i = 0, j = obj.parents.length; i < j; i++) { this._model.data[obj.parents[i]].children_d = $.vakata.array_filter(this._model.data[obj.parents[i]].children_d, function (v) { return $.inArray(v, obj.children_d) === -1; }); } for(k = 0, l = obj.children_d.length; k < l; k++) { if(this._model.data[obj.children_d[k]].state.selected) { c = true; } delete this._model.data[obj.children_d[k]]; } if (c) { this._data.core.selected = $.vakata.array_filter(this._data.core.selected, function (v) { return $.inArray(v, obj.children_d) === -1; }); } obj.children = []; obj.children_d = []; if(c) { this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected }); } } obj.state.failed = false; obj.state.loading = true; if (obj.id !== $.jstree.root) { dom.children(".jstree-anchor").attr('aria-busy', true); } else { dom.attr('aria-busy', true); } dom.addClass("jstree-loading"); this._load_node(obj, function (status) { obj = this._model.data[obj.id]; obj.state.loading = false; obj.state.loaded = status; obj.state.failed = !obj.state.loaded; var dom = this.get_node(obj, true), i = 0, j = 0, m = this._model.data, has_children = false; for(i = 0, j = obj.children.length; i < j; i++) { if(m[obj.children[i]] && !m[obj.children[i]].state.hidden) { has_children = true; break; } } if(obj.state.loaded && dom && dom.length) { dom.removeClass('jstree-closed jstree-open jstree-leaf'); if (!has_children) { dom.addClass('jstree-leaf'); } else { if (obj.id !== '#') { dom.addClass(obj.state.opened ? 'jstree-open' : 'jstree-closed'); } } } if (obj.id !== $.jstree.root) { dom.children(".jstree-anchor").attr('aria-busy', false); } else { dom.attr('aria-busy', false); } dom.removeClass("jstree-loading"); /** * triggered after a node is loaded * @event * @name load_node.jstree * @param {Object} node the node that was loading * @param {Boolean} status was the node loaded successfully */ this.trigger('load_node', { "node" : obj, "status" : status }); if(callback) { callback.call(this, obj, status); } }.bind(this)); return true; }, /** * load an array of nodes (will also load unavailable nodes as soon as they appear in the structure). Used internally. * @private * @name _load_nodes(nodes [, callback]) * @param {array} nodes * @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 */ _load_nodes : function (nodes, callback, is_callback, force_reload) { var r = true, c = function () { this._load_nodes(nodes, callback, true); }, m = this._model.data, i, j, tmp = []; for(i = 0, j = nodes.length; i < j; i++) { if(m[nodes[i]] && ( (!m[nodes[i]].state.loaded && !m[nodes[i]].state.failed) || (!is_callback && force_reload) )) { if(!this.is_loading(nodes[i])) { this.load_node(nodes[i], c); } r = false; } } if(r) { for(i = 0, j = nodes.length; i < j; i++) { if(m[nodes[i]] && m[nodes[i]].state.loaded) { tmp.push(nodes[i]); } } if(callback && !callback.done) { callback.call(this, tmp); callback.done = true; } } }, /** * loads all unloaded nodes * @name load_all([obj, callback]) * @param {mixed} obj the node to load recursively, omit to load all nodes in the tree * @param {function} callback a function to be executed once loading all the nodes is complete, * @trigger load_all.jstree */ load_all : function (obj, callback) { if(!obj) { obj = $.jstree.root; } obj = this.get_node(obj); if(!obj) { return false; } var to_load = [], m = this._model.data, c = m[obj.id].children_d, i, j; if(obj.state && !obj.state.loaded) { to_load.push(obj.id); } for(i = 0, j = c.length; i < j; i++) { if(m[c[i]] && m[c[i]].state && !m[c[i]].state.loaded) { to_load.push(c[i]); } } if(to_load.length) { this._load_nodes(to_load, function () { this.load_all(obj, callback); }); } else { /** * triggered after a load_all call completes * @event * @name load_all.jstree * @param {Object} node the recursively loaded node */ if(callback) { callback.call(this, obj); } this.trigger('load_all', { "node" : obj }); } }, /** * handles the actual loading of a node. Used only internally. * @private * @name _load_node(obj [, callback]) * @param {mixed} obj * @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 * @return {Boolean} */ _load_node : function (obj, callback) { var s = this.settings.core.data, t; var notTextOrCommentNode = function notTextOrCommentNode () { return this.nodeType !== 3 && this.nodeType !== 8; }; // use original HTML if(!s) { if(obj.id === $.jstree.root) { return this._append_html_data(obj, this._data.core.original_container_html.clone(true), function (status) { callback.call(this, status); }); } else { return callback.call(this, false); } // return callback.call(this, obj.id === $.jstree.root ? this._append_html_data(obj, this._data.core.original_container_html.clone(true)) : false); } if($.vakata.is_function(s)) { return s.call(this, obj, function (d) { if(d === false) { callback.call(this, false); } else { this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $($.parseHTML(d)).filter(notTextOrCommentNode) : d, function (status) { callback.call(this, status); }); } // 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)); }.bind(this)); } if(typeof s === 'object') { if(s.url) { s = $.extend(true, {}, s); if($.vakata.is_function(s.url)) { s.url = s.url.call(this, obj); } if($.vakata.is_function(s.data)) { s.data = s.data.call(this, obj); } return $.ajax(s) .done(function (d,t,x) { var type = x.getResponseHeader('Content-Type'); if((type && type.indexOf('json') !== -1) || typeof d === "object") { return this._append_json_data(obj, d, function (status) { callback.call(this, status