/* =========================================================
* bootstrap-treeview.js v1.2.0
* =========================================================
* Copyright 2013 Jonathan Miles
* Project URL : http://www.jondmiles.com/bootstrap-treeview
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
; (function ($, window, document, undefined) {
/*global jQuery, console*/
'use strict';
var pluginName = 'treeview';
var _default = {};
_default.settings = {
injectStyle: true,
levels: 2,
expandIcon: 'glyphicon glyphicon-plus',
collapseIcon: 'glyphicon glyphicon-minus',
emptyIcon: 'glyphicon',
nodeIcon: '',
selectedIcon: '',
checkedIcon: 'glyphicon glyphicon-check',
uncheckedIcon: 'glyphicon glyphicon-unchecked',
color: undefined, // '#000000',
backColor: undefined, // '#FFFFFF',
borderColor: undefined, // '#dddddd',
onhoverColor: '#F5F5F5',
selectedColor: '#FFFFFF',
selectedBackColor: '#428bca',
searchResultColor: '#D9534F',
searchResultBackColor: undefined, //'#FFFFFF',
enableLinks: false,
highlightSelected: true,
highlightSearchResults: true,
showBorder: true,
showIcon: true,
showCheckbox: false,
showTags: false,
multiSelect: false,
// Event handlers
onNodeChecked: undefined,
onNodeCollapsed: undefined,
onNodeDisabled: undefined,
onNodeEnabled: undefined,
onNodeExpanded: undefined,
onNodeSelected: undefined,
onNodeUnchecked: undefined,
onNodeUnselected: undefined,
onSearchComplete: undefined,
onSearchCleared: undefined,
onDragStart: undefined
};
_default.options = {
silent: false,
ignoreChildren: false
};
_default.searchOptions = {
ignoreCase: true,
exactMatch: false,
revealResults: true
};
var Tree = function (element, options) {
this.$element = $(element);
this.elementId = element.id;
this.styleId = this.elementId + '-style';
this.init(options);
return {
// Options (public access)
options: this.options,
// Initialize / destroy methods
init: $.proxy(this.init, this),
remove: $.proxy(this.remove, this),
// Get methods
getNode: $.proxy(this.getNode, this),
getParent: $.proxy(this.getParent, this),
getSiblings: $.proxy(this.getSiblings, this),
getSelected: $.proxy(this.getSelected, this),
getUnselected: $.proxy(this.getUnselected, this),
getExpanded: $.proxy(this.getExpanded, this),
getCollapsed: $.proxy(this.getCollapsed, this),
getChecked: $.proxy(this.getChecked, this),
getUnchecked: $.proxy(this.getUnchecked, this),
getDisabled: $.proxy(this.getDisabled, this),
getEnabled: $.proxy(this.getEnabled, this),
// Select methods
selectNode: $.proxy(this.selectNode, this),
unselectNode: $.proxy(this.unselectNode, this),
toggleNodeSelected: $.proxy(this.toggleNodeSelected, this),
// Expand / collapse methods
collapseAll: $.proxy(this.collapseAll, this),
collapseNode: $.proxy(this.collapseNode, this),
expandAll: $.proxy(this.expandAll, this),
expandNode: $.proxy(this.expandNode, this),
toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this),
revealNode: $.proxy(this.revealNode, this),
// Expand / collapse methods
checkAll: $.proxy(this.checkAll, this),
checkNode: $.proxy(this.checkNode, this),
uncheckAll: $.proxy(this.uncheckAll, this),
uncheckNode: $.proxy(this.uncheckNode, this),
toggleNodeChecked: $.proxy(this.toggleNodeChecked, this),
// Disable / enable methods
disableAll: $.proxy(this.disableAll, this),
disableNode: $.proxy(this.disableNode, this),
enableAll: $.proxy(this.enableAll, this),
enableNode: $.proxy(this.enableNode, this),
toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this),
// Search methods
search: $.proxy(this.search, this),
clearSearch: $.proxy(this.clearSearch, this)
};
};
Tree.prototype.init = function (options) {
this.tree = [];
this.nodes = [];
if (options.data) {
if (typeof options.data === 'string') {
options.data = $.parseJSON(options.data);
}
this.tree = $.extend(true, [], options.data);
delete options.data;
}
this.options = $.extend({}, _default.settings, options);
this.destroy();
this.subscribeEvents();
this.setInitialStates({ nodes: this.tree }, 0);
this.render();
};
Tree.prototype.remove = function () {
this.destroy();
$.removeData(this, pluginName);
$('#' + this.styleId).remove();
};
Tree.prototype.destroy = function () {
if (!this.initialized) return;
this.$wrapper.remove();
this.$wrapper = null;
// Switch off events
this.unsubscribeEvents();
// Reset this.initialized flag
this.initialized = false;
};
Tree.prototype.unsubscribeEvents = function () {
this.$element.off('click');
this.$element.off('nodeChecked');
this.$element.off('nodeCollapsed');
this.$element.off('nodeDisabled');
this.$element.off('nodeEnabled');
this.$element.off('nodeExpanded');
this.$element.off('nodeSelected');
this.$element.off('nodeUnchecked');
this.$element.off('nodeUnselected');
this.$element.off('searchComplete');
this.$element.off('searchCleared');
};
Tree.prototype.subscribeEvents = function () {
this.unsubscribeEvents();
this.$element.on('click', $.proxy(this.clickHandler, this));
if (typeof (this.options.onNodeChecked) === 'function') {
this.$element.on('nodeChecked', this.options.onNodeChecked);
}
if (typeof (this.options.onNodeCollapsed) === 'function') {
this.$element.on('nodeCollapsed', this.options.onNodeCollapsed);
}
if (typeof (this.options.onNodeDisabled) === 'function') {
this.$element.on('nodeDisabled', this.options.onNodeDisabled);
}
if (typeof (this.options.onNodeEnabled) === 'function') {
this.$element.on('nodeEnabled', this.options.onNodeEnabled);
}
if (typeof (this.options.onNodeExpanded) === 'function') {
this.$element.on('nodeExpanded', this.options.onNodeExpanded);
}
if (typeof (this.options.onNodeSelected) === 'function') {
this.$element.on('nodeSelected', this.options.onNodeSelected);
}
if (typeof (this.options.onNodeUnchecked) === 'function') {
this.$element.on('nodeUnchecked', this.options.onNodeUnchecked);
}
if (typeof (this.options.onNodeUnselected) === 'function') {
this.$element.on('nodeUnselected', this.options.onNodeUnselected);
}
if (typeof (this.options.onSearchComplete) === 'function') {
this.$element.on('searchComplete', this.options.onSearchComplete);
}
if (typeof (this.options.onSearchCleared) === 'function') {
this.$element.on('searchCleared', this.options.onSearchCleared);
}
};
/*
Recurse the tree structure and ensure all nodes have
valid initial states. User defined states will be preserved.
For performance we also take this opportunity to
index nodes in a flattened structure
*/
Tree.prototype.setInitialStates = function (node, level) {
if (!node.nodes) return;
level += 1;
var parent = node;
var _this = this;
$.each(node.nodes, function checkStates(index, node) {
// nodeId : unique, incremental identifier
node.nodeId = _this.nodes.length;
// parentId : transversing up the tree
node.parentId = parent.nodeId;
// if not provided set selectable default value
if (!node.hasOwnProperty('selectable')) {
node.selectable = true;
}
// where provided we should preserve states
node.state = node.state || {};
// set checked state; unless set always false
if (!node.state.hasOwnProperty('checked')) {
node.state.checked = false;
}
// set enabled state; unless set always false
if (!node.state.hasOwnProperty('disabled')) {
node.state.disabled = false;
}
// set expanded state; if not provided based on levels
if (!node.state.hasOwnProperty('expanded')) {
if (!node.state.disabled &&
(level < _this.options.levels) &&
(node.nodes && node.nodes.length > 0)) {
node.state.expanded = true;
}
else {
node.state.expanded = false;
}
}
// set selected state; unless set always false
if (!node.state.hasOwnProperty('selected')) {
node.state.selected = false;
}
// index nodes in a flattened structure for use later
_this.nodes.push(node);
// recurse child nodes and transverse the tree
if (node.nodes) {
_this.setInitialStates(node, level);
}
});
};
Tree.prototype.clickHandler = function (event) {
if (!this.options.enableLinks) event.preventDefault();
var target = $(event.target);
var node = this.findNode(target);
if (!node || node.state.disabled) return;
var classList = target.attr('class') ? target.attr('class').split(' ') : [];
if ((classList.indexOf('expand-icon') !== -1)) {
this.toggleExpandedState(node, _default.options);
this.render();
}
else if ((classList.indexOf('check-icon') !== -1)) {
this.toggleCheckedState(node, _default.options);
this.render();
}
else {
if (node.selectable) {
this.toggleSelectedState(node, _default.options);
} else {
this.toggleExpandedState(node, _default.options);
}
this.render();
}
};
// Looks up the DOM for the closest parent list item to retrieve the
// data attribute nodeid, which is used to lookup the node in the flattened structure.
Tree.prototype.findNode = function (target) {
var nodeId = target.closest('li.list-group-item').attr('data-nodeid');
var node = this.nodes[nodeId];
if (!node) {
console.log('Error: node does not exist');
}
return node;
};
Tree.prototype.toggleExpandedState = function (node, options) {
if (!node) return;
this.setExpandedState(node, !node.state.expanded, options);
};
Tree.prototype.setExpandedState = function (node, state, options) {
if (state === node.state.expanded) return;
if (state && node.nodes) {
// Expand a node
node.state.expanded = true;
if (!options.silent) {
this.$element.trigger('nodeExpanded', $.extend(true, {}, node));
}
}
else if (!state) {
// Collapse a node
node.state.expanded = false;
if (!options.silent) {
this.$element.trigger('nodeCollapsed', $.extend(true, {}, node));
}
// Collapse child nodes
if (node.nodes && !options.ignoreChildren) {
$.each(node.nodes, $.proxy(function (index, node) {
this.setExpandedState(node, false, options);
}, this));
}
}
};
Tree.prototype.toggleSelectedState = function (node, options) {
if (!node) return;
this.setSelectedState(node, !node.state.selected, options);
};
Tree.prototype.setSelectedState = function (node, state, options) {
if (state === node.state.selected) return;
if (state) {
// If multiSelect false, unselect previously selected
if (!this.options.multiSelect) {
$.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) {
this.setSelectedState(node, false, options);
}, this));
}
// Continue selecting node
node.state.selected = true;
if (!options.silent) {
this.$element.trigger('nodeSelected', $.extend(true, {}, node));
}
}
else {
// Unselect node
node.state.selected = false;
if (!options.silent) {
this.$element.trigger('nodeUnselected', $.extend(true, {}, node));
}
}
};
Tree.prototype.toggleCheckedState = function (node, options) {
if (!node) return;
this.setCheckedState(node, !node.state.checked, options);
};
Tree.prototype.setCheckedState = function (node, state, options) {
if (state === node.state.checked) return;
if (state) {
// Check node
node.state.checked = true;
if (!options.silent) {
this.$element.trigger('nodeChecked', $.extend(true, {}, node));
}
}
else {
// Uncheck node
node.state.checked = false;
if (!options.silent) {
this.$element.trigger('nodeUnchecked', $.extend(true, {}, node));
}
}
};
Tree.prototype.setDisabledState = function (node, state, options) {
if (state === node.state.disabled) return;
if (state) {
// Disable node
node.state.disabled = true;
// Disable all other states
this.setExpandedState(node, false, options);
this.setSelectedState(node, false, options);
this.setCheckedState(node, false, options);
if (!options.silent) {
this.$element.trigger('nodeDisabled', $.extend(true, {}, node));
}
}
else {
// Enabled node
node.state.disabled = false;
if (!options.silent) {
this.$element.trigger('nodeEnabled', $.extend(true