/* ========================================================= * 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: 'fa fa-plus', collapseIcon: 'fa fa-minus', emptyIcon: 'glyphicon', nodeIcon: '', selectedIcon: '', checkedIcon: 'fa fa-check', uncheckedIcon: 'fa fa-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)) { $('').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: '