/* ========================================================= * bootstrap-treeview.js v2.0.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, backColor: undefined, borderColor: undefined, onhoverColor: '#F5F5F5', selectedColor: '#FFFFFF', selectedBackColor: '#428bca', searchResultColor: '#D9534F', searchResultBackColor: undefined, highlightSelected: true, highlightSearchResults: true, showBorder: true, showIcon: true, showCheckbox: false, showTags: false, multiSelect: false, preventUnselect: false, // Event handlers onLoading: undefined, onLoadingFailed: undefined, onInitialized: undefined, onNodeRendered: undefined, onRendered: undefined, onDestroyed: undefined, 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 }; _default.dataUrl = { method: 'GET', dataType: 'json', cache: false }; 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), // Query methods findNodes: $.proxy(this.findNodes, this), getNodes: $.proxy(this.getNodes, this), // todo document + test getParents: $.proxy(this.getParents, 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), // Tree manipulation methods addNode: $.proxy(this.addNode, this), addNodeAfter: $.proxy(this.addNodeAfter, this), addNodeBefore: $.proxy(this.addNodeBefore, this), removeNode: $.proxy(this.removeNode, this), updateNode: $.proxy(this.updateNode, this), rootNode: $.proxy(this.rootNode, 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), // Check / uncheck 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._initialized = false; this._options = $.extend({}, _default.settings, options); this._destroy(); this._subscribeEvents(); this._triggerEvent('loading', null, _default.options); this._load(options) .then($.proxy(function (data) { // load done return this._tree = $.extend(true, [], data); }, this), $.proxy(function (error) { // load fail this._triggerEvent('loadingFailed', error, _default.options); }, this)) .then($.proxy(function (treeData) { // initialize data return this._setInitialStates({ nodes: treeData }, 0); }, this)) .then($.proxy(function () { // render to DOM this._render(); }, this)); }; Tree.prototype._load = function (options) { var done = new $.Deferred(); if (options.data) { this._loadLocalData(options, done); } else if (options.dataUrl) { this._loadRemoteData(options, done); } return done.promise(); }; Tree.prototype._loadRemoteData = function (options, done) { $.ajax($.extend(true, {}, _default.dataUrl, options.dataUrl)) .done(function (data) { done.resolve(data); }) .fail(function (xhr, status, error) { done.reject(error); }); }; Tree.prototype._loadLocalData = function (options, done) { done.resolve((typeof options.data === 'string') ? $.parseJSON(options.data) : $.extend(true, [], options.data)); }; Tree.prototype._remove = function () { this._destroy(); $.removeData(this, pluginName); $('#' + this._styleId).remove(); }; Tree.prototype._destroy = function () { if (!this._initialized) return; this._initialized = false; this._triggerEvent('destroyed', null, _default.options); // Switch off events this._unsubscribeEvents(); // Tear down this.$wrapper.remove(); this.$wrapper = null; }; Tree.prototype._unsubscribeEvents = function () { this.$element.off('loading'); this.$element.off('loadingFailed'); this.$element.off('initialized'); this.$element.off('nodeRendered'); this.$element.off('rendered'); this.$element.off('destroyed'); 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(); if (typeof (this._options.onLoading) === 'function') { this.$element.on('loading', this._options.onLoading); } if (typeof (this._options.onLoadingFailed) === 'function') { this.$element.on('loadingFailed', this._options.onLoadingFailed); } if (typeof (this._options.onInitialized) === 'function') { this.$element.on('initialized', this._options.onInitialized); } if (typeof (this._options.onNodeRendered) === 'function') { this.$element.on('nodeRendered', this._options.onNodeRendered); } if (typeof (this._options.onRendered) === 'function') { this.$element.on('rendered', this._options.onRendered); } if (typeof (this._options.onDestroyed) === 'function') { this.$element.on('destroyed', this._options.onDestroyed); } 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); } }; Tree.prototype._triggerEvent = function (event, data, options) { if (options && !options.silent) { this.$element.trigger(event, $.extend(true, {}, data)); } } /* 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 ordered structure */ Tree.prototype._setInitialStates = function (node, level) { this._nodes = {}; return $.when.apply(this, this._setInitialState(node, level)) .done($.proxy(function () { this._orderedNodes = this._sortNodes(); this._triggerEvent('initialized', this._orderedNodes, _default.options); return; }, this)); }; Tree.prototype._setInitialState = function (node, level, done) { if (!node.nodes) return; level += 1; done = done || []; var parent = node; $.each(node.nodes, $.proxy(function (index, node) { var deferred = new $.Deferred(); done.push(deferred.promise()); // level : hierarchical tree level, starts at 1 node.level = level; // index : relative to siblings node.index = index; // nodeId : unique, hierarchical identifier node.nodeId = (parent && parent.nodeId) ? parent.nodeId + '.' + node.index : (level - 1) + '.' + node.index; // 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; } // set visible state; based parent state plus levels if ((parent && parent.state && parent.state.expanded) || (level <= this._options.levels)) { node.state.visible = true; } else { node.state.visible = false; } // recurse child nodes and transverse the tree, depth-first if (node.nodes) { if (node.nodes.length > 0) { this._setInitialState(node, level, done); } else { delete node.nodes; } } // add / update indexed collection this._nodes[node.nodeId] = node; // mark task as complete deferred.resolve(); }, this)); return done; }; Tree.prototype._sortNodes = function () { return $.map(Object.keys(this._nodes).sort(), $.proxy(function (value, index) { return this._nodes[value]; }, this)); }; Tree.prototype._clickHandler = function (event) { var target = $(event.target); var node = this.targetNode(target); if (!node || node.state.disabled) return; var classList = target.attr('class') ? target.attr('class').split(' ') : []; if ((classList.indexOf('expand-icon') !== -1)) { this._toggleExpanded(node, $.extend({}, _default.options)); } else if ((classList.indexOf('check-icon') !== -1)) { this._toggleChecked(node, $.extend({}, _default.options)); } else { if (node.selectable) { this._toggleSelected(node, $.extend({}, _default.options)); } else { this._toggleExpanded(node, $.extend({}, _default.options)); } } }; // 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.targetNode = 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._toggleExpanded = function (node, options) { if (!node) return; this._setExpanded(node, !node.state.expanded, options); }; Tree.prototype._setExpanded = function (node, state, options) { // We never pass options when rendering, so the only time // we need to validate state is from user interaction if (options && state === node.state.expanded) return; if (state && node.nodes) { // Set node state node.state.expanded = true; // Set element if (node.$el) { node.$el.children('span.expand-icon') .removeClass(this._options.expandIcon) .addClass(this._options.collapseIcon); } // Expand children if (node.nodes && options) { $.each(node.nodes, $.proxy(function (index, node) { this._setVisible(node, true, options); }, this)); } // Optionally trigger event this._triggerEvent('nodeExpanded', node, options); } else if (!state) { // Set node state node.state.expanded = false; // Set element if (node.$el) { node.$el.children('span.expand-icon') .removeClass(this._options.collapseIcon) .addClass(this._options.expandIcon); } // Collapse children if (node.nodes && options) { $.each(node.nodes, $.proxy(function (index, node) { this._setVisible(node, false, options); this._setExpanded(node, false, options); }, this)); } // Optionally trigger event this._triggerEvent('nodeCollapsed', node, options); } }; Tree.prototype._setVisible = function (node, state, options) { if (options && state === node.state.visible) return; if (state) { // Set node state node.state.visible = true; // Set element if (node.$el) { node.$el.removeClass('node-hidden'); } } else { // Set node state to unchecked node.state.visible = false; // Set element if (node.$el) { node.$el.addClass('node-hidden'); } } }; Tree.prototype._toggleSelected = function (node, options) { if (!node) return; this._setSelected(node, !node.state.selected, options); return this; }; Tree.prototype._setSelected = function (node, state, options) { // We never pass options when rendering, so the only time // we need to validate state is from user interaction if (options && (state === node.state.selected)) return; if (state) { // If multiSelect false, unselect previously selected if (!this._options.multiSelect) { $.each(this._findNodes('true', 'state.selected'), $.proxy(function (index, node) { this._setSelected(node, false, $.extend(options, {unselecting: true})); }, this)); } // Set node state node.state.selected = true; // Set element if (node.$el) { node.$el.addClass('node-selected'); if (node.selectedIcon || this._options.selectedIcon) { node.$el.children('span.node-icon') .removeClass(node.icon || this._options.nodeIcon) .addClass(node.selectedIcon || this._options.selectedIcon); } } // Optionally trigger event this._triggerEvent('nodeSelected', node, options); } else { // If preventUnselect true + only one remaining selection, disable unselect if (this._options.preventUnselect && (options && !options.unselecting) && (this._findNodes('true', 'state.selected').length === 1)) { return this; } // Set node state node.state.selected = false; // Set element if (node.$el) { node.$el.removeClass('node-selected'); if (node.selectedIcon || this._options.selectedIcon) { node.$el.children('span.node-icon') .removeClass(node.selectedIcon || this._options.selectedIcon) .addClass(node.icon || this._options.nodeIcon); } } // Optionally trigger event this._triggerEvent('nodeUnselected', node, options); } return this; }; Tree.prototype._toggleChecked = function (node, options) { if (!node) return; this._setChecked(node, !node.state.checked, options); }; Tree.prototype._setChecked = function (node, state, options) { // We never pass options when rendering, so the only time // we need to validate state is from user interaction if (options && state === node.state.checked) return; if (state) { // Set node state node.state.checked = true; // Set element if (node.$el) { node.$el.addClass('node-checked'); node.$el.children('span.check-icon') .removeClass(this._options.uncheckedIcon) .addClass(this._options.checkedIcon); } // Optionally trigger event this._triggerEvent('nodeChecked', node, options); } else { // Set node state to unchecked node.state.checked = false; // Set element if (node.$el) { node.$el.removeClass('node-checked'); node.$el.children('span.check-icon') .removeClass(this._options.checkedIcon) .addClass(this._options.uncheckedIcon); } // Optionally trigger event this._triggerEvent('nodeUnchecked', node, options); } }; Tree.prototype._setDisabled = function (node, state, options) { // We never pass options when rendering, so the only time // we need to validate state is from user interaction if (options && state === node.state.disabled) return; if (state) { // Set node state to disabled node.state.disabled = true; // Disable all other states this._setSelected(node, false, options); this._setChecked(node, false, options); this._setExpanded(node, false, options); // Set element if (node.$el) { node.$el.addClass('node-disabled'); } // Optionally trigger event this._triggerEvent('nodeDisabled', node, options); } else { // Set node state to enabled node.state.disabled = false; // Set element if (node.$el) { node.$el.removeClass('node-disabled'); } // Optionally trigger event this._triggerEvent('nodeEnabled', node, options); } }; Tree.prototype._setSearchResult = function (node, state, options) { if (options && state === node.searchResult) return; if (state) { node.searchResult = true; if (node.$el) { node.$el.addClass('node-result'); node.$el.removeClass('node-not-match'); } } else { node.searchResult = false; if (node.$el) { node.$el.removeClass('node-result'); node.$el.addClass('node-not-match'); } } }; Tree.prototype._render = function () { if (!this._initialized) { // Setup first time only components this.$wrapper = $(this._template.tree); this.$element.empty() .addClass(pluginName) .append(this.$wrapper); this._injectStyle(); this._initialized = true; } var previousNode; $.each(this._orderedNodes, $.proxy(function (id, node) { this._renderNode(node, previousNode); previousNode = node; }, this)); this._triggerEvent('rendered', this._orderedNodes, _default.options); }; Tree.prototype._renderNode = function (node, previousNode) { if (!node) return; if (!node.$el) { node.$el = this._newNodeEl(node, previousNode) .addClass('node-' + this._elementId); } else { node.$el.empty(); } // Set / update nodeid; it can change as a result of addNode etc. node.$el.attr('data-nodeId', node.nodeId); // Add indent/spacer to mimic tree structure for (var i = 0; i < (node.level - 1); i++) { node.$el.append(this._template.indent); } // Add expand / collapse or empty spacer icons node.$el .append($(this._template.icon) .addClass(node.nodes ? 'expand-icon' : this._options.emptyIcon) ); // Add node icon if (this._options.showIcon) { node.$el .append($(this._template.icon) .addClass('node-icon') .addClass(node.icon || this._options.nodeIcon) ); } // Add checkable icon if (this._options.showCheckbox) { node.$el .append($(this._template.icon) .addClass('check-icon') ); } // Add text node.$el.append(node.text); // Add tags as badges if (this._options.showTags && node.tags) { $.each(node.tags, $.proxy(function addTag(id, tag) { node.$el .append($(this._template.badge) .append(tag) ); }, this)); } // Set various node states this._setSelected(node, node.state.selected); this._setChecked(node, node.state.checked); this._setSearchResult(node, node.searchResult); this._setExpanded(node, node.state.expanded); this._setDisabled(node, node.state.disabled); this._setVisible(node, node.state.visible); // Trigger nodeRendered event this._triggerEvent('nodeRendered', node, _default.options); }; // Creates a new node element from template and // ensures the template is inserted at the correct position Tree.prototype._newNodeEl = function (node, previousNode) { var $el = $(this._template.node); if (previousNode) { // typical usage, as nodes are rendered in // sort order we add after the previous element this.$wrapper.children() .eq(previousNode.$el.index()) .after($el); } else { // we use prepend instead of append, // to cater for root inserts i.e. nodeId 0.0 this.$wrapper.prepend($el); } return $el; }; // Recursively remove node elements from DOM Tree.prototype._removeNodeEl = function (node) { if (!node) return; if (node.nodes) { $.each(node.nodes, $.proxy(function (index, node) { this._removeNodeEl(node); }, this)); } node.$el.remove(); }; // Expand node, rendering it's immediate children Tree.prototype._expandNode = function (node) { if (!node.nodes) return; $.each(node.nodes.slice(0).reverse(), $.proxy(function (index, childNode) { childNode.level = node.level + 1; this._renderNode(childNode, node.$el); }, this)); }; // 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 + '{'; // Basic bootstrap style overrides 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 + ';' + '}'; } // Style search results if (this._options.highlightSearchResults && (this._options.searchResultColor || this._options.searchResultBackColor)) { var innerStyle = '' if (this._options.searchResultColor) { innerStyle += 'color:' + this._options.searchResultColor + ';'; } if (this._options.searchResultBackColor) { innerStyle += 'background-color:' + this._options.searchResultBackColor + ';'; } style += '.node-' + this._elementId + '.node-result{' + innerStyle + '}'; style += '.node-' + this._elementId + '.node-result:hover{' + innerStyle + '}'; } // Style selected nodes if (this._options.highlightSelected && (this._options.selectedColor || this._options.selectedBackColor)) { var innerStyle = '' if (this._options.selectedColor) { innerStyle += 'color:' + this._options.selectedColor + ';'; } if (this._options.selectedBackColor) { innerStyle += 'background-color:' + this._options.selectedBackColor + ';'; } style += '.node-' + this._elementId + '.node-selected{' + innerStyle + '}'; style += '.node-' + this._elementId + '.node-selected:hover{' + innerStyle + '}'; } // Node level style overrides $.each(this._orderedNodes, $.proxy(function (index, node) { if (node.color || node.backColor) { var innerStyle = ''; if (node.color) { innerStyle += 'color:' + node.color + ';'; } if (node.backColor) { innerStyle += 'background-color:' + node.backColor + ';'; } style += '.node-' + this._elementId + '[data-nodeId="' + node.nodeId + '"]{' + innerStyle + '}'; } }, this)); return this._css + style; }; Tree.prototype._template = { tree: '', node: '
  • ', indent: '', icon: '', badge: '' }; 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 an array of matching node objects. @param {String} pattern - A pattern to match against a given field @return {String} field - Field to query pattern against */ Tree.prototype.findNodes = function (pattern, field) { return this._findNodes(pattern, field); }; /** Returns an ordered aarray of node objects. @return {Array} nodes - An array of all nodes */ Tree.prototype.getNodes = function () { return this._orderedNodes; }; /** Returns parent nodes for given nodes, if valid otherwise returns undefined. @param {Array} nodes - An array of nodes @returns {Array} nodes - An array of parent nodes */ Tree.prototype.getParents = function (nodes) { if (!(nodes instanceof Array)) { nodes = [nodes]; } var parentNodes = []; $.each(nodes, $.proxy(function (index, node) { var parentNode = node.parentId ? this._nodes[node.parentId] : false; if (parentNode) { parentNodes.push(parentNode); } }, this)); return parentNodes; }; /** Returns an array of sibling nodes for given nodes, if valid otherwise returns undefined. @param {Array} nodes - An array of nodes @returns {Array} nodes - An array of sibling nodes */ Tree.prototype.getSiblings = function (nodes) { if (!(nodes instanceof Array)) { nodes = [nodes]; } var siblingNodes = []; $.each(nodes, $.proxy(function (index, node) { var parent = this.getParents([node]); var nodes = parent[0] ? parent[0].nodes : this._tree; siblingNodes = nodes.filter(function (obj) { return obj.nodeId !== node.nodeId; }); }, this)); // flatten possible nested array before returning return $.map(siblingNodes, function (obj) { return obj; }); }; /** Returns an array of selected nodes. @returns {Array} nodes - Selected nodes */ Tree.prototype.getSelected = function () { return this._findNodes('true', 'state.selected'); }; /** Returns an array of unselected nodes. @returns {Array} nodes - Unselected nodes */ Tree.prototype.getUnselected = function () { return this._findNodes('false', 'state.selected'); }; /** Returns an array of expanded nodes. @returns {Array} nodes - Expanded nodes */ Tree.prototype.getExpanded = function () { return this._findNodes('true', 'state.expanded'); }; /** Returns an array of collapsed nodes. @returns {Array} nodes - Collapsed nodes */ Tree.prototype.getCollapsed = function () { return this._findNodes('false', 'state.expanded'); }; /** Returns an array of checked nodes. @returns {Array} nodes - Checked nodes */ Tree.prototype.getChecked = function () { return this._findNodes('true', 'state.checked'); }; /** Returns an array of unchecked nodes. @returns {Array} nodes - Unchecked nodes */ Tree.prototype.getUnchecked = function () { return this._findNodes('false', 'state.checked'); }; /** Returns an array of disabled nodes. @returns {Array} nodes - Disabled nodes */ Tree.prototype.getDisabled = function () { return this._findNodes('true', 'state.disabled'); }; /** Returns an array of enabled nodes. @returns {Array} nodes - Enabled nodes */ Tree.prototype.getEnabled = function () { return this._findNodes('false', 'state.disabled'); }; /** Add nodes to the tree. @param {Array} nodes - An array of nodes to add @param {optional Object} parentNode - The node to which nodes will be added as children @param {optional number} index - Zero based insert index @param {optional Object} options */ Tree.prototype.addNode = function (nodes, parentNode, index, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } if (parentNode instanceof Array) { parentNode = parentNode[0]; } options = $.extend({}, _default.options, options); // identify target nodes; either the tree's root or a parent's child nodes var targetNodes; if (parentNode && parentNode.nodes) { targetNodes = parentNode.nodes; } else if (parentNode) { targetNodes = parentNode.nodes = []; } else { targetNodes = this._tree; } // inserting nodes at specified positions $.each(nodes, $.proxy(function (i, node) { var insertIndex = (typeof(index) === 'number') ? (index + i) : (targetNodes.length + 1); targetNodes.splice(insertIndex, 0, node); }, this)); // initialize new state and render changes this._setInitialStates({nodes: this._tree}, 0) .done($.proxy(function () { if (parentNode && !parentNode.state.expanded) { this._setExpanded(parentNode, true, options); } this._render(); }, this)); } /** Add nodes to the tree after given node. @param {Array} nodes - An array of nodes to add @param {Object} node - The node to which nodes will be added after @param {optional Object} options */ Tree.prototype.addNodeAfter = function (nodes, node, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } if (node instanceof Array) { node = node[0]; } options = $.extend({}, _default.options, options); this.addNode(nodes, this.getParents(node)[0], (node.index + 1), options); } /** Add nodes to the tree before given node. @param {Array} nodes - An array of nodes to add @param {Object} node - The node to which nodes will be added before @param {optional Object} options */ Tree.prototype.addNodeBefore = function (nodes, node, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } if (node instanceof Array) { node = node[0]; } options = $.extend({}, _default.options, options); this.addNode(nodes, this.getParents(node)[0], node.index, options); } /** Removes given nodes from the tree. @param {Array} nodes - An array of nodes to remove @param {optional Object} options */ Tree.prototype.removeNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); var targetNodes, parentNode; $.each(nodes, $.proxy(function (index, node) { if(!node) return false; // remove nodes from tree parentNode = this._nodes[node.parentId]; if (parentNode) { targetNodes = parentNode.nodes; } else { targetNodes = this._tree; } targetNodes.splice(node.index, 1); // remove node from DOM this._removeNodeEl(node); }, this)); // initialize new state and render changes this._setInitialStates({nodes: this._tree}, 0) .done(this._render.bind(this)); }; Tree.prototype.rootNode = function () { return this._tree; }; /** Updates / replaces a given tree node @param {Object} node - A single node to be replaced @param {Object} newNode - THe replacement node @param {optional Object} options */ Tree.prototype.updateNode = function (node, newNode, options) { if (node instanceof Array) { node = node[0]; } options = $.extend({}, _default.options, options); // insert new node var targetNodes; var parentNode = this._nodes[node.parentId]; if (parentNode) { targetNodes = parentNode.nodes; } else { targetNodes = this._tree; } targetNodes.splice(node.index, 1, newNode); // remove old node from DOM this._removeNodeEl(node); // initialize new state and render changes this._setInitialStates({nodes: this._tree}, 0) .done(this._render.bind(this)); }; /** Selects given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.selectNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setSelected(node, true, options); }, this)); }; /** Unselects given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.unselectNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setSelected(node, false, options); }, this)); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.toggleNodeSelected = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._toggleSelected(node, options); }, this)); }; /** Collapse all tree nodes @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { options = $.extend({}, _default.options, options); options.levels = options.levels || 999; this.collapseNode(this._tree, options); }; /** Collapse a given tree node @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.collapseNode = function (nodes, options) { options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setExpanded(node, false, options); }, this)); }; /** Expand all tree nodes @param {optional Object} options */ Tree.prototype.expandAll = function (options) { options = $.extend({}, _default.options, options); options.levels = options.levels || 999; this.expandNode(this._tree, options); }; /** Expand given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.expandNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setExpanded(node, true, options); if (node.nodes) { this._expandLevels(node.nodes, options.levels-1, options); } }, this)); }; Tree.prototype._expandLevels = function (nodes, level, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setExpanded(node, (level > 0) ? true : false, options); if (node.nodes) { this._expandLevels(node.nodes, level-1, options); } }, this)); }; /** Reveals given tree nodes, expanding the tree from node to root. @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.revealNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { var parentNode = node; var tmpNode; while (tmpNode = this.getParents([parentNode])[0]) { parentNode = tmpNode; this._setExpanded(parentNode, true, options); }; }, this)); }; /** Toggles a node's expanded state; collapsing if expanded, expanding if collapsed. @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.toggleNodeExpanded = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._toggleExpanded(node, options); }, this)); }; /** Check all tree nodes @param {optional Object} options */ Tree.prototype.checkAll = function (options) { options = $.extend({}, _default.options, options); var nodes = this._findNodes('false', 'state.checked'); $.each(nodes, $.proxy(function (index, node) { this._setChecked(node, true, options); }, this)); }; /** Checks given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.checkNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setChecked(node, true, options); }, this)); }; /** Uncheck all tree nodes @param {optional Object} options */ Tree.prototype.uncheckAll = function (options) { options = $.extend({}, _default.options, options); var nodes = this._findNodes('true', 'state.checked'); $.each(nodes, $.proxy(function (index, node) { this._setChecked(node, false, options); }, this)); }; /** Uncheck given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.uncheckNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setChecked(node, false, options); }, this)); }; /** Toggles a node's checked state; checking if unchecked, unchecking if checked. @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.toggleNodeChecked = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._toggleChecked(node, options); }, this)); }; /** Disable all tree nodes @param {optional Object} options */ Tree.prototype.disableAll = function (options) { options = $.extend({}, _default.options, options); var nodes = this._findNodes('false', 'state.disabled'); $.each(nodes, $.proxy(function (index, node) { this._setDisabled(node, true, options); }, this)); }; /** Disable given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.disableNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setDisabled(node, true, options); }, this)); }; /** Enable all tree nodes @param {optional Object} options */ Tree.prototype.enableAll = function (options) { options = $.extend({}, _default.options, options); var nodes = this._findNodes('true', 'state.disabled'); $.each(nodes, $.proxy(function (index, node) { this._setDisabled(node, false, options); }, this)); }; /** Enable given tree nodes @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.enableNode = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setDisabled(node, false, options); }, this)); }; /** Toggles a node's disabled state; disabling is enabled, enabling if disabled. @param {Array} nodes - An array of nodes @param {optional Object} options */ Tree.prototype.toggleNodeDisabled = function (nodes, options) { if (!(nodes instanceof Array)) { nodes = [nodes]; } options = $.extend({}, _default.options, options); $.each(nodes, $.proxy(function (index, node) { this._setDisabled(node, !node.state.disabled, options); }, this)); }; /** 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); var previous = this._getSearchResults(); var results = []; if (pattern && pattern.length > 0) { if (options.exactMatch) { pattern = '^' + pattern + '$'; } var modifier = 'g'; if (options.ignoreCase) { modifier += 'i'; } results = this._findNodes(pattern, 'text', modifier); } // Clear previous results no longer matched $.each(this._diffArray(results, previous), $.proxy(function (index, node) { this._setSearchResult(node, false, options); }, this)); // Set new results $.each(this._diffArray(previous, results), $.proxy(function (index, node) { this._setSearchResult(node, true, options); }, this)); // Reveal hidden nodes if (results && options.revealResults) { this.revealNode(results); } // 상위 노드들도 검색 대상에 포함 201-12-27 $.each(results, $.proxy(function (index, node) { var parentNode = node; var tmpNode; while (tmpNode = this.getParents([parentNode])[0]) { parentNode = tmpNode; this._setSearchResult(parentNode, true, options); }; }, this)); this._triggerEvent('searchComplete', results, options); return results; }; /** Clears previous search results */ Tree.prototype.clearSearch = function (options) { options = $.extend({}, { render: true }, options); var results = $.each(this._getSearchResults(), $.proxy(function (index, node) { this._setSearchResult(node, false, options); }, this)); this._triggerEvent('searchCleared', results, options); }; Tree.prototype._getSearchResults = function () { return this._findNodes('true', 'searchResult'); }; Tree.prototype._diffArray = function (a, b) { var diff = []; $.grep(b, function (n) { if ($.inArray(n, a) === -1) { diff.push(n); } }); return diff; }; /** Find nodes that match a given criteria @param {String} pattern - A given string to match against @param {optional String} attribute - Attribute to compare pattern against @param {optional String} modifier - Valid RegEx modifiers @return {Array} nodes - Nodes that match your criteria */ Tree.prototype._findNodes = function (pattern, attribute, modifier) { attribute = attribute || 'text'; modifier = modifier || 'g'; return $.grep(this._orderedNodes, $.proxy(function (node) { var val = this._getNodeValue(node, attribute); if (typeof val === 'string') { return val.match(new RegExp(pattern, modifier)); } }, this)); }; /** 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);