\n";
- }
+ $params['multiple'] = self::$multipleSelect;
+ $params['delimiter'] = $delimiter;
+ $params['cur_value'] = $cur_value;
+ $params = json_encode( $params );
+ $text = "";
return $text;
}
public static function getParameters() {
$params = parent::getParameters();
$params[] = [
'name' => 'top category',
'type' => 'string',
'description' => wfMessage( 'pf_forminputs_topcategory' )->text()
];
$params[] = [
'name' => 'structure',
'type' => 'text',
'description' => wfMessage( 'pf_forminputs_structure' )->text()
];
$params[] = [
'name' => 'hideroot',
'type' => 'boolean',
'description' => wfMessage( 'pf_forminputs_hideroot' )->text()
];
$params[] = [
'name' => 'depth',
'type' => 'int',
'description' => wfMessage( 'pf_forminputs_depth' )->text()
];
$params[] = [
'name' => 'height',
'type' => 'int',
'description' => wfMessage( 'pf_forminputs_height' )->text()
];
$params[] = [
'name' => 'width',
'type' => 'int',
'description' => wfMessage( 'pf_forminputs_width' )->text()
];
return $params;
}
/**
* Returns the HTML code to be included in the output page for this input.
* @return string
*/
public function getHtmlText() {
return self::getHTML(
$this->mCurrentValue,
$this->mInputName,
$this->mIsMandatory,
$this->mIsDisabled,
$this->mOtherArgs
);
}
/**
* Creates a Title object from a user-provided (and thus unsafe) string
* @param string $title
* @return null|Title
*/
static function makeTitle( $title ) {
$title = trim( $title );
if ( strval( $title ) === '' ) {
return null;
}
# The title must be in the category namespace
# Ignore a leading Category: if there is one
$t = Title::newFromText( $title, NS_CATEGORY );
if ( !$t || $t->getNamespace() != NS_CATEGORY || $t->getInterwiki() != '' ) {
// If we were given something like "Wikipedia:Foo" or "Template:",
// try it again but forced.
$title = "Category:$title";
$t = Title::newFromText( $title );
}
return $t;
}
}
diff --git a/libs/PF_FullCalendar.js b/libs/PF_FullCalendar.js
index 3e9f77b4..e9e4a011 100644
--- a/libs/PF_FullCalendar.js
+++ b/libs/PF_FullCalendar.js
@@ -1,1178 +1,1178 @@
/**
* Code to integrate the FullCalendar JavaScript library into Page Forms.
*
* @author Priyanshu Varshney
*/
/* global moment */
( function ( $, mw, pf ) {
'use strict';
$( '.pfFullCalendarJS' ).each( function() {
$( '#fullCalendarLoading' ).css("display", "block");
// This counter is used to assign unique ids to the calendar events.
// If the event is deleted, we lose that unique id and can't be used gain.
var counter = 0;
var monthNames = mw.config.get('monthMessages');
// Stuff from PF_FormPrinter.php
var calendarParams = mw.config.get( 'wgPageFormsCalendarParams' );
var calendarGridValues = mw.config.get( 'wgPageFormsCalendarValues' );
var calendarHTML = mw.config.get('wgPageFormsCalendarHTML');
var $fcDiv = $( this );
var calendarId = $fcDiv.attr( 'id' );
var templateName = $fcDiv.attr( 'template-name' );
var eventTitleField = $fcDiv.attr( 'title-field' );
var eventDateField = $fcDiv.attr( 'event-date-field' );
var eventStartDateField = $fcDiv.attr( 'event-start-date-field' );
var eventEndDateField = $fcDiv.attr( 'event-end-date-field' );
var flagOneDayEvent = true;
var pageLoaded = false;
var isEventEndDateTime = false;
var isEventStartTime = false;
if( eventDateField === undefined ) {
flagOneDayEvent = false;
}
var fieldType=[];
var englishMonthNames = [ 'January', 'February',
'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November',
'December' ];
// From here the game begins - getting the form HTML to be used as the popup form -
// for the calendar interface
var formHtml = calendarHTML[templateName];
var popup = '' );
$("[class|='fancybox-close-small']").attr("type", "button");
$('#popupForm').find(".pfTreeInput").each( function() {
$(this).applyFancytree();
});
// Prepare the popup form for editing
for( i =0 ; i').attr( 'type', 'hidden' ).attr( 'name', inputName ).attr( 'value',finalFieldValues[inputName] ).appendTo( '#pfForm' );
}
}
for( var k =0;k').attr( 'type', 'hidden' ).attr( 'name', entryName ).attr( 'value',eventsNoDate[k][parameterName] ).appendTo( '#pfForm' );
}
}
});
});
}( jQuery, mediaWiki, pf ) );
diff --git a/libs/PF_tree.js b/libs/PF_tree.js
index 4cea4b05..1dd8a3ba 100644
--- a/libs/PF_tree.js
+++ b/libs/PF_tree.js
@@ -1,90 +1,105 @@
/**
- * Defines the applyFancytree() function, which turns an HTML "tree" of
+ * Defines the applyJStree() function, which turns an HTML "tree" of
* checkboxes or radiobuttons into a dynamic and collapsible tree of options
- * using the Fancytree JS library.
+ * using the jsTree JS library.
*
* @author Mathias Lidal
* @author Yaron Koren
* @author Priyanshu Varshney
+ * @author Amr El-Absy
*/
-( function( $, mw, pf ) {
- 'use strict';
-
- // Attach the Fancytree widget to an existing
element
- // and pass the tree options as an argument to the fancytree() function.
- jQuery.fn.applyFancytree = function() {
- var node = this;
- var selectMode = 2;
- var checkboxClass = "fancytree-checkbox";
- if (node.find(":input:radio").length) {
- selectMode = 1;
- checkboxClass = "fancytree-radio";
- }
+ ( function ($, mw, pf) {
- var newClassNames = {
- checkbox: checkboxClass,
- selected: "fancytree-selected"
- };
+ pf.TreeInput = function (elem) {
+ this.element = elem;
+ this.id = $(this.element).attr('id');
+ };
- node.fancytree({
- checkbox: true,
- autoScroll: true,
- minExpandLevel: 5,
- _classNames: newClassNames,
- selectMode: selectMode,
- // click event allows user to de/select the checkbox
- // by just selecting the title
- click: function(event, data) {
- var node = data.node,
- // Only for click and dblclick events
- // 'title' | 'prefix' | 'expander' | 'checkbox' | 'icon'
- targetType = data.targetType;
- if ( targetType === "expander" ) {
- data.node.toggleExpanded();
- } else if ( targetType === "checkbox" ||
- targetType === "title" ) {
- data.node.toggleSelected();
- }
- return false;
- },
+ var TreeInput_proto = new pf.TreeInput();
- // Un/check checkboxes/radiobuttons recursively after
- // selection.
- select: function (event, data) {
- if ( data.node === undefined ) {
- return;
- }
- var inputkey = "chb-" + data.node.key;
- var checkBoxes = node.find("[id='" + inputkey + "']");
- checkBoxes.attr("checked", !checkBoxes.attr("checked"));
- },
- // Prevent reappearing of checkbox when node is
- // collapsed.
- expand: function(select, data) {
- if ( data.node === undefined ) {
- return;
+ TreeInput_proto.setOptions = function () {
+ var data = $(this.element).attr('data');
+ this.data = JSON.parse(data);
+ var params = $(this.element).attr('params');
+ this.params = JSON.parse(params);
+ this.delimiter = this.params.delimiter;
+ this.multiple = this.params.multiple;
+ this.values = [];
+ this.cur_value = this.params.cur_value;
+
+ var options = {
+ 'plugins' : [ 'checkbox' ],
+ 'core' : {
+ 'data' : this.data,
+ 'multiple': this.multiple,
+ 'themes' : {
+ "icons": false
}
- $("#chb-" + data.node.key).attr("checked",
- data.node.isSelected()).addClass("hidden");
},
+ 'checkbox': {
+ 'three_state': false,
+ 'cascade': "none"
+ }
+ };
- });
+ return options;
+ };
- // Update real checkboxes according to selections.
- $.map(node.fancytree("getTree").getSelectedNodes(),
- function (data) {
- if ( data.node === undefined ) {
- return;
- }
- $("#chb-" + data.node.key).attr("checked", true);
- data.node.setActive();
- });
- var activeNode = node.fancytree("getTree").getActiveNode();
- if (activeNode !== null) {
- activeNode.setActive(false);
+ TreeInput_proto.check = function( data ) {
+ var div_id = $(this.element).attr('id');
+ var input_name = div_id.replace("treeinput", "");
+ var input = $(this.element).next('input.PFTree_data');
+
+ if ( this.multiple ) {
+ this.values.push( data );
+ var data_string = this.values.join( this.delimiter );
+ input.attr( 'value', data_string );
+ } else {
+ this.values.push( data );
+ input.attr('value', data);
}
+ };
+
+ TreeInput_proto.uncheck = function( data ) {
+ var div_id = $( this.element ).attr('id');
+ var input_name = div_id.replace( "treeinput", "" );
+ var input = $( this.element ).next( 'input.PFTree_data' );
+ this.values.splice( this.values.indexOf( data ), 1 );
+ var data_string = this.values.join( this.delimiter );
+ input.attr( 'value', data_string );
};
-}( jQuery, mediaWiki, pf ) );
+ TreeInput_proto.setCurValue = function () {
+ if ( this.cur_value !== null && this.cur_value !== undefined && this.cur_value !== "" ) {
+ var div_id = $( this.element ).attr('id');
+ var input_name = div_id.replace( "treeinput", "" );
+ var input = $( this.element ).next( 'input.PFTree_data' );
+
+ input.attr( 'value', this.cur_value );
+ this.values = this.cur_value.split( this.delimiter );
+ }
+ };
+
+ pf.TreeInput.prototype = TreeInput_proto;
+
+} (jQuery, mediaWiki, pf) );
+
+$.fn.extend({
+ applyJSTree: function () {
+ var tree = new pf.TreeInput(this);
+ var options = tree.setOptions();
+
+ $(this).jstree(options);
+
+ $(this).bind('select_node.jstree', function (evt, data) {
+ tree.check(data.node.text);
+ });
+ $(this).bind('deselect_node.jstree', function (evt, data) {
+ tree.uncheck(data.node.text);
+ });
+
+ tree.setCurValue();
+ }
+});
diff --git a/libs/PageForms.js b/libs/PageForms.js
index d613e55c..cb3ef299 100644
--- a/libs/PageForms.js
+++ b/libs/PageForms.js
@@ -1,1886 +1,1906 @@
/**
* PageForms.js
*
* Javascript utility functions for the Page Forms extension.
*
* @author Yaron Koren
* @author Sanyam Goyal
* @author Stephan Gambke
* @author Jeffrey Stuckman
* @author Harold Solbrig
* @author Eugene Mednikov
*/
/*global wgPageFormsShowOnSelect, wgPageFormsFieldProperties, wgPageFormsCargoFields, wgPageFormsDependentFields, validateAll, alert, mwTinyMCEInit, pf, Sortable*/
// Activate autocomplete functionality for the specified field
( function ( $, mw ) {
/* extending jQuery functions for custom highlighting */
$.ui.autocomplete.prototype._renderItem = function( ul, item) {
var delim = this.element[0].delimiter;
var term;
if ( delim === null ) {
term = this.term;
} else {
term = this.term.split( delim ).pop();
}
var re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
term.replace(/([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1") +
")(?![^<>]*>)(?![^&;]+;)", "gi");
// HTML-encode the value's label.
var itemLabel = $('').text(item.label).html();
var loc = itemLabel.search(re);
var t;
if (loc >= 0) {
t = itemLabel.substr(0, loc) +
'' + itemLabel.substr(loc, term.length) + '' +
itemLabel.substr(loc + term.length);
} else {
t = itemLabel;
}
return $( "" )
.data( "item.autocomplete", item )
.append( " " + t + "" )
.appendTo( ul );
};
$.fn.attachAutocomplete = function() {
try {
return this.each(function() {
// Get all the necessary values from the input's "autocompletesettings"
// attribute. This should probably be done as three separate attributes,
// instead.
var field_string = $(this).attr("autocompletesettings");
if ( typeof field_string === 'undefined' ) {
return;
}
var field_values = field_string.split(',');
var delimiter = null;
var data_source = field_values[0];
if (field_values[1] === 'list') {
delimiter = ",";
if (field_values[2] !== null && field_values[2] !== '' && field_values[2] !== undefined) {
delimiter = field_values[2];
}
}
// Modify the delimiter. If it's "\n", change it to an actual
// newline - otherwise, add a space to the end.
// This doesn't cover the case of a delimiter that's a newline
// plus something else, like ".\n" or "\n\n", but as far as we
// know no one has yet needed that.
if ( delimiter !== null && delimiter !== '' && delimiter !== undefined ) {
if ( delimiter === "\\n" ) {
delimiter = "\n";
} else {
delimiter += " ";
}
}
// Store this value within the object, so that it can be used
// during highlighting of the search term as well.
this.delimiter = delimiter;
/* extending jQuery functions */
$.extend( $.ui.autocomplete, {
filter: function(array, term) {
var wgPageFormsAutocompleteOnAllChars = mw.config.get( 'wgPageFormsAutocompleteOnAllChars' );
var matcher;
if ( wgPageFormsAutocompleteOnAllChars ) {
matcher = new RegExp($.ui.autocomplete.escapeRegex(term), "i" );
} else {
matcher = new RegExp("(^|\\s)" + $.ui.autocomplete.escapeRegex(term), "i" );
}
// This may be an associative array instead of a
// regular one - grep() requires a regular one.
// (Is this "if" check necessary, or useful?)
if ( typeof array === 'object' ) {
// Unfortunately, Object.values() is
// not supported on all browsers.
array = Object.keys(array).map(function(key) {
return array[key];
});
}
return $.grep( array, function(value) {
return matcher.test( value.label || value.value || value );
});
}
} );
var values = $(this).data('autocompletevalues');
if ( !values ) {
var wgPageFormsAutocompleteValues = mw.config.get( 'wgPageFormsAutocompleteValues' );
values = wgPageFormsAutocompleteValues[field_string];
}
var split = function (val) {
return val.split(delimiter);
};
var extractLast = function (term) {
return split(term).pop();
};
if (values !== null && values !== undefined) {
// Local autocompletion
if (delimiter !== null && delimiter !== undefined) {
// Autocomplete for multiple values
var thisInput = $(this);
$(this).autocomplete({
minLength: 0,
source: function(request, response) {
// We need to re-get the set of values, since
// the "values" variable gets overwritten.
values = thisInput.data( 'autocompletevalues' );
if ( !values ) {
values = wgPageFormsAutocompleteValues[field_string];
}
response($.ui.autocomplete.filter(values, extractLast(request.term)));
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
var terms = split( this.value );
// remove the current input
terms.pop();
// add the selected item
terms.push( ui.item.value );
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join(delimiter);
return false;
}
});
} else {
// Autocomplete for a single value
$(this).autocomplete({
// Unfortunately, Object.values() is
// not supported on all browsers.
source: ( typeof values === 'object' ) ? Object.keys(values).map(function(key) { return values[key]; }) : values
});
}
} else {
// Remote autocompletion.
var myServer = mw.util.wikiScript( 'api' );
var autocomplete_type = $(this).attr("autocompletedatatype");
if ( autocomplete_type === 'cargo field' ) {
var table_and_field = data_source.split('|');
myServer += "?action=pfautocomplete&format=json&cargo_table=" + table_and_field[0] + "&cargo_field=" + table_and_field[1];
} else {
myServer += "?action=pfautocomplete&format=json&" + autocomplete_type + "=" + data_source;
}
if (delimiter !== null && delimiter !== undefined) {
$(this).autocomplete({
source: function(request, response) {
$.getJSON(myServer, {
substr: extractLast(request.term)
}, function( data ) {
response($.map(data.pfautocomplete, function(item) {
return {
value: item.title
};
}));
});
},
search: function() {
// custom minLength
var term = extractLast(this.value);
if (term.length < 1) {
return false;
}
},
focus: function() {
// prevent value inserted on focus
return false;
},
select: function(event, ui) {
var terms = split( this.value );
// remove the current input
terms.pop();
// add the selected item
terms.push( ui.item.value );
// add placeholder to get the comma-and-space at the end
terms.push("");
this.value = terms.join(delimiter);
return false;
}
} );
} else {
$(this).autocomplete({
minLength: 1,
source: function(request, response) {
$.ajax({
url: myServer,
dataType: "json",
data: {
substr:request.term
},
success: function( data ) {
response($.map(data.pfautocomplete, function(item) {
return {
value: item.title
};
}));
}
});
},
open: function() {
$(this).removeClass("ui-corner-all").addClass("ui-corner-top");
},
close: function() {
$(this).removeClass("ui-corner-top").addClass("ui-corner-all");
}
} );
}
}
});
} catch ( error ) {
// Autocompletion (and specifically, the call to
// this.menu.element in line 195 of jquery.ui.autocomplete.js)
// for some reason sometimes fails when doing a preview of the
// form definition. It's not that importatnt, so, in lieu of
// showing it to the user (or debugging it), we'll just catch
// the error and log it in the console.
window.console.log("Error setting autocompletion: " + error);
}
};
/*
* Functions to register/unregister methods for the initialization and
* validation of inputs.
*/
// Initialize data object to hold initialization and validation data
function setupPF() {
$("#pfForm").data("PageForms",{
initFunctions : [],
validationFunctions : []
});
}
// Register a validation method
//
// More than one method may be registered for one input by subsequent calls to
// PageForms_registerInputValidation.
//
// Validation functions and their data are stored in a numbered array
//
// @param valfunction The validation functions. Must take a string (the input's id) and an object as parameters
// @param param The parameter object given to the validation function
$.fn.PageForms_registerInputValidation = function(valfunction, param) {
if ( ! this.attr("id") ) {
return this;
}
if ( ! $("#pfForm").data("PageForms") ) {
setupPF();
}
$("#pfForm").data("PageForms").validationFunctions.push({
input : this.attr("id"),
valfunction : valfunction,
parameters : param
});
return this;
};
// Register an initialization method
//
// More than one method may be registered for one input by subsequent calls to
// PageForms_registerInputInit. This method also executes the initFunction
// if the element referenced by /this/ is not part of a multipleTemplateStarter.
//
// Initialization functions and their data are stored in a associative array
//
// @param initFunction The initialization function. Must take a string (the input's id) and an object as parameters
// @param param The parameter object given to the initialization function
// @param noexecute If set, the initialization method will not be executed here
$.fn.PageForms_registerInputInit = function( initFunction, param, noexecute ) {
// return if element has no id
if ( ! this.attr("id") ) {
return this;
}
// setup data structure if necessary
if ( ! $("#pfForm").data("PageForms") ) {
setupPF();
}
// if no initialization function for this input was registered yet,
// create entry
if ( ! $("#pfForm").data("PageForms").initFunctions[this.attr("id")] ) {
$("#pfForm").data("PageForms").initFunctions[this.attr("id")] = [];
}
// record initialization function
$("#pfForm").data("PageForms").initFunctions[this.attr("id")].push({
initFunction : initFunction,
parameters : param
});
// execute initialization if input is not part of multipleTemplateStarter
// and if not forbidden
if ( this.closest(".multipleTemplateStarter").length === 0 && !noexecute) {
var input = this;
// ensure initFunction is only executed after doc structure is complete
$(function() {
if ( initFunction !== undefined ) {
initFunction ( input.attr("id"), param );
}
});
}
return this;
};
// Unregister all validation methods for the element referenced by /this/
$.fn.PageForms_unregisterInputValidation = function() {
var pfdata = $("#pfForm").data("PageForms");
if ( this.attr("id") && pfdata ) {
// delete every validation method for this input
for ( var i = 0; i < pfdata.validationFunctions.length; i++ ) {
if ( typeof pfdata.validationFunctions[i] !== 'undefined' &&
pfdata.validationFunctions[i].input === this.attr("id") ) {
delete pfdata.validationFunctions[i];
}
}
}
return this;
};
// Unregister all initialization methods for the element referenced by /this/
$.fn.PageForms_unregisterInputInit = function() {
if ( this.attr("id") && $("#pfForm").data("PageForms") ) {
delete $("#pfForm").data("PageForms").initFunctions[this.attr("id")];
}
return this;
};
/*
* Functions for handling 'show on select'
*/
// Display a div that would otherwise be hidden by "show on select".
function showDiv( div_id, instanceWrapperDiv, initPage ) {
var speed = initPage ? 0 : 'fast';
var elem;
if ( instanceWrapperDiv !== null ) {
elem = $('[data-origID="' + div_id + '"]', instanceWrapperDiv);
} else {
elem = $('#' + div_id);
}
elem
.addClass('shownByPF')
.find(".hiddenByPF")
.removeClass('hiddenByPF')
.addClass('shownByPF')
.find(".disabledByPF")
.prop('disabled', false)
.removeClass('disabledByPF');
elem.each( function() {
if ( $(this).css('display') === 'none' ) {
$(this).slideDown(speed, function() {
$(this).fadeTo(speed,1);
});
}
});
// Now re-show any form elements that are meant to be shown due
// to the current value of form inputs in this div that are now
// being uncovered.
var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' );
elem.find(".pfShowIfSelected, .pfShowIfChecked").each( function() {
var uncoveredInput = $(this);
var uncoveredInputID = null;
if ( instanceWrapperDiv === null ) {
uncoveredInputID = uncoveredInput.attr("id");
} else {
uncoveredInputID = uncoveredInput.attr("data-origID");
}
var showOnSelectVals = wgPageFormsShowOnSelect[uncoveredInputID];
if ( showOnSelectVals !== undefined ) {
var inputVal = uncoveredInput.val();
for ( var i = 0; i < showOnSelectVals.length; i++ ) {
var options = showOnSelectVals[i][0];
var div_id2 = showOnSelectVals[i][1];
if ( uncoveredInput.hasClass( 'pfShowIfSelected' ) ) {
showDivIfSelected( options, div_id2, inputVal, instanceWrapperDiv, initPage );
} else {
uncoveredInput.showDivIfChecked( options, div_id2, instanceWrapperDiv, initPage );
}
}
}
});
}
// Hide a div due to "show on select". The CSS class is there so that PF can
// ignore the div's contents when the form is submitted.
function hideDiv( div_id, instanceWrapperDiv, initPage ) {
var speed = initPage ? 0 : 'fast';
var elem;
// IDs can't contain spaces, and jQuery won't work with such IDs - if
// this one has a space, display an alert.
if ( div_id.indexOf( ' ' ) > -1 ) {
// TODO - this should probably be a language value, instead of
// hardcoded in English.
alert( "Warning: this form has \"show on select\" pointing to an invalid element ID (\"" + div_id + "\") - IDs in HTML cannot contain spaces." );
}
if ( instanceWrapperDiv !== null ) {
elem = instanceWrapperDiv.find('[data-origID=' + div_id + ']');
} else {
elem = $('#' + div_id);
}
// If we're just setting up the page, and this element has already
// been marked to be shown by some other input, don't hide it.
if ( initPage && elem.hasClass('shownByPF') ) {
return;
}
elem.find("span, div").addClass('hiddenByPF');
elem.each( function() {
if ( $(this).css('display') !== 'none' ) {
// if 'display' is not 'hidden', but the element is hidden otherwise
// (e.g. by having height = 0), just hide it, else animate the hiding
if ( $(this).is(':hidden') ) {
$(this).hide();
} else {
$(this).fadeTo(speed, 0, function() {
$(this).slideUp(speed);
});
}
}
});
// Also, recursively hide further elements that are only shown because
// inputs within this now-hidden div were checked/selected.
var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' );
elem.find(".pfShowIfSelected, .pfShowIfChecked").each( function() {
var showOnSelectVals;
if ( instanceWrapperDiv === null ) {
showOnSelectVals = wgPageFormsShowOnSelect[$(this).attr("id")];
} else {
showOnSelectVals = wgPageFormsShowOnSelect[$(this).attr("data-origID")];
}
if ( showOnSelectVals !== undefined ) {
for ( var i = 0; i < showOnSelectVals.length; i++ ) {
//var options = showOnSelectVals[i][0];
var div_id2 = showOnSelectVals[i][1];
hideDiv( div_id2, instanceWrapperDiv, initPage );
}
}
});
}
// Show this div if the current value is any of the relevant options -
// otherwise, hide it.
function showDivIfSelected(options, div_id, inputVal, instanceWrapperDiv, initPage) {
for ( var i = 0; i < options.length; i++ ) {
// If it's a listbox and the user has selected more than one
// value, it'll be an array - handle either case.
if (($.isArray(inputVal) && $.inArray(options[i], inputVal) >= 0) ||
(!$.isArray(inputVal) && (inputVal === options[i]))) {
showDiv( div_id, instanceWrapperDiv, initPage );
return;
}
}
hideDiv( div_id, instanceWrapperDiv, initPage );
}
// Used for handling 'show on select' for the 'dropdown' and 'listbox' inputs.
$.fn.showIfSelected = function(partOfMultiple, initPage) {
var inputVal = this.val(),
wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ),
showOnSelectVals,
instanceWrapperDiv;
if ( partOfMultiple ) {
showOnSelectVals = wgPageFormsShowOnSelect[this.attr("data-origID")];
instanceWrapperDiv = this.closest('.multipleTemplateInstance');
} else {
showOnSelectVals = wgPageFormsShowOnSelect[this.attr("id")];
instanceWrapperDiv = null;
}
if ( showOnSelectVals !== undefined ) {
for ( var i = 0; i < showOnSelectVals.length; i++ ) {
var options = showOnSelectVals[i][0];
var div_id = showOnSelectVals[i][1];
showDivIfSelected( options, div_id, inputVal, instanceWrapperDiv, initPage );
}
}
return this;
};
// Show this div if any of the relevant selections are checked -
// otherwise, hide it.
$.fn.showDivIfChecked = function(options, div_id, instanceWrapperDiv, initPage ) {
for ( var i = 0; i < options.length; i++ ) {
if ($(this).find('[value="' + options[i] + '"]').is(":checked")) {
showDiv( div_id, instanceWrapperDiv, initPage );
return this;
}
}
hideDiv( div_id, instanceWrapperDiv, initPage );
return this;
};
// Used for handling 'show on select' for the 'checkboxes' and 'radiobutton'
// inputs.
$.fn.showIfChecked = function(partOfMultiple, initPage) {
var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ),
showOnSelectVals,
instanceWrapperDiv,
i;
if ( partOfMultiple ) {
showOnSelectVals = wgPageFormsShowOnSelect[this.attr("data-origID")];
instanceWrapperDiv = this.closest('.multipleTemplateInstance');
} else {
showOnSelectVals = wgPageFormsShowOnSelect[this.attr("id")];
instanceWrapperDiv = null;
}
if ( showOnSelectVals !== undefined ) {
for ( i = 0; i < showOnSelectVals.length; i++ ) {
var options = showOnSelectVals[i][0];
var div_id = showOnSelectVals[i][1];
this.showDivIfChecked( options, div_id, instanceWrapperDiv, initPage );
}
}
return this;
};
// Used for handling 'show on select' for the 'checkbox' input.
$.fn.showIfCheckedCheckbox = function( partOfMultiple, initPage ) {
var wgPageFormsShowOnSelect = mw.config.get( 'wgPageFormsShowOnSelect' ),
divIDs,
instanceWrapperDiv,
i;
if (partOfMultiple) {
divIDs = wgPageFormsShowOnSelect[this.attr("data-origID")];
instanceWrapperDiv = this.closest(".multipleTemplateInstance");
} else {
divIDs = wgPageFormsShowOnSelect[this.attr("id")];
instanceWrapperDiv = null;
}
for ( i = 0; i < divIDs.length; i++ ) {
var divID = divIDs[i];
if ($(this).is(":checked")) {
showDiv( divID, instanceWrapperDiv, initPage );
} else {
hideDiv( divID, instanceWrapperDiv, initPage );
}
}
return this;
};
/*
* Validation functions
*/
// Set the error message for an input.
$.fn.setErrorMessage = function(msg, val) {
var container = this.find('.pfErrorMessages');
container.html($('
').addClass( 'errorMessage' ).text( mw.msg( msg, val ) ));
};
// Append an error message to the end of an input.
$.fn.addErrorMessage = function(msg, val) {
this.find('input').addClass('inputError');
this.find('select2-container').addClass('inputError');
this.append($('
').addClass( 'errorMessage' ).text( mw.msg( msg, val ) ));
// If this is part of a minimized multiple-template instance, add a
// red border around the instance rectangle to make it easier to find.
this.parents( '.multipleTemplateInstance.minimized' ).css( 'border', '1px solid red' );
};
$.fn.isAtMaxInstances = function() {
var numInstances = this.find("div.multipleTemplateInstance").length;
var maximumInstances = this.attr("maximumInstances");
if ( numInstances >= maximumInstances ) {
this.parent().setErrorMessage( 'pf_too_many_instances_error', maximumInstances );
return true;
}
return false;
};
$.fn.validateNumInstances = function() {
var minimumInstances = this.attr("minimumInstances");
var maximumInstances = this.attr("maximumInstances");
var numInstances = this.find("div.multipleTemplateInstance").length;
if ( numInstances < minimumInstances ) {
this.parent().addErrorMessage( 'pf_too_few_instances_error', minimumInstances );
return false;
} else if ( numInstances > maximumInstances ) {
this.parent().addErrorMessage( 'pf_too_many_instances_error', maximumInstances );
return false;
} else {
return true;
}
};
$.fn.validateMandatoryField = function() {
var fieldVal = this.find(".mandatoryField").val();
var isEmpty;
if (fieldVal === null) {
isEmpty = true;
} else if ($.isArray(fieldVal)) {
isEmpty = (fieldVal.length === 0);
} else {
isEmpty = (fieldVal.replace(/\s+/, '') === '');
}
if (isEmpty) {
this.addErrorMessage( 'pf_blank_error' );
return false;
} else {
return true;
}
};
$.fn.validateUniqueField = function() {
var UNDEFINED = "undefined";
var field = this.find(".uniqueField");
var fieldVal = field.val();
if (typeof fieldVal === UNDEFINED || fieldVal.replace(/\s+/, '') === '') {
return true;
}
var fieldOrigVal = field.prop("defaultValue");
if (fieldVal === fieldOrigVal) {
return true;
}
var categoryFieldName = field.prop("id") + "_unique_for_category";
var categoryField = $("[name=" + categoryFieldName + "]");
var category = categoryField.val();
var namespaceFieldName = field.prop("id") + "_unique_for_namespace";
var namespaceField = $("[name=" + namespaceFieldName + "]");
var namespace = namespaceField.val();
var url = mw.config.get( 'wgScriptPath' ) + "/api.php?format=json&action=";
var query,
isNotUnique;
// SMW
var propertyFieldName = field.prop("id") + "_unique_property",
propertyField = $("[name=" + propertyFieldName + "]"),
property = propertyField.val();
if (typeof property !== UNDEFINED && property.replace(/\s+/, '') !== '') {
query = "[[" + property + "::" + fieldVal + "]]";
if (typeof category !== UNDEFINED &&
category.replace(/\s+/, '') !== '') {
query += "[[Category:" + category + "]]";
}
if (typeof namespace !== UNDEFINED) {
if (namespace.replace(/\s+/, '') !== '') {
query += "[[:" + namespace + ":+]]";
} else {
query += "[[:+]]";
}
}
var conceptFieldName = field.prop("id") + "_unique_for_concept";
var conceptField = $("[name=" + conceptFieldName + "]");
var concept = conceptField.val();
if (typeof concept !== UNDEFINED &&
concept.replace(/\s+/, '') !== '') {
query += "[[Concept:" + concept + "]]";
}
query += "|limit=1";
query = encodeURIComponent(query);
url += "ask&query=" + query;
isNotUnique = true;
$.ajax({
url: url,
dataType: 'json',
async: false,
success: function(data) {
if (data.query.meta.count === 0) {
isNotUnique = false;
}
}
});
if (isNotUnique) {
this.addErrorMessage( 'pf_not_unique_error' );
return false;
} else {
return true;
}
}
// Cargo
var cargoTableFieldName = field.prop("id") + "_unique_cargo_table";
var cargoTableField = $("[name=" + cargoTableFieldName + "]");
var cargoTable = cargoTableField.val();
var cargoFieldFieldName = field.prop("id") + "_unique_cargo_field";
var cargoFieldField = $("[name=" + cargoFieldFieldName + "]");
var cargoField = cargoFieldField.val();
if (typeof cargoTable !== UNDEFINED && cargoTable.replace(/\s+/, '') !== ''
&& typeof cargoField !== UNDEFINED
&& cargoField.replace(/\s+/, '') !== '') {
query = "&where=" + cargoField + "+=+'" + fieldVal + "'";
if (typeof category !== UNDEFINED &&
category.replace(/\s+/, '') !== '') {
category = category.replace(/\s/, '_');
query += "+AND+cl_to=" + category + "+AND+cl_from=_pageID";
cargoTable += ",categorylinks";
}
if (typeof namespace !== UNDEFINED) {
query += "+AND+_pageNamespace=";
if (namespace.replace(/\s+/, '') !== '') {
var ns = mw.config.get('wgNamespaceIds')[namespace.toLowerCase()];
if (typeof ns !== UNDEFINED) {
query += ns;
}
} else {
query += "0";
}
}
query += "&limit=1";
url += "cargoquery&tables=" + cargoTable + "&fields=" + cargoField +
query;
isNotUnique = true;
$.ajax({
url: url,
dataType: 'json',
async: false,
success: function(data) {
if (data.cargoquery.length === 0) {
isNotUnique = false;
}
}
});
if (isNotUnique) {
this.addErrorMessage( 'pf_not_unique_error' );
return false;
} else {
return true;
}
}
return true;
};
$.fn.validateMandatoryComboBox = function() {
var combobox = this.find('.mandatoryField');
if (combobox.val() === null) {
this.addErrorMessage( 'pf_blank_error' );
return false;
} else {
return true;
}
};
$.fn.validateMandatoryDateField = function() {
if (this.find(".dayInput").val() === '' ||
this.find(".monthInput").val() === '' ||
this.find(".yearInput").val() === '') {
this.addErrorMessage( 'pf_blank_error' );
return false;
} else {
return true;
}
};
$.fn.validateMandatoryRadioButton = function() {
- var checkedValue = this.find("input:checked").val();
- if ( !checkedValue || checkedValue == '' ) {
- this.addErrorMessage( 'pf_blank_error' );
- return false;
+ if ( $(this).hasClass( 'pfTreeInput' ) ) {
+ var input_value = $(this).siblings( 'input' ).attr( 'value' );
+ if ( input_value === undefined || input_value === '' ) {
+ this.addErrorMessage( 'pf_blank_error' );
+ return false;
+ } else {
+ return true;
+ }
} else {
- return true;
+ var checkedValue = this.find("input:checked").val();
+ if (!checkedValue || checkedValue == '') {
+ this.addErrorMessage('pf_blank_error');
+ return false;
+ } else {
+ return true;
+ }
}
};
$.fn.validateMandatoryCheckboxes = function() {
// Get the number of checked checkboxes within this span - must
// be at least one.
- var numChecked = this.find("input:checked").size();
- if (numChecked === 0) {
- this.addErrorMessage( 'pf_blank_error' );
- return false;
+ if ( $( this ).hasClass( 'pfTreeInput' ) ) {
+ var input_value = $( this ).siblings( 'input' ).attr( 'value' );
+ if ( input_value === undefined || input_value === '' ) {
+ this.addErrorMessage( 'pf_blank_error' );
+ return false;
+ } else {
+ return true;
+ }
} else {
- return true;
+ var numChecked = this.find("input:checked").size();
+ if (numChecked === 0) {
+ this.addErrorMessage('pf_blank_error');
+ return false;
+ } else {
+ return true;
+ }
}
};
/*
* Type-based validation
*/
$.fn.validateURLField = function() {
var fieldVal = this.find("input").val();
var url_protocol = mw.config.get( 'wgUrlProtocols' );
//removing backslash before colon from url_protocol string
url_protocol = url_protocol.replace( /\\:/, ':' );
//removing '//' from wgUrlProtocols as this causes to match any protocol in regexp
url_protocol = url_protocol.replace( /\|\\\/\\\//, '' );
var url_regexp = new RegExp( '(' + url_protocol + ')' + '(\\w+:{0,1}\\w*@)?(\\S+)(:[0-9]+)?(\/|\/([\\w#!:.?+=&%@!\\-\/]))?' );
if (fieldVal === "" || url_regexp.test(fieldVal)) {
return true;
} else {
this.addErrorMessage( 'pf_bad_url_error' );
return false;
}
};
$.fn.validateEmailField = function() {
var fieldVal = this.find("input").val();
// code borrowed from http://javascript.internet.com/forms/email-validation---basic.html
var email_regexp = /^\s*\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,6})+\s*$/;
if (fieldVal === '' || email_regexp.test(fieldVal)) {
return true;
} else {
this.addErrorMessage( 'pf_bad_email_error' );
return false;
}
};
$.fn.validateNumberField = function() {
var fieldVal = this.find("input").val();
// Handle "E notation"/scientific notation ("1.2e-3") in addition
// to regular numbers
if (fieldVal === '' ||
fieldVal.match(/^\s*[\-+]?((\d+[\.,]?\d*)|(\d*[\.,]?\d+))([eE]?[\-\+]?\d+)?\s*$/)) {
return true;
} else {
this.addErrorMessage( 'pf_bad_number_error' );
return false;
}
};
$.fn.validateIntegerField = function() {
var fieldVal = this.find("input").val();
if ( fieldVal === '' || fieldVal == parseInt( fieldVal, 10 ) ) {
return true;
} else {
this.addErrorMessage( 'pf_bad_integer_error' );
return false;
}
};
$.fn.validateDateField = function() {
// validate only if day and year fields are both filled in
var dayVal = this.find(".dayInput").val();
var yearVal = this.find(".yearInput").val();
if (dayVal === '' || yearVal === '') {
return true;
} else if (dayVal.match(/^\d+$/) && dayVal <= 31) {
// no year validation, since it can also include
// 'BC' and possibly other non-number strings
return true;
} else {
this.addErrorMessage( 'pf_bad_date_error' );
return false;
}
};
// Standalone pipes are not allowed, because they mess up the template
// parsing; unless they're part of a call to a template or a parser function.
$.fn.checkForPipes = function() {
var fieldVal = this.find("input, textarea").val();
// We need to check for a few different things because this is
// called for a variety of different input types.
if ( fieldVal === undefined || fieldVal === '' ) {
fieldVal = this.text();
}
if ( fieldVal === undefined || fieldVal === '' ) {
return true;
}
if ( fieldVal.indexOf( '|' ) < 0 ) {
return true;
}
// Also allow pipes within special tags, like
or .
// Code copied, more or less, from PFTemplateInForm::escapeNonTemplatePipes().
var startAndEndTags = [
[ '
' ],
[ '' ],
[ '' ],
[ '' ]
];
for ( var i in startAndEndTags ) {
var startTag = startAndEndTags[i][0];
var endTag = startAndEndTags[i][1];
var pattern = RegExp( "(" + startTag + "[^]*?)\\|([^]*?" + endTag + ")", 'i' );
var matches;
while ( ( matches = fieldVal.match( pattern ) ) !== null ) {
// Special handling, to avoid escaping pipes
// within a string that looks like:
// startTag ... endTag | startTag ... endTag
if ( matches[1].includes( endTag ) &&
matches[2].includes( startTag ) ) {
fieldVal = fieldVal.replace( pattern, "$1" + "\2" + "$2");
} else {
fieldVal = fieldVal.replace( pattern, "$1" + "\1" + "$2" );
}
}
}
fieldVal = fieldVal.replace( "\2", '|' );
// Now check for pipes outside of brackets.
var nextPipe,
nextDoubleBracketsStart,
nextDoubleBracketsEnd;
// There's at least one pipe - here's where the real work begins.
// We do a mini-parsing of the string to try to make sure that every
// pipe is within either double square brackets (links) or double
// curly brackets (parser functions, template calls).
// For simplicity's sake, turn all curly brackets into square brackets,
// so we only have to check for one thing.
// This will incorrectly allow bad text like "[[a|b}}", but hopefully
// that's not a major problem.
fieldVal = fieldVal.replace( /{{/g, '[[' );
fieldVal = fieldVal.replace( /}}/g, ']]' );
var curIndex = 0;
var numUnclosedBrackets = 0;
while ( true ) {
nextDoubleBracketsStart = fieldVal.indexOf( '[[', curIndex );
if ( numUnclosedBrackets === 0 ) {
nextPipe = fieldVal.indexOf( '|', curIndex );
if ( nextPipe < 0 ) {
return true;
}
if ( nextDoubleBracketsStart < 0 || nextPipe < nextDoubleBracketsStart ) {
// There's a pipe where it shouldn't be.
this.addErrorMessage( 'pf_pipe_error' );
return false;
}
} else {
if ( nextDoubleBracketsEnd < 0 ) {
// Something is malformed - might as well throw
// an error.
this.addErrorMessage( 'pf_pipe_error' );
return false;
}
}
nextDoubleBracketsEnd = fieldVal.indexOf( ']]', curIndex );
if ( nextDoubleBracketsStart >= 0 && nextDoubleBracketsStart < nextDoubleBracketsEnd ) {
numUnclosedBrackets++;
curIndex = nextDoubleBracketsStart + 2;
} else {
numUnclosedBrackets--;
curIndex = nextDoubleBracketsEnd + 2;
}
}
// We'll never get here, but let's have this line anyway.
return true;
};
window.validateAll = function () {
// Hook that fires on form submission, before the validation.
mw.hook('pf.formValidationBefore').fire();
var num_errors = 0;
// Remove all old error messages.
$(".errorMessage").remove();
// Make sure all inputs are ignored in the "starter" instance
// of any multiple-instance template.
$(".multipleTemplateStarter").find("span, div").addClass("hiddenByPF");
$(".multipleTemplateList").each( function() {
if (! $(this).validateNumInstances() ) {
num_errors += 1;
}
});
$("span.inputSpan.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
if (! $(this).validateMandatoryField() ) {
num_errors += 1;
}
});
$("div.ui-widget.mandatory").not(".hiddenByPF").each( function() {
if (! $(this).validateMandatoryComboBox() ) {
num_errors += 1;
}
});
$("span.dateInput.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
if (! $(this).validateMandatoryDateField() ) {
num_errors += 1;
}
});
$("span.radioButtonSpan.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
if (! $(this).validateMandatoryRadioButton() ) {
num_errors += 1;
}
});
$("span.checkboxesSpan.mandatoryFieldSpan").not(".hiddenByPF").each( function() {
if (! $(this).validateMandatoryCheckboxes() ) {
num_errors += 1;
}
});
$("div.pfTreeInput.mandatory").not(".hiddenByPF").each( function() {
// @HACK - handle both the options for tree, checkboxes and
// radiobuttons, at the same time, regardless of which one is
// being used. This seems to work fine, though.
if (! $(this).validateMandatoryCheckboxes() ) {
num_errors += 1;
}
if (! $(this).validateMandatoryRadioButton() ) {
num_errors += 1;
}
});
$("span.inputSpan.uniqueFieldSpan").not(".hiddenByPF").each( function() {
if (! $(this).validateUniqueField() ) {
num_errors += 1;
}
});
$("span.inputSpan, div.pfComboBox").not(".hiddenByPF, .freeText, .pageSection").each( function() {
if (! $(this).checkForPipes() ) {
num_errors += 1;
}
});
$("span.URLInput").not(".hiddenByPF").each( function() {
if (! $(this).validateURLField() ) {
num_errors += 1;
}
});
$("span.emailInput").not(".hiddenByPF").each( function() {
if (! $(this).validateEmailField() ) {
num_errors += 1;
}
});
$("span.numberInput").not(".hiddenByPF").each( function() {
if (! $(this).validateNumberField() ) {
num_errors += 1;
}
});
$("span.integerInput").not(".hiddenByPF").each( function() {
if (! $(this).validateIntegerField() ) {
num_errors += 1;
}
});
$("span.dateInput").not(".hiddenByPF").each( function() {
if (! $(this).validateDateField() ) {
num_errors += 1;
}
});
$("input.modifiedInput").not(".hiddenByPF").each( function() {
// No separate function needed.
$(this).parent().addErrorMessage( 'pf_modified_input_error' );
num_errors += 1;
});
// call registered validation functions
var pfdata = $("#pfForm").data('PageForms');
if ( pfdata && pfdata.validationFunctions.length > 0 ) { // found data object?
// for every registered input
for ( var i = 0; i < pfdata.validationFunctions.length; i++ ) {
// if input is not part of multipleTemplateStarter
if ( typeof pfdata.validationFunctions[i] !== 'undefined' &&
$("#" + pfdata.validationFunctions[i].input).closest(".multipleTemplateStarter").length === 0 &&
$("#" + pfdata.validationFunctions[i].input).closest(".hiddenByPF").length === 0 ) {
if (! pfdata.validationFunctions[i].valfunction(
pfdata.validationFunctions[i].input,
pfdata.validationFunctions[i].parameters)
) {
num_errors += 1;
}
}
}
}
if (num_errors > 0) {
// add error header, if it's not there already
if ($("#form_error_header").size() === 0) {
$("#contentSub").append('
');
}
scroll(0, 0);
} else {
// Disable inputs hidden due to either "show on select" or
// because they're part of the "starter" div for
// multiple-instance templates, so that they aren't
// submitted by the form.
$('.hiddenByPF').find("input, select, textarea").not(':disabled')
.prop('disabled', true)
.addClass('disabledByPF');
//remove error box if it exists because there are no errors in the form now
$("#contentSub").find(".errorbox").remove();
}
// Hook that fires on form submission, after the validation.
mw.hook('pf.formValidationAfter').fire();
return (num_errors === 0);
};
/**
* Minimize all instances if the total height of all the instances
* is over 800 pixels - to allow for easier navigation and sorting.
*/
$.fn.possiblyMinimizeAllOpenInstances = function() {
if ( ! this.hasClass( 'minimizeAll' ) ) {
return;
}
var displayedFieldsWhenMinimized = this.attr('data-displayed-fields-when-minimized');
var allDisplayedFields = null;
if ( displayedFieldsWhenMinimized ) {
allDisplayedFields = displayedFieldsWhenMinimized.split(',').map(function(item) {
return item.trim().toLowerCase();
});
}
this.find('.multipleTemplateInstance').not('.minimized').each( function() {
var instance = $(this);
instance.addClass('minimized');
var valuesStr = '';
instance.find( "input[type != 'hidden'][type != 'button'], select, textarea, div.ve-ce-surface" ).each( function() {
// If the set of fields to be displayed was specified in
// the form definition, check against that list.
if ( allDisplayedFields !== null ) {
var fieldFullName = $(this).attr('name');
if ( !fieldFullName ) {
return;
}
var matches = fieldFullName.match(/.*\[.*\]\[(.*)\]/);
var fieldRealName = matches[1].toLowerCase();
if ( !allDisplayedFields.includes( fieldRealName ) ) {
return;
}
}
var curVal = $(this).val();
if ( $(this).hasClass('ve-ce-surface') ) {
// Special handling for VisualEditor/VEForAll textareas.
curVal = $(this).text();
}
if ( typeof curVal !== 'string' || curVal === '' ) {
return;
}
var inputType = $(this).attr('type');
if ( inputType === 'checkbox' || inputType === 'radio' ) {
if ( ! $(this).is(':checked') ) {
return;
}
}
if ( curVal.length > 70 ) {
curVal = curVal.substring(0, 70) + "...";
}
if ( valuesStr !== '' ) {
valuesStr += ' · ';
}
valuesStr += curVal;
});
if ( valuesStr === '' ) {
valuesStr = 'No data';
}
instance.find('.instanceMain').fadeOut( "medium", function() {
instance.find('.instanceRearranger').after('
");
+ this.element.attr('aria-activedescendant','j'+this._id+'_loading');
+ }
+ this.load_node($.jstree.root, function (o, s) {
+ if(s) {
+ this.get_container_ul()[0].className = c;
+ if(this._firstChild(this.get_container_ul()[0])) {
+ this.element.attr('aria-activedescendant',this._firstChild(this.get_container_ul()[0]).id);
+ }
+ this.set_state($.extend(true, {}, this._data.core.state), function () {
+ /**
+ * triggered when a `refresh` call completes
+ * @event
+ * @name refresh.jstree
+ */
+ this.trigger('refresh');
+ });
+ }
+ this._data.core.state = null;
+ });
+ },
+ /**
+ * refreshes a node in the tree (reload its children) all opened nodes inside that node are reloaded with calls to `load_node`.
+ * @name refresh_node(obj)
+ * @param {mixed} obj the node
+ * @trigger refresh_node.jstree
+ */
+ refresh_node : function (obj) {
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ var opened = [], to_load = [], s = this._data.core.selected.concat([]);
+ to_load.push(obj.id);
+ if(obj.state.opened === true) { opened.push(obj.id); }
+ this.get_node(obj, true).find('.jstree-open').each(function() { to_load.push(this.id); opened.push(this.id); });
+ this._load_nodes(to_load, $.proxy(function (nodes) {
+ this.open_node(opened, false, 0);
+ this.select_node(s);
+ /**
+ * triggered when a node is refreshed
+ * @event
+ * @name refresh_node.jstree
+ * @param {Object} node - the refreshed node
+ * @param {Array} nodes - an array of the IDs of the nodes that were reloaded
+ */
+ this.trigger('refresh_node', { 'node' : obj, 'nodes' : nodes });
+ }, this), false, true);
+ },
+ /**
+ * set (change) the ID of a node
+ * @name set_id(obj, id)
+ * @param {mixed} obj the node
+ * @param {String} id the new ID
+ * @return {Boolean}
+ * @trigger set_id.jstree
+ */
+ set_id : function (obj, id) {
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ var i, j, m = this._model.data, old = obj.id;
+ id = id.toString();
+ // update parents (replace current ID with new one in children and children_d)
+ m[obj.parent].children[$.inArray(obj.id, m[obj.parent].children)] = id;
+ for(i = 0, j = obj.parents.length; i < j; i++) {
+ m[obj.parents[i]].children_d[$.inArray(obj.id, m[obj.parents[i]].children_d)] = id;
+ }
+ // update children (replace current ID with new one in parent and parents)
+ for(i = 0, j = obj.children.length; i < j; i++) {
+ m[obj.children[i]].parent = id;
+ }
+ for(i = 0, j = obj.children_d.length; i < j; i++) {
+ m[obj.children_d[i]].parents[$.inArray(obj.id, m[obj.children_d[i]].parents)] = id;
+ }
+ i = $.inArray(obj.id, this._data.core.selected);
+ if(i !== -1) { this._data.core.selected[i] = id; }
+ // update model and obj itself (obj.id, this._model.data[KEY])
+ i = this.get_node(obj.id, true);
+ if(i) {
+ i.attr('id', id); //.children('.jstree-anchor').attr('id', id + '_anchor').end().attr('aria-labelledby', id + '_anchor');
+ if(this.element.attr('aria-activedescendant') === obj.id) {
+ this.element.attr('aria-activedescendant', id);
+ }
+ }
+ delete m[obj.id];
+ obj.id = id;
+ obj.li_attr.id = id;
+ m[id] = obj;
+ /**
+ * triggered when a node id value is changed
+ * @event
+ * @name set_id.jstree
+ * @param {Object} node
+ * @param {String} old the old id
+ */
+ this.trigger('set_id',{ "node" : obj, "new" : obj.id, "old" : old });
+ return true;
+ },
+ /**
+ * get the text value of a node
+ * @name get_text(obj)
+ * @param {mixed} obj the node
+ * @return {String}
+ */
+ get_text : function (obj) {
+ obj = this.get_node(obj);
+ return (!obj || obj.id === $.jstree.root) ? false : obj.text;
+ },
+ /**
+ * set the text value of a node. Used internally, please use `rename_node(obj, val)`.
+ * @private
+ * @name set_text(obj, val)
+ * @param {mixed} obj the node, you can pass an array to set the text on multiple nodes
+ * @param {String} val the new text value
+ * @return {Boolean}
+ * @trigger set_text.jstree
+ */
+ set_text : function (obj, val) {
+ var t1, t2;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.set_text(obj[t1], val);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ obj.text = val;
+ if(this.get_node(obj, true).length) {
+ this.redraw_node(obj.id);
+ }
+ /**
+ * triggered when a node text value is changed
+ * @event
+ * @name set_text.jstree
+ * @param {Object} obj
+ * @param {String} text the new value
+ */
+ this.trigger('set_text',{ "obj" : obj, "text" : val });
+ return true;
+ },
+ /**
+ * gets a JSON representation of a node (or the whole tree)
+ * @name get_json([obj, options])
+ * @param {mixed} obj
+ * @param {Object} options
+ * @param {Boolean} options.no_state do not return state information
+ * @param {Boolean} options.no_id do not return ID
+ * @param {Boolean} options.no_children do not include children
+ * @param {Boolean} options.no_data do not include node data
+ * @param {Boolean} options.no_li_attr do not include LI attributes
+ * @param {Boolean} options.no_a_attr do not include A attributes
+ * @param {Boolean} options.flat return flat JSON instead of nested
+ * @return {Object}
+ */
+ get_json : function (obj, options, flat) {
+ obj = this.get_node(obj || $.jstree.root);
+ if(!obj) { return false; }
+ if(options && options.flat && !flat) { flat = []; }
+ var tmp = {
+ 'id' : obj.id,
+ 'text' : obj.text,
+ 'icon' : this.get_icon(obj),
+ 'li_attr' : $.extend(true, {}, obj.li_attr),
+ 'a_attr' : $.extend(true, {}, obj.a_attr),
+ 'state' : {},
+ 'data' : options && options.no_data ? false : $.extend(true, $.isArray(obj.data)?[]:{}, obj.data)
+ //( this.get_node(obj, true).length ? this.get_node(obj, true).data() : obj.data ),
+ }, i, j;
+ if(options && options.flat) {
+ tmp.parent = obj.parent;
+ }
+ else {
+ tmp.children = [];
+ }
+ if(!options || !options.no_state) {
+ for(i in obj.state) {
+ if(obj.state.hasOwnProperty(i)) {
+ tmp.state[i] = obj.state[i];
+ }
+ }
+ } else {
+ delete tmp.state;
+ }
+ if(options && options.no_li_attr) {
+ delete tmp.li_attr;
+ }
+ if(options && options.no_a_attr) {
+ delete tmp.a_attr;
+ }
+ if(options && options.no_id) {
+ delete tmp.id;
+ if(tmp.li_attr && tmp.li_attr.id) {
+ delete tmp.li_attr.id;
+ }
+ if(tmp.a_attr && tmp.a_attr.id) {
+ delete tmp.a_attr.id;
+ }
+ }
+ if(options && options.flat && obj.id !== $.jstree.root) {
+ flat.push(tmp);
+ }
+ if(!options || !options.no_children) {
+ for(i = 0, j = obj.children.length; i < j; i++) {
+ if(options && options.flat) {
+ this.get_json(obj.children[i], options, flat);
+ }
+ else {
+ tmp.children.push(this.get_json(obj.children[i], options));
+ }
+ }
+ }
+ return options && options.flat ? flat : (obj.id === $.jstree.root ? tmp.children : tmp);
+ },
+ /**
+ * create a new node (do not confuse with load_node)
+ * @name create_node([par, node, pos, callback, is_loaded])
+ * @param {mixed} par the parent node (to create a root node use either "#" (string) or `null`)
+ * @param {mixed} node the data for the new node (a valid JSON object, or a simple string with the name)
+ * @param {mixed} pos the index at which to insert the node, "first" and "last" are also supported, default is "last"
+ * @param {Function} callback a function to be called once the node is created
+ * @param {Boolean} is_loaded internal argument indicating if the parent node was succesfully loaded
+ * @return {String} the ID of the newly create node
+ * @trigger model.jstree, create_node.jstree
+ */
+ create_node : function (par, node, pos, callback, is_loaded) {
+ if(par === null) { par = $.jstree.root; }
+ par = this.get_node(par);
+ if(!par) { return false; }
+ pos = pos === undefined ? "last" : pos;
+ if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
+ return this.load_node(par, function () { this.create_node(par, node, pos, callback, true); });
+ }
+ if(!node) { node = { "text" : this.get_string('New node') }; }
+ if(typeof node === "string") {
+ node = { "text" : node };
+ } else {
+ node = $.extend(true, {}, node);
+ }
+ if(node.text === undefined) { node.text = this.get_string('New node'); }
+ var tmp, dpc, i, j;
+
+ if(par.id === $.jstree.root) {
+ if(pos === "before") { pos = "first"; }
+ if(pos === "after") { pos = "last"; }
+ }
+ switch(pos) {
+ case "before":
+ tmp = this.get_node(par.parent);
+ pos = $.inArray(par.id, tmp.children);
+ par = tmp;
+ break;
+ case "after" :
+ tmp = this.get_node(par.parent);
+ pos = $.inArray(par.id, tmp.children) + 1;
+ par = tmp;
+ break;
+ case "inside":
+ case "first":
+ pos = 0;
+ break;
+ case "last":
+ pos = par.children.length;
+ break;
+ default:
+ if(!pos) { pos = 0; }
+ break;
+ }
+ if(pos > par.children.length) { pos = par.children.length; }
+ if(!node.id) { node.id = true; }
+ if(!this.check("create_node", node, par, pos)) {
+ this.settings.core.error.call(this, this._data.core.last_error);
+ return false;
+ }
+ if(node.id === true) { delete node.id; }
+ node = this._parse_model_from_json(node, par.id, par.parents.concat());
+ if(!node) { return false; }
+ tmp = this.get_node(node);
+ dpc = [];
+ dpc.push(node);
+ dpc = dpc.concat(tmp.children_d);
+ this.trigger('model', { "nodes" : dpc, "parent" : par.id });
+
+ par.children_d = par.children_d.concat(dpc);
+ for(i = 0, j = par.parents.length; i < j; i++) {
+ this._model.data[par.parents[i]].children_d = this._model.data[par.parents[i]].children_d.concat(dpc);
+ }
+ node = tmp;
+ tmp = [];
+ for(i = 0, j = par.children.length; i < j; i++) {
+ tmp[i >= pos ? i+1 : i] = par.children[i];
+ }
+ tmp[pos] = node.id;
+ par.children = tmp;
+
+ this.redraw_node(par, true);
+ /**
+ * triggered when a node is created
+ * @event
+ * @name create_node.jstree
+ * @param {Object} node
+ * @param {String} parent the parent's ID
+ * @param {Number} position the position of the new node among the parent's children
+ */
+ this.trigger('create_node', { "node" : this.get_node(node), "parent" : par.id, "position" : pos });
+ if(callback) { callback.call(this, this.get_node(node)); }
+ return node.id;
+ },
+ /**
+ * set the text value of a node
+ * @name rename_node(obj, val)
+ * @param {mixed} obj the node, you can pass an array to rename multiple nodes to the same name
+ * @param {String} val the new text value
+ * @return {Boolean}
+ * @trigger rename_node.jstree
+ */
+ rename_node : function (obj, val) {
+ var t1, t2, old;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.rename_node(obj[t1], val);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ old = obj.text;
+ if(!this.check("rename_node", obj, this.get_parent(obj), val)) {
+ this.settings.core.error.call(this, this._data.core.last_error);
+ return false;
+ }
+ this.set_text(obj, val); // .apply(this, Array.prototype.slice.call(arguments))
+ /**
+ * triggered when a node is renamed
+ * @event
+ * @name rename_node.jstree
+ * @param {Object} node
+ * @param {String} text the new value
+ * @param {String} old the old value
+ */
+ this.trigger('rename_node', { "node" : obj, "text" : val, "old" : old });
+ return true;
+ },
+ /**
+ * remove a node
+ * @name delete_node(obj)
+ * @param {mixed} obj the node, you can pass an array to delete multiple nodes
+ * @return {Boolean}
+ * @trigger delete_node.jstree, changed.jstree
+ */
+ delete_node : function (obj) {
+ var t1, t2, par, pos, tmp, i, j, k, l, c, top, lft;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.delete_node(obj[t1]);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ par = this.get_node(obj.parent);
+ pos = $.inArray(obj.id, par.children);
+ c = false;
+ if(!this.check("delete_node", obj, par, pos)) {
+ this.settings.core.error.call(this, this._data.core.last_error);
+ return false;
+ }
+ if(pos !== -1) {
+ par.children = $.vakata.array_remove(par.children, pos);
+ }
+ tmp = obj.children_d.concat([]);
+ tmp.push(obj.id);
+ 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, tmp) === -1;
+ });
+ }
+ for(k = 0, l = tmp.length; k < l; k++) {
+ if(this._model.data[tmp[k]].state.selected) {
+ c = true;
+ break;
+ }
+ }
+ if (c) {
+ this._data.core.selected = $.vakata.array_filter(this._data.core.selected, function (v) {
+ return $.inArray(v, tmp) === -1;
+ });
+ }
+ /**
+ * triggered when a node is deleted
+ * @event
+ * @name delete_node.jstree
+ * @param {Object} node
+ * @param {String} parent the parent's ID
+ */
+ this.trigger('delete_node', { "node" : obj, "parent" : par.id });
+ if(c) {
+ this.trigger('changed', { 'action' : 'delete_node', 'node' : obj, 'selected' : this._data.core.selected, 'parent' : par.id });
+ }
+ for(k = 0, l = tmp.length; k < l; k++) {
+ delete this._model.data[tmp[k]];
+ }
+ if($.inArray(this._data.core.focused, tmp) !== -1) {
+ this._data.core.focused = null;
+ top = this.element[0].scrollTop;
+ lft = this.element[0].scrollLeft;
+ if(par.id === $.jstree.root) {
+ if (this._model.data[$.jstree.root].children[0]) {
+ this.get_node(this._model.data[$.jstree.root].children[0], true).children('.jstree-anchor').focus();
+ }
+ }
+ else {
+ this.get_node(par, true).children('.jstree-anchor').focus();
+ }
+ this.element[0].scrollTop = top;
+ this.element[0].scrollLeft = lft;
+ }
+ this.redraw_node(par, true);
+ return true;
+ },
+ /**
+ * check if an operation is premitted on the tree. Used internally.
+ * @private
+ * @name check(chk, obj, par, pos)
+ * @param {String} chk the operation to check, can be "create_node", "rename_node", "delete_node", "copy_node" or "move_node"
+ * @param {mixed} obj the node
+ * @param {mixed} par the parent
+ * @param {mixed} pos the position to insert at, or if "rename_node" - the new name
+ * @param {mixed} more some various additional information, for example if a "move_node" operations is triggered by DND this will be the hovered node
+ * @return {Boolean}
+ */
+ check : function (chk, obj, par, pos, more) {
+ obj = obj && obj.id ? obj : this.get_node(obj);
+ par = par && par.id ? par : this.get_node(par);
+ var tmp = chk.match(/^move_node|copy_node|create_node$/i) ? par : obj,
+ chc = this.settings.core.check_callback;
+ if(chk === "move_node" || chk === "copy_node") {
+ if((!more || !more.is_multi) && (chk === "move_node" && $.inArray(obj.id, par.children) === pos)) {
+ this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_08', 'reason' : 'Moving node to its current position', 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
+ return false;
+ }
+ if((!more || !more.is_multi) && (obj.id === par.id || (chk === "move_node" && $.inArray(obj.id, par.children) === pos) || $.inArray(par.id, obj.children_d) !== -1)) {
+ this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_01', 'reason' : 'Moving parent inside child', 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
+ return false;
+ }
+ }
+ if(tmp && tmp.data) { tmp = tmp.data; }
+ if(tmp && tmp.functions && (tmp.functions[chk] === false || tmp.functions[chk] === true)) {
+ if(tmp.functions[chk] === false) {
+ this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_02', 'reason' : 'Node data prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
+ }
+ return tmp.functions[chk];
+ }
+ if(chc === false || ($.isFunction(chc) && chc.call(this, chk, obj, par, pos, more) === false) || (chc && chc[chk] === false)) {
+ this._data.core.last_error = { 'error' : 'check', 'plugin' : 'core', 'id' : 'core_03', 'reason' : 'User config for core.check_callback prevents function: ' + chk, 'data' : JSON.stringify({ 'chk' : chk, 'pos' : pos, 'obj' : obj && obj.id ? obj.id : false, 'par' : par && par.id ? par.id : false }) };
+ return false;
+ }
+ return true;
+ },
+ /**
+ * get the last error
+ * @name last_error()
+ * @return {Object}
+ */
+ last_error : function () {
+ return this._data.core.last_error;
+ },
+ /**
+ * move a node to a new parent
+ * @name move_node(obj, par [, pos, callback, is_loaded])
+ * @param {mixed} obj the node to move, pass an array to move multiple nodes
+ * @param {mixed} par the new parent
+ * @param {mixed} pos the position to insert at (besides integer values, "first" and "last" are supported, as well as "before" and "after"), defaults to integer `0`
+ * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
+ * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
+ * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
+ * @param {Boolean} instance internal parameter indicating if the node comes from another instance
+ * @trigger move_node.jstree
+ */
+ move_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) {
+ var t1, t2, old_par, old_pos, new_par, old_ins, is_multi, dpc, tmp, i, j, k, l, p;
+
+ par = this.get_node(par);
+ pos = pos === undefined ? 0 : pos;
+ if(!par) { return false; }
+ if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
+ return this.load_node(par, function () { this.move_node(obj, par, pos, callback, true, false, origin); });
+ }
+
+ if($.isArray(obj)) {
+ if(obj.length === 1) {
+ obj = obj[0];
+ }
+ else {
+ //obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ if((tmp = this.move_node(obj[t1], par, pos, callback, is_loaded, false, origin))) {
+ par = tmp;
+ pos = "after";
+ }
+ }
+ this.redraw();
+ return true;
+ }
+ }
+ obj = obj && obj.id ? obj : this.get_node(obj);
+
+ if(!obj || obj.id === $.jstree.root) { return false; }
+
+ old_par = (obj.parent || $.jstree.root).toString();
+ new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent);
+ old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
+ is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
+ old_pos = old_ins && old_ins._id && old_par && old_ins._model.data[old_par] && old_ins._model.data[old_par].children ? $.inArray(obj.id, old_ins._model.data[old_par].children) : -1;
+ if(old_ins && old_ins._id) {
+ obj = old_ins._model.data[obj.id];
+ }
+
+ if(is_multi) {
+ if((tmp = this.copy_node(obj, par, pos, callback, is_loaded, false, origin))) {
+ if(old_ins) { old_ins.delete_node(obj); }
+ return tmp;
+ }
+ return false;
+ }
+ //var m = this._model.data;
+ if(par.id === $.jstree.root) {
+ if(pos === "before") { pos = "first"; }
+ if(pos === "after") { pos = "last"; }
+ }
+ switch(pos) {
+ case "before":
+ pos = $.inArray(par.id, new_par.children);
+ break;
+ case "after" :
+ pos = $.inArray(par.id, new_par.children) + 1;
+ break;
+ case "inside":
+ case "first":
+ pos = 0;
+ break;
+ case "last":
+ pos = new_par.children.length;
+ break;
+ default:
+ if(!pos) { pos = 0; }
+ break;
+ }
+ if(pos > new_par.children.length) { pos = new_par.children.length; }
+ if(!this.check("move_node", obj, new_par, pos, { 'core' : true, 'origin' : origin, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) {
+ this.settings.core.error.call(this, this._data.core.last_error);
+ return false;
+ }
+ if(obj.parent === new_par.id) {
+ dpc = new_par.children.concat();
+ tmp = $.inArray(obj.id, dpc);
+ if(tmp !== -1) {
+ dpc = $.vakata.array_remove(dpc, tmp);
+ if(pos > tmp) { pos--; }
+ }
+ tmp = [];
+ for(i = 0, j = dpc.length; i < j; i++) {
+ tmp[i >= pos ? i+1 : i] = dpc[i];
+ }
+ tmp[pos] = obj.id;
+ new_par.children = tmp;
+ this._node_changed(new_par.id);
+ this.redraw(new_par.id === $.jstree.root);
+ }
+ else {
+ // clean old parent and up
+ tmp = obj.children_d.concat();
+ tmp.push(obj.id);
+ for(i = 0, j = obj.parents.length; i < j; i++) {
+ dpc = [];
+ p = old_ins._model.data[obj.parents[i]].children_d;
+ for(k = 0, l = p.length; k < l; k++) {
+ if($.inArray(p[k], tmp) === -1) {
+ dpc.push(p[k]);
+ }
+ }
+ old_ins._model.data[obj.parents[i]].children_d = dpc;
+ }
+ old_ins._model.data[old_par].children = $.vakata.array_remove_item(old_ins._model.data[old_par].children, obj.id);
+
+ // insert into new parent and up
+ for(i = 0, j = new_par.parents.length; i < j; i++) {
+ this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(tmp);
+ }
+ dpc = [];
+ for(i = 0, j = new_par.children.length; i < j; i++) {
+ dpc[i >= pos ? i+1 : i] = new_par.children[i];
+ }
+ dpc[pos] = obj.id;
+ new_par.children = dpc;
+ new_par.children_d.push(obj.id);
+ new_par.children_d = new_par.children_d.concat(obj.children_d);
+
+ // update object
+ obj.parent = new_par.id;
+ tmp = new_par.parents.concat();
+ tmp.unshift(new_par.id);
+ p = obj.parents.length;
+ obj.parents = tmp;
+
+ // update object children
+ tmp = tmp.concat();
+ for(i = 0, j = obj.children_d.length; i < j; i++) {
+ this._model.data[obj.children_d[i]].parents = this._model.data[obj.children_d[i]].parents.slice(0,p*-1);
+ Array.prototype.push.apply(this._model.data[obj.children_d[i]].parents, tmp);
+ }
+
+ if(old_par === $.jstree.root || new_par.id === $.jstree.root) {
+ this._model.force_full_redraw = true;
+ }
+ if(!this._model.force_full_redraw) {
+ this._node_changed(old_par);
+ this._node_changed(new_par.id);
+ }
+ if(!skip_redraw) {
+ this.redraw();
+ }
+ }
+ if(callback) { callback.call(this, obj, new_par, pos); }
+ /**
+ * triggered when a node is moved
+ * @event
+ * @name move_node.jstree
+ * @param {Object} node
+ * @param {String} parent the parent's ID
+ * @param {Number} position the position of the node among the parent's children
+ * @param {String} old_parent the old parent of the node
+ * @param {Number} old_position the old position of the node
+ * @param {Boolean} is_multi do the node and new parent belong to different instances
+ * @param {jsTree} old_instance the instance the node came from
+ * @param {jsTree} new_instance the instance of the new parent
+ */
+ this.trigger('move_node', { "node" : obj, "parent" : new_par.id, "position" : pos, "old_parent" : old_par, "old_position" : old_pos, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id), 'old_instance' : old_ins, 'new_instance' : this });
+ return obj.id;
+ },
+ /**
+ * copy a node to a new parent
+ * @name copy_node(obj, par [, pos, callback, is_loaded])
+ * @param {mixed} obj the node to copy, pass an array to copy multiple nodes
+ * @param {mixed} par the new parent
+ * @param {mixed} pos the position to insert at (besides integer values, "first" and "last" are supported, as well as "before" and "after"), defaults to integer `0`
+ * @param {function} callback a function to call once the move is completed, receives 3 arguments - the node, the new parent and the position
+ * @param {Boolean} is_loaded internal parameter indicating if the parent node has been loaded
+ * @param {Boolean} skip_redraw internal parameter indicating if the tree should be redrawn
+ * @param {Boolean} instance internal parameter indicating if the node comes from another instance
+ * @trigger model.jstree copy_node.jstree
+ */
+ copy_node : function (obj, par, pos, callback, is_loaded, skip_redraw, origin) {
+ var t1, t2, dpc, tmp, i, j, node, old_par, new_par, old_ins, is_multi;
+
+ par = this.get_node(par);
+ pos = pos === undefined ? 0 : pos;
+ if(!par) { return false; }
+ if(!pos.toString().match(/^(before|after)$/) && !is_loaded && !this.is_loaded(par)) {
+ return this.load_node(par, function () { this.copy_node(obj, par, pos, callback, true, false, origin); });
+ }
+
+ if($.isArray(obj)) {
+ if(obj.length === 1) {
+ obj = obj[0];
+ }
+ else {
+ //obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ if((tmp = this.copy_node(obj[t1], par, pos, callback, is_loaded, true, origin))) {
+ par = tmp;
+ pos = "after";
+ }
+ }
+ this.redraw();
+ return true;
+ }
+ }
+ obj = obj && obj.id ? obj : this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+
+ old_par = (obj.parent || $.jstree.root).toString();
+ new_par = (!pos.toString().match(/^(before|after)$/) || par.id === $.jstree.root) ? par : this.get_node(par.parent);
+ old_ins = origin ? origin : (this._model.data[obj.id] ? this : $.jstree.reference(obj.id));
+ is_multi = !old_ins || !old_ins._id || (this._id !== old_ins._id);
+
+ if(old_ins && old_ins._id) {
+ obj = old_ins._model.data[obj.id];
+ }
+
+ if(par.id === $.jstree.root) {
+ if(pos === "before") { pos = "first"; }
+ if(pos === "after") { pos = "last"; }
+ }
+ switch(pos) {
+ case "before":
+ pos = $.inArray(par.id, new_par.children);
+ break;
+ case "after" :
+ pos = $.inArray(par.id, new_par.children) + 1;
+ break;
+ case "inside":
+ case "first":
+ pos = 0;
+ break;
+ case "last":
+ pos = new_par.children.length;
+ break;
+ default:
+ if(!pos) { pos = 0; }
+ break;
+ }
+ if(pos > new_par.children.length) { pos = new_par.children.length; }
+ if(!this.check("copy_node", obj, new_par, pos, { 'core' : true, 'origin' : origin, 'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id) })) {
+ this.settings.core.error.call(this, this._data.core.last_error);
+ return false;
+ }
+ node = old_ins ? old_ins.get_json(obj, { no_id : true, no_data : true, no_state : true }) : obj;
+ if(!node) { return false; }
+ if(node.id === true) { delete node.id; }
+ node = this._parse_model_from_json(node, new_par.id, new_par.parents.concat());
+ if(!node) { return false; }
+ tmp = this.get_node(node);
+ if(obj && obj.state && obj.state.loaded === false) { tmp.state.loaded = false; }
+ dpc = [];
+ dpc.push(node);
+ dpc = dpc.concat(tmp.children_d);
+ this.trigger('model', { "nodes" : dpc, "parent" : new_par.id });
+
+ // insert into new parent and up
+ for(i = 0, j = new_par.parents.length; i < j; i++) {
+ this._model.data[new_par.parents[i]].children_d = this._model.data[new_par.parents[i]].children_d.concat(dpc);
+ }
+ dpc = [];
+ for(i = 0, j = new_par.children.length; i < j; i++) {
+ dpc[i >= pos ? i+1 : i] = new_par.children[i];
+ }
+ dpc[pos] = tmp.id;
+ new_par.children = dpc;
+ new_par.children_d.push(tmp.id);
+ new_par.children_d = new_par.children_d.concat(tmp.children_d);
+
+ if(new_par.id === $.jstree.root) {
+ this._model.force_full_redraw = true;
+ }
+ if(!this._model.force_full_redraw) {
+ this._node_changed(new_par.id);
+ }
+ if(!skip_redraw) {
+ this.redraw(new_par.id === $.jstree.root);
+ }
+ if(callback) { callback.call(this, tmp, new_par, pos); }
+ /**
+ * triggered when a node is copied
+ * @event
+ * @name copy_node.jstree
+ * @param {Object} node the copied node
+ * @param {Object} original the original node
+ * @param {String} parent the parent's ID
+ * @param {Number} position the position of the node among the parent's children
+ * @param {String} old_parent the old parent of the node
+ * @param {Number} old_position the position of the original node
+ * @param {Boolean} is_multi do the node and new parent belong to different instances
+ * @param {jsTree} old_instance the instance the node came from
+ * @param {jsTree} new_instance the instance of the new parent
+ */
+ this.trigger('copy_node', { "node" : tmp, "original" : obj, "parent" : new_par.id, "position" : pos, "old_parent" : old_par, "old_position" : old_ins && old_ins._id && old_par && old_ins._model.data[old_par] && old_ins._model.data[old_par].children ? $.inArray(obj.id, old_ins._model.data[old_par].children) : -1,'is_multi' : (old_ins && old_ins._id && old_ins._id !== this._id), 'is_foreign' : (!old_ins || !old_ins._id), 'old_instance' : old_ins, 'new_instance' : this });
+ return tmp.id;
+ },
+ /**
+ * cut a node (a later call to `paste(obj)` would move the node)
+ * @name cut(obj)
+ * @param {mixed} obj multiple objects can be passed using an array
+ * @trigger cut.jstree
+ */
+ cut : function (obj) {
+ if(!obj) { obj = this._data.core.selected.concat(); }
+ if(!$.isArray(obj)) { obj = [obj]; }
+ if(!obj.length) { return false; }
+ var tmp = [], o, t1, t2;
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ o = this.get_node(obj[t1]);
+ if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); }
+ }
+ if(!tmp.length) { return false; }
+ ccp_node = tmp;
+ ccp_inst = this;
+ ccp_mode = 'move_node';
+ /**
+ * triggered when nodes are added to the buffer for moving
+ * @event
+ * @name cut.jstree
+ * @param {Array} node
+ */
+ this.trigger('cut', { "node" : obj });
+ },
+ /**
+ * copy a node (a later call to `paste(obj)` would copy the node)
+ * @name copy(obj)
+ * @param {mixed} obj multiple objects can be passed using an array
+ * @trigger copy.jstree
+ */
+ copy : function (obj) {
+ if(!obj) { obj = this._data.core.selected.concat(); }
+ if(!$.isArray(obj)) { obj = [obj]; }
+ if(!obj.length) { return false; }
+ var tmp = [], o, t1, t2;
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ o = this.get_node(obj[t1]);
+ if(o && o.id && o.id !== $.jstree.root) { tmp.push(o); }
+ }
+ if(!tmp.length) { return false; }
+ ccp_node = tmp;
+ ccp_inst = this;
+ ccp_mode = 'copy_node';
+ /**
+ * triggered when nodes are added to the buffer for copying
+ * @event
+ * @name copy.jstree
+ * @param {Array} node
+ */
+ this.trigger('copy', { "node" : obj });
+ },
+ /**
+ * get the current buffer (any nodes that are waiting for a paste operation)
+ * @name get_buffer()
+ * @return {Object} an object consisting of `mode` ("copy_node" or "move_node"), `node` (an array of objects) and `inst` (the instance)
+ */
+ get_buffer : function () {
+ return { 'mode' : ccp_mode, 'node' : ccp_node, 'inst' : ccp_inst };
+ },
+ /**
+ * check if there is something in the buffer to paste
+ * @name can_paste()
+ * @return {Boolean}
+ */
+ can_paste : function () {
+ return ccp_mode !== false && ccp_node !== false; // && ccp_inst._model.data[ccp_node];
+ },
+ /**
+ * copy or move the previously cut or copied nodes to a new parent
+ * @name paste(obj [, pos])
+ * @param {mixed} obj the new parent
+ * @param {mixed} pos the position to insert at (besides integer, "first" and "last" are supported), defaults to integer `0`
+ * @trigger paste.jstree
+ */
+ paste : function (obj, pos) {
+ obj = this.get_node(obj);
+ if(!obj || !ccp_mode || !ccp_mode.match(/^(copy_node|move_node)$/) || !ccp_node) { return false; }
+ if(this[ccp_mode](ccp_node, obj, pos, false, false, false, ccp_inst)) {
+ /**
+ * triggered when paste is invoked
+ * @event
+ * @name paste.jstree
+ * @param {String} parent the ID of the receiving node
+ * @param {Array} node the nodes in the buffer
+ * @param {String} mode the performed operation - "copy_node" or "move_node"
+ */
+ this.trigger('paste', { "parent" : obj.id, "node" : ccp_node, "mode" : ccp_mode });
+ }
+ ccp_node = false;
+ ccp_mode = false;
+ ccp_inst = false;
+ },
+ /**
+ * clear the buffer of previously copied or cut nodes
+ * @name clear_buffer()
+ * @trigger clear_buffer.jstree
+ */
+ clear_buffer : function () {
+ ccp_node = false;
+ ccp_mode = false;
+ ccp_inst = false;
+ /**
+ * triggered when the copy / cut buffer is cleared
+ * @event
+ * @name clear_buffer.jstree
+ */
+ this.trigger('clear_buffer');
+ },
+ /**
+ * put a node in edit mode (input field to rename the node)
+ * @name edit(obj [, default_text, callback])
+ * @param {mixed} obj
+ * @param {String} default_text the text to populate the input with (if omitted or set to a non-string value the node's text value is used)
+ * @param {Function} callback a function to be called once the text box is blurred, it is called in the instance's scope and receives the node, a status parameter (true if the rename is successful, false otherwise) and a boolean indicating if the user cancelled the edit. You can access the node's title using .text
+ */
+ edit : function (obj, default_text, callback) {
+ var rtl, w, a, s, t, h1, h2, fn, tmp, cancel = false;
+ obj = this.get_node(obj);
+ if(!obj) { return false; }
+ if(!this.check("edit", obj, this.get_parent(obj))) {
+ this.settings.core.error.call(this, this._data.core.last_error);
+ return false;
+ }
+ tmp = obj;
+ default_text = typeof default_text === 'string' ? default_text : obj.text;
+ this.set_text(obj, "");
+ obj = this._open_to(obj);
+ tmp.text = default_text;
+
+ rtl = this._data.core.rtl;
+ w = this.element.width();
+ this._data.core.focused = tmp.id;
+ a = obj.children('.jstree-anchor').focus();
+ s = $('');
+ /*!
+ oi = obj.children("i:visible"),
+ ai = a.children("i:visible"),
+ w1 = oi.width() * oi.length,
+ w2 = ai.width() * ai.length,
+ */
+ t = default_text;
+ h1 = $("<"+"div>
", { css : { "position" : "absolute", "top" : "-200px", "left" : (rtl ? "0px" : "-1000px"), "visibility" : "hidden" } }).appendTo(document.body);
+ h2 = $("<"+"input />", {
+ "value" : t,
+ "class" : "jstree-rename-input",
+ // "size" : t.length,
+ "css" : {
+ "padding" : "0",
+ "border" : "1px solid silver",
+ "box-sizing" : "border-box",
+ "display" : "inline-block",
+ "height" : (this._data.core.li_height) + "px",
+ "lineHeight" : (this._data.core.li_height) + "px",
+ "width" : "150px" // will be set a bit further down
+ },
+ "blur" : $.proxy(function (e) {
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ var i = s.children(".jstree-rename-input"),
+ v = i.val(),
+ f = this.settings.core.force_text,
+ nv;
+ if(v === "") { v = t; }
+ h1.remove();
+ s.replaceWith(a);
+ s.remove();
+ t = f ? t : $('').append($.parseHTML(t)).html();
+ obj = this.get_node(obj);
+ this.set_text(obj, t);
+ nv = !!this.rename_node(obj, f ? $('').text(v).text() : $('').append($.parseHTML(v)).html());
+ if(!nv) {
+ this.set_text(obj, t); // move this up? and fix #483
+ }
+ this._data.core.focused = tmp.id;
+ setTimeout($.proxy(function () {
+ var node = this.get_node(tmp.id, true);
+ if(node.length) {
+ this._data.core.focused = tmp.id;
+ node.children('.jstree-anchor').focus();
+ }
+ }, this), 0);
+ if(callback) {
+ callback.call(this, tmp, nv, cancel);
+ }
+ h2 = null;
+ }, this),
+ "keydown" : function (e) {
+ var key = e.which;
+ if(key === 27) {
+ cancel = true;
+ this.value = t;
+ }
+ if(key === 27 || key === 13 || key === 37 || key === 38 || key === 39 || key === 40 || key === 32) {
+ e.stopImmediatePropagation();
+ }
+ if(key === 27 || key === 13) {
+ e.preventDefault();
+ this.blur();
+ }
+ },
+ "click" : function (e) { e.stopImmediatePropagation(); },
+ "mousedown" : function (e) { e.stopImmediatePropagation(); },
+ "keyup" : function (e) {
+ h2.width(Math.min(h1.text("pW" + this.value).width(),w));
+ },
+ "keypress" : function(e) {
+ if(e.which === 13) { return false; }
+ }
+ });
+ fn = {
+ fontFamily : a.css('fontFamily') || '',
+ fontSize : a.css('fontSize') || '',
+ fontWeight : a.css('fontWeight') || '',
+ fontStyle : a.css('fontStyle') || '',
+ fontStretch : a.css('fontStretch') || '',
+ fontVariant : a.css('fontVariant') || '',
+ letterSpacing : a.css('letterSpacing') || '',
+ wordSpacing : a.css('wordSpacing') || ''
+ };
+ s.attr('class', a.attr('class')).append(a.contents().clone()).append(h2);
+ a.replaceWith(s);
+ h1.css(fn);
+ h2.css(fn).width(Math.min(h1.text("pW" + h2[0].value).width(),w))[0].select();
+ $(document).one('mousedown.jstree touchstart.jstree dnd_start.vakata', function (e) {
+ if (h2 && e.target !== h2) {
+ $(h2).blur();
+ }
+ });
+ },
+
+
+ /**
+ * changes the theme
+ * @name set_theme(theme_name [, theme_url])
+ * @param {String} theme_name the name of the new theme to apply
+ * @param {mixed} theme_url the location of the CSS file for this theme. Omit or set to `false` if you manually included the file. Set to `true` to autoload from the `core.themes.dir` directory.
+ * @trigger set_theme.jstree
+ */
+ set_theme : function (theme_name, theme_url) {
+ if(!theme_name) { return false; }
+ if(theme_url === true) {
+ var dir = this.settings.core.themes.dir;
+ if(!dir) { dir = $.jstree.path + '/themes'; }
+ theme_url = dir + '/' + theme_name + '/style.css';
+ }
+ if(theme_url && $.inArray(theme_url, themes_loaded) === -1) {
+ $('head').append('<'+'link rel="stylesheet" href="' + theme_url + '" type="text/css" />');
+ themes_loaded.push(theme_url);
+ }
+ if(this._data.core.themes.name) {
+ this.element.removeClass('jstree-' + this._data.core.themes.name);
+ }
+ this._data.core.themes.name = theme_name;
+ this.element.addClass('jstree-' + theme_name);
+ this.element[this.settings.core.themes.responsive ? 'addClass' : 'removeClass' ]('jstree-' + theme_name + '-responsive');
+ /**
+ * triggered when a theme is set
+ * @event
+ * @name set_theme.jstree
+ * @param {String} theme the new theme
+ */
+ this.trigger('set_theme', { 'theme' : theme_name });
+ },
+ /**
+ * gets the name of the currently applied theme name
+ * @name get_theme()
+ * @return {String}
+ */
+ get_theme : function () { return this._data.core.themes.name; },
+ /**
+ * changes the theme variant (if the theme has variants)
+ * @name set_theme_variant(variant_name)
+ * @param {String|Boolean} variant_name the variant to apply (if `false` is used the current variant is removed)
+ */
+ set_theme_variant : function (variant_name) {
+ if(this._data.core.themes.variant) {
+ this.element.removeClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
+ }
+ this._data.core.themes.variant = variant_name;
+ if(variant_name) {
+ this.element.addClass('jstree-' + this._data.core.themes.name + '-' + this._data.core.themes.variant);
+ }
+ },
+ /**
+ * gets the name of the currently applied theme variant
+ * @name get_theme()
+ * @return {String}
+ */
+ get_theme_variant : function () { return this._data.core.themes.variant; },
+ /**
+ * shows a striped background on the container (if the theme supports it)
+ * @name show_stripes()
+ */
+ show_stripes : function () {
+ this._data.core.themes.stripes = true;
+ this.get_container_ul().addClass("jstree-striped");
+ /**
+ * triggered when stripes are shown
+ * @event
+ * @name show_stripes.jstree
+ */
+ this.trigger('show_stripes');
+ },
+ /**
+ * hides the striped background on the container
+ * @name hide_stripes()
+ */
+ hide_stripes : function () {
+ this._data.core.themes.stripes = false;
+ this.get_container_ul().removeClass("jstree-striped");
+ /**
+ * triggered when stripes are hidden
+ * @event
+ * @name hide_stripes.jstree
+ */
+ this.trigger('hide_stripes');
+ },
+ /**
+ * toggles the striped background on the container
+ * @name toggle_stripes()
+ */
+ toggle_stripes : function () { if(this._data.core.themes.stripes) { this.hide_stripes(); } else { this.show_stripes(); } },
+ /**
+ * shows the connecting dots (if the theme supports it)
+ * @name show_dots()
+ */
+ show_dots : function () {
+ this._data.core.themes.dots = true;
+ this.get_container_ul().removeClass("jstree-no-dots");
+ /**
+ * triggered when dots are shown
+ * @event
+ * @name show_dots.jstree
+ */
+ this.trigger('show_dots');
+ },
+ /**
+ * hides the connecting dots
+ * @name hide_dots()
+ */
+ hide_dots : function () {
+ this._data.core.themes.dots = false;
+ this.get_container_ul().addClass("jstree-no-dots");
+ /**
+ * triggered when dots are hidden
+ * @event
+ * @name hide_dots.jstree
+ */
+ this.trigger('hide_dots');
+ },
+ /**
+ * toggles the connecting dots
+ * @name toggle_dots()
+ */
+ toggle_dots : function () { if(this._data.core.themes.dots) { this.hide_dots(); } else { this.show_dots(); } },
+ /**
+ * show the node icons
+ * @name show_icons()
+ */
+ show_icons : function () {
+ this._data.core.themes.icons = true;
+ this.get_container_ul().removeClass("jstree-no-icons");
+ /**
+ * triggered when icons are shown
+ * @event
+ * @name show_icons.jstree
+ */
+ this.trigger('show_icons');
+ },
+ /**
+ * hide the node icons
+ * @name hide_icons()
+ */
+ hide_icons : function () {
+ this._data.core.themes.icons = false;
+ this.get_container_ul().addClass("jstree-no-icons");
+ /**
+ * triggered when icons are hidden
+ * @event
+ * @name hide_icons.jstree
+ */
+ this.trigger('hide_icons');
+ },
+ /**
+ * toggle the node icons
+ * @name toggle_icons()
+ */
+ toggle_icons : function () { if(this._data.core.themes.icons) { this.hide_icons(); } else { this.show_icons(); } },
+ /**
+ * show the node ellipsis
+ * @name show_icons()
+ */
+ show_ellipsis : function () {
+ this._data.core.themes.ellipsis = true;
+ this.get_container_ul().addClass("jstree-ellipsis");
+ /**
+ * triggered when ellisis is shown
+ * @event
+ * @name show_ellipsis.jstree
+ */
+ this.trigger('show_ellipsis');
+ },
+ /**
+ * hide the node ellipsis
+ * @name hide_ellipsis()
+ */
+ hide_ellipsis : function () {
+ this._data.core.themes.ellipsis = false;
+ this.get_container_ul().removeClass("jstree-ellipsis");
+ /**
+ * triggered when ellisis is hidden
+ * @event
+ * @name hide_ellipsis.jstree
+ */
+ this.trigger('hide_ellipsis');
+ },
+ /**
+ * toggle the node ellipsis
+ * @name toggle_icons()
+ */
+ toggle_ellipsis : function () { if(this._data.core.themes.ellipsis) { this.hide_ellipsis(); } else { this.show_ellipsis(); } },
+ /**
+ * set the node icon for a node
+ * @name set_icon(obj, icon)
+ * @param {mixed} obj
+ * @param {String} icon the new icon - can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class
+ */
+ set_icon : function (obj, icon) {
+ var t1, t2, dom, old;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.set_icon(obj[t1], icon);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ old = obj.icon;
+ obj.icon = icon === true || icon === null || icon === undefined || icon === '' ? true : icon;
+ dom = this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon");
+ if(icon === false) {
+ dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel");
+ this.hide_icon(obj);
+ }
+ else if(icon === true || icon === null || icon === undefined || icon === '') {
+ dom.removeClass('jstree-themeicon-custom ' + old).css("background","").removeAttr("rel");
+ if(old === false) { this.show_icon(obj); }
+ }
+ else if(icon.indexOf("/") === -1 && icon.indexOf(".") === -1) {
+ dom.removeClass(old).css("background","");
+ dom.addClass(icon + ' jstree-themeicon-custom').attr("rel",icon);
+ if(old === false) { this.show_icon(obj); }
+ }
+ else {
+ dom.removeClass(old).css("background","");
+ dom.addClass('jstree-themeicon-custom').css("background", "url('" + icon + "') center center no-repeat").attr("rel",icon);
+ if(old === false) { this.show_icon(obj); }
+ }
+ return true;
+ },
+ /**
+ * get the node icon for a node
+ * @name get_icon(obj)
+ * @param {mixed} obj
+ * @return {String}
+ */
+ get_icon : function (obj) {
+ obj = this.get_node(obj);
+ return (!obj || obj.id === $.jstree.root) ? false : obj.icon;
+ },
+ /**
+ * hide the icon on an individual node
+ * @name hide_icon(obj)
+ * @param {mixed} obj
+ */
+ hide_icon : function (obj) {
+ var t1, t2;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.hide_icon(obj[t1]);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj === $.jstree.root) { return false; }
+ obj.icon = false;
+ this.get_node(obj, true).children(".jstree-anchor").children(".jstree-themeicon").addClass('jstree-themeicon-hidden');
+ return true;
+ },
+ /**
+ * show the icon on an individual node
+ * @name show_icon(obj)
+ * @param {mixed} obj
+ */
+ show_icon : function (obj) {
+ var t1, t2, dom;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.show_icon(obj[t1]);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj === $.jstree.root) { return false; }
+ dom = this.get_node(obj, true);
+ obj.icon = dom.length ? dom.children(".jstree-anchor").children(".jstree-themeicon").attr('rel') : true;
+ if(!obj.icon) { obj.icon = true; }
+ dom.children(".jstree-anchor").children(".jstree-themeicon").removeClass('jstree-themeicon-hidden');
+ return true;
+ }
+ };
+
+ // helpers
+ $.vakata = {};
+ // collect attributes
+ $.vakata.attributes = function(node, with_values) {
+ node = $(node)[0];
+ var attr = with_values ? {} : [];
+ if(node && node.attributes) {
+ $.each(node.attributes, function (i, v) {
+ if($.inArray(v.name.toLowerCase(),['style','contenteditable','hasfocus','tabindex']) !== -1) { return; }
+ if(v.value !== null && $.trim(v.value) !== '') {
+ if(with_values) { attr[v.name] = v.value; }
+ else { attr.push(v.name); }
+ }
+ });
+ }
+ return attr;
+ };
+ $.vakata.array_unique = function(array) {
+ var a = [], i, j, l, o = {};
+ for(i = 0, l = array.length; i < l; i++) {
+ if(o[array[i]] === undefined) {
+ a.push(array[i]);
+ o[array[i]] = true;
+ }
+ }
+ return a;
+ };
+ // remove item from array
+ $.vakata.array_remove = function(array, from) {
+ array.splice(from, 1);
+ return array;
+ //var rest = array.slice((to || from) + 1 || array.length);
+ //array.length = from < 0 ? array.length + from : from;
+ //array.push.apply(array, rest);
+ //return array;
+ };
+ // remove item from array
+ $.vakata.array_remove_item = function(array, item) {
+ var tmp = $.inArray(item, array);
+ return tmp !== -1 ? $.vakata.array_remove(array, tmp) : array;
+ };
+ $.vakata.array_filter = function(c,a,b,d,e) {
+ if (c.filter) {
+ return c.filter(a, b);
+ }
+ d=[];
+ for (e in c) {
+ if (~~e+''===e+'' && e>=0 && a.call(b,c[e],+e,c)) {
+ d.push(c[e]);
+ }
+ }
+ return d;
+ };
+
+
+/**
+ * ### Changed plugin
+ *
+ * This plugin adds more information to the `changed.jstree` event. The new data is contained in the `changed` event data property, and contains a lists of `selected` and `deselected` nodes.
+ */
+
+ $.jstree.plugins.changed = function (options, parent) {
+ var last = [];
+ this.trigger = function (ev, data) {
+ var i, j;
+ if(!data) {
+ data = {};
+ }
+ if(ev.replace('.jstree','') === 'changed') {
+ data.changed = { selected : [], deselected : [] };
+ var tmp = {};
+ for(i = 0, j = last.length; i < j; i++) {
+ tmp[last[i]] = 1;
+ }
+ for(i = 0, j = data.selected.length; i < j; i++) {
+ if(!tmp[data.selected[i]]) {
+ data.changed.selected.push(data.selected[i]);
+ }
+ else {
+ tmp[data.selected[i]] = 2;
+ }
+ }
+ for(i = 0, j = last.length; i < j; i++) {
+ if(tmp[last[i]] === 1) {
+ data.changed.deselected.push(last[i]);
+ }
+ }
+ last = data.selected.slice();
+ }
+ /**
+ * triggered when selection changes (the "changed" plugin enhances the original event with more data)
+ * @event
+ * @name changed.jstree
+ * @param {Object} node
+ * @param {Object} action the action that caused the selection to change
+ * @param {Array} selected the current selection
+ * @param {Object} changed an object containing two properties `selected` and `deselected` - both arrays of node IDs, which were selected or deselected since the last changed event
+ * @param {Object} event the event (if any) that triggered this changed event
+ * @plugin changed
+ */
+ parent.trigger.call(this, ev, data);
+ };
+ this.refresh = function (skip_loading, forget_state) {
+ last = [];
+ return parent.refresh.apply(this, arguments);
+ };
+ };
+
+/**
+ * ### Checkbox plugin
+ *
+ * This plugin renders checkbox icons in front of each node, making multiple selection much easier.
+ * It also supports tri-state behavior, meaning that if a node has a few of its children checked it will be rendered as undetermined, and state will be propagated up.
+ */
+
+ var _i = document.createElement('I');
+ _i.className = 'jstree-icon jstree-checkbox';
+ _i.setAttribute('role', 'presentation');
+ /**
+ * stores all defaults for the checkbox plugin
+ * @name $.jstree.defaults.checkbox
+ * @plugin checkbox
+ */
+ $.jstree.defaults.checkbox = {
+ /**
+ * a boolean indicating if checkboxes should be visible (can be changed at a later time using `show_checkboxes()` and `hide_checkboxes`). Defaults to `true`.
+ * @name $.jstree.defaults.checkbox.visible
+ * @plugin checkbox
+ */
+ visible : true,
+ /**
+ * a boolean indicating if checkboxes should cascade down and have an undetermined state. Defaults to `true`.
+ * @name $.jstree.defaults.checkbox.three_state
+ * @plugin checkbox
+ */
+ three_state : true,
+ /**
+ * a boolean indicating if clicking anywhere on the node should act as clicking on the checkbox. Defaults to `true`.
+ * @name $.jstree.defaults.checkbox.whole_node
+ * @plugin checkbox
+ */
+ whole_node : true,
+ /**
+ * a boolean indicating if the selected style of a node should be kept, or removed. Defaults to `true`.
+ * @name $.jstree.defaults.checkbox.keep_selected_style
+ * @plugin checkbox
+ */
+ keep_selected_style : true,
+ /**
+ * This setting controls how cascading and undetermined nodes are applied.
+ * If 'up' is in the string - cascading up is enabled, if 'down' is in the string - cascading down is enabled, if 'undetermined' is in the string - undetermined nodes will be used.
+ * If `three_state` is set to `true` this setting is automatically set to 'up+down+undetermined'. Defaults to ''.
+ * @name $.jstree.defaults.checkbox.cascade
+ * @plugin checkbox
+ */
+ cascade : '',
+ /**
+ * This setting controls if checkbox are bound to the general tree selection or to an internal array maintained by the checkbox plugin. Defaults to `true`, only set to `false` if you know exactly what you are doing.
+ * @name $.jstree.defaults.checkbox.tie_selection
+ * @plugin checkbox
+ */
+ tie_selection : true,
+
+ /**
+ * This setting controls if cascading down affects disabled checkboxes
+ * @name $.jstree.defaults.checkbox.cascade_to_disabled
+ * @plugin checkbox
+ */
+ cascade_to_disabled : true,
+
+ /**
+ * This setting controls if cascading down affects hidden checkboxes
+ * @name $.jstree.defaults.checkbox.cascade_to_hidden
+ * @plugin checkbox
+ */
+ cascade_to_hidden : true
+ };
+ $.jstree.plugins.checkbox = function (options, parent) {
+ this.bind = function () {
+ parent.bind.call(this);
+ this._data.checkbox.uto = false;
+ this._data.checkbox.selected = [];
+ if(this.settings.checkbox.three_state) {
+ this.settings.checkbox.cascade = 'up+down+undetermined';
+ }
+ this.element
+ .on("init.jstree", $.proxy(function () {
+ this._data.checkbox.visible = this.settings.checkbox.visible;
+ if(!this.settings.checkbox.keep_selected_style) {
+ this.element.addClass('jstree-checkbox-no-clicked');
+ }
+ if(this.settings.checkbox.tie_selection) {
+ this.element.addClass('jstree-checkbox-selection');
+ }
+ }, this))
+ .on("loading.jstree", $.proxy(function () {
+ this[ this._data.checkbox.visible ? 'show_checkboxes' : 'hide_checkboxes' ]();
+ }, this));
+ if(this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
+ this.element
+ .on('changed.jstree uncheck_node.jstree check_node.jstree uncheck_all.jstree check_all.jstree move_node.jstree copy_node.jstree redraw.jstree open_node.jstree', $.proxy(function () {
+ // only if undetermined is in setting
+ if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
+ this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
+ }, this));
+ }
+ if(!this.settings.checkbox.tie_selection) {
+ this.element
+ .on('model.jstree', $.proxy(function (e, data) {
+ var m = this._model.data,
+ p = m[data.parent],
+ dpc = data.nodes,
+ i, j;
+ for(i = 0, j = dpc.length; i < j; i++) {
+ m[dpc[i]].state.checked = m[dpc[i]].state.checked || (m[dpc[i]].original && m[dpc[i]].original.state && m[dpc[i]].original.state.checked);
+ if(m[dpc[i]].state.checked) {
+ this._data.checkbox.selected.push(dpc[i]);
+ }
+ }
+ }, this));
+ }
+ if(this.settings.checkbox.cascade.indexOf('up') !== -1 || this.settings.checkbox.cascade.indexOf('down') !== -1) {
+ this.element
+ .on('model.jstree', $.proxy(function (e, data) {
+ var m = this._model.data,
+ p = m[data.parent],
+ dpc = data.nodes,
+ chd = [],
+ c, i, j, k, l, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection;
+
+ if(s.indexOf('down') !== -1) {
+ // apply down
+ if(p.state[ t ? 'selected' : 'checked' ]) {
+ for(i = 0, j = dpc.length; i < j; i++) {
+ m[dpc[i]].state[ t ? 'selected' : 'checked' ] = true;
+ }
+
+ this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(dpc);
+ }
+ else {
+ for(i = 0, j = dpc.length; i < j; i++) {
+ if(m[dpc[i]].state[ t ? 'selected' : 'checked' ]) {
+ for(k = 0, l = m[dpc[i]].children_d.length; k < l; k++) {
+ m[m[dpc[i]].children_d[k]].state[ t ? 'selected' : 'checked' ] = true;
+ }
+ this._data[ t ? 'core' : 'checkbox' ].selected = this._data[ t ? 'core' : 'checkbox' ].selected.concat(m[dpc[i]].children_d);
+ }
+ }
+ }
+ }
+
+ if(s.indexOf('up') !== -1) {
+ // apply up
+ for(i = 0, j = p.children_d.length; i < j; i++) {
+ if(!m[p.children_d[i]].children.length) {
+ chd.push(m[p.children_d[i]].parent);
+ }
+ }
+ chd = $.vakata.array_unique(chd);
+ for(k = 0, l = chd.length; k < l; k++) {
+ p = m[chd[k]];
+ while(p && p.id !== $.jstree.root) {
+ c = 0;
+ for(i = 0, j = p.children.length; i < j; i++) {
+ c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
+ }
+ if(c === j) {
+ p.state[ t ? 'selected' : 'checked' ] = true;
+ this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
+ tmp = this.get_node(p, true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', true).children('.jstree-anchor').addClass( t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ else {
+ break;
+ }
+ p = this.get_node(p.parent);
+ }
+ }
+ }
+
+ this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected);
+ }, this))
+ .on(this.settings.checkbox.tie_selection ? 'select_node.jstree' : 'check_node.jstree', $.proxy(function (e, data) {
+ var self = this,
+ obj = data.node,
+ m = this._model.data,
+ par = this.get_node(obj.parent),
+ i, j, c, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection,
+ sel = {}, cur = this._data[ t ? 'core' : 'checkbox' ].selected;
+
+ for (i = 0, j = cur.length; i < j; i++) {
+ sel[cur[i]] = true;
+ }
+
+ // apply down
+ if(s.indexOf('down') !== -1) {
+ //this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_unique(this._data[ t ? 'core' : 'checkbox' ].selected.concat(obj.children_d));
+ var selectedIds = this._cascade_new_checked_state(obj.id, true);
+ var temp = obj.children_d.concat(obj.id);
+ for (i = 0, j = temp.length; i < j; i++) {
+ if (selectedIds.indexOf(temp[i]) > -1) {
+ sel[temp[i]] = true;
+ }
+ else {
+ delete sel[temp[i]];
+ }
+ }
+ }
+
+ // apply up
+ if(s.indexOf('up') !== -1) {
+ while(par && par.id !== $.jstree.root) {
+ c = 0;
+ for(i = 0, j = par.children.length; i < j; i++) {
+ c += m[par.children[i]].state[ t ? 'selected' : 'checked' ];
+ }
+ if(c === j) {
+ par.state[ t ? 'selected' : 'checked' ] = true;
+ sel[par.id] = true;
+ //this._data[ t ? 'core' : 'checkbox' ].selected.push(par.id);
+ tmp = this.get_node(par, true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ else {
+ break;
+ }
+ par = this.get_node(par.parent);
+ }
+ }
+
+ cur = [];
+ for (i in sel) {
+ if (sel.hasOwnProperty(i)) {
+ cur.push(i);
+ }
+ }
+ this._data[ t ? 'core' : 'checkbox' ].selected = cur;
+ }, this))
+ .on(this.settings.checkbox.tie_selection ? 'deselect_all.jstree' : 'uncheck_all.jstree', $.proxy(function (e, data) {
+ var obj = this.get_node($.jstree.root),
+ m = this._model.data,
+ i, j, tmp;
+ for(i = 0, j = obj.children_d.length; i < j; i++) {
+ tmp = m[obj.children_d[i]];
+ if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
+ tmp.original.state.undetermined = false;
+ }
+ }
+ }, this))
+ .on(this.settings.checkbox.tie_selection ? 'deselect_node.jstree' : 'uncheck_node.jstree', $.proxy(function (e, data) {
+ var self = this,
+ obj = data.node,
+ dom = this.get_node(obj, true),
+ i, j, tmp, s = this.settings.checkbox.cascade, t = this.settings.checkbox.tie_selection,
+ cur = this._data[ t ? 'core' : 'checkbox' ].selected, sel = {},
+ stillSelectedIds = [],
+ allIds = obj.children_d.concat(obj.id);
+
+ // apply down
+ if(s.indexOf('down') !== -1) {
+ var selectedIds = this._cascade_new_checked_state(obj.id, false);
+
+ cur = $.vakata.array_filter(cur, function(id) {
+ return allIds.indexOf(id) === -1 || selectedIds.indexOf(id) > -1;
+ });
+ }
+
+ // only apply up if cascade up is enabled and if this node is not selected
+ // (if all child nodes are disabled and cascade_to_disabled === false then this node will till be selected).
+ if(s.indexOf('up') !== -1 && cur.indexOf(obj.id) === -1) {
+ for(i = 0, j = obj.parents.length; i < j; i++) {
+ tmp = this._model.data[obj.parents[i]];
+ tmp.state[ t ? 'selected' : 'checked' ] = false;
+ if(tmp && tmp.original && tmp.original.state && tmp.original.state.undetermined) {
+ tmp.original.state.undetermined = false;
+ }
+ tmp = this.get_node(obj.parents[i], true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+
+ cur = $.vakata.array_filter(cur, function(id) {
+ return obj.parents.indexOf(id) === -1;
+ });
+ }
+
+ this._data[ t ? 'core' : 'checkbox' ].selected = cur;
+ }, this));
+ }
+ if(this.settings.checkbox.cascade.indexOf('up') !== -1) {
+ this.element
+ .on('delete_node.jstree', $.proxy(function (e, data) {
+ // apply up (whole handler)
+ var p = this.get_node(data.parent),
+ m = this._model.data,
+ i, j, c, tmp, t = this.settings.checkbox.tie_selection;
+ while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) {
+ c = 0;
+ for(i = 0, j = p.children.length; i < j; i++) {
+ c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
+ }
+ if(j > 0 && c === j) {
+ p.state[ t ? 'selected' : 'checked' ] = true;
+ this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
+ tmp = this.get_node(p, true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ else {
+ break;
+ }
+ p = this.get_node(p.parent);
+ }
+ }, this))
+ .on('move_node.jstree', $.proxy(function (e, data) {
+ // apply up (whole handler)
+ var is_multi = data.is_multi,
+ old_par = data.old_parent,
+ new_par = this.get_node(data.parent),
+ m = this._model.data,
+ p, c, i, j, tmp, t = this.settings.checkbox.tie_selection;
+ if(!is_multi) {
+ p = this.get_node(old_par);
+ while(p && p.id !== $.jstree.root && !p.state[ t ? 'selected' : 'checked' ]) {
+ c = 0;
+ for(i = 0, j = p.children.length; i < j; i++) {
+ c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
+ }
+ if(j > 0 && c === j) {
+ p.state[ t ? 'selected' : 'checked' ] = true;
+ this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
+ tmp = this.get_node(p, true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ else {
+ break;
+ }
+ p = this.get_node(p.parent);
+ }
+ }
+ p = new_par;
+ while(p && p.id !== $.jstree.root) {
+ c = 0;
+ for(i = 0, j = p.children.length; i < j; i++) {
+ c += m[p.children[i]].state[ t ? 'selected' : 'checked' ];
+ }
+ if(c === j) {
+ if(!p.state[ t ? 'selected' : 'checked' ]) {
+ p.state[ t ? 'selected' : 'checked' ] = true;
+ this._data[ t ? 'core' : 'checkbox' ].selected.push(p.id);
+ tmp = this.get_node(p, true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ }
+ else {
+ if(p.state[ t ? 'selected' : 'checked' ]) {
+ p.state[ t ? 'selected' : 'checked' ] = false;
+ this._data[ t ? 'core' : 'checkbox' ].selected = $.vakata.array_remove_item(this._data[ t ? 'core' : 'checkbox' ].selected, p.id);
+ tmp = this.get_node(p, true);
+ if(tmp && tmp.length) {
+ tmp.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ else {
+ break;
+ }
+ }
+ p = this.get_node(p.parent);
+ }
+ }, this));
+ }
+ };
+ /**
+ * get an array of all nodes whose state is "undetermined"
+ * @name get_undetermined([full])
+ * @param {boolean} full: if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
+ * @return {Array}
+ * @plugin checkbox
+ */
+ this.get_undetermined = function (full) {
+ if (this.settings.checkbox.cascade.indexOf('undetermined') === -1) {
+ return [];
+ }
+ var i, j, k, l, o = {}, m = this._model.data, t = this.settings.checkbox.tie_selection, s = this._data[ t ? 'core' : 'checkbox' ].selected, p = [], tt = this, r = [];
+ for(i = 0, j = s.length; i < j; i++) {
+ if(m[s[i]] && m[s[i]].parents) {
+ for(k = 0, l = m[s[i]].parents.length; k < l; k++) {
+ if(o[m[s[i]].parents[k]] !== undefined) {
+ break;
+ }
+ if(m[s[i]].parents[k] !== $.jstree.root) {
+ o[m[s[i]].parents[k]] = true;
+ p.push(m[s[i]].parents[k]);
+ }
+ }
+ }
+ }
+ // attempt for server side undetermined state
+ this.element.find('.jstree-closed').not(':has(.jstree-children)')
+ .each(function () {
+ var tmp = tt.get_node(this), tmp2;
+
+ if(!tmp) { return; }
+
+ if(!tmp.state.loaded) {
+ if(tmp.original && tmp.original.state && tmp.original.state.undetermined && tmp.original.state.undetermined === true) {
+ if(o[tmp.id] === undefined && tmp.id !== $.jstree.root) {
+ o[tmp.id] = true;
+ p.push(tmp.id);
+ }
+ for(k = 0, l = tmp.parents.length; k < l; k++) {
+ if(o[tmp.parents[k]] === undefined && tmp.parents[k] !== $.jstree.root) {
+ o[tmp.parents[k]] = true;
+ p.push(tmp.parents[k]);
+ }
+ }
+ }
+ }
+ else {
+ for(i = 0, j = tmp.children_d.length; i < j; i++) {
+ tmp2 = m[tmp.children_d[i]];
+ if(!tmp2.state.loaded && tmp2.original && tmp2.original.state && tmp2.original.state.undetermined && tmp2.original.state.undetermined === true) {
+ if(o[tmp2.id] === undefined && tmp2.id !== $.jstree.root) {
+ o[tmp2.id] = true;
+ p.push(tmp2.id);
+ }
+ for(k = 0, l = tmp2.parents.length; k < l; k++) {
+ if(o[tmp2.parents[k]] === undefined && tmp2.parents[k] !== $.jstree.root) {
+ o[tmp2.parents[k]] = true;
+ p.push(tmp2.parents[k]);
+ }
+ }
+ }
+ }
+ }
+ });
+ for (i = 0, j = p.length; i < j; i++) {
+ if(!m[p[i]].state[ t ? 'selected' : 'checked' ]) {
+ r.push(full ? m[p[i]] : p[i]);
+ }
+ }
+ return r;
+ };
+ /**
+ * set the undetermined state where and if necessary. Used internally.
+ * @private
+ * @name _undetermined()
+ * @plugin checkbox
+ */
+ this._undetermined = function () {
+ if(this.element === null) { return; }
+ var p = this.get_undetermined(false), i, j, s;
+
+ this.element.find('.jstree-undetermined').removeClass('jstree-undetermined');
+ for (i = 0, j = p.length; i < j; i++) {
+ s = this.get_node(p[i], true);
+ if(s && s.length) {
+ s.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-undetermined');
+ }
+ }
+ };
+ this.redraw_node = function(obj, deep, is_callback, force_render) {
+ obj = parent.redraw_node.apply(this, arguments);
+ if(obj) {
+ var i, j, tmp = null, icon = null;
+ for(i = 0, j = obj.childNodes.length; i < j; i++) {
+ if(obj.childNodes[i] && obj.childNodes[i].className && obj.childNodes[i].className.indexOf("jstree-anchor") !== -1) {
+ tmp = obj.childNodes[i];
+ break;
+ }
+ }
+ if(tmp) {
+ if(!this.settings.checkbox.tie_selection && this._model.data[obj.id].state.checked) { tmp.className += ' jstree-checked'; }
+ icon = _i.cloneNode(false);
+ if(this._model.data[obj.id].state.checkbox_disabled) { icon.className += ' jstree-checkbox-disabled'; }
+ tmp.insertBefore(icon, tmp.childNodes[0]);
+ }
+ }
+ if(!is_callback && this.settings.checkbox.cascade.indexOf('undetermined') !== -1) {
+ if(this._data.checkbox.uto) { clearTimeout(this._data.checkbox.uto); }
+ this._data.checkbox.uto = setTimeout($.proxy(this._undetermined, this), 50);
+ }
+ return obj;
+ };
+ /**
+ * show the node checkbox icons
+ * @name show_checkboxes()
+ * @plugin checkbox
+ */
+ this.show_checkboxes = function () { this._data.core.themes.checkboxes = true; this.get_container_ul().removeClass("jstree-no-checkboxes"); };
+ /**
+ * hide the node checkbox icons
+ * @name hide_checkboxes()
+ * @plugin checkbox
+ */
+ this.hide_checkboxes = function () { this._data.core.themes.checkboxes = false; this.get_container_ul().addClass("jstree-no-checkboxes"); };
+ /**
+ * toggle the node icons
+ * @name toggle_checkboxes()
+ * @plugin checkbox
+ */
+ this.toggle_checkboxes = function () { if(this._data.core.themes.checkboxes) { this.hide_checkboxes(); } else { this.show_checkboxes(); } };
+ /**
+ * checks if a node is in an undetermined state
+ * @name is_undetermined(obj)
+ * @param {mixed} obj
+ * @return {Boolean}
+ */
+ this.is_undetermined = function (obj) {
+ obj = this.get_node(obj);
+ var s = this.settings.checkbox.cascade, i, j, t = this.settings.checkbox.tie_selection, d = this._data[ t ? 'core' : 'checkbox' ].selected, m = this._model.data;
+ if(!obj || obj.state[ t ? 'selected' : 'checked' ] === true || s.indexOf('undetermined') === -1 || (s.indexOf('down') === -1 && s.indexOf('up') === -1)) {
+ return false;
+ }
+ if(!obj.state.loaded && obj.original.state.undetermined === true) {
+ return true;
+ }
+ for(i = 0, j = obj.children_d.length; i < j; i++) {
+ if($.inArray(obj.children_d[i], d) !== -1 || (!m[obj.children_d[i]].state.loaded && m[obj.children_d[i]].original.state.undetermined)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ /**
+ * disable a node's checkbox
+ * @name disable_checkbox(obj)
+ * @param {mixed} obj an array can be used too
+ * @trigger disable_checkbox.jstree
+ * @plugin checkbox
+ */
+ this.disable_checkbox = function (obj) {
+ var t1, t2, dom;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.disable_checkbox(obj[t1]);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) {
+ return false;
+ }
+ dom = this.get_node(obj, true);
+ if(!obj.state.checkbox_disabled) {
+ obj.state.checkbox_disabled = true;
+ if(dom && dom.length) {
+ dom.children('.jstree-anchor').children('.jstree-checkbox').addClass('jstree-checkbox-disabled');
+ }
+ /**
+ * triggered when an node's checkbox is disabled
+ * @event
+ * @name disable_checkbox.jstree
+ * @param {Object} node
+ * @plugin checkbox
+ */
+ this.trigger('disable_checkbox', { 'node' : obj });
+ }
+ };
+ /**
+ * enable a node's checkbox
+ * @name enable_checkbox(obj)
+ * @param {mixed} obj an array can be used too
+ * @trigger enable_checkbox.jstree
+ * @plugin checkbox
+ */
+ this.enable_checkbox = function (obj) {
+ var t1, t2, dom;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.enable_checkbox(obj[t1]);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) {
+ return false;
+ }
+ dom = this.get_node(obj, true);
+ if(obj.state.checkbox_disabled) {
+ obj.state.checkbox_disabled = false;
+ if(dom && dom.length) {
+ dom.children('.jstree-anchor').children('.jstree-checkbox').removeClass('jstree-checkbox-disabled');
+ }
+ /**
+ * triggered when an node's checkbox is enabled
+ * @event
+ * @name enable_checkbox.jstree
+ * @param {Object} node
+ * @plugin checkbox
+ */
+ this.trigger('enable_checkbox', { 'node' : obj });
+ }
+ };
+
+ this.activate_node = function (obj, e) {
+ if($(e.target).hasClass('jstree-checkbox-disabled')) {
+ return false;
+ }
+ if(this.settings.checkbox.tie_selection && (this.settings.checkbox.whole_node || $(e.target).hasClass('jstree-checkbox'))) {
+ e.ctrlKey = true;
+ }
+ if(this.settings.checkbox.tie_selection || (!this.settings.checkbox.whole_node && !$(e.target).hasClass('jstree-checkbox'))) {
+ return parent.activate_node.call(this, obj, e);
+ }
+ if(this.is_disabled(obj)) {
+ return false;
+ }
+ if(this.is_checked(obj)) {
+ this.uncheck_node(obj, e);
+ }
+ else {
+ this.check_node(obj, e);
+ }
+ this.trigger('activate_node', { 'node' : this.get_node(obj) });
+ };
+
+ /**
+ * Cascades checked state to a node and all its descendants. This function does NOT affect hidden and disabled nodes (or their descendants).
+ * However if these unaffected nodes are already selected their ids will be included in the returned array.
+ * @private
+ * @param {string} id the node ID
+ * @param {bool} checkedState should the nodes be checked or not
+ * @returns {Array} Array of all node id's (in this tree branch) that are checked.
+ */
+ this._cascade_new_checked_state = function (id, checkedState) {
+ var self = this;
+ var t = this.settings.checkbox.tie_selection;
+ var node = this._model.data[id];
+ var selectedNodeIds = [];
+ var selectedChildrenIds = [], i, j, selectedChildIds;
+
+ if (
+ (this.settings.checkbox.cascade_to_disabled || !node.state.disabled) &&
+ (this.settings.checkbox.cascade_to_hidden || !node.state.hidden)
+ ) {
+ //First try and check/uncheck the children
+ if (node.children) {
+ for (i = 0, j = node.children.length; i < j; i++) {
+ var childId = node.children[i];
+ selectedChildIds = self._cascade_new_checked_state(childId, checkedState);
+ selectedNodeIds = selectedNodeIds.concat(selectedChildIds);
+ if (selectedChildIds.indexOf(childId) > -1) {
+ selectedChildrenIds.push(childId);
+ }
+ }
+ }
+
+ var dom = self.get_node(node, true);
+
+ //A node's state is undetermined if some but not all of it's children are checked/selected .
+ var undetermined = selectedChildrenIds.length > 0 && selectedChildrenIds.length < node.children.length;
+
+ if(node.original && node.original.state && node.original.state.undetermined) {
+ node.original.state.undetermined = undetermined;
+ }
+
+ //If a node is undetermined then remove selected class
+ if (undetermined) {
+ node.state[ t ? 'selected' : 'checked' ] = false;
+ dom.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ //Otherwise, if the checkedState === true (i.e. the node is being checked now) and all of the node's children are checked (if it has any children),
+ //check the node and style it correctly.
+ else if (checkedState && selectedChildrenIds.length === node.children.length) {
+ node.state[ t ? 'selected' : 'checked' ] = checkedState;
+ selectedNodeIds.push(node.id);
+
+ dom.attr('aria-selected', true).children('.jstree-anchor').addClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ else {
+ node.state[ t ? 'selected' : 'checked' ] = false;
+ dom.attr('aria-selected', false).children('.jstree-anchor').removeClass(t ? 'jstree-clicked' : 'jstree-checked');
+ }
+ }
+ else {
+ selectedChildIds = this.get_checked_descendants(id);
+
+ if (node.state[ t ? 'selected' : 'checked' ]) {
+ selectedChildIds.push(node.id);
+ }
+
+ selectedNodeIds = selectedNodeIds.concat(selectedChildIds);
+ }
+
+ return selectedNodeIds;
+ };
+
+ /**
+ * Gets ids of nodes selected in branch (of tree) specified by id (does not include the node specified by id)
+ * @name get_checked_descendants(obj)
+ * @param {string} id the node ID
+ * @return {Array} array of IDs
+ * @plugin checkbox
+ */
+ this.get_checked_descendants = function (id) {
+ var self = this;
+ var t = self.settings.checkbox.tie_selection;
+ var node = self._model.data[id];
+
+ return $.vakata.array_filter(node.children_d, function(_id) {
+ return self._model.data[_id].state[ t ? 'selected' : 'checked' ];
+ });
+ };
+
+ /**
+ * check a node (only if tie_selection in checkbox settings is false, otherwise select_node will be called internally)
+ * @name check_node(obj)
+ * @param {mixed} obj an array can be used to check multiple nodes
+ * @trigger check_node.jstree
+ * @plugin checkbox
+ */
+ this.check_node = function (obj, e) {
+ if(this.settings.checkbox.tie_selection) { return this.select_node(obj, false, true, e); }
+ var dom, t1, t2, th;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.check_node(obj[t1], e);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) {
+ return false;
+ }
+ dom = this.get_node(obj, true);
+ if(!obj.state.checked) {
+ obj.state.checked = true;
+ this._data.checkbox.selected.push(obj.id);
+ if(dom && dom.length) {
+ dom.children('.jstree-anchor').addClass('jstree-checked');
+ }
+ /**
+ * triggered when an node is checked (only if tie_selection in checkbox settings is false)
+ * @event
+ * @name check_node.jstree
+ * @param {Object} node
+ * @param {Array} selected the current selection
+ * @param {Object} event the event (if any) that triggered this check_node
+ * @plugin checkbox
+ */
+ this.trigger('check_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
+ }
+ };
+ /**
+ * uncheck a node (only if tie_selection in checkbox settings is false, otherwise deselect_node will be called internally)
+ * @name uncheck_node(obj)
+ * @param {mixed} obj an array can be used to uncheck multiple nodes
+ * @trigger uncheck_node.jstree
+ * @plugin checkbox
+ */
+ this.uncheck_node = function (obj, e) {
+ if(this.settings.checkbox.tie_selection) { return this.deselect_node(obj, false, e); }
+ var t1, t2, dom;
+ if($.isArray(obj)) {
+ obj = obj.slice();
+ for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
+ this.uncheck_node(obj[t1], e);
+ }
+ return true;
+ }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) {
+ return false;
+ }
+ dom = this.get_node(obj, true);
+ if(obj.state.checked) {
+ obj.state.checked = false;
+ this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, obj.id);
+ if(dom.length) {
+ dom.children('.jstree-anchor').removeClass('jstree-checked');
+ }
+ /**
+ * triggered when an node is unchecked (only if tie_selection in checkbox settings is false)
+ * @event
+ * @name uncheck_node.jstree
+ * @param {Object} node
+ * @param {Array} selected the current selection
+ * @param {Object} event the event (if any) that triggered this uncheck_node
+ * @plugin checkbox
+ */
+ this.trigger('uncheck_node', { 'node' : obj, 'selected' : this._data.checkbox.selected, 'event' : e });
+ }
+ };
+
+ /**
+ * checks all nodes in the tree (only if tie_selection in checkbox settings is false, otherwise select_all will be called internally)
+ * @name check_all()
+ * @trigger check_all.jstree, changed.jstree
+ * @plugin checkbox
+ */
+ this.check_all = function () {
+ if(this.settings.checkbox.tie_selection) { return this.select_all(); }
+ var tmp = this._data.checkbox.selected.concat([]), i, j;
+ this._data.checkbox.selected = this._model.data[$.jstree.root].children_d.concat();
+ for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
+ if(this._model.data[this._data.checkbox.selected[i]]) {
+ this._model.data[this._data.checkbox.selected[i]].state.checked = true;
+ }
+ }
+ this.redraw(true);
+ /**
+ * triggered when all nodes are checked (only if tie_selection in checkbox settings is false)
+ * @event
+ * @name check_all.jstree
+ * @param {Array} selected the current selection
+ * @plugin checkbox
+ */
+ this.trigger('check_all', { 'selected' : this._data.checkbox.selected });
+ };
+ /**
+ * uncheck all checked nodes (only if tie_selection in checkbox settings is false, otherwise deselect_all will be called internally)
+ * @name uncheck_all()
+ * @trigger uncheck_all.jstree
+ * @plugin checkbox
+ */
+ this.uncheck_all = function () {
+ if(this.settings.checkbox.tie_selection) { return this.deselect_all(); }
+ var tmp = this._data.checkbox.selected.concat([]), i, j;
+ for(i = 0, j = this._data.checkbox.selected.length; i < j; i++) {
+ if(this._model.data[this._data.checkbox.selected[i]]) {
+ this._model.data[this._data.checkbox.selected[i]].state.checked = false;
+ }
+ }
+ this._data.checkbox.selected = [];
+ this.element.find('.jstree-checked').removeClass('jstree-checked');
+ /**
+ * triggered when all nodes are unchecked (only if tie_selection in checkbox settings is false)
+ * @event
+ * @name uncheck_all.jstree
+ * @param {Object} node the previous selection
+ * @param {Array} selected the current selection
+ * @plugin checkbox
+ */
+ this.trigger('uncheck_all', { 'selected' : this._data.checkbox.selected, 'node' : tmp });
+ };
+ /**
+ * checks if a node is checked (if tie_selection is on in the settings this function will return the same as is_selected)
+ * @name is_checked(obj)
+ * @param {mixed} obj
+ * @return {Boolean}
+ * @plugin checkbox
+ */
+ this.is_checked = function (obj) {
+ if(this.settings.checkbox.tie_selection) { return this.is_selected(obj); }
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ return obj.state.checked;
+ };
+ /**
+ * get an array of all checked nodes (if tie_selection is on in the settings this function will return the same as get_selected)
+ * @name get_checked([full])
+ * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
+ * @return {Array}
+ * @plugin checkbox
+ */
+ this.get_checked = function (full) {
+ if(this.settings.checkbox.tie_selection) { return this.get_selected(full); }
+ return full ? $.map(this._data.checkbox.selected, $.proxy(function (i) { return this.get_node(i); }, this)) : this._data.checkbox.selected.slice();
+ };
+ /**
+ * get an array of all top level checked nodes (ignoring children of checked nodes) (if tie_selection is on in the settings this function will return the same as get_top_selected)
+ * @name get_top_checked([full])
+ * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
+ * @return {Array}
+ * @plugin checkbox
+ */
+ this.get_top_checked = function (full) {
+ if(this.settings.checkbox.tie_selection) { return this.get_top_selected(full); }
+ var tmp = this.get_checked(true),
+ obj = {}, i, j, k, l;
+ for(i = 0, j = tmp.length; i < j; i++) {
+ obj[tmp[i].id] = tmp[i];
+ }
+ for(i = 0, j = tmp.length; i < j; i++) {
+ for(k = 0, l = tmp[i].children_d.length; k < l; k++) {
+ if(obj[tmp[i].children_d[k]]) {
+ delete obj[tmp[i].children_d[k]];
+ }
+ }
+ }
+ tmp = [];
+ for(i in obj) {
+ if(obj.hasOwnProperty(i)) {
+ tmp.push(i);
+ }
+ }
+ return full ? $.map(tmp, $.proxy(function (i) { return this.get_node(i); }, this)) : tmp;
+ };
+ /**
+ * get an array of all bottom level checked nodes (ignoring selected parents) (if tie_selection is on in the settings this function will return the same as get_bottom_selected)
+ * @name get_bottom_checked([full])
+ * @param {mixed} full if set to `true` the returned array will consist of the full node objects, otherwise - only IDs will be returned
+ * @return {Array}
+ * @plugin checkbox
+ */
+ this.get_bottom_checked = function (full) {
+ if(this.settings.checkbox.tie_selection) { return this.get_bottom_selected(full); }
+ var tmp = this.get_checked(true),
+ obj = [], i, j;
+ for(i = 0, j = tmp.length; i < j; i++) {
+ if(!tmp[i].children.length) {
+ obj.push(tmp[i].id);
+ }
+ }
+ return full ? $.map(obj, $.proxy(function (i) { return this.get_node(i); }, this)) : obj;
+ };
+ this.load_node = function (obj, callback) {
+ var k, l, i, j, c, tmp;
+ if(!$.isArray(obj) && !this.settings.checkbox.tie_selection) {
+ tmp = this.get_node(obj);
+ if(tmp && tmp.state.loaded) {
+ for(k = 0, l = tmp.children_d.length; k < l; k++) {
+ if(this._model.data[tmp.children_d[k]].state.checked) {
+ c = true;
+ this._data.checkbox.selected = $.vakata.array_remove_item(this._data.checkbox.selected, tmp.children_d[k]);
+ }
+ }
+ }
+ }
+ return parent.load_node.apply(this, arguments);
+ };
+ this.get_state = function () {
+ var state = parent.get_state.apply(this, arguments);
+ if(this.settings.checkbox.tie_selection) { return state; }
+ state.checkbox = this._data.checkbox.selected.slice();
+ return state;
+ };
+ this.set_state = function (state, callback) {
+ var res = parent.set_state.apply(this, arguments);
+ if(res && state.checkbox) {
+ if(!this.settings.checkbox.tie_selection) {
+ this.uncheck_all();
+ var _this = this;
+ $.each(state.checkbox, function (i, v) {
+ _this.check_node(v);
+ });
+ }
+ delete state.checkbox;
+ this.set_state(state, callback);
+ return false;
+ }
+ return res;
+ };
+ this.refresh = function (skip_loading, forget_state) {
+ if(this.settings.checkbox.tie_selection) {
+ this._data.checkbox.selected = [];
+ }
+ return parent.refresh.apply(this, arguments);
+ };
+ };
+
+ // include the checkbox plugin by default
+ // $.jstree.defaults.plugins.push("checkbox");
+
+
+/**
+ * ### Conditionalselect plugin
+ *
+ * This plugin allows defining a callback to allow or deny node selection by user input (activate node method).
+ */
+
+ /**
+ * a callback (function) which is invoked in the instance's scope and receives two arguments - the node and the event that triggered the `activate_node` call. Returning false prevents working with the node, returning true allows invoking activate_node. Defaults to returning `true`.
+ * @name $.jstree.defaults.checkbox.visible
+ * @plugin checkbox
+ */
+ $.jstree.defaults.conditionalselect = function () { return true; };
+ $.jstree.plugins.conditionalselect = function (options, parent) {
+ // own function
+ this.activate_node = function (obj, e) {
+ if(this.settings.conditionalselect.call(this, this.get_node(obj), e)) {
+ return parent.activate_node.call(this, obj, e);
+ }
+ };
+ };
+
+
+/**
+ * ### Contextmenu plugin
+ *
+ * Shows a context menu when a node is right-clicked.
+ */
+
+ /**
+ * stores all defaults for the contextmenu plugin
+ * @name $.jstree.defaults.contextmenu
+ * @plugin contextmenu
+ */
+ $.jstree.defaults.contextmenu = {
+ /**
+ * a boolean indicating if the node should be selected when the context menu is invoked on it. Defaults to `true`.
+ * @name $.jstree.defaults.contextmenu.select_node
+ * @plugin contextmenu
+ */
+ select_node : true,
+ /**
+ * a boolean indicating if the menu should be shown aligned with the node. Defaults to `true`, otherwise the mouse coordinates are used.
+ * @name $.jstree.defaults.contextmenu.show_at_node
+ * @plugin contextmenu
+ */
+ show_at_node : true,
+ /**
+ * an object of actions, or a function that accepts a node and a callback function and calls the callback function with an object of actions available for that node (you can also return the items too).
+ *
+ * Each action consists of a key (a unique name) and a value which is an object with the following properties (only label and action are required). Once a menu item is activated the `action` function will be invoked with an object containing the following keys: item - the contextmenu item definition as seen below, reference - the DOM node that was used (the tree node), element - the contextmenu DOM element, position - an object with x/y properties indicating the position of the menu.
+ *
+ * * `separator_before` - a boolean indicating if there should be a separator before this item
+ * * `separator_after` - a boolean indicating if there should be a separator after this item
+ * * `_disabled` - a boolean indicating if this action should be disabled
+ * * `label` - a string - the name of the action (could be a function returning a string)
+ * * `title` - a string - an optional tooltip for the item
+ * * `action` - a function to be executed if this item is chosen, the function will receive
+ * * `icon` - a string, can be a path to an icon or a className, if using an image that is in the current directory use a `./` prefix, otherwise it will be detected as a class
+ * * `shortcut` - keyCode which will trigger the action if the menu is open (for example `113` for rename, which equals F2)
+ * * `shortcut_label` - shortcut label (like for example `F2` for rename)
+ * * `submenu` - an object with the same structure as $.jstree.defaults.contextmenu.items which can be used to create a submenu - each key will be rendered as a separate option in a submenu that will appear once the current item is hovered
+ *
+ * @name $.jstree.defaults.contextmenu.items
+ * @plugin contextmenu
+ */
+ items : function (o, cb) { // Could be an object directly
+ return {
+ "create" : {
+ "separator_before" : false,
+ "separator_after" : true,
+ "_disabled" : false, //(this.check("create_node", data.reference, {}, "last")),
+ "label" : "Create",
+ "action" : function (data) {
+ var inst = $.jstree.reference(data.reference),
+ obj = inst.get_node(data.reference);
+ inst.create_node(obj, {}, "last", function (new_node) {
+ try {
+ inst.edit(new_node);
+ } catch (ex) {
+ setTimeout(function () { inst.edit(new_node); },0);
+ }
+ });
+ }
+ },
+ "rename" : {
+ "separator_before" : false,
+ "separator_after" : false,
+ "_disabled" : false, //(this.check("rename_node", data.reference, this.get_parent(data.reference), "")),
+ "label" : "Rename",
+ /*!
+ "shortcut" : 113,
+ "shortcut_label" : 'F2',
+ "icon" : "glyphicon glyphicon-leaf",
+ */
+ "action" : function (data) {
+ var inst = $.jstree.reference(data.reference),
+ obj = inst.get_node(data.reference);
+ inst.edit(obj);
+ }
+ },
+ "remove" : {
+ "separator_before" : false,
+ "icon" : false,
+ "separator_after" : false,
+ "_disabled" : false, //(this.check("delete_node", data.reference, this.get_parent(data.reference), "")),
+ "label" : "Delete",
+ "action" : function (data) {
+ var inst = $.jstree.reference(data.reference),
+ obj = inst.get_node(data.reference);
+ if(inst.is_selected(obj)) {
+ inst.delete_node(inst.get_selected());
+ }
+ else {
+ inst.delete_node(obj);
+ }
+ }
+ },
+ "ccp" : {
+ "separator_before" : true,
+ "icon" : false,
+ "separator_after" : false,
+ "label" : "Edit",
+ "action" : false,
+ "submenu" : {
+ "cut" : {
+ "separator_before" : false,
+ "separator_after" : false,
+ "label" : "Cut",
+ "action" : function (data) {
+ var inst = $.jstree.reference(data.reference),
+ obj = inst.get_node(data.reference);
+ if(inst.is_selected(obj)) {
+ inst.cut(inst.get_top_selected());
+ }
+ else {
+ inst.cut(obj);
+ }
+ }
+ },
+ "copy" : {
+ "separator_before" : false,
+ "icon" : false,
+ "separator_after" : false,
+ "label" : "Copy",
+ "action" : function (data) {
+ var inst = $.jstree.reference(data.reference),
+ obj = inst.get_node(data.reference);
+ if(inst.is_selected(obj)) {
+ inst.copy(inst.get_top_selected());
+ }
+ else {
+ inst.copy(obj);
+ }
+ }
+ },
+ "paste" : {
+ "separator_before" : false,
+ "icon" : false,
+ "_disabled" : function (data) {
+ return !$.jstree.reference(data.reference).can_paste();
+ },
+ "separator_after" : false,
+ "label" : "Paste",
+ "action" : function (data) {
+ var inst = $.jstree.reference(data.reference),
+ obj = inst.get_node(data.reference);
+ inst.paste(obj);
+ }
+ }
+ }
+ }
+ };
+ }
+ };
+
+ $.jstree.plugins.contextmenu = function (options, parent) {
+ this.bind = function () {
+ parent.bind.call(this);
+
+ var last_ts = 0, cto = null, ex, ey;
+ this.element
+ .on("init.jstree loading.jstree ready.jstree", $.proxy(function () {
+ this.get_container_ul().addClass('jstree-contextmenu');
+ }, this))
+ .on("contextmenu.jstree", ".jstree-anchor", $.proxy(function (e, data) {
+ if (e.target.tagName.toLowerCase() === 'input') {
+ return;
+ }
+ e.preventDefault();
+ last_ts = e.ctrlKey ? +new Date() : 0;
+ if(data || cto) {
+ last_ts = (+new Date()) + 10000;
+ }
+ if(cto) {
+ clearTimeout(cto);
+ }
+ if(!this.is_loading(e.currentTarget)) {
+ this.show_contextmenu(e.currentTarget, e.pageX, e.pageY, e);
+ }
+ }, this))
+ .on("click.jstree", ".jstree-anchor", $.proxy(function (e) {
+ if(this._data.contextmenu.visible && (!last_ts || (+new Date()) - last_ts > 250)) { // work around safari & macOS ctrl+click
+ $.vakata.context.hide();
+ }
+ last_ts = 0;
+ }, this))
+ .on("touchstart.jstree", ".jstree-anchor", function (e) {
+ if(!e.originalEvent || !e.originalEvent.changedTouches || !e.originalEvent.changedTouches[0]) {
+ return;
+ }
+ ex = e.originalEvent.changedTouches[0].clientX;
+ ey = e.originalEvent.changedTouches[0].clientY;
+ cto = setTimeout(function () {
+ $(e.currentTarget).trigger('contextmenu', true);
+ }, 750);
+ })
+ .on('touchmove.vakata.jstree', function (e) {
+ if(cto && e.originalEvent && e.originalEvent.changedTouches && e.originalEvent.changedTouches[0] && (Math.abs(ex - e.originalEvent.changedTouches[0].clientX) > 10 || Math.abs(ey - e.originalEvent.changedTouches[0].clientY) > 10)) {
+ clearTimeout(cto);
+ $.vakata.context.hide();
+ }
+ })
+ .on('touchend.vakata.jstree', function (e) {
+ if(cto) {
+ clearTimeout(cto);
+ }
+ });
+
+ /*!
+ if(!('oncontextmenu' in document.body) && ('ontouchstart' in document.body)) {
+ var el = null, tm = null;
+ this.element
+ .on("touchstart", ".jstree-anchor", function (e) {
+ el = e.currentTarget;
+ tm = +new Date();
+ $(document).one("touchend", function (e) {
+ e.target = document.elementFromPoint(e.originalEvent.targetTouches[0].pageX - window.pageXOffset, e.originalEvent.targetTouches[0].pageY - window.pageYOffset);
+ e.currentTarget = e.target;
+ tm = ((+(new Date())) - tm);
+ if(e.target === el && tm > 600 && tm < 1000) {
+ e.preventDefault();
+ $(el).trigger('contextmenu', e);
+ }
+ el = null;
+ tm = null;
+ });
+ });
+ }
+ */
+ $(document).on("context_hide.vakata.jstree", $.proxy(function (e, data) {
+ this._data.contextmenu.visible = false;
+ $(data.reference).removeClass('jstree-context');
+ }, this));
+ };
+ this.teardown = function () {
+ if(this._data.contextmenu.visible) {
+ $.vakata.context.hide();
+ }
+ parent.teardown.call(this);
+ };
+
+ /**
+ * prepare and show the context menu for a node
+ * @name show_contextmenu(obj [, x, y])
+ * @param {mixed} obj the node
+ * @param {Number} x the x-coordinate relative to the document to show the menu at
+ * @param {Number} y the y-coordinate relative to the document to show the menu at
+ * @param {Object} e the event if available that triggered the contextmenu
+ * @plugin contextmenu
+ * @trigger show_contextmenu.jstree
+ */
+ this.show_contextmenu = function (obj, x, y, e) {
+ obj = this.get_node(obj);
+ if(!obj || obj.id === $.jstree.root) { return false; }
+ var s = this.settings.contextmenu,
+ d = this.get_node(obj, true),
+ a = d.children(".jstree-anchor"),
+ o = false,
+ i = false;
+ if(s.show_at_node || x === undefined || y === undefined) {
+ o = a.offset();
+ x = o.left;
+ y = o.top + this._data.core.li_height;
+ }
+ if(this.settings.contextmenu.select_node && !this.is_selected(obj)) {
+ this.activate_node(obj, e);
+ }
+
+ i = s.items;
+ if($.isFunction(i)) {
+ i = i.call(this, obj, $.proxy(function (i) {
+ this._show_contextmenu(obj, x, y, i);
+ }, this));
+ }
+ if($.isPlainObject(i)) {
+ this._show_contextmenu(obj, x, y, i);
+ }
+ };
+ /**
+ * show the prepared context menu for a node
+ * @name _show_contextmenu(obj, x, y, i)
+ * @param {mixed} obj the node
+ * @param {Number} x the x-coordinate relative to the document to show the menu at
+ * @param {Number} y the y-coordinate relative to the document to show the menu at
+ * @param {Number} i the object of items to show
+ * @plugin contextmenu
+ * @trigger show_contextmenu.jstree
+ * @private
+ */
+ this._show_contextmenu = function (obj, x, y, i) {
+ var d = this.get_node(obj, true),
+ a = d.children(".jstree-anchor");
+ $(document).one("context_show.vakata.jstree", $.proxy(function (e, data) {
+ var cls = 'jstree-contextmenu jstree-' + this.get_theme() + '-contextmenu';
+ $(data.element).addClass(cls);
+ a.addClass('jstree-context');
+ }, this));
+ this._data.contextmenu.visible = true;
+ $.vakata.context.show(a, { 'x' : x, 'y' : y }, i);
+ /**
+ * triggered when the contextmenu is shown for a node
+ * @event
+ * @name show_contextmenu.jstree
+ * @param {Object} node the node
+ * @param {Number} x the x-coordinate of the menu relative to the document
+ * @param {Number} y the y-coordinate of the menu relative to the document
+ * @plugin contextmenu
+ */
+ this.trigger('show_contextmenu', { "node" : obj, "x" : x, "y" : y });
+ };
+ };
+
+ // contextmenu helper
+ (function ($) {
+ var right_to_left = false,
+ vakata_context = {
+ element : false,
+ reference : false,
+ position_x : 0,
+ position_y : 0,
+ items : [],
+ html : "",
+ is_visible : false
+ };
+
+ $.vakata.context = {
+ settings : {
+ hide_onmouseleave : 0,
+ icons : true
+ },
+ _trigger : function (event_name) {
+ $(document).triggerHandler("context_" + event_name + ".vakata", {
+ "reference" : vakata_context.reference,
+ "element" : vakata_context.element,
+ "position" : {
+ "x" : vakata_context.position_x,
+ "y" : vakata_context.position_y
+ }
+ });
+ },
+ _execute : function (i) {
+ i = vakata_context.items[i];
+ return i && (!i._disabled || ($.isFunction(i._disabled) && !i._disabled({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }))) && i.action ? i.action.call(null, {
+ "item" : i,
+ "reference" : vakata_context.reference,
+ "element" : vakata_context.element,
+ "position" : {
+ "x" : vakata_context.position_x,
+ "y" : vakata_context.position_y
+ }
+ }) : false;
+ },
+ _parse : function (o, is_callback) {
+ if(!o) { return false; }
+ if(!is_callback) {
+ vakata_context.html = "";
+ vakata_context.items = [];
+ }
+ var str = "",
+ sep = false,
+ tmp;
+
+ if(is_callback) { str += "<"+"ul>"; }
+ $.each(o, function (i, val) {
+ if(!val) { return true; }
+ vakata_context.items.push(val);
+ if(!sep && val.separator_before) {
+ str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
+ }
+ sep = false;
+ str += "<"+"li class='" + (val._class || "") + (val._disabled === true || ($.isFunction(val._disabled) && val._disabled({ "item" : val, "reference" : vakata_context.reference, "element" : vakata_context.element })) ? " vakata-contextmenu-disabled " : "") + "' "+(val.shortcut?" data-shortcut='"+val.shortcut+"' ":'')+">";
+ str += "<"+"a href='#' rel='" + (vakata_context.items.length - 1) + "' " + (val.title ? "title='" + val.title + "'" : "") + ">";
+ if($.vakata.context.settings.icons) {
+ str += "<"+"i ";
+ if(val.icon) {
+ if(val.icon.indexOf("/") !== -1 || val.icon.indexOf(".") !== -1) { str += " style='background:url(\"" + val.icon + "\") center center no-repeat' "; }
+ else { str += " class='" + val.icon + "' "; }
+ }
+ str += "><"+"/i><"+"span class='vakata-contextmenu-sep'> <"+"/span>";
+ }
+ str += ($.isFunction(val.label) ? val.label({ "item" : i, "reference" : vakata_context.reference, "element" : vakata_context.element }) : val.label) + (val.shortcut?' '+ (val.shortcut_label || '') +'':'') + "<"+"/a>";
+ if(val.submenu) {
+ tmp = $.vakata.context._parse(val.submenu, true);
+ if(tmp) { str += tmp; }
+ }
+ str += "<"+"/li>";
+ if(val.separator_after) {
+ str += "<"+"li class='vakata-context-separator'><"+"a href='#' " + ($.vakata.context.settings.icons ? '' : 'style="margin-left:0px;"') + "> <"+"/a><"+"/li>";
+ sep = true;
+ }
+ });
+ str = str.replace(/
<\/li\>$/,"");
+ if(is_callback) { str += "
"; }
+ /**
+ * triggered on the document when the contextmenu is parsed (HTML is built)
+ * @event
+ * @plugin contextmenu
+ * @name context_parse.vakata
+ * @param {jQuery} reference the element that was right clicked
+ * @param {jQuery} element the DOM element of the menu itself
+ * @param {Object} position the x & y coordinates of the menu
+ */
+ if(!is_callback) { vakata_context.html = str; $.vakata.context._trigger("parse"); }
+ return str.length > 10 ? str : false;
+ },
+ _show_submenu : function (o) {
+ o = $(o);
+ if(!o.length || !o.children("ul").length) { return; }
+ var e = o.children("ul"),
+ xl = o.offset().left,
+ x = xl + o.outerWidth(),
+ y = o.offset().top,
+ w = e.width(),
+ h = e.height(),
+ dw = $(window).width() + $(window).scrollLeft(),
+ dh = $(window).height() + $(window).scrollTop();
+ // може да се спести е една проверка - дали няма някой от класовете вече нагоре
+ if(right_to_left) {
+ o[x - (w + 10 + o.outerWidth()) < 0 ? "addClass" : "removeClass"]("vakata-context-left");
+ }
+ else {
+ o[x + w > dw && xl > dw - x ? "addClass" : "removeClass"]("vakata-context-right");
+ }
+ if(y + h + 10 > dh) {
+ e.css("bottom","-1px");
+ }
+
+ //if does not fit - stick it to the side
+ if (o.hasClass('vakata-context-right')) {
+ if (xl < w) {
+ e.css("margin-right", xl - w);
+ }
+ } else {
+ if (dw - x < w) {
+ e.css("margin-left", dw - x - w);
+ }
+ }
+
+ e.show();
+ },
+ show : function (reference, position, data) {
+ var o, e, x, y, w, h, dw, dh, cond = true;
+ if(vakata_context.element && vakata_context.element.length) {
+ vakata_context.element.width('');
+ }
+ switch(cond) {
+ case (!position && !reference):
+ return false;
+ case (!!position && !!reference):
+ vakata_context.reference = reference;
+ vakata_context.position_x = position.x;
+ vakata_context.position_y = position.y;
+ break;
+ case (!position && !!reference):
+ vakata_context.reference = reference;
+ o = reference.offset();
+ vakata_context.position_x = o.left + reference.outerHeight();
+ vakata_context.position_y = o.top;
+ break;
+ case (!!position && !reference):
+ vakata_context.position_x = position.x;
+ vakata_context.position_y = position.y;
+ break;
+ }
+ if(!!reference && !data && $(reference).data('vakata_contextmenu')) {
+ data = $(reference).data('vakata_contextmenu');
+ }
+ if($.vakata.context._parse(data)) {
+ vakata_context.element.html(vakata_context.html);
+ }
+ if(vakata_context.items.length) {
+ vakata_context.element.appendTo(document.body);
+ e = vakata_context.element;
+ x = vakata_context.position_x;
+ y = vakata_context.position_y;
+ w = e.width();
+ h = e.height();
+ dw = $(window).width() + $(window).scrollLeft();
+ dh = $(window).height() + $(window).scrollTop();
+ if(right_to_left) {
+ x -= (e.outerWidth() - $(reference).outerWidth());
+ if(x < $(window).scrollLeft() + 20) {
+ x = $(window).scrollLeft() + 20;
+ }
+ }
+ if(x + w + 20 > dw) {
+ x = dw - (w + 20);
+ }
+ if(y + h + 20 > dh) {
+ y = dh - (h + 20);
+ }
+
+ vakata_context.element
+ .css({ "left" : x, "top" : y })
+ .show()
+ .find('a').first().focus().parent().addClass("vakata-context-hover");
+ vakata_context.is_visible = true;
+ /**
+ * triggered on the document when the contextmenu is shown
+ * @event
+ * @plugin contextmenu
+ * @name context_show.vakata
+ * @param {jQuery} reference the element that was right clicked
+ * @param {jQuery} element the DOM element of the menu itself
+ * @param {Object} position the x & y coordinates of the menu
+ */
+ $.vakata.context._trigger("show");
+ }
+ },
+ hide : function () {
+ if(vakata_context.is_visible) {
+ vakata_context.element.hide().find("ul").hide().end().find(':focus').blur().end().detach();
+ vakata_context.is_visible = false;
+ /**
+ * triggered on the document when the contextmenu is hidden
+ * @event
+ * @plugin contextmenu
+ * @name context_hide.vakata
+ * @param {jQuery} reference the element that was right clicked
+ * @param {jQuery} element the DOM element of the menu itself
+ * @param {Object} position the x & y coordinates of the menu
+ */
+ $.vakata.context._trigger("hide");
+ }
+ }
+ };
+ $(function () {
+ right_to_left = $(document.body).css("direction") === "rtl";
+ var to = false;
+
+ vakata_context.element = $("
");
+ vakata_context.element
+ .on("mouseenter", "li", function (e) {
+ e.stopImmediatePropagation();
+
+ if($.contains(this, e.relatedTarget)) {
+ // премахнато заради delegate mouseleave по-долу
+ // $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
+ return;
+ }
+
+ if(to) { clearTimeout(to); }
+ vakata_context.element.find(".vakata-context-hover").removeClass("vakata-context-hover").end();
+
+ $(this)
+ .siblings().find("ul").hide().end().end()
+ .parentsUntil(".vakata-context", "li").addBack().addClass("vakata-context-hover");
+ $.vakata.context._show_submenu(this);
+ })
+ // тестово - дали не натоварва?
+ .on("mouseleave", "li", function (e) {
+ if($.contains(this, e.relatedTarget)) { return; }
+ $(this).find(".vakata-context-hover").addBack().removeClass("vakata-context-hover");
+ })
+ .on("mouseleave", function (e) {
+ $(this).find(".vakata-context-hover").removeClass("vakata-context-hover");
+ if($.vakata.context.settings.hide_onmouseleave) {
+ to = setTimeout(
+ (function (t) {
+ return function () { $.vakata.context.hide(); };
+ }(this)), $.vakata.context.settings.hide_onmouseleave);
+ }
+ })
+ .on("click", "a", function (e) {
+ e.preventDefault();
+ //})
+ //.on("mouseup", "a", function (e) {
+ if(!$(this).blur().parent().hasClass("vakata-context-disabled") && $.vakata.context._execute($(this).attr("rel")) !== false) {
+ $.vakata.context.hide();
+ }
+ })
+ .on('keydown', 'a', function (e) {
+ var o = null;
+ switch(e.which) {
+ case 13:
+ case 32:
+ e.type = "click";
+ e.preventDefault();
+ $(e.currentTarget).trigger(e);
+ break;
+ case 37:
+ if(vakata_context.is_visible) {
+ vakata_context.element.find(".vakata-context-hover").last().closest("li").first().find("ul").hide().find(".vakata-context-hover").removeClass("vakata-context-hover").end().end().children('a').focus();
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ break;
+ case 38:
+ if(vakata_context.is_visible) {
+ o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").prevAll("li:not(.vakata-context-separator)").first();
+ if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").last(); }
+ o.addClass("vakata-context-hover").children('a').focus();
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ break;
+ case 39:
+ if(vakata_context.is_visible) {
+ vakata_context.element.find(".vakata-context-hover").last().children("ul").show().children("li:not(.vakata-context-separator)").removeClass("vakata-context-hover").first().addClass("vakata-context-hover").children('a').focus();
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ break;
+ case 40:
+ if(vakata_context.is_visible) {
+ o = vakata_context.element.find("ul:visible").addBack().last().children(".vakata-context-hover").removeClass("vakata-context-hover").nextAll("li:not(.vakata-context-separator)").first();
+ if(!o.length) { o = vakata_context.element.find("ul:visible").addBack().last().children("li:not(.vakata-context-separator)").first(); }
+ o.addClass("vakata-context-hover").children('a').focus();
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ break;
+ case 27:
+ $.vakata.context.hide();
+ e.preventDefault();
+ break;
+ default:
+ //console.log(e.which);
+ break;
+ }
+ })
+ .on('keydown', function (e) {
+ e.preventDefault();
+ var a = vakata_context.element.find('.vakata-contextmenu-shortcut-' + e.which).parent();
+ if(a.parent().not('.vakata-context-disabled')) {
+ a.click();
+ }
+ });
+
+ $(document)
+ .on("mousedown.vakata.jstree", function (e) {
+ if(vakata_context.is_visible && vakata_context.element[0] !== e.target && !$.contains(vakata_context.element[0], e.target)) {
+ $.vakata.context.hide();
+ }
+ })
+ .on("context_show.vakata.jstree", function (e, data) {
+ vakata_context.element.find("li:has(ul)").children("a").addClass("vakata-context-parent");
+ if(right_to_left) {
+ vakata_context.element.addClass("vakata-context-rtl").css("direction", "rtl");
+ }
+ // also apply a RTL class?
+ vakata_context.element.find("ul").hide().end();
+ });
+ });
+ }($));
+ // $.jstree.defaults.plugins.push("contextmenu");
+
+
+/**
+ * ### Drag'n'drop plugin
+ *
+ * Enables dragging and dropping of nodes in the tree, resulting in a move or copy operations.
+ */
+
+ /**
+ * stores all defaults for the drag'n'drop plugin
+ * @name $.jstree.defaults.dnd
+ * @plugin dnd
+ */
+ $.jstree.defaults.dnd = {
+ /**
+ * a boolean indicating if a copy should be possible while dragging (by pressint the meta key or Ctrl). Defaults to `true`.
+ * @name $.jstree.defaults.dnd.copy
+ * @plugin dnd
+ */
+ copy : true,
+ /**
+ * a number indicating how long a node should remain hovered while dragging to be opened. Defaults to `500`.
+ * @name $.jstree.defaults.dnd.open_timeout
+ * @plugin dnd
+ */
+ open_timeout : 500,
+ /**
+ * a function invoked each time a node is about to be dragged, invoked in the tree's scope and receives the nodes about to be dragged as an argument (array) and the event that started the drag - return `false` to prevent dragging
+ * @name $.jstree.defaults.dnd.is_draggable
+ * @plugin dnd
+ */
+ is_draggable : true,
+ /**
+ * a boolean indicating if checks should constantly be made while the user is dragging the node (as opposed to checking only on drop), default is `true`
+ * @name $.jstree.defaults.dnd.check_while_dragging
+ * @plugin dnd
+ */
+ check_while_dragging : true,
+ /**
+ * a boolean indicating if nodes from this tree should only be copied with dnd (as opposed to moved), default is `false`
+ * @name $.jstree.defaults.dnd.always_copy
+ * @plugin dnd
+ */
+ always_copy : false,
+ /**
+ * when dropping a node "inside", this setting indicates the position the node should go to - it can be an integer or a string: "first" (same as 0) or "last", default is `0`
+ * @name $.jstree.defaults.dnd.inside_pos
+ * @plugin dnd
+ */
+ inside_pos : 0,
+ /**
+ * when starting the drag on a node that is selected this setting controls if all selected nodes are dragged or only the single node, default is `true`, which means all selected nodes are dragged when the drag is started on a selected node
+ * @name $.jstree.defaults.dnd.drag_selection
+ * @plugin dnd
+ */
+ drag_selection : true,
+ /**
+ * controls whether dnd works on touch devices. If left as boolean true dnd will work the same as in desktop browsers, which in some cases may impair scrolling. If set to boolean false dnd will not work on touch devices. There is a special third option - string "selected" which means only selected nodes can be dragged on touch devices.
+ * @name $.jstree.defaults.dnd.touch
+ * @plugin dnd
+ */
+ touch : true,
+ /**
+ * controls whether items can be dropped anywhere on the node, not just on the anchor, by default only the node anchor is a valid drop target. Works best with the wholerow plugin. If enabled on mobile depending on the interface it might be hard for the user to cancel the drop, since the whole tree container will be a valid drop target.
+ * @name $.jstree.defaults.dnd.large_drop_target
+ * @plugin dnd
+ */
+ large_drop_target : false,
+ /**
+ * controls whether a drag can be initiated from any part of the node and not just the text/icon part, works best with the wholerow plugin. Keep in mind it can cause problems with tree scrolling on mobile depending on the interface - in that case set the touch option to "selected".
+ * @name $.jstree.defaults.dnd.large_drag_target
+ * @plugin dnd
+ */
+ large_drag_target : false,
+ /**
+ * controls whether use HTML5 dnd api instead of classical. That will allow better integration of dnd events with other HTML5 controls.
+ * @reference http://caniuse.com/#feat=dragndrop
+ * @name $.jstree.defaults.dnd.use_html5
+ * @plugin dnd
+ */
+ use_html5: false
+ };
+ var drg, elm;
+ // TODO: now check works by checking for each node individually, how about max_children, unique, etc?
+ $.jstree.plugins.dnd = function (options, parent) {
+ this.init = function (el, options) {
+ parent.init.call(this, el, options);
+ this.settings.dnd.use_html5 = this.settings.dnd.use_html5 && ('draggable' in document.createElement('span'));
+ };
+ this.bind = function () {
+ parent.bind.call(this);
+
+ this.element
+ .on(this.settings.dnd.use_html5 ? 'dragstart.jstree' : 'mousedown.jstree touchstart.jstree', this.settings.dnd.large_drag_target ? '.jstree-node' : '.jstree-anchor', $.proxy(function (e) {
+ if(this.settings.dnd.large_drag_target && $(e.target).closest('.jstree-node')[0] !== e.currentTarget) {
+ return true;
+ }
+ if(e.type === "touchstart" && (!this.settings.dnd.touch || (this.settings.dnd.touch === 'selected' && !$(e.currentTarget).closest('.jstree-node').children('.jstree-anchor').hasClass('jstree-clicked')))) {
+ return true;
+ }
+ var obj = this.get_node(e.target),
+ mlt = this.is_selected(obj) && this.settings.dnd.drag_selection ? this.get_top_selected().length : 1,
+ txt = (mlt > 1 ? mlt + ' ' + this.get_string('nodes') : this.get_text(e.currentTarget));
+ if(this.settings.core.force_text) {
+ txt = $.vakata.html.escape(txt);
+ }
+ if(obj && obj.id && obj.id !== $.jstree.root && (e.which === 1 || e.type === "touchstart" || e.type === "dragstart") &&
+ (this.settings.dnd.is_draggable === true || ($.isFunction(this.settings.dnd.is_draggable) && this.settings.dnd.is_draggable.call(this, (mlt > 1 ? this.get_top_selected(true) : [obj]), e)))
+ ) {
+ drg = { 'jstree' : true, 'origin' : this, 'obj' : this.get_node(obj,true), 'nodes' : mlt > 1 ? this.get_top_selected() : [obj.id] };
+ elm = e.currentTarget;
+ if (this.settings.dnd.use_html5) {
+ $.vakata.dnd._trigger('start', e, { 'helper': $(), 'element': elm, 'data': drg });
+ } else {
+ this.element.trigger('mousedown.jstree');
+ return $.vakata.dnd.start(e, drg, '