ng-js-tree
Version:
Angular Directive for the famous JsTree
1,480 lines (1,463 loc) • 258 kB
JavaScript
/*globals jQuery, define, exports, require, window, document, postMessage */
(function (factory) {
"use strict";
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
}
else if(typeof exports === 'object') {
factory(require('jquery'));
}
else {
factory(jQuery);
}
}(function ($, undefined) {
"use strict";
/*!
* jsTree 3.0.8
* 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: browser: true, ass: true, bitwise: true, continue: true, nomen: true, plusplus: true, regexp: true, unparam: true, todo: true, white: true
*/
// 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'),
_d = document, _node = _d.createElement('LI'), _temp1, _temp2;
_node.setAttribute('role', 'treeitem');
_temp1 = _d.createElement('I');
_temp1.className = 'jstree-icon jstree-ocl';
_temp1.setAttribute('role', 'presentation');
_node.appendChild(_temp1);
_temp1 = _d.createElement('A');
_temp1.className = 'jstree-anchor';
_temp1.setAttribute('href','#');
_temp1.setAttribute('tabindex','-1');
_temp2 = _d.createElement('I');
_temp2.className = 'jstree-icon jstree-themeicon';
_temp2.setAttribute('role', 'presentation');
_temp1.appendChild(_temp2);
_node.appendChild(_temp1);
_temp1 = _temp2 = null;
/**
* 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.0.8',
/**
* 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
};
/**
* 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]);
}
});
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
},
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 = 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))) {
$(this).data('jstree', new $.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[':'].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' or 'copy_node'
* // 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 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
};
$.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;
},
/**
* used to decorate an instance with a plugin. 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 : {
'#' : {
id : '#',
parent : null,
parents : [],
children : [],
children_d : [],
state : { loaded : false }
}
},
changed : [],
force_full_redraw : false,
redraw_timeout : false,
default_state : {
loaded : true,
opened : false,
selected : false,
disabled : false
}
};
this.element = $(el).addClass('jstree jstree-' + this._id);
this.settings = options;
this.element.bind("destroyed", $.proxy(this.teardown, this));
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().height() || 24;
/**
* triggered after the loading text is shown and before loading starts
* @event
* @name loading.jstree
*/
this.trigger("loading");
this.load_node('#');
},
/**
* 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) {
if(this._wrk) {
try {
window.URL.revokeObjectURL(this._wrk);
this._wrk = null;
}
catch (ignore) { }
}
if(!keep_html) { this.element.empty(); }
this.element.unbind("destroyed", this.teardown);
this.teardown();
},
/**
* 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;
this.element
.on("dblclick.jstree", function () {
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("click.jstree", ".jstree-ocl", $.proxy(function (e) {
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 === "INPUT") { return true; }
var o = null;
if(this._data.core.rtl) {
if(e.which === 37) { e.which = 39; }
else if(e.which === 39) { e.which = 37; }
}
switch(e.which) {
case 32: // aria defines space only with Ctrl
if(e.ctrlKey) {
e.type = "click";
$(e.currentTarget).trigger(e);
}
break;
case 13: // enter
e.type = "click";
$(e.currentTarget).trigger(e);
break;
case 37: // right
e.preventDefault();
if(this.is_open(e.currentTarget)) {
this.close_node(e.currentTarget);
}
else {
o = this.get_parent(e.currentTarget);
if(o && o.id !== '#') { this.get_node(o, true).children('.jstree-anchor').focus(); }
}
break;
case 38: // up
e.preventDefault();
o = this.get_prev_dom(e.currentTarget);
if(o && o.length) { o.children('.jstree-anchor').focus(); }
break;
case 39: // left
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)) {
o = this.get_node(e.currentTarget, true).children('.jstree-children')[0];
if(o) { $(this._firstChild(o)).children('.jstree-anchor').focus(); }
}
break;
case 40: // down
e.preventDefault();
o = this.get_next_dom(e.currentTarget);
if(o && o.length) { o.children('.jstree-anchor').focus(); }
break;
case 106: // aria defines * on numpad as open_all - not very common
this.open_all();
break;
case 36: // home
e.preventDefault();
o = this._firstChild(this.get_container_ul()[0]);
if(o) { $(o).children('.jstree-anchor').filter(':visible').focus(); }
break;
case 35: // end
e.preventDefault();
this.element.find('.jstree-anchor').filter(':visible').last().focus();
break;
/*
// delete
case 46:
e.preventDefault();
o = this.get_node(e.currentTarget);
if(o && o.id && o.id !== '#') {
o = this.is_selected(o) ? this.get_selected() : o;
this.delete_node(o);
}
break;
// f2
case 113:
e.preventDefault();
o = this.get_node(e.currentTarget);
if(o && o.id && o.id !== '#') {
// this.edit(o);
}
break;
default:
// console.log(e.which);
break;
*/
}
}, this))
.on("load_node.jstree", $.proxy(function (e, data) {
if(data.status) {
if(data.node.id === '#' && !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 && !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
*/
setTimeout($.proxy(function () { this.trigger("ready"); }, this), 0);
}
}
}, this))
// quick searching when the tree is focused
.on('keypress.jstree', $.proxy(function (e) {
if(e.target.tagName === "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 + '+$').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.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))
.on('blur.jstree', '.jstree-anchor', $.proxy(function (e) {
this._data.core.focused = null;
$(e.currentTarget).filter('.jstree-hovered').mouseleave();
}, 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).mouseleave();
$(e.currentTarget).mouseenter();
}, this))
.on('focus.jstree', $.proxy(function () {
if(!this._data.core.focused) {
this.get_node(this.element.attr('aria-activedescendant'), true).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;
}
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 = $(obj, 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 = $(obj, this.element)).length && dom.hasClass('jstree')) {
obj = this._model.data['#'];
}
else {
return false;
}
if(as_dom) {
obj = obj.id === '#' ? 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 === '#' || !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").next(".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 === '#') {
return false;
}
return obj.parent;
},
/**
* get a jQuery collection of all the children of a node (node must be rendered)
* @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(k = 0, l = obj.children_d.length; k < l; k++) {
for(i = 0, j = obj.parents.length; i < j; i++) {
this._model.data[obj.parents[i]].children_d = $.vakata.array_remove_item(this._model.data[obj.parents[i]].children_d, obj.children_d[k]);
}
if(this._model.data[obj.children_d[k]].state.selected) {
c = true;
this._data.core.selected = $.vakata.array_remove_item(this._data.core.selected, obj.children_d[k]);
}
delete this._model.data[obj.children_d[k]];
}
obj.children = [];
obj.children_d = [];
if(c) {
this.trigger('changed', { 'action' : 'load_node', 'node' : obj, 'selected' : this._data.core.selected });
}
}
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;
var dom = this.get_node(obj, true);
if(obj.state.loaded && !obj.children.length && dom && dom.length && !dom.hasClass('jstree-leaf')) {
dom.removeClass('jstree-closed jstree-open').addClass('jstree-leaf');
}
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 the 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) {
var r = true,
c = function () { this._load_nodes(nodes, callback, true); },
m = this._model.data, i, j;
for(i = 0, j = nodes.length; i < j; i++) {
if(m[nodes[i]] && (!m[nodes[i]].state.loaded || !is_callback)) {
if(!this.is_loading(nodes[i])) {
this.load_node(nodes[i], c);
}
r = false;
}
}
if(r) {
if(callback && !callback.done) {
callback.call(this, nodes);
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 = '#'; }
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;
// use original HTML
if(!s) {
if(obj.id === '#') {
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 === '#' ? 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);
}
this[typeof d === 'string' ? '_append_html_data' : '_append_json_data'](obj, typeof d === 'string' ? $(d) : 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.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.indexOf('html') !== -1 || typeof d === "string") {
return this._append_html_data(obj, $(d), 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) {
callback.call(this, false);
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 }) };
this.settings.core.error.call(this, this._data.core.last_error);
}, this));
}
t = ($.isArray(s) || $.isPlainObject(s)) ? JSON.parse(JSON.stringify(s)) : s;
if(obj.id === '#') {
return this._append_json_data(obj, t, function (status) {
callback.call(this, status);
});
}
else {
this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_05', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
this.settings.core.error.call(this, this._data.core.last_error);
return callback.call(this, false);
}
//return callback.call(this, (obj.id === "#" ? this._append_json_data(obj, t) : false) );
}
if(typeof s === 'string') {
if(obj.id === '#') {
return this._append_html_data(obj, $(s), function (status) {
callback.call(this, status);
});
}
else {
this._data.core.last_error = { 'error' : 'nodata', 'plugin' : 'core', 'id' : 'core_06', 'reason' : 'Could not load node', 'data' : JSON.stringify({ 'id' : obj.id }) };
this.settings.core.error.call(this, this._data.core.last_error);
return callback.call(this, false);
}
//return callback.call(this, (obj.id === "#" ? this._append_html_data(obj, $(s)) : false) );
}
return callback.call(this, false);
},
/**
* adds a node to the list of nodes to redraw. Used only internally.
* @private
* @name _node_changed(obj [, callback])
* @param {mixed} obj
*/
_node_changed : function (obj) {
obj = this.get_node(obj);
if(obj) {
this._model.changed.push(obj.id);
}
},
/**
* appends HTML content to the tree. Used internally.
* @private
* @name _append_html_data(obj, data)
* @param {mixed} obj the node to append to
* @param {String} data the HTML string to parse and append
* @trigger model.jstree, changed.jstree
*/
_append_html_data : function (dom, data, cb) {
dom = this.get_node(dom);
dom.children = [];
dom.children_d = [];
var dat = data.is('ul') ? data.children() : data,
par = dom.id,
chd = [],
dpc = [],
m = this._model.data,
p = m[par],
s = this._data.core.selected.length,
tmp, i, j;
dat.each($.proxy(function (i, v) {
tmp = this._parse_model_from_html($(v), par, p.parents.concat());
if(tmp) {
chd.push(tmp);
dpc.push(tmp);
if(m[tmp].children_d.length) {
dpc = dpc.concat(m[tmp].children_d);
}
}
}, this));
p.children = chd;
p.children_d = dpc;
for(i = 0, j = p.parents.length; i < j; i++) {
m[p.parents[i]].children_d = m[p.parents[i]].children_d.concat(dpc);
}
/**
* triggered when new data is inserted to the tree model
* @event
* @name model.jstree
* @param {Array} nodes an array of node IDs
* @param {String} parent the parent ID of the nodes
*/
this.trigger('model', { "nodes" : dpc, 'parent' : par });
if(par !== '#') {
this._node_changed(par);
this.redraw();
}
else {
this.get_container_ul().children('.jstree-initial-node').remove();
this.redraw(true);
}
if(this._data.core.selected.length !== s) {
this.trigger('changed', { 'action' : 'model', 'selected' : this._data.core.selected });
}
cb.call(this, true);
},
/**
* appends JSON content to the tree. Used internally.
* @private
* @name _append_json_data(obj, data)
* @param {mixed} obj the node to append to
* @param {String} data the JSON object to parse and append
* @param {Boolean} force_processing internal param - do not set
* @trigger model.jstree, changed.jstree
*/
_append_json_data : function (dom, data, cb, force_processing) {
dom = this.get_node(dom);
dom.children = [];
dom.children_d = [];
// *%$@!!!
if(data.d) {
data = data.d;
if(typeof data === "string") {
data = JSON.parse(data);
}
}
if(!$.isArray(data)) { data = [data]; }
var w = null,
args = {
'df' : this._model.default_state,
'dat' : data,
'par' : dom.id,
'm' : this._model.data,
't_id' : this._id,
't_cnt' : this._cnt,
'sel' : this._data.core.selected
},
func = function (data, undefined) {
if(data.data) { data = data.data; }
var dat = data.dat,
par = data.par,
chd = [],
dpc = [],
add = [],
df = data.df,
t_id = data.t_id,
t_cnt = data.t_cnt,
m = data.m,
p = m[par],
sel = data.sel,
tmp, i, j, rslt,
parse_flat = function (d, p, ps) {
if(!ps) { ps = []; }
else { ps = ps.concat(); }
if(p) { ps.unshift(p); }
var tid = d.id.toString(),
i, j, c, e,
tmp = {
id : tid,
text : d.text || '',
icon : d.icon !== undefined ? d.icon : true,