1250 lines
33 KiB
JavaScript
1250 lines
33 KiB
JavaScript
|
/* =========================================================
|
||
|
* 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
|
||
|
};
|
||
|
|
||
|
_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, {}, node));
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Tree.prototype.render = function () {
|
||
|
|
||
|
if (!this.initialized) {
|
||
|
|
||
|
// Setup first time only components
|
||
|
this.$element.addClass(pluginName);
|
||
|
this.$wrapper = $(this.template.list);
|
||
|
|
||
|
this.injectStyle();
|
||
|
|
||
|
this.initialized = true;
|
||
|
}
|
||
|
|
||
|
this.$element.empty().append(this.$wrapper.empty());
|
||
|
|
||
|
// Build tree
|
||
|
this.buildTree(this.tree, 0);
|
||
|
};
|
||
|
|
||
|
// Starting from the root node, and recursing down the
|
||
|
// structure we build the tree one node at a time
|
||
|
Tree.prototype.buildTree = function (nodes, level) {
|
||
|
|
||
|
if (!nodes) return;
|
||
|
level += 1;
|
||
|
|
||
|
var _this = this;
|
||
|
$.each(nodes, function addNodes(id, node) {
|
||
|
|
||
|
var treeItem = $(_this.template.item)
|
||
|
.addClass('node-' + _this.elementId)
|
||
|
.addClass(node.state.checked ? 'node-checked' : '')
|
||
|
.addClass(node.state.disabled ? 'node-disabled': '')
|
||
|
.addClass(node.state.selected ? 'node-selected' : '')
|
||
|
.addClass(node.searchResult ? 'search-result' : '')
|
||
|
.attr('data-nodeid', node.nodeId)
|
||
|
.attr('style', _this.buildStyleOverride(node));
|
||
|
|
||
|
// Add indent/spacer to mimic tree structure
|
||
|
for (var i = 0; i < (level - 1); i++) {
|
||
|
treeItem.append(_this.template.indent);
|
||
|
}
|
||
|
|
||
|
// Add expand, collapse or empty spacer icons
|
||
|
var classList = [];
|
||
|
if (node.nodes) {
|
||
|
classList.push('expand-icon');
|
||
|
if (node.state.expanded) {
|
||
|
classList.push(_this.options.collapseIcon);
|
||
|
}
|
||
|
else {
|
||
|
classList.push(_this.options.expandIcon);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
classList.push(_this.options.emptyIcon);
|
||
|
}
|
||
|
|
||
|
treeItem
|
||
|
.append($(_this.template.icon)
|
||
|
.addClass(classList.join(' '))
|
||
|
);
|
||
|
|
||
|
|
||
|
// Add node icon
|
||
|
if (_this.options.showIcon) {
|
||
|
|
||
|
var classList = ['node-icon'];
|
||
|
|
||
|
classList.push(node.icon || _this.options.nodeIcon);
|
||
|
if (node.state.selected) {
|
||
|
classList.pop();
|
||
|
classList.push(node.selectedIcon || _this.options.selectedIcon ||
|
||
|
node.icon || _this.options.nodeIcon);
|
||
|
}
|
||
|
|
||
|
treeItem
|
||
|
.append($(_this.template.icon)
|
||
|
.addClass(classList.join(' '))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Add check / unchecked icon
|
||
|
if (_this.options.showCheckbox) {
|
||
|
|
||
|
var classList = ['check-icon'];
|
||
|
if (node.state.checked) {
|
||
|
classList.push(_this.options.checkedIcon);
|
||
|
}
|
||
|
else {
|
||
|
classList.push(_this.options.uncheckedIcon);
|
||
|
}
|
||
|
|
||
|
treeItem
|
||
|
.append($(_this.template.icon)
|
||
|
.addClass(classList.join(' '))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Add text
|
||
|
if (_this.options.enableLinks) {
|
||
|
// Add hyperlink
|
||
|
treeItem
|
||
|
.append($(_this.template.link)
|
||
|
.attr('href', node.href)
|
||
|
.append(node.text)
|
||
|
);
|
||
|
}
|
||
|
else {
|
||
|
// otherwise just text
|
||
|
treeItem
|
||
|
.append(node.text);
|
||
|
}
|
||
|
|
||
|
// Add tags as badges
|
||
|
if (_this.options.showTags && node.tags) {
|
||
|
$.each(node.tags, function addTag(id, tag) {
|
||
|
treeItem
|
||
|
.append($(_this.template.badge)
|
||
|
.append(tag)
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Add item to the tree
|
||
|
_this.$wrapper.append(treeItem);
|
||
|
|
||
|
// Recursively add child ndoes
|
||
|
if (node.nodes && node.state.expanded && !node.state.disabled) {
|
||
|
return _this.buildTree(node.nodes, level);
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
// Define any node level style override for
|
||
|
// 1. selectedNode
|
||
|
// 2. node|data assigned color overrides
|
||
|
Tree.prototype.buildStyleOverride = function (node) {
|
||
|
|
||
|
if (node.state.disabled) return '';
|
||
|
|
||
|
var color = node.color;
|
||
|
var backColor = node.backColor;
|
||
|
|
||
|
if (this.options.highlightSelected && node.state.selected) {
|
||
|
if (this.options.selectedColor) {
|
||
|
color = this.options.selectedColor;
|
||
|
}
|
||
|
if (this.options.selectedBackColor) {
|
||
|
backColor = this.options.selectedBackColor;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) {
|
||
|
if (this.options.searchResultColor) {
|
||
|
color = this.options.searchResultColor;
|
||
|
}
|
||
|
if (this.options.searchResultBackColor) {
|
||
|
backColor = this.options.searchResultBackColor;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 'color:' + color +
|
||
|
';background-color:' + backColor + ';';
|
||
|
};
|
||
|
|
||
|
// Add inline style into head
|
||
|
Tree.prototype.injectStyle = function () {
|
||
|
|
||
|
if (this.options.injectStyle && !document.getElementById(this.styleId)) {
|
||
|
$('<style type="text/css" id="' + this.styleId + '"> ' + this.buildStyle() + ' </style>').appendTo('head');
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Construct trees style based on user options
|
||
|
Tree.prototype.buildStyle = function () {
|
||
|
|
||
|
var style = '.node-' + this.elementId + '{';
|
||
|
|
||
|
if (this.options.color) {
|
||
|
style += 'color:' + this.options.color + ';';
|
||
|
}
|
||
|
|
||
|
if (this.options.backColor) {
|
||
|
style += 'background-color:' + this.options.backColor + ';';
|
||
|
}
|
||
|
|
||
|
if (!this.options.showBorder) {
|
||
|
style += 'border:none;';
|
||
|
}
|
||
|
else if (this.options.borderColor) {
|
||
|
style += 'border:1px solid ' + this.options.borderColor + ';';
|
||
|
}
|
||
|
style += '}';
|
||
|
|
||
|
if (this.options.onhoverColor) {
|
||
|
style += '.node-' + this.elementId + ':not(.node-disabled):hover{' +
|
||
|
'background-color:' + this.options.onhoverColor + ';' +
|
||
|
'}';
|
||
|
}
|
||
|
|
||
|
return this.css + style;
|
||
|
};
|
||
|
|
||
|
Tree.prototype.template = {
|
||
|
list: '<ul class="list-group"></ul>',
|
||
|
item: '<li class="list-group-item"></li>',
|
||
|
indent: '<span class="indent"></span>',
|
||
|
icon: '<span class="icon"></span>',
|
||
|
link: '<a href="#" style="color:inherit;"></a>',
|
||
|
badge: '<span class="badge"></span>'
|
||
|
};
|
||
|
|
||
|
Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}'
|
||
|
|
||
|
|
||
|
/**
|
||
|
Returns a single node object that matches the given node id.
|
||
|
@param {Number} nodeId - A node's unique identifier
|
||
|
@return {Object} node - Matching node
|
||
|
*/
|
||
|
Tree.prototype.getNode = function (nodeId) {
|
||
|
return this.nodes[nodeId];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns the parent node of a given node, if valid otherwise returns undefined.
|
||
|
@param {Object|Number} identifier - A valid node or node id
|
||
|
@returns {Object} node - The parent node
|
||
|
*/
|
||
|
Tree.prototype.getParent = function (identifier) {
|
||
|
var node = this.identifyNode(identifier);
|
||
|
return this.nodes[node.parentId];
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of sibling nodes for a given node, if valid otherwise returns undefined.
|
||
|
@param {Object|Number} identifier - A valid node or node id
|
||
|
@returns {Array} nodes - Sibling nodes
|
||
|
*/
|
||
|
Tree.prototype.getSiblings = function (identifier) {
|
||
|
var node = this.identifyNode(identifier);
|
||
|
var parent = this.getParent(node);
|
||
|
var nodes = parent ? parent.nodes : this.tree;
|
||
|
return nodes.filter(function (obj) {
|
||
|
return obj.nodeId !== node.nodeId;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of selected nodes.
|
||
|
@returns {Array} nodes - Selected nodes
|
||
|
*/
|
||
|
Tree.prototype.getSelected = function () {
|
||
|
return this.findNodes('true', 'g', 'state.selected');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of unselected nodes.
|
||
|
@returns {Array} nodes - Unselected nodes
|
||
|
*/
|
||
|
Tree.prototype.getUnselected = function () {
|
||
|
return this.findNodes('false', 'g', 'state.selected');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of expanded nodes.
|
||
|
@returns {Array} nodes - Expanded nodes
|
||
|
*/
|
||
|
Tree.prototype.getExpanded = function () {
|
||
|
return this.findNodes('true', 'g', 'state.expanded');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of collapsed nodes.
|
||
|
@returns {Array} nodes - Collapsed nodes
|
||
|
*/
|
||
|
Tree.prototype.getCollapsed = function () {
|
||
|
return this.findNodes('false', 'g', 'state.expanded');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of checked nodes.
|
||
|
@returns {Array} nodes - Checked nodes
|
||
|
*/
|
||
|
Tree.prototype.getChecked = function () {
|
||
|
return this.findNodes('true', 'g', 'state.checked');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of unchecked nodes.
|
||
|
@returns {Array} nodes - Unchecked nodes
|
||
|
*/
|
||
|
Tree.prototype.getUnchecked = function () {
|
||
|
return this.findNodes('false', 'g', 'state.checked');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of disabled nodes.
|
||
|
@returns {Array} nodes - Disabled nodes
|
||
|
*/
|
||
|
Tree.prototype.getDisabled = function () {
|
||
|
return this.findNodes('true', 'g', 'state.disabled');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Returns an array of enabled nodes.
|
||
|
@returns {Array} nodes - Enabled nodes
|
||
|
*/
|
||
|
Tree.prototype.getEnabled = function () {
|
||
|
return this.findNodes('false', 'g', 'state.disabled');
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Set a node state to selected
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.selectNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setSelectedState(node, true, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Set a node state to unselected
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.unselectNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setSelectedState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Toggles a node selected state; selecting if unselected, unselecting if selected.
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.toggleNodeSelected = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.toggleSelectedState(node, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Collapse all tree nodes
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.collapseAll = function (options) {
|
||
|
var identifiers = this.findNodes('true', 'g', 'state.expanded');
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setExpandedState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Collapse a given tree node
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.collapseNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setExpandedState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Expand all tree nodes
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.expandAll = function (options) {
|
||
|
options = $.extend({}, _default.options, options);
|
||
|
|
||
|
if (options && options.levels) {
|
||
|
this.expandLevels(this.tree, options.levels, options);
|
||
|
}
|
||
|
else {
|
||
|
var identifiers = this.findNodes('false', 'g', 'state.expanded');
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setExpandedState(node, true, options);
|
||
|
}, this));
|
||
|
}
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Expand a given tree node
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.expandNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setExpandedState(node, true, options);
|
||
|
if (node.nodes && (options && options.levels)) {
|
||
|
this.expandLevels(node.nodes, options.levels-1, options);
|
||
|
}
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
Tree.prototype.expandLevels = function (nodes, level, options) {
|
||
|
options = $.extend({}, _default.options, options);
|
||
|
|
||
|
$.each(nodes, $.proxy(function (index, node) {
|
||
|
this.setExpandedState(node, (level > 0) ? true : false, options);
|
||
|
if (node.nodes) {
|
||
|
this.expandLevels(node.nodes, level-1, options);
|
||
|
}
|
||
|
}, this));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Reveals a given tree node, expanding the tree from node to root.
|
||
|
@param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.revealNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
var parentNode = this.getParent(node);
|
||
|
while (parentNode) {
|
||
|
this.setExpandedState(parentNode, true, options);
|
||
|
parentNode = this.getParent(parentNode);
|
||
|
};
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed.
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.toggleNodeExpanded = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.toggleExpandedState(node, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Check all tree nodes
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.checkAll = function (options) {
|
||
|
var identifiers = this.findNodes('false', 'g', 'state.checked');
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setCheckedState(node, true, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Check a given tree node
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.checkNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setCheckedState(node, true, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Uncheck all tree nodes
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.uncheckAll = function (options) {
|
||
|
var identifiers = this.findNodes('true', 'g', 'state.checked');
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setCheckedState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Uncheck a given tree node
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.uncheckNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setCheckedState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Toggles a nodes checked state; checking if unchecked, unchecking if checked.
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.toggleNodeChecked = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.toggleCheckedState(node, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Disable all tree nodes
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.disableAll = function (options) {
|
||
|
var identifiers = this.findNodes('false', 'g', 'state.disabled');
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setDisabledState(node, true, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Disable a given tree node
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.disableNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setDisabledState(node, true, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Enable all tree nodes
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.enableAll = function (options) {
|
||
|
var identifiers = this.findNodes('true', 'g', 'state.disabled');
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setDisabledState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Enable a given tree node
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.enableNode = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setDisabledState(node, false, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Toggles a nodes disabled state; disabling is enabled, enabling if disabled.
|
||
|
@param {Object|Number} identifiers - A valid node, node id or array of node identifiers
|
||
|
@param {optional Object} options
|
||
|
*/
|
||
|
Tree.prototype.toggleNodeDisabled = function (identifiers, options) {
|
||
|
this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
|
||
|
this.setDisabledState(node, !node.state.disabled, options);
|
||
|
}, this));
|
||
|
|
||
|
this.render();
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
Common code for processing multiple identifiers
|
||
|
*/
|
||
|
Tree.prototype.forEachIdentifier = function (identifiers, options, callback) {
|
||
|
|
||
|
options = $.extend({}, _default.options, options);
|
||
|
|
||
|
if (!(identifiers instanceof Array)) {
|
||
|
identifiers = [identifiers];
|
||
|
}
|
||
|
|
||
|
$.each(identifiers, $.proxy(function (index, identifier) {
|
||
|
callback(this.identifyNode(identifier), options);
|
||
|
}, this));
|
||
|
};
|
||
|
|
||
|
/*
|
||
|
Identifies a node from either a node id or object
|
||
|
*/
|
||
|
Tree.prototype.identifyNode = function (identifier) {
|
||
|
return ((typeof identifier) === 'number') ?
|
||
|
this.nodes[identifier] :
|
||
|
identifier;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Searches the tree for nodes (text) that match given criteria
|
||
|
@param {String} pattern - A given string to match against
|
||
|
@param {optional Object} options - Search criteria options
|
||
|
@return {Array} nodes - Matching nodes
|
||
|
*/
|
||
|
Tree.prototype.search = function (pattern, options) {
|
||
|
options = $.extend({}, _default.searchOptions, options);
|
||
|
|
||
|
this.clearSearch({ render: false });
|
||
|
|
||
|
var results = [];
|
||
|
if (pattern && pattern.length > 0) {
|
||
|
|
||
|
if (options.exactMatch) {
|
||
|
pattern = '^' + pattern + '$';
|
||
|
}
|
||
|
|
||
|
var modifier = 'g';
|
||
|
if (options.ignoreCase) {
|
||
|
modifier += 'i';
|
||
|
}
|
||
|
|
||
|
results = this.findNodes(pattern, modifier);
|
||
|
|
||
|
// Add searchResult property to all matching nodes
|
||
|
// This will be used to apply custom styles
|
||
|
// and when identifying result to be cleared
|
||
|
$.each(results, function (index, node) {
|
||
|
node.searchResult = true;
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// If revealResults, then render is triggered from revealNode
|
||
|
// otherwise we just call render.
|
||
|
if (options.revealResults) {
|
||
|
this.revealNode(results);
|
||
|
}
|
||
|
else {
|
||
|
this.render();
|
||
|
}
|
||
|
|
||
|
this.$element.trigger('searchComplete', $.extend(true, {}, results));
|
||
|
|
||
|
return results;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Clears previous search results
|
||
|
*/
|
||
|
Tree.prototype.clearSearch = function (options) {
|
||
|
|
||
|
options = $.extend({}, { render: true }, options);
|
||
|
|
||
|
var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) {
|
||
|
node.searchResult = false;
|
||
|
});
|
||
|
|
||
|
if (options.render) {
|
||
|
this.render();
|
||
|
}
|
||
|
|
||
|
this.$element.trigger('searchCleared', $.extend(true, {}, results));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Find nodes that match a given criteria
|
||
|
@param {String} pattern - A given string to match against
|
||
|
@param {optional String} modifier - Valid RegEx modifiers
|
||
|
@param {optional String} attribute - Attribute to compare pattern against
|
||
|
@return {Array} nodes - Nodes that match your criteria
|
||
|
*/
|
||
|
Tree.prototype.findNodes = function (pattern, modifier, attribute) {
|
||
|
|
||
|
modifier = modifier || 'g';
|
||
|
attribute = attribute || 'text';
|
||
|
|
||
|
var _this = this;
|
||
|
return $.grep(this.nodes, function (node) {
|
||
|
var val = _this.getNodeValue(node, attribute);
|
||
|
if (typeof val === 'string') {
|
||
|
return val.match(new RegExp(pattern, modifier));
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
Recursive find for retrieving nested attributes values
|
||
|
All values are return as strings, unless invalid
|
||
|
@param {Object} obj - Typically a node, could be any object
|
||
|
@param {String} attr - Identifies an object property using dot notation
|
||
|
@return {String} value - Matching attributes string representation
|
||
|
*/
|
||
|
Tree.prototype.getNodeValue = function (obj, attr) {
|
||
|
var index = attr.indexOf('.');
|
||
|
if (index > 0) {
|
||
|
var _obj = obj[attr.substring(0, index)];
|
||
|
var _attr = attr.substring(index + 1, attr.length);
|
||
|
return this.getNodeValue(_obj, _attr);
|
||
|
}
|
||
|
else {
|
||
|
if (obj.hasOwnProperty(attr)) {
|
||
|
return obj[attr].toString();
|
||
|
}
|
||
|
else {
|
||
|
return undefined;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
var logError = function (message) {
|
||
|
if (window.console) {
|
||
|
window.console.error(message);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Prevent against multiple instantiations,
|
||
|
// handle updates and method calls
|
||
|
$.fn[pluginName] = function (options, args) {
|
||
|
|
||
|
var result;
|
||
|
|
||
|
this.each(function () {
|
||
|
var _this = $.data(this, pluginName);
|
||
|
if (typeof options === 'string') {
|
||
|
if (!_this) {
|
||
|
logError('Not initialized, can not call method : ' + options);
|
||
|
}
|
||
|
else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') {
|
||
|
logError('No such method : ' + options);
|
||
|
}
|
||
|
else {
|
||
|
if (!(args instanceof Array)) {
|
||
|
args = [ args ];
|
||
|
}
|
||
|
result = _this[options].apply(_this, args);
|
||
|
}
|
||
|
}
|
||
|
else if (typeof options === 'boolean') {
|
||
|
result = _this;
|
||
|
}
|
||
|
else {
|
||
|
$.data(this, pluginName, new Tree(this, $.extend(true, {}, options)));
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return result || this;
|
||
|
};
|
||
|
|
||
|
})(jQuery, window, document);
|