diff --git a/Gruntfile.js b/Gruntfile.js index a495f9555..47c021065 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -42,7 +42,10 @@ module.exports = function(grunt) { { expand: true, cwd: 'src/css', src: '*', dest: 'public/css/' }, { expand: true, cwd: 'src/js', src: '*', dest: 'public/js/' }, { expand: true, cwd: 'node_modules/bootstrap/dist/', src: '**/*', dest: 'public/libs/bootstrap/' }, - { expand: true, cwd: 'node_modules/jquery/dist/', src: '*', dest: 'public/libs/jquery' } + { expand: true, cwd: 'node_modules/jquery/dist/', src: '*', dest: 'public/libs/jquery' }, + // setup unminified + { expand: true, cwd: 'src/css', src: '*', dest: 'dist/' }, + { expand: true, cwd: 'src/js', src: '*', dest: 'dist/' } ] } } diff --git a/bower.json b/bower.json index 1539026ed..6c12302b9 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "patternfly-bootstrap-treeview", "description": "Tree View for Twitter Bootstrap", - "version": "2.1.2", + "version": "2.1.3", "homepage": "https://github.com/patternfly/patternfly-bootstrap-treeview", "main": "dist/bootstrap-treeview.js", "keywords": [ diff --git a/dist/bootstrap-treeview.css b/dist/bootstrap-treeview.css new file mode 100644 index 000000000..35a92849e --- /dev/null +++ b/dist/bootstrap-treeview.css @@ -0,0 +1,52 @@ +/* ========================================================= + * patternfly-bootstrap-treeview.css v2.1.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. + * ========================================================= */ + +.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; +} + +.treeview .node-hidden { + display: none; +} + +.treeview span.image { + display: inline-block; + width: 12px; + height: 1.19em; + vertical-align: middle; + background-size: contain; + background-repeat: no-repeat; + margin-right: 5px; + line-height: 1em; +} diff --git a/dist/bootstrap-treeview.js b/dist/bootstrap-treeview.js new file mode 100644 index 000000000..b18b68471 --- /dev/null +++ b/dist/bootstrap-treeview.js @@ -0,0 +1,1940 @@ +/* ========================================================= + * patternfly-bootstrap-treeview.js v2.1.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', + loadingIcon: 'glyphicon glyphicon-hourglass', + emptyIcon: 'glyphicon', + nodeIcon: '', + selectedIcon: '', + checkedIcon: 'glyphicon glyphicon-check', + partiallyCheckedIcon: 'glyphicon glyphicon-expand', + uncheckedIcon: 'glyphicon glyphicon-unchecked', + tagsClass: 'badge', + + color: undefined, + backColor: undefined, + borderColor: undefined, + changedNodeColor: '#39A5DC', + onhoverColor: '#F5F5F5', + selectedColor: '#FFFFFF', + selectedBackColor: '#428bca', + searchResultColor: '#D9534F', + searchResultBackColor: undefined, + + highlightSelected: true, + highlightSearchResults: true, + showBorder: true, + showIcon: true, + showImage: false, + showCheckbox: false, + checkboxFirst: false, + highlightChanges: false, + showTags: false, + multiSelect: false, + preventUnselect: false, + allowReselect: false, + hierarchicalCheck: false, + propagateCheckEvent: false, + wrapNodeText: 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), + + // 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), + unmarkCheckboxChanges: $.proxy(this.unmarkCheckboxChanges, 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); + + // Cache empty icon DOM template + this._template.icon.empty.addClass(this._options.emptyIcon); + + 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') ? + JSON.parse(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._inheritCheckboxChanges(); + 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; + } + + // if not provided set checkable default value + if (!node.hasOwnProperty('checkable')) { + node.checkable = 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; + } + + // convert the undefined string if hierarchical checks are enabled + if (this._options.hierarchicalCheck && node.state.checked === 'undefined') { + node.state.checked = undefined; + } + + // 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(function (a, b) { + if (a === b) return 0; + var a = a.split('.').map(function (level) { return parseInt(level); }); + var b = b.split('.').map(function (level) { return parseInt(level); }); + + var c = Math.max(a.length, b.length); + for (var i=0; i 0) return +1; + if (a[i] - b[i] < 0) return -1; + }; + + }), $.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)) { + if (node.checkable) { + 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; + + // Lazy-load the child nodes if possible + if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { + this._lazyLoad(node); + } else { + this._setExpanded(node, !node.state.expanded, options); + } + }; + + Tree.prototype._lazyLoad = function (node) { + // Show a different icon while loading the child nodes + node.$el.children('span.expand-icon') + .removeClass(this._options.expandIcon) + .addClass(this._options.loadingIcon); + + var _this = this; + this._options.lazyLoad(node, function (nodes) { + // Adding the node will expand its parent automatically + _this.addNode(nodes, node); + }); + // Only the first expand should do a lazy-load + delete node.lazyLoad; + }; + + 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) + .removeClass(this._options.loadingIcon) + .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)) { + // Fire the nodeSelected event if reselection is allowed + if (this._options.allowReselect) { + this._triggerEvent('nodeSelected', node, options); + } + 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._inheritCheckboxChanges = function () { + if (this._options.showCheckbox && this._options.highlightChanges) { + this._checkedNodes = $.grep(this._orderedNodes, function (node) { + return node.state.checked; + }); + } + }; + + Tree.prototype._toggleChecked = function (node, options) { + if (!node) return; + + if (this._options.hierarchicalCheck) { + // Event propagation to the parent/child nodes + var childOptions = $.extend({}, options, {silent: options.silent || !this._options.propagateCheckEvent}); + + var state, currentNode = node; + // Temporarily swap the tree state + node.state.checked = !node.state.checked; + + // Iterate through each parent node + while (currentNode = this._nodes[currentNode.parentId]) { + + // Calculate the state + state = currentNode.nodes.reduce(function (acc, curr) { + return (acc === curr.state.checked) ? acc : undefined; + }, currentNode.nodes[0].state.checked); + + // Set the state + this._setChecked(currentNode, state, childOptions); + } + + if (node.nodes && node.nodes.length > 0) { + // Copy the content of the array + var child, children = node.nodes.slice(); + // Iterate through each child node + while (children && children.length > 0) { + child = children.pop(); + + // Set the state + this._setChecked(child, node.state.checked, childOptions); + + // Append children to the end of the list + if (child.nodes && child.nodes.length > 0) { + children = children.concat(child.nodes.slice()); + } + } + } + // Swap back the tree state + node.state.checked = !node.state.checked; + } + + 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; + + // Highlight the node if its checkbox has unsaved changes + if (this._options.highlightChanges) { + node.$el.toggleClass('node-check-changed', (this._checkedNodes.indexOf(node) == -1) == state); + } + + if (state) { + + // Set node state + node.state.checked = true; + + // Set element + if (node.$el) { + node.$el.addClass('node-checked').removeClass('node-checked-partial'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .removeClass(this._options.partiallyCheckedIcon) + .addClass(this._options.checkedIcon); + } + + // Optionally trigger event + this._triggerEvent('nodeChecked', node, options); + } + else if (state === undefined && this._options.hierarchicalCheck) { + + // Set node state to partially checked + node.state.checked = undefined; + + // Set element + if (node.$el) { + node.$el.addClass('node-checked-partial').removeClass('node-checked'); + node.$el.children('span.check-icon') + .removeClass(this._options.uncheckedIcon) + .removeClass(this._options.checkedIcon) + .addClass(this._options.partiallyCheckedIcon); + } + + // Optionally trigger event, partially checked is technically unchecked + this._triggerEvent('nodeUnchecked', node, options); + } else { + + // Set node state to unchecked + node.state.checked = false; + + // Set element + if (node.$el) { + node.$el.removeClass('node-checked node-checked-partial'); + node.$el.children('span.check-icon') + .removeClass(this._options.checkedIcon) + .removeClass(this._options.partiallyCheckedIcon) + .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 + if (options && !options.keepState) { + 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'); + } + } + else { + + node.searchResult = false; + + if (node.$el) { + node.$el.removeClass('node-result'); + } + } + }; + + Tree.prototype._render = function () { + if (!this._initialized) { + + // Setup first time only components + this.$wrapper = this._template.tree.clone(); + 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(); + } + + // Append .classes to the node + node.$el.addClass(node.class); + + // Set the #id of the node if specified + if (node.id) { + node.$el.attr('id', node.id); + } + + // Append custom data- attributes to the node + if (node.dataAttr) { + $.each(node.dataAttr, function (key, value) { + node.$el.attr('data-' + key, value); + }); + } + + // Set / update nodeid; it can change as a result of addNode etc. + node.$el.attr('data-nodeId', node.nodeId); + + // Set the tooltip attribute if present + if (node.tooltip) { + node.$el.attr('title', node.tooltip); + } + + // Add indent/spacer to mimic tree structure + for (var i = 0; i < (node.level - 1); i++) { + node.$el.append(this._template.indent.clone()); + } + + // Add expand / collapse or empty spacer icons + node.$el + .append( + node.nodes || node.lazyLoad ? this._template.icon.expand.clone() : this._template.icon.empty.clone() + ); + + // Add checkbox and node icons + if (this._options.checkboxFirst) { + this._addCheckbox(node); + this._addIcon(node); + this._addImage(node); + } else { + this._addIcon(node); + this._addImage(node); + this._addCheckbox(node); + } + + // Add text + if (this._options.wrapNodeText) { + var wrapper = this._template.text.clone(); + node.$el.append(wrapper); + wrapper.append(node.text); + } else { + 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.clone() + .addClass( + typeof node.tagsClass[id] === 'string' ? + node.tagsClass[id] : + this._options.tagsClass + ) + .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); + }; + + // Add checkable icon + Tree.prototype._addCheckbox = function (node) { + if (this._options.showCheckbox && (node.hideCheckbox === undefined || node.hideCheckbox === false)) { + node.$el + .append(this._template.icon.check.clone()); + } + } + + // Add node icon + Tree.prototype._addIcon = function (node) { + if (this._options.showIcon && !(this._options.showImage && node.image)) { + node.$el + .append(this._template.icon.node.clone() + .addClass(node.icon || this._options.nodeIcon) + ); + } + } + + Tree.prototype._addImage = function (node) { + if (this._options.showImage && node.image) { + node.$el + .append(this._template.image.clone() + .addClass('node-image') + .css('background-image', "url('" + node.image + "')") + ); + } + } + + // 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.clone(); + + if (previousNode) { + // typical usage, as nodes are rendered in + // sort order we add after the previous element + previousNode.$el.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 + '}'; + } + + // Style changed nodes + if (this._options.highlightChanges) { + var innerStyle = 'color: ' + this._options.changedNodeColor + ';'; + style += '.node-' + this._elementId + '.node-check-changed{' + 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: { + node: $(''), + expand: $(''), + check: $(''), + empty: $('') + }, + image: $(''), + badge: $(''), + text: $('') + }; + + 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) { + + // 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)); + }; + + /** + 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) { + // Do not re-expand already expanded nodes + if (node.state.expanded) return; + + if (typeof(this._options.lazyLoad) === 'function' && node.lazyLoad) { + this._lazyLoad(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 = $.grep(this._orderedNodes, function (node) { + return !node.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 = $.grep(this._orderedNodes, function (node) { + return node.state.checked || node.state.checked === undefined; + }); + $.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)); + }; + + /** + Saves the current state of checkboxes as default, cleaning up any highlighted changes + */ + Tree.prototype.unmarkCheckboxChanges = function () { + this._inheritCheckboxChanges(); + + $.each(this._nodes, function (index, node) { + node.$el.removeClass('node-check-changed'); + }); + }; + + /** + 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); + } + + 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) && obj[attr] !== undefined) { + 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; + if (this.length == 0) { + throw "No element has been found!"; + } + + 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); diff --git a/package.json b/package.json index 8b8121ce7..19a143246 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "patternfly-bootstrap-treeview", "description": "Tree View for Twitter Bootstrap", - "version": "2.1.1", + "version": "2.1.3", "homepage": "https://github.com/patternfly/patternfly-bootstrap-treeview", "author": { "name": "Red Hat" @@ -14,7 +14,7 @@ "url": "https://github.com/patternfly/patternfly-bootstrap-treeview/issues" }, "license": "Apache-2.0", - "main": "src/bootstrap-treeview.js", + "main": "dist/bootstrap-treeview.js", "scripts": { "start": "node app", "test": "grunt test"