jstree
Version:
jQuery tree plugin
1,487 lines (1,469 loc) • 164 kB
JavaScript
/*!
* jsTree {{VERSION}}
* 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 */
/*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";
// 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
/**
* 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 : '{{VERSION}}',
/**
* 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 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.
*
* 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,
/**
* 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').focus(); }
}
},
'up': function (e) {
// up
e.preventDefault();
var o = this.get_prev_dom(e.currentTarget);
if(o && o.length) { o.children('.jstree-anchor').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').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').focus(); }
}
},
'down': function (e) {
// down
e.preventDefault();
var o = this.get_next_dom(e.currentTarget);
if(o && o.length) { o.children('.jstree-anchor').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').focus(); }
},
'end': function (e) {
// end
e.preventDefault();
this.element.find('.jstree-anchor').filter(':visible').last().focus();
},
'f2': function (e) {
// f2 - safe to include - if check_callback is false it will fail
e.preventDefault();
this.edit(e.currentTarget);
}
}
};
$.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='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>");
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', 'treeitem');
_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');
_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] || e.which);
parts = parts.sort().join('-').toLowerCase();
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", $.proxy(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
}
}, 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", $.proxy(function (e) {
this.toggle_node(e.target);
}, this))
.on("dblclick.jstree", ".jstree-anchor", $.proxy(function (e) {
if(e.target.tagName && e.target.tagName.toLowerCase() === "input") { return true; }
if(this.settings.core.dblclick_toggle) {
this.toggle_node(e.target);
}
}, this))
.on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
e.preventDefault();
if(e.currentTarget !== document.activeElement) { $(e.currentTarget).focus(); }
this.activate_node(e.currentTarget, e);
}, this))
.on('keydown.jstree', '.jstree-anchor', $.proxy(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;
}
}
}, this))
.on("load_node.jstree", $.proxy(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($.proxy(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");
}
}, this), 0);
}
}
}, this))
// quick searching when the tree is focused
.on('keypress.jstree', $.proxy(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($.proxy(function (i, v) {
if($(v).text().toLowerCase().indexOf(word) === 0) {
$(v).focus();
end = true;
return false;
}
}, this));
if(end) { return; }
// match for whole word from the beginning of the tree
col.slice(0, ind).each($.proxy(function (i, v) {
if($(v).text().toLowerCase().indexOf(word) === 0) {
$(v).focus();
end = true;
return false;
}
}, 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($.proxy(function (i, v) {
if($(v).text().toLowerCase().charAt(0) === chr) {
$(v).focus();
end = true;
return false;
}
}, this));
if(end) { return; }
// search from the beginning
col.slice(0, ind + 1).each($.proxy(function (i, v) {
if($(v).text().toLowerCase().charAt(0) === chr) {
$(v).focus();
end = true;
return false;
}
}, this));
if(end) { return; }
}
}, this))
// THEME RELATED
.on("init.jstree", $.proxy(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);
}, this))
.on("loading.jstree", $.proxy(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" ]();
}, this))
.on('blur.jstree', '.jstree-anchor', $.proxy(function (e) {
this._data.core.focused = null;
$(e.currentTarget).filter('.jstree-hovered').trigger('mouseleave');
this.element.attr('tabindex', '0');
}, this))
.on('focus.jstree', '.jstree-anchor', $.proxy(function (e) {
var tmp = this.get_node(e.currentTarget);
if(tmp && tmp.id) {
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');
}, this))
.on('focus.jstree', $.proxy(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').focus();
}
}
}, this))
.on('mouseenter.jstree', '.jstree-anchor', $.proxy(function (e) {
this.hover_node(e.currentTarget);
}, this))
.on('mouseleave.jstree', '.jstree-anchor', $.proxy(function (e) {
this.dehover_node(e.currentTarget);
}, 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($.isFunction(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 = 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 k, l, i, j, c;
if($.isArray(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;
this.get_node(obj, true).addClass("jstree-loading").attr('aria-busy',true);
this._load_node(obj, $.proxy(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');
}
}
}
dom.removeClass("jstree-loading").attr('aria-busy',false);
/**
* 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);
}
}, 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($.isFunction(s)) {
return s.call(this, obj, $.proxy(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));
}, this));
}
if(typeof s === 'object') {
if(s.url) {
s = $.extend(true, {}, s);
if($.isFunction(s.url)) {
s.url = s.url.call(this, obj);
}
if($.isFunction(s.data)) {
s.data = s.data.call(this, obj);
}
return $.ajax(s)
.done($.proxy(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); });
//return callback.call(this, this._append_json_data(obj, d));
}
if((type && type.indexOf('html') !== -1) || typeof d === "string") {
return this._append_html_data(obj, $($.parseHTML(d)).filter(notTextOrCommentNode), function (status) { callback.call(this, status); });
// return callback.call(this, this._append_html_data(obj, $(d)));
}
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 }) };
this.settings.core.error.call(this, this._data.core.last_error);
return callback.call(this, false);
}, this))
.fail($.proxy(function (f) {
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 }) };
callback.call(this, false);
this.settings.core.error.call(this, this._data.core.last_error);
}, this));
}
if ($.isArray(s)) {
t = $.extend(true, [], s);
} else if ($.isPlainObject(s)) {
t = $.extend(true, {}, s);
} else {
t = s;
}
if(obj.id === $.jstree.root) {
return this._append_json_data(obj, t, function (status) {
callback.call(this, status);
});
}
else {
this._data.core.last_error = { 'error' : 'nodata', 'plug