diff --git a/CHANGELOG.md b/CHANGELOG.md index 8caad5b3a..a90b082c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,82 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -## v1.1.0 - 29th March, 2015 +## v1.2.0 - 9th May, 2015 ### New Features -- Node states `expanded` and `selected` to set a node's initial state -- Methods `getNode`, `getParent` and `getSiblings` methods -- Methods `selectNode`, `unselectNode` and `toggleNodeSelected` methods -- Events `nodeUnselected` -- Option `multiSelect` -- Methods `expandAll`, `collapseAll`, `expandNode`, `collapseNode` and `toggleNodeExpanded` methods -- Events `nodeExpanded` and `nodeCollapsed` events -- Methods `search` and `clearSearch` methods, -- Events `searchComplete` and `searchCleared` events -- Options `highlightSearchResults`, `searchResultColor` and `searchResultBackColor` + +- Disable nodes, allow a tree node to disabled (not selectable, expandable or checkable) + + - Added node state property `disabled` to set a node initial state + + - Methods `disableAll`, `disableNode`, `enableAll`, `enableNode` and `toggleNodeDisabled` added to control state programmatically + + - Events `nodeDisabled` and `nodeEnabled` + +- Checkable nodes, allows a tree node to be checked or unchecked. + + - Added node state property `checked` to set a node initial state + + - Pass option `{showCheckbox: true}` to initialize tree view with checkboxes + + - Use options `checkedIcon` and `uncheckedIcon` to configure checkbox icons + + - Methods `checkAll`, `checkNode`, `uncheckAll`, `uncheckNode` and `toggleNodeChecked` to control state programmatically + + - Events `nodeChecked` and `nodeUnchecked` + +- New option + node property `selectedIcon` to support displaying different icons when a node is selected. + +- New search option `{ revealResults : true | false }` which when set to true will automatically expand the tree view to reveal matching nodes + +- New method `revealNode` which expands the tree view to reveal a given node + +- New methods to retrieve nodes by state : `getSelected`, `getUnselected`, `getExpanded`, `getCollapsed`, `getChecked`, `getUnchecked`, `getDisabled` and `getEnabled` + + +### Changes +- Removed nodeIcon by default, by popular demand. Use `{nodeIcon: 'glyphicon glyphicon-stop'}` in initial options to add a node icon. + +- Search behaviour, by default search will the expand tree view and reveal results. Alternatively pass `{revealResults:false}` + +- Method collapseNode accepts new option `{ ignoreChildren: true | false }`. The default is false, passing true will leave child nodes uncollapsed + + +### Bug Fixes +- Remove unnecessary render in clearSearch when called from search + +- Child nodes should collapse by default on collapseNode + +- Incorrect expand collapse icon displayed when nodes array is empty + + + + +## v1.1.0 - 29th March, 2015 + +### New Features + +- Added node state properties `expanded` and `selected` so a node's intial state can be set + +- New get methods `getNode`, `getParent` and `getSiblings` for retrieving nodes and their immediate relations + +- New select methods `selectNode`, `unselectNode` and `toggleNodeSelected` + +- Adding `nodeUnselected` event + +- New global option `multiSelect` which allows multiple nodes to hold the selected state, default is false + +- New expand collapse methods `expandAll`, `collapseAll`, `expandNode`, `collapseNode` and `toggleNodeExpanded` + +- Adding events `nodeExpanded` and `nodeCollapsed` + +- New methods `search` and `clearSearch` which allow you to query the tree view for nodes based on a `text` value + +- Adding events `searchComplete` and `searchCleared` + +- New global options `highlightSearchResults`, `searchResultColor` and `searchResultBackColor` for configuring how search results are displayed + + ## v1.0.2 - 6th February, 2015 @@ -24,4 +87,5 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Bug Fixes - Events not unbound when re-initialised + - CSS selectors too general, affecting other page elements diff --git a/README.md b/README.md index c72ffa93f..c0a724235 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ or using npm: $ npm install bootstrap-treeview ``` -or [download](https://github.com/jonmiles/bootstrap-treeview/releases/tag/v1.1.0) manually. +or [download](https://github.com/jonmiles/bootstrap-treeview/releases/tag/v1.2.0) manually. @@ -125,13 +125,16 @@ If you want to do more, here's the full node specification { text: "Node 1", icon: "glyphicon glyphicon-stop", + selectedIcon: "glyphicon glyphicon-stop", color: "#000000", backColor: "#FFFFFF", href: "#node-1", selectable: true, state: { - expanded: true, - selected: true + checked: true, + disabled: true, + expanded: true, + selected: true }, tags: ['available'], nodes: [ @@ -159,6 +162,11 @@ For simplicity we directly leverage [Bootstraps Glyphicons support](http://getbo By providing the base class you retain full control over the icons used. If you want to use your own then just add your class to this icon field. +#### selectedIcon +`String` `Optional` + +The icon displayed on a given node when selected, typically to the left of the text. + #### color `String` `Optional` @@ -181,9 +189,18 @@ Whether or not a node is selectable in the tree. False indicates the node should #### state `Object` `Optional` - Describes a node's initial state. +#### state.checked +`Boolean` `Default: false` + +Whether or not a node is checked, represented by a checkbox style glyphicon. + +#### state.disabled +`Boolean` `Default: false` + +Whether or not a node is disabled (not selectable, expandable or checkable). + #### state.expanded `Boolean` `Default: false` @@ -240,6 +257,11 @@ String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal Sets the border color for the component; set showBorder to false if you don't want a visible border. +#### checkedIcon +String, class names(s). Default: "glyphicon glyphicon-check" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) + +Sets the icon to be as a checked checkbox, used in conjunction with showCheckbox. + #### collapseIcon String, class name(s). Default: "glyphicon glyphicon-minus" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) @@ -295,6 +317,11 @@ String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal Sets the default background color activated when the users cursor hovers over a node. +#### selectedIcon +String, class name(s). Default: "glyphicon glyphicon-stop" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) + +Sets the default icon to be used on all selected nodes, except when overridden on a per node basis in data. + #### searchResultBackColor String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: undefined, inherits. @@ -320,11 +347,25 @@ Boolean. Default: true Whether or not to display a border around nodes. +#### showCheckbox +Boolean. Default: false + +Whether or not to display checkboxes on nodes. + +#### showIcon +Boolean. Default: true + +Whether or not to display a nodes icon. + #### showTags Boolean. Default: false Whether or not to display tags to the right of each node. The values of which must be provided in the data structure on a per node basis. +#### uncheckedIcon +String, class names(s). Default: "glyphicon glyphicon-unchecked" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) + +Sets the icon to be as an unchecked checkbox, used in conjunction with showCheckbox. ## Methods @@ -362,6 +403,26 @@ $('#tree').data('treeview') The following is a list of all available methods. +#### checkAll(options) + +Checks all tree nodes + +```javascript +$('#tree').treeview('checkAll', { silent: true }); +``` + +Triggers `nodeChecked` event; pass silent to suppress events. + +#### checkNode(node | nodeId, options) + +Checks a given tree node, accepts node or nodeId. + +```javascript +$('#tree').treeview('checkNode', [ nodeId, { silent: true } ]); +``` + +Triggers `nodeChecked` event; pass silent to suppress events. + #### clearSearch() Clear the tree view of any previous search results e.g. remove their highlighted state. @@ -384,14 +445,54 @@ Triggers `nodeCollapsed` event; pass silent to suppress events. #### collapseNode(node | nodeId, options) -Collapse a given tree node, accepts node or nodeId +Collapse a given tree node and it's child nodes. If you don't want to collapse the child nodes, pass option `{ ignoreChildren: true }`. ```javascript -$('#tree').treeview('collapseNode', [ nodeId, { silent: true } ]); +$('#tree').treeview('collapseNode', [ nodeId, { silent: true, ignoreChildren: false } ]); ``` Triggers `nodeCollapsed` event; pass silent to suppress events. +#### disableAll(options) + +Disable all tree nodes + +```javascript +$('#tree').treeview('disableAll', { silent: true }); +``` + +Triggers `nodeDisabled` event; pass silent to suppress events. + +#### disableNode(node | nodeId, options) + +Disable a given tree node, accepts node or nodeId. + +```javascript +$('#tree').treeview('disableNode', [ nodeId, { silent: true } ]); +``` + +Triggers `nodeDisabled` event; pass silent to suppress events. + +#### enableAll(options) + +Enable all tree nodes + +```javascript +$('#tree').treeview('enableAll', { silent: true }); +``` + +Triggers `nodeEnabled` event; pass silent to suppress events. + +#### enableNode(node | nodeId, options) + +Enable a given tree node, accepts node or nodeId. + +```javascript +$('#tree').treeview('enableNode', [ nodeId, { silent: true } ]); +``` + +Triggers `nodeEnabled` event; pass silent to suppress events. + #### expandAll(options) Expand all tree nodes. Optionally can be expanded to any given number of levels. @@ -412,6 +513,38 @@ $('#tree').treeview('expandNode', [ nodeId, { levels: 2, silent: true } ]); Triggers `nodeExpanded` event; pass silent to suppress events. +#### getCollapsed() + +Returns an array of collapsed nodes e.g. state.expanded = false. + +```javascript +$('#tree').treeview('getCollapsed', nodeId); +``` + +#### getDisabled() + +Returns an array of disabled nodes e.g. state.disabled = true. + +```javascript +$('#tree').treeview('getDisabled', nodeId); +``` + +#### getEnabled() + +Returns an array of enabled nodes e.g. state.disabled = false. + +```javascript +$('#tree').treeview('getEnabled', nodeId); +``` + +#### getExpanded() + +Returns an array of expanded nodes e.g. state.expanded = true. + +```javascript +$('#tree').treeview('getExpanded', nodeId); +``` + #### getNode(nodeId) Returns a single node object that matches the given node id. @@ -428,6 +561,14 @@ Returns the parent node of a given node, if valid otherwise returns undefined. $('#tree').treeview('getParent', node); ``` +#### getSelected() + +Returns an array of selected nodes e.g. state.selected = true. + +```javascript +$('#tree').treeview('getSelected', nodeId); +``` + #### getSiblings(node | nodeId) Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. @@ -436,6 +577,14 @@ Returns an array of sibling nodes for a given node, if valid otherwise returns u $('#tree').treeview('getSiblings', node); ``` +#### getUnselected() + +Returns an array of unselected nodes e.g. state.selected = false. + +```javascript +$('#tree').treeview('getUnselected', nodeId); +``` + #### remove() Removes the tree view component. Removing attached events, internal attached objects, and added HTML elements. @@ -444,6 +593,16 @@ Removes the tree view component. Removing attached events, internal attached obj $('#tree').treeview('remove'); ``` +#### revealNode(node | nodeId, options) + +Reveals a given tree node, expanding the tree from node to root. + +```javascript +$('#tree').treeview('revealNode', [ nodeId, { silent: true } ]); +``` + +Triggers `nodeExpanded` event; pass silent to suppress events. + #### search(pattern, options) Searches the tree view for nodes that match a given string, highlighting them in the tree. @@ -452,8 +611,9 @@ Returns an array of matching nodes. ```javascript $('#tree').treeview('search', [ 'Parent', { - ignoreCase: true, - exactMatch: false + ignoreCase: true, // case insensitive + exactMatch: false, // like or equals + revealResults: true, // reveal matching nodes }]); ``` @@ -469,6 +629,26 @@ $('#tree').treeview('selectNode', [ nodeId, { silent: true } ]); Triggers `nodeSelected` event; pass silent to suppress events. +#### toggleNodeChecked(node | nodeId, options) + +Toggles a nodes checked state; checking if unchecked, unchecking if checked. + +```javascript +$('#tree').treeview('toggleNodeChecked', [ nodeId, { silent: true } ]); +``` + +Triggers either `nodeChecked` or `nodeUnchecked` event; pass silent to suppress events. + +#### toggleNodeDisabled(node | nodeId, options) + +Toggles a nodes disabled state; disabling if enabled, enabling if disabled. + +```javascript +$('#tree').treeview('toggleNodeDisabled', [ nodeId, { silent: true } ]); +``` + +Triggers either `nodeDisabled` or `nodeEnabled` event; pass silent to suppress events. + #### toggleNodeExpanded(node | nodeId, options) Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. @@ -489,6 +669,26 @@ $('#tree').treeview('toggleNodeSelected', [ nodeId, { silent: true } ]); Triggers either `nodeSelected` or `nodeUnselected` event; pass silent to suppress events. +#### uncheckAll(options) + +Uncheck all tree nodes. + +```javascript +$('#tree').treeview('uncheckAll', { silent: true }); +``` + +Triggers `nodeUnchecked` event; pass silent to suppress events. + +#### uncheckNode(node | nodeId, options) + +Uncheck a given tree node, accepts node or nodeId. + +```javascript +$('#tree').treeview('uncheckNode', [ nodeId, { silent: true } ]); +``` + +Triggers `nodeUnchecked` event; pass silent to suppress events. + #### unselectNode(node | nodeId, options) Unselects a given tree node, accepts node or nodeId. @@ -527,12 +727,20 @@ $('#tree').on('nodeSelected', function(event, data) { ### List of Events +`nodeChecked (event, node)` - A node is checked. + `nodeCollapsed (event, node)` - A node is collapsed. +`nodeDisabled (event, node)` - A node is disabled. + +`nodeEnabled (event, node)` - A node is enabled. + `nodeExpanded (event, node)` - A node is expanded. `nodeSelected (event, node)` - A node is selected. +`nodeUnchecked (event, node)` - A node is unchecked. + `nodeUnselected (event, node)` - A node is unselected. `searchComplete (event, results)` - After a search completes diff --git a/bower.json b/bower.json index c18fae82e..1b009d7da 100644 --- a/bower.json +++ b/bower.json @@ -1,7 +1,7 @@ { "name": "bootstrap-treeview", "description": "Tree View for Twitter Bootstrap", - "version": "1.1.0", + "version": "1.2.0", "homepage": "https://github.com/jonmiles/bootstrap-treeview", "main": [ "dist/bootstrap-treeview.min.js", diff --git a/dist/bootstrap-treeview.min.css b/dist/bootstrap-treeview.min.css index a1205eac2..57a348a87 100644 --- a/dist/bootstrap-treeview.min.css +++ b/dist/bootstrap-treeview.min.css @@ -1 +1 @@ -.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.expand-collapse{width:1rem;height:1rem}.treeview span.icon{margin-left:10px;margin-right:5px} \ No newline at end of file +.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} \ No newline at end of file diff --git a/dist/bootstrap-treeview.min.js b/dist/bootstrap-treeview.min.js index eb0293de6..9e2803815 100644 --- a/dist/bootstrap-treeview.min.js +++ b/dist/bootstrap-treeview.min.js @@ -1 +1 @@ -!function(a,b,c,d){"use strict";var e="treeview",f={injectStyle:!0,levels:2,expandIcon:"glyphicon glyphicon-plus",collapseIcon:"glyphicon glyphicon-minus",emptyIcon:"glyphicon",nodeIcon:"glyphicon glyphicon-stop",color:d,backColor:d,borderColor:d,onhoverColor:"#F5F5F5",selectedColor:"#FFFFFF",selectedBackColor:"#428bca",searchResultColor:"#D9534F",searchResultBackColor:d,enableLinks:!1,highlightSelected:!0,highlightSearchResults:!0,showBorder:!0,showTags:!1,multiSelect:!1,onNodeCollapsed:d,onNodeExpanded:d,onNodeSelected:d,onNodeUnselected:d,onSearchComplete:d,onSearchCleared:d},g=function(b,c){return this.$element=a(b),this.elementId=b.id,this.styleId=this.elementId+"-style",this.init(c),{options:this.options,init:a.proxy(this.init,this),remove:a.proxy(this.remove,this),getNode:a.proxy(this.getNode,this),getParent:a.proxy(this.getParent,this),getSiblings:a.proxy(this.getSiblings,this),selectNode:a.proxy(this.selectNode,this),unselectNode:a.proxy(this.unselectNode,this),toggleNodeSelected:a.proxy(this.toggleNodeSelected,this),collapseAll:a.proxy(this.collapseAll,this),collapseNode:a.proxy(this.collapseNode,this),expandAll:a.proxy(this.expandAll,this),expandNode:a.proxy(this.expandNode,this),toggleNodeExpanded:a.proxy(this.toggleNodeExpanded,this),search:a.proxy(this.search,this),clearSearch:a.proxy(this.clearSearch,this)}};g.prototype.init=function(b){this.tree=[],this.nodes=[],b.data&&("string"==typeof b.data&&(b.data=a.parseJSON(b.data)),this.tree=a.extend(!0,[],b.data),delete b.data),this.options=a.extend({},f,b),this.destroy(),this.subscribeEvents(),this.setInitialStates({nodes:this.tree},0),this.render()},g.prototype.remove=function(){this.destroy(),a.removeData(this,e),a("#"+this.styleId).remove()},g.prototype.destroy=function(){this.initialized&&(this.$wrapper.remove(),this.$wrapper=null,this.unsubscribeEvents(),this.initialized=!1)},g.prototype.unsubscribeEvents=function(){this.$element.off("click"),"function"==typeof this.options.onNodeCollapsed&&this.$element.off("nodeCollapsed"),"function"==typeof this.options.onNodeExpanded&&this.$element.off("nodeExpanded"),"function"==typeof this.options.onNodeSelected&&this.$element.off("nodeSelected"),"function"==typeof this.options.onNodeUnselected&&this.$element.off("nodeUnselected"),"function"==typeof this.options.onSearchComplete&&this.$element.off("searchComplete"),"function"==typeof this.options.onSearchCleared&&this.$element.off("searchCleared")},g.prototype.subscribeEvents=function(){this.unsubscribeEvents(),this.$element.on("click",a.proxy(this.clickHandler,this)),"function"==typeof this.options.onNodeCollapsed&&this.$element.on("nodeCollapsed",this.options.onNodeCollapsed),"function"==typeof this.options.onNodeExpanded&&this.$element.on("nodeExpanded",this.options.onNodeExpanded),"function"==typeof this.options.onNodeSelected&&this.$element.on("nodeSelected",this.options.onNodeSelected),"function"==typeof this.options.onNodeUnselected&&this.$element.on("nodeUnselected",this.options.onNodeUnselected),"function"==typeof this.options.onSearchComplete&&this.$element.on("searchComplete",this.options.onSearchComplete),"function"==typeof this.options.onSearchCleared&&this.$element.on("searchCleared",this.options.onSearchCleared)},g.prototype.setInitialStates=function(b,c){if(b.nodes){c+=1;var d=b,e=this;a.each(b.nodes,function(a,b){b.nodeId=e.nodes.length,b.parentId=d.nodeId,b.hasOwnProperty("selectable")||(b.selectable=!0),b.state=b.state||{},b.state.hasOwnProperty("expanded")||(b.state.expanded=cg;g++)f.append(d.template.indent);return f.append(e.nodes?e.state.expanded?a(d.template.expandCollapseIcon).addClass("click-collapse").addClass(d.options.collapseIcon):a(d.template.expandCollapseIcon).addClass("click-expand").addClass(d.options.expandIcon):a(d.template.expandCollapseIcon).addClass(d.options.emptyIcon)),f.append(a(d.template.icon).addClass(e.icon?e.icon:d.options.nodeIcon)),f.append(d.options.enableLinks?a(d.template.link).attr("href",e.href).append(e.text):e.text),d.options.showTags&&e.tags&&a.each(e.tags,function(b,c){f.append(a(d.template.badge).append(c))}),d.$wrapper.append(f),e.nodes&&e.state.expanded?d.buildTree(e.nodes,c):void 0})}},g.prototype.buildStyleOverride=function(a){var b=a.color,c=a.backColor;return this.options.highlightSelected&&a.state.selected&&(this.options.selectedColor&&(b=this.options.selectedColor),this.options.selectedBackColor&&(c=this.options.selectedBackColor)),this.options.highlightSearchResults&&a.searchResult&&(this.options.searchResultColor&&(b=this.options.searchResultColor),this.options.searchResultBackColor&&(c=this.options.searchResultBackColor)),"color:"+b+";background-color:"+c+";"},g.prototype.injectStyle=function(){this.options.injectStyle&&!c.getElementById(this.styleId)&&a('").appendTo("head")},g.prototype.buildStyle=function(){var a=".node-"+this.elementId+"{";return this.options.color&&(a+="color:"+this.options.color+";"),this.options.backColor&&(a+="background-color:"+this.options.backColor+";"),this.options.showBorder?this.options.borderColor&&(a+="border:1px solid "+this.options.borderColor+";"):a+="border:none;",a+="}",this.options.onhoverColor&&(a+=".node-"+this.elementId+":hover{background-color:"+this.options.onhoverColor+";}"),this.css+a},g.prototype.template={list:'',item:'
  • ',indent:'',expandCollapseIcon:'',icon:'',link:'',badge:''},g.prototype.css=".treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.expand-collapse{width:1rem;height:1rem}.treeview span.icon{margin-left:10px;margin-right:5px}",g.prototype.getNode=function(a){return this.nodes[a]},g.prototype.getParent=function(a){var b=this.identifyNode(a);return this.nodes[b.parentId]},g.prototype.getSiblings=function(a){var b=this.identifyNode(a),c=this.getParent(b),d=c?c.nodes:this.tree;return d.filter(function(a){return a.nodeId!==b.nodeId})},g.prototype.selectNode=function(a,b){var c=this.isSilent(b);this.setSelectedState(this.identifyNode(a),!0,c),this.render()},g.prototype.unselectNode=function(a,b){var c=this.isSilent(b);this.setSelectedState(this.identifyNode(a),!1,c),this.render()},g.prototype.toggleNodeSelected=function(a,b){this.toggleSelectedState(this.identifyNode(a),this.isSilent(b))},g.prototype.collapseAll=function(b){var c=this.isSilent(b);a.each(this.nodes,a.proxy(function(a,b){this.setExpandedState(b,!1,c)},this)),this.render()},g.prototype.collapseNode=function(a,b){var c=this.isSilent(b);this.setExpandedState(this.identifyNode(a),!1,c),this.render()},g.prototype.expandAll=function(b){var c=this.isSilent(b);b&&b.levels?this.expandLevels(this.tree,b.levels,c):a.each(this.nodes,a.proxy(function(a,b){this.setExpandedState(b,!0,c)},this)),this.render()},g.prototype.expandNode=function(a,b){var c=this.isSilent(b),d=this.identifyNode(a);this.setExpandedState(d,!0,c),d.nodes&&b&&b.levels&&this.expandLevels(d.nodes,b.levels-1,c),this.render()},g.prototype.expandLevels=function(b,c,d){a.each(b,a.proxy(function(a,b){this.setExpandedState(b,c>0?!0:!1),b.nodes&&this.expandLevels(b.nodes,c-1,d)},this))},g.prototype.toggleNodeExpanded=function(a,b){this.toggleExpandedState(this.identifyNode(a),this.isSilent(b))},g.prototype.identifyNode=function(a){return"number"==typeof a?this.nodes[a]:a},g.prototype.isSilent=function(a){return a&&a.hasOwnProperty("silent")?a.silent:!1},g.prototype.search=function(b,c){this.clearSearch();var d=[];if(b&&b.length>0){c.exactMatch&&(b="^"+b+"$");var e="g";c.ignoreCase&&(e+="i"),d=this.findNodes(b,e),a.each(d,function(a,b){b.searchResult=!0}),this.render()}return this.$element.trigger("searchComplete",a.extend(!0,{},d)),d},g.prototype.clearSearch=function(){var b=a.each(this.findNodes("true","g","searchResult"),function(a,b){b.searchResult=!1});this.render(),this.$element.trigger("searchCleared",a.extend(!0,{},b))},g.prototype.findNodes=function(b,c,d){c=c||"g",d=d||"text";var e=this;return a.grep(this.nodes,function(a){var f=e.getNodeValue(a,d);return"string"==typeof f?f.match(new RegExp(b,c)):void 0})},g.prototype.getNodeValue=function(a,b){var c=b.indexOf(".");if(c>0){var e=a[b.substring(0,c)],f=b.substring(c+1,b.length);return this.getNodeValue(e,f)}return a.hasOwnProperty(b)?a[b].toString():d};var h=function(a){b.console&&b.console.error(a)};a.fn[e]=function(b,c){var d;return this.each(function(){var f=a.data(this,e);"string"==typeof b?f?a.isFunction(f[b])&&"_"!==b.charAt(0)?(c instanceof Array||(c=[c]),d=f[b].apply(f,c)):h("No such method : "+b):h("Not initialized, can not call method : "+b):"boolean"==typeof b?d=f:a.data(this,e,new g(this,a.extend(!0,{},b)))}),d||this}}(jQuery,window,document); \ No newline at end of file +!function(a,b,c,d){"use strict";var e="treeview",f={};f.settings={injectStyle:!0,levels:2,expandIcon:"glyphicon glyphicon-plus",collapseIcon:"glyphicon glyphicon-minus",emptyIcon:"glyphicon",nodeIcon:"",selectedIcon:"",checkedIcon:"glyphicon glyphicon-check",uncheckedIcon:"glyphicon glyphicon-unchecked",color:d,backColor:d,borderColor:d,onhoverColor:"#F5F5F5",selectedColor:"#FFFFFF",selectedBackColor:"#428bca",searchResultColor:"#D9534F",searchResultBackColor:d,enableLinks:!1,highlightSelected:!0,highlightSearchResults:!0,showBorder:!0,showIcon:!0,showCheckbox:!1,showTags:!1,multiSelect:!1,onNodeChecked:d,onNodeCollapsed:d,onNodeDisabled:d,onNodeEnabled:d,onNodeExpanded:d,onNodeSelected:d,onNodeUnchecked:d,onNodeUnselected:d,onSearchComplete:d,onSearchCleared:d},f.options={silent:!1,ignoreChildren:!1},f.searchOptions={ignoreCase:!0,exactMatch:!1,revealResults:!0};var g=function(b,c){return this.$element=a(b),this.elementId=b.id,this.styleId=this.elementId+"-style",this.init(c),{options:this.options,init:a.proxy(this.init,this),remove:a.proxy(this.remove,this),getNode:a.proxy(this.getNode,this),getParent:a.proxy(this.getParent,this),getSiblings:a.proxy(this.getSiblings,this),getSelected:a.proxy(this.getSelected,this),getUnselected:a.proxy(this.getUnselected,this),getExpanded:a.proxy(this.getExpanded,this),getCollapsed:a.proxy(this.getCollapsed,this),getChecked:a.proxy(this.getChecked,this),getUnchecked:a.proxy(this.getUnchecked,this),getDisabled:a.proxy(this.getDisabled,this),getEnabled:a.proxy(this.getEnabled,this),selectNode:a.proxy(this.selectNode,this),unselectNode:a.proxy(this.unselectNode,this),toggleNodeSelected:a.proxy(this.toggleNodeSelected,this),collapseAll:a.proxy(this.collapseAll,this),collapseNode:a.proxy(this.collapseNode,this),expandAll:a.proxy(this.expandAll,this),expandNode:a.proxy(this.expandNode,this),toggleNodeExpanded:a.proxy(this.toggleNodeExpanded,this),revealNode:a.proxy(this.revealNode,this),checkAll:a.proxy(this.checkAll,this),checkNode:a.proxy(this.checkNode,this),uncheckAll:a.proxy(this.uncheckAll,this),uncheckNode:a.proxy(this.uncheckNode,this),toggleNodeChecked:a.proxy(this.toggleNodeChecked,this),disableAll:a.proxy(this.disableAll,this),disableNode:a.proxy(this.disableNode,this),enableAll:a.proxy(this.enableAll,this),enableNode:a.proxy(this.enableNode,this),toggleNodeDisabled:a.proxy(this.toggleNodeDisabled,this),search:a.proxy(this.search,this),clearSearch:a.proxy(this.clearSearch,this)}};g.prototype.init=function(b){this.tree=[],this.nodes=[],b.data&&("string"==typeof b.data&&(b.data=a.parseJSON(b.data)),this.tree=a.extend(!0,[],b.data),delete b.data),this.options=a.extend({},f.settings,b),this.destroy(),this.subscribeEvents(),this.setInitialStates({nodes:this.tree},0),this.render()},g.prototype.remove=function(){this.destroy(),a.removeData(this,e),a("#"+this.styleId).remove()},g.prototype.destroy=function(){this.initialized&&(this.$wrapper.remove(),this.$wrapper=null,this.unsubscribeEvents(),this.initialized=!1)},g.prototype.unsubscribeEvents=function(){this.$element.off("click"),this.$element.off("nodeChecked"),this.$element.off("nodeCollapsed"),this.$element.off("nodeDisabled"),this.$element.off("nodeEnabled"),this.$element.off("nodeExpanded"),this.$element.off("nodeSelected"),this.$element.off("nodeUnchecked"),this.$element.off("nodeUnselected"),this.$element.off("searchComplete"),this.$element.off("searchCleared")},g.prototype.subscribeEvents=function(){this.unsubscribeEvents(),this.$element.on("click",a.proxy(this.clickHandler,this)),"function"==typeof this.options.onNodeChecked&&this.$element.on("nodeChecked",this.options.onNodeChecked),"function"==typeof this.options.onNodeCollapsed&&this.$element.on("nodeCollapsed",this.options.onNodeCollapsed),"function"==typeof this.options.onNodeDisabled&&this.$element.on("nodeDisabled",this.options.onNodeDisabled),"function"==typeof this.options.onNodeEnabled&&this.$element.on("nodeEnabled",this.options.onNodeEnabled),"function"==typeof this.options.onNodeExpanded&&this.$element.on("nodeExpanded",this.options.onNodeExpanded),"function"==typeof this.options.onNodeSelected&&this.$element.on("nodeSelected",this.options.onNodeSelected),"function"==typeof this.options.onNodeUnchecked&&this.$element.on("nodeUnchecked",this.options.onNodeUnchecked),"function"==typeof this.options.onNodeUnselected&&this.$element.on("nodeUnselected",this.options.onNodeUnselected),"function"==typeof this.options.onSearchComplete&&this.$element.on("searchComplete",this.options.onSearchComplete),"function"==typeof this.options.onSearchCleared&&this.$element.on("searchCleared",this.options.onSearchCleared)},g.prototype.setInitialStates=function(b,c){if(b.nodes){c+=1;var d=b,e=this;a.each(b.nodes,function(a,b){b.nodeId=e.nodes.length,b.parentId=d.nodeId,b.hasOwnProperty("selectable")||(b.selectable=!0),b.state=b.state||{},b.state.hasOwnProperty("checked")||(b.state.checked=!1),b.state.hasOwnProperty("disabled")||(b.state.disabled=!1),b.state.hasOwnProperty("expanded")||(!b.state.disabled&&c0?b.state.expanded=!0:b.state.expanded=!1),b.state.hasOwnProperty("selected")||(b.state.selected=!1),e.nodes.push(b),b.nodes&&e.setInitialStates(b,c)})}},g.prototype.clickHandler=function(b){this.options.enableLinks||b.preventDefault();var c=a(b.target),d=this.findNode(c);if(d&&!d.state.disabled){var e=c.attr("class")?c.attr("class").split(" "):[];-1!==e.indexOf("expand-icon")?(this.toggleExpandedState(d,f.options),this.render()):-1!==e.indexOf("check-icon")?(this.toggleCheckedState(d,f.options),this.render()):(d.selectable?this.toggleSelectedState(d,f.options):this.toggleExpandedState(d,f.options),this.render())}},g.prototype.findNode=function(a){var b=a.closest("li.list-group-item").attr("data-nodeid"),c=this.nodes[b];return c||console.log("Error: node does not exist"),c},g.prototype.toggleExpandedState=function(a,b){a&&this.setExpandedState(a,!a.state.expanded,b)},g.prototype.setExpandedState=function(b,c,d){c!==b.state.expanded&&(c&&b.nodes?(b.state.expanded=!0,d.silent||this.$element.trigger("nodeExpanded",a.extend(!0,{},b))):c||(b.state.expanded=!1,d.silent||this.$element.trigger("nodeCollapsed",a.extend(!0,{},b)),b.nodes&&!d.ignoreChildren&&a.each(b.nodes,a.proxy(function(a,b){this.setExpandedState(b,!1,d)},this))))},g.prototype.toggleSelectedState=function(a,b){a&&this.setSelectedState(a,!a.state.selected,b)},g.prototype.setSelectedState=function(b,c,d){c!==b.state.selected&&(c?(this.options.multiSelect||a.each(this.findNodes("true","g","state.selected"),a.proxy(function(a,b){this.setSelectedState(b,!1,d)},this)),b.state.selected=!0,d.silent||this.$element.trigger("nodeSelected",a.extend(!0,{},b))):(b.state.selected=!1,d.silent||this.$element.trigger("nodeUnselected",a.extend(!0,{},b))))},g.prototype.toggleCheckedState=function(a,b){a&&this.setCheckedState(a,!a.state.checked,b)},g.prototype.setCheckedState=function(b,c,d){c!==b.state.checked&&(c?(b.state.checked=!0,d.silent||this.$element.trigger("nodeChecked",a.extend(!0,{},b))):(b.state.checked=!1,d.silent||this.$element.trigger("nodeUnchecked",a.extend(!0,{},b))))},g.prototype.setDisabledState=function(b,c,d){c!==b.state.disabled&&(c?(b.state.disabled=!0,this.setExpandedState(b,!1,d),this.setSelectedState(b,!1,d),this.setCheckedState(b,!1,d),d.silent||this.$element.trigger("nodeDisabled",a.extend(!0,{},b))):(b.state.disabled=!1,d.silent||this.$element.trigger("nodeEnabled",a.extend(!0,{},b))))},g.prototype.render=function(){this.initialized||(this.$element.addClass(e),this.$wrapper=a(this.template.list),this.injectStyle(),this.initialized=!0),this.$element.empty().append(this.$wrapper.empty()),this.buildTree(this.tree,0)},g.prototype.buildTree=function(b,c){if(b){c+=1;var d=this;a.each(b,function(b,e){for(var f=a(d.template.item).addClass("node-"+d.elementId).addClass(e.state.checked?"node-checked":"").addClass(e.state.disabled?"node-disabled":"").addClass(e.state.selected?"node-selected":"").addClass(e.searchResult?"search-result":"").attr("data-nodeid",e.nodeId).attr("style",d.buildStyleOverride(e)),g=0;c-1>g;g++)f.append(d.template.indent);var h=[];if(e.nodes?(h.push("expand-icon"),h.push(e.state.expanded?d.options.collapseIcon:d.options.expandIcon)):h.push(d.options.emptyIcon),f.append(a(d.template.icon).addClass(h.join(" "))),d.options.showIcon){var h=["node-icon"];h.push(e.icon||d.options.nodeIcon),e.state.selected&&(h.pop(),h.push(e.selectedIcon||d.options.selectedIcon||e.icon||d.options.nodeIcon)),f.append(a(d.template.icon).addClass(h.join(" ")))}if(d.options.showCheckbox){var h=["check-icon"];h.push(e.state.checked?d.options.checkedIcon:d.options.uncheckedIcon),f.append(a(d.template.icon).addClass(h.join(" ")))}return f.append(d.options.enableLinks?a(d.template.link).attr("href",e.href).append(e.text):e.text),d.options.showTags&&e.tags&&a.each(e.tags,function(b,c){f.append(a(d.template.badge).append(c))}),d.$wrapper.append(f),e.nodes&&e.state.expanded&&!e.state.disabled?d.buildTree(e.nodes,c):void 0})}},g.prototype.buildStyleOverride=function(a){if(a.state.disabled)return"";var b=a.color,c=a.backColor;return this.options.highlightSelected&&a.state.selected&&(this.options.selectedColor&&(b=this.options.selectedColor),this.options.selectedBackColor&&(c=this.options.selectedBackColor)),this.options.highlightSearchResults&&a.searchResult&&!a.state.disabled&&(this.options.searchResultColor&&(b=this.options.searchResultColor),this.options.searchResultBackColor&&(c=this.options.searchResultBackColor)),"color:"+b+";background-color:"+c+";"},g.prototype.injectStyle=function(){this.options.injectStyle&&!c.getElementById(this.styleId)&&a('").appendTo("head")},g.prototype.buildStyle=function(){var a=".node-"+this.elementId+"{";return this.options.color&&(a+="color:"+this.options.color+";"),this.options.backColor&&(a+="background-color:"+this.options.backColor+";"),this.options.showBorder?this.options.borderColor&&(a+="border:1px solid "+this.options.borderColor+";"):a+="border:none;",a+="}",this.options.onhoverColor&&(a+=".node-"+this.elementId+":not(.node-disabled):hover{background-color:"+this.options.onhoverColor+";}"),this.css+a},g.prototype.template={list:'',item:'
  • ',indent:'',icon:'',link:'',badge:''},g.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}",g.prototype.getNode=function(a){return this.nodes[a]},g.prototype.getParent=function(a){var b=this.identifyNode(a);return this.nodes[b.parentId]},g.prototype.getSiblings=function(a){var b=this.identifyNode(a),c=this.getParent(b),d=c?c.nodes:this.tree;return d.filter(function(a){return a.nodeId!==b.nodeId})},g.prototype.getSelected=function(){return this.findNodes("true","g","state.selected")},g.prototype.getUnselected=function(){return this.findNodes("false","g","state.selected")},g.prototype.getExpanded=function(){return this.findNodes("true","g","state.expanded")},g.prototype.getCollapsed=function(){return this.findNodes("false","g","state.expanded")},g.prototype.getChecked=function(){return this.findNodes("true","g","state.checked")},g.prototype.getUnchecked=function(){return this.findNodes("false","g","state.checked")},g.prototype.getDisabled=function(){return this.findNodes("true","g","state.disabled")},g.prototype.getEnabled=function(){return this.findNodes("false","g","state.disabled")},g.prototype.selectNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setSelectedState(a,!0,b)},this)),this.render()},g.prototype.unselectNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setSelectedState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeSelected=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleSelectedState(a,b)},this)),this.render()},g.prototype.collapseAll=function(b){var c=this.findNodes("true","g","state.expanded");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setExpandedState(a,!1,b)},this)),this.render()},g.prototype.collapseNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setExpandedState(a,!1,b)},this)),this.render()},g.prototype.expandAll=function(b){if(b=a.extend({},f.options,b),b&&b.levels)this.expandLevels(this.tree,b.levels,b);else{var c=this.findNodes("false","g","state.expanded");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setExpandedState(a,!0,b)},this))}this.render()},g.prototype.expandNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setExpandedState(a,!0,b),a.nodes&&b&&b.levels&&this.expandLevels(a.nodes,b.levels-1,b)},this)),this.render()},g.prototype.expandLevels=function(b,c,d){d=a.extend({},f.options,d),a.each(b,a.proxy(function(a,b){this.setExpandedState(b,c>0?!0:!1,d),b.nodes&&this.expandLevels(b.nodes,c-1,d)},this))},g.prototype.revealNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){for(var c=this.getParent(a);c;)this.setExpandedState(c,!0,b),c=this.getParent(c)},this)),this.render()},g.prototype.toggleNodeExpanded=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleExpandedState(a,b)},this)),this.render()},g.prototype.checkAll=function(b){var c=this.findNodes("false","g","state.checked");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setCheckedState(a,!0,b)},this)),this.render()},g.prototype.checkNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setCheckedState(a,!0,b)},this)),this.render()},g.prototype.uncheckAll=function(b){var c=this.findNodes("true","g","state.checked");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setCheckedState(a,!1,b)},this)),this.render()},g.prototype.uncheckNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setCheckedState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeChecked=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.toggleCheckedState(a,b)},this)),this.render()},g.prototype.disableAll=function(b){var c=this.findNodes("false","g","state.disabled");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setDisabledState(a,!0,b)},this)),this.render()},g.prototype.disableNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!0,b)},this)),this.render()},g.prototype.enableAll=function(b){var c=this.findNodes("true","g","state.disabled");this.forEachIdentifier(c,b,a.proxy(function(a,b){this.setDisabledState(a,!1,b)},this)),this.render()},g.prototype.enableNode=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!1,b)},this)),this.render()},g.prototype.toggleNodeDisabled=function(b,c){this.forEachIdentifier(b,c,a.proxy(function(a,b){this.setDisabledState(a,!a.state.disabled,b)},this)),this.render()},g.prototype.forEachIdentifier=function(b,c,d){c=a.extend({},f.options,c),b instanceof Array||(b=[b]),a.each(b,a.proxy(function(a,b){d(this.identifyNode(b),c)},this))},g.prototype.identifyNode=function(a){return"number"==typeof a?this.nodes[a]:a},g.prototype.search=function(b,c){c=a.extend({},f.searchOptions,c),this.clearSearch({render:!1});var d=[];if(b&&b.length>0){c.exactMatch&&(b="^"+b+"$");var e="g";c.ignoreCase&&(e+="i"),d=this.findNodes(b,e),a.each(d,function(a,b){b.searchResult=!0})}return c.revealResults?this.revealNode(d):this.render(),this.$element.trigger("searchComplete",a.extend(!0,{},d)),d},g.prototype.clearSearch=function(b){b=a.extend({},{render:!0},b);var c=a.each(this.findNodes("true","g","searchResult"),function(a,b){b.searchResult=!1});b.render&&this.render(),this.$element.trigger("searchCleared",a.extend(!0,{},c))},g.prototype.findNodes=function(b,c,d){c=c||"g",d=d||"text";var e=this;return a.grep(this.nodes,function(a){var f=e.getNodeValue(a,d);return"string"==typeof f?f.match(new RegExp(b,c)):void 0})},g.prototype.getNodeValue=function(a,b){var c=b.indexOf(".");if(c>0){var e=a[b.substring(0,c)],f=b.substring(c+1,b.length);return this.getNodeValue(e,f)}return a.hasOwnProperty(b)?a[b].toString():d};var h=function(a){b.console&&b.console.error(a)};a.fn[e]=function(b,c){var d;return this.each(function(){var f=a.data(this,e);"string"==typeof b?f?a.isFunction(f[b])&&"_"!==b.charAt(0)?(c instanceof Array||(c=[c]),d=f[b].apply(f,c)):h("No such method : "+b):h("Not initialized, can not call method : "+b):"boolean"==typeof b?d=f:a.data(this,e,new g(this,a.extend(!0,{},b)))}),d||this}}(jQuery,window,document); \ No newline at end of file diff --git a/package.json b/package.json index e5cda80e2..31ef86ddb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bootstrap-treeview", "description": "Tree View for Twitter Bootstrap", - "version": "1.1.0", + "version": "1.2.0", "homepage": "https://github.com/jonmiles/bootstrap-treeview", "author": { "name": "Jonathan Miles" diff --git a/public/css/bootstrap-treeview.css b/public/css/bootstrap-treeview.css index 863ab3f22..23c6cf066 100644 --- a/public/css/bootstrap-treeview.css +++ b/public/css/bootstrap-treeview.css @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.css v1.0.2 + * bootstrap-treeview.css v1.2.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -26,12 +26,12 @@ margin-right: 10px; } -.treeview span.expand-collapse { - width: 1rem; - height: 1rem; -} - .treeview span.icon { - margin-left: 10px; + width: 12px; margin-right: 5px; } + +.treeview .node-disabled { + color: silver; + cursor: not-allowed; +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index b84fb22fb..6f23ccd23 100644 --- a/public/index.html +++ b/public/index.html @@ -85,6 +85,12 @@

    Input

    Exact Match +
    + +
    @@ -107,6 +113,18 @@

    Input

    +
    + +
    +
    + +
    @@ -122,6 +140,8 @@

    Tree

    +

    Events

    +
    @@ -133,6 +153,12 @@

    Input

    +
    + +
    @@ -169,22 +195,94 @@

    Tree

    +

    Events

    +
    +
    +
    +
    +
    +

    Checkable Tree

    +
    +

    Input

    +
    + + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +

    Tree

    +
    +
    +
    +

    Events

    +

    -

    Events

    +

    Disabled Tree

    Input

    -
    +
    + + +
    +
    + +
    +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +
    -

    Output

    -
    +

    Tree

    +
    - +

    Events

    +
    @@ -434,22 +532,6 @@

    data: defaultData }); - $('#treeview-events').treeview({ - data: defaultData, - onNodeCollapsed: function(event, node) { - $('#event-output').prepend('

    ' + node.text + ' was collapsed

    '); - }, - onNodeExpanded: function (event, node) { - $('#event-output').prepend('

    ' + node.text + ' was expanded

    '); - }, - onNodeSelected: function(event, node) { - $('#event-output').prepend('

    ' + node.text + ' was selected

    '); - }, - onNodeUnselected: function (event, node) { - $('#event-output').prepend('

    ' + node.text + ' was unselected

    '); - } - }); - var $searchableTree = $('#treeview-searchable').treeview({ @@ -460,7 +542,8 @@

    var pattern = $('#input-search').val(); var options = { ignoreCase: $('#chk-ignore-case').is(':checked'), - exactMatch: $('#chk-exact-match').is(':checked') + exactMatch: $('#chk-exact-match').is(':checked'), + revealResults: $('#chk-reveal-results').is(':checked') }; var results = $searchableTree.treeview('search', [ pattern, options ]); @@ -481,72 +564,192 @@

    }); + var initSelectableTree = function() { + return $('#treeview-selectable').treeview({ + data: defaultData, + multiSelect: $('#chk-select-multi').is(':checked'), + onNodeSelected: function(event, node) { + $('#selectable-output').prepend('

    ' + node.text + ' was selected

    '); + }, + onNodeUnselected: function (event, node) { + $('#selectable-output').prepend('

    ' + node.text + ' was unselected

    '); + } + }); + }; + var $selectableTree = initSelectableTree(); - var $selectableTree = $('#treeview-selectable').treeview({ - data: defaultData - }); - - var findSelectableNode = function() { - return $selectableTree.treeview('search', [ $('#input-select-node').val(), { ignoreCase: false, exactMatch: true } ]); + var findSelectableNodes = function() { + return $selectableTree.treeview('search', [ $('#input-select-node').val(), { ignoreCase: false, exactMatch: false } ]); }; - var selectableNode = findSelectableNode(); + var selectableNodes = findSelectableNodes(); + + $('#chk-select-multi:checkbox').on('change', function () { + console.log('multi-select change'); + $selectableTree = initSelectableTree(); + selectableNodes = findSelectableNodes(); + }); // Select/unselect/toggle nodes $('#input-select-node').on('keyup', function (e) { - selectableNode = findSelectableNode(); - $('.select-node').prop('disabled', !(selectableNode.length === 1)); + selectableNodes = findSelectableNodes(); + $('.select-node').prop('disabled', !(selectableNodes.length >= 1)); }); $('#btn-select-node.select-node').on('click', function (e) { - $selectableTree.treeview('selectNode', selectableNode[0]); + $selectableTree.treeview('selectNode', [ selectableNodes, { silent: $('#chk-select-silent').is(':checked') }]); }); $('#btn-unselect-node.select-node').on('click', function (e) { - $selectableTree.treeview('unselectNode', selectableNode[0]); + $selectableTree.treeview('unselectNode', [ selectableNodes, { silent: $('#chk-select-silent').is(':checked') }]); }); $('#btn-toggle-selected.select-node').on('click', function (e) { - $selectableTree.treeview('toggleNodeSelected', selectableNode[0]); + $selectableTree.treeview('toggleNodeSelected', [ selectableNodes, { silent: $('#chk-select-silent').is(':checked') }]); }); var $expandibleTree = $('#treeview-expandible').treeview({ - data: defaultData + data: defaultData, + onNodeCollapsed: function(event, node) { + $('#expandible-output').prepend('

    ' + node.text + ' was collapsed

    '); + }, + onNodeExpanded: function (event, node) { + $('#expandible-output').prepend('

    ' + node.text + ' was expanded

    '); + } }); - var findExpandibleNode = function() { - return $expandibleTree.treeview('search', [ $('#input-expand-node').val(), { ignoreCase: false, exactMatch: true } ]); + var findExpandibleNodess = function() { + return $expandibleTree.treeview('search', [ $('#input-expand-node').val(), { ignoreCase: false, exactMatch: false } ]); }; - var expandibleNode = findExpandibleNode(); + var expandibleNodes = findExpandibleNodess(); // Expand/collapse/toggle nodes $('#input-expand-node').on('keyup', function (e) { - expandibleNode = findExpandibleNode(); - $('.expand-node').prop('disabled', !(expandibleNode.length === 1)); + expandibleNodes = findExpandibleNodess(); + $('.expand-node').prop('disabled', !(expandibleNodes.length >= 1)); }); $('#btn-expand-node.expand-node').on('click', function (e) { var levels = $('#select-expand-node-levels').val(); - $expandibleTree.treeview('expandNode', [ expandibleNode[0], { levels: levels } ]); + $expandibleTree.treeview('expandNode', [ expandibleNodes, { levels: levels, silent: $('#chk-expand-silent').is(':checked') }]); }); $('#btn-collapse-node.expand-node').on('click', function (e) { - $expandibleTree.treeview('collapseNode', expandibleNode[0]); + $expandibleTree.treeview('collapseNode', [ expandibleNodes, { silent: $('#chk-expand-silent').is(':checked') }]); }); $('#btn-toggle-expanded.expand-node').on('click', function (e) { - $expandibleTree.treeview('toggleNodeExpanded', expandibleNode[0]); + $expandibleTree.treeview('toggleNodeExpanded', [ expandibleNodes, { silent: $('#chk-expand-silent').is(':checked') }]); }); // Expand/collapse all $('#btn-expand-all').on('click', function (e) { var levels = $('#select-expand-all-levels').val(); - $expandibleTree.treeview('expandAll', { levels: levels }); + $expandibleTree.treeview('expandAll', { levels: levels, silent: $('#chk-expand-silent').is(':checked') }); }); $('#btn-collapse-all').on('click', function (e) { - $expandibleTree.treeview('collapseAll'); + $expandibleTree.treeview('collapseAll', { silent: $('#chk-expand-silent').is(':checked') }); + }); + + + + var $checkableTree = $('#treeview-checkable').treeview({ + data: defaultData, + showIcon: false, + showCheckbox: true, + onNodeChecked: function(event, node) { + $('#checkable-output').prepend('

    ' + node.text + ' was checked

    '); + }, + onNodeUnchecked: function (event, node) { + $('#checkable-output').prepend('

    ' + node.text + ' was unchecked

    '); + } + }); + + var findCheckableNodess = function() { + return $checkableTree.treeview('search', [ $('#input-check-node').val(), { ignoreCase: false, exactMatch: false } ]); + }; + var checkableNodes = findCheckableNodess(); + + // Check/uncheck/toggle nodes + $('#input-check-node').on('keyup', function (e) { + checkableNodes = findCheckableNodess(); + $('.check-node').prop('disabled', !(checkableNodes.length >= 1)); + }); + + $('#btn-check-node.check-node').on('click', function (e) { + $checkableTree.treeview('checkNode', [ checkableNodes, { silent: $('#chk-check-silent').is(':checked') }]); + }); + + $('#btn-uncheck-node.check-node').on('click', function (e) { + $checkableTree.treeview('uncheckNode', [ checkableNodes, { silent: $('#chk-check-silent').is(':checked') }]); + }); + + $('#btn-toggle-checked.check-node').on('click', function (e) { + $checkableTree.treeview('toggleNodeChecked', [ checkableNodes, { silent: $('#chk-check-silent').is(':checked') }]); + }); + + // Check/uncheck all + $('#btn-check-all').on('click', function (e) { + $checkableTree.treeview('checkAll', { silent: $('#chk-check-silent').is(':checked') }); + }); + + $('#btn-uncheck-all').on('click', function (e) { + $checkableTree.treeview('uncheckAll', { silent: $('#chk-check-silent').is(':checked') }); + }); + + + + var $disabledTree = $('#treeview-disabled').treeview({ + data: defaultData, + onNodeDisabled: function(event, node) { + $('#disabled-output').prepend('

    ' + node.text + ' was disabled

    '); + }, + onNodeEnabled: function (event, node) { + $('#disabled-output').prepend('

    ' + node.text + ' was enabled

    '); + }, + onNodeCollapsed: function(event, node) { + $('#disabled-output').prepend('

    ' + node.text + ' was collapsed

    '); + }, + onNodeUnchecked: function (event, node) { + $('#disabled-output').prepend('

    ' + node.text + ' was unchecked

    '); + }, + onNodeUnselected: function (event, node) { + $('#disabled-output').prepend('

    ' + node.text + ' was unselected

    '); + } + }); + + var findDisabledNodes = function() { + return $disabledTree.treeview('search', [ $('#input-disable-node').val(), { ignoreCase: false, exactMatch: false } ]); + }; + var disabledNodes = findDisabledNodes(); + + // Expand/collapse/toggle nodes + $('#input-disable-node').on('keyup', function (e) { + disabledNodes = findDisabledNodes(); + $('.disable-node').prop('disabled', !(disabledNodes.length >= 1)); + }); + + $('#btn-disable-node.disable-node').on('click', function (e) { + $disabledTree.treeview('disableNode', [ disabledNodes, { silent: $('#chk-disable-silent').is(':checked') }]); + }); + + $('#btn-enable-node.disable-node').on('click', function (e) { + $disabledTree.treeview('enableNode', [ disabledNodes, { silent: $('#chk-disable-silent').is(':checked') }]); + }); + + $('#btn-toggle-disabled.disable-node').on('click', function (e) { + $disabledTree.treeview('toggleNodeDisabled', [ disabledNodes, { silent: $('#chk-disable-silent').is(':checked') }]); + }); + + // Expand/collapse all + $('#btn-disable-all').on('click', function (e) { + $disabledTree.treeview('disableAll', { silent: $('#chk-disable-silent').is(':checked') }); + }); + + $('#btn-enable-all').on('click', function (e) { + $disabledTree.treeview('enableAll', { silent: $('#chk-disable-silent').is(':checked') }); }); diff --git a/public/js/bootstrap-treeview.js b/public/js/bootstrap-treeview.js index bdda5bc03..7a82a2eeb 100644 --- a/public/js/bootstrap-treeview.js +++ b/public/js/bootstrap-treeview.js @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.js v1.0.2 + * bootstrap-treeview.js v1.2.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -25,7 +25,9 @@ var pluginName = 'treeview'; - var defaults = { + var _default = {}; + + _default.settings = { injectStyle: true, @@ -34,7 +36,10 @@ expandIcon: 'glyphicon glyphicon-plus', collapseIcon: 'glyphicon glyphicon-minus', emptyIcon: 'glyphicon', - nodeIcon: 'glyphicon glyphicon-stop', + nodeIcon: '', + selectedIcon: '', + checkedIcon: 'glyphicon glyphicon-check', + uncheckedIcon: 'glyphicon glyphicon-unchecked', color: undefined, // '#000000', backColor: undefined, // '#FFFFFF', @@ -49,18 +54,35 @@ highlightSelected: true, highlightSearchResults: true, showBorder: true, + showIcon: true, + showCheckbox: false, showTags: false, multiSelect: false, // Event handlers + onNodeChecked: undefined, onNodeCollapsed: undefined, + onNodeDisabled: undefined, + onNodeEnabled: undefined, onNodeExpanded: undefined, onNodeSelected: undefined, + onNodeUnchecked: undefined, onNodeUnselected: undefined, onSearchComplete: undefined, onSearchCleared: undefined }; + _default.options = { + silent: false, + ignoreChildren: false + }; + + _default.searchOptions = { + ignoreCase: true, + exactMatch: false, + revealResults: true + }; + var Tree = function (element, options) { this.$element = $(element); @@ -82,6 +104,14 @@ getNode: $.proxy(this.getNode, this), getParent: $.proxy(this.getParent, this), getSiblings: $.proxy(this.getSiblings, this), + getSelected: $.proxy(this.getSelected, this), + getUnselected: $.proxy(this.getUnselected, this), + getExpanded: $.proxy(this.getExpanded, this), + getCollapsed: $.proxy(this.getCollapsed, this), + getChecked: $.proxy(this.getChecked, this), + getUnchecked: $.proxy(this.getUnchecked, this), + getDisabled: $.proxy(this.getDisabled, this), + getEnabled: $.proxy(this.getEnabled, this), // Select methods selectNode: $.proxy(this.selectNode, this), @@ -94,6 +124,21 @@ expandAll: $.proxy(this.expandAll, this), expandNode: $.proxy(this.expandNode, this), toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), + revealNode: $.proxy(this.revealNode, this), + + // Expand / collapse methods + checkAll: $.proxy(this.checkAll, this), + checkNode: $.proxy(this.checkNode, this), + uncheckAll: $.proxy(this.uncheckAll, this), + uncheckNode: $.proxy(this.uncheckNode, this), + toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), + + // Disable / enable methods + disableAll: $.proxy(this.disableAll, this), + disableNode: $.proxy(this.disableNode, this), + enableAll: $.proxy(this.enableAll, this), + enableNode: $.proxy(this.enableNode, this), + toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), // Search methods search: $.proxy(this.search, this), @@ -113,7 +158,7 @@ this.tree = $.extend(true, [], options.data); delete options.data; } - this.options = $.extend({}, defaults, options); + this.options = $.extend({}, _default.settings, options); this.destroy(); this.subscribeEvents(); @@ -144,30 +189,16 @@ Tree.prototype.unsubscribeEvents = function () { this.$element.off('click'); - - if (typeof (this.options.onNodeCollapsed) === 'function') { - this.$element.off('nodeCollapsed'); - } - - if (typeof (this.options.onNodeExpanded) === 'function') { - this.$element.off('nodeExpanded'); - } - - if (typeof (this.options.onNodeSelected) === 'function') { - this.$element.off('nodeSelected'); - } - - if (typeof (this.options.onNodeUnselected) === 'function') { - this.$element.off('nodeUnselected'); - } - - if (typeof (this.options.onSearchComplete) === 'function') { - this.$element.off('searchComplete'); - } - - if (typeof (this.options.onSearchCleared) === 'function') { - this.$element.off('searchCleared'); - } + 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 () { @@ -176,10 +207,22 @@ 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); } @@ -188,6 +231,10 @@ 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); } @@ -209,7 +256,7 @@ */ Tree.prototype.setInitialStates = function (node, level) { - if (!node.nodes) { return; } + if (!node.nodes) return; level += 1; var parent = node; @@ -227,12 +274,24 @@ node.selectable = true; } - // where provided we shouuld preserve states + // 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 (level < _this.options.levels) { + if (!node.state.disabled && + (level < _this.options.levels) && + (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } else { @@ -257,22 +316,32 @@ Tree.prototype.clickHandler = function (event) { - if (!this.options.enableLinks) { event.preventDefault(); } + if (!this.options.enableLinks) event.preventDefault(); - var target = $(event.target), - classList = target.attr('class') ? target.attr('class').split(' ') : [], - node = this.findNode(target); + var target = $(event.target); + var node = this.findNode(target); + if (!node || node.state.disabled) return; + + var classList = target.attr('class') ? target.attr('class').split(' ') : []; + if ((classList.indexOf('expand-icon') !== -1)) { - if ((classList.indexOf('click-expand') != -1) || - (classList.indexOf('click-collapse') != -1)) { - this.toggleExpandedState(node); + this.toggleExpandedState(node, _default.options); + this.render(); } - else if (node) { + else if ((classList.indexOf('check-icon') !== -1)) { + + this.toggleCheckedState(node, _default.options); + this.render(); + } + else { + if (node.selectable) { - this.toggleSelectedState(node); + this.toggleSelectedState(node, _default.options); } else { - this.toggleExpandedState(node); + this.toggleExpandedState(node, _default.options); } + + this.render(); } }; @@ -280,8 +349,8 @@ // data attribute nodeid, which is used to lookup the node in the flattened structure. Tree.prototype.findNode = function (target) { - var nodeId = target.closest('li.list-group-item').attr('data-nodeid'), - node = this.nodes[nodeId]; + 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'); @@ -289,61 +358,126 @@ return node; }; - Tree.prototype.toggleExpandedState = function (node, silent) { + Tree.prototype.toggleExpandedState = function (node, options) { if (!node) return; - this.setExpandedState(node, !node.state.expanded, silent); - this.render(); + this.setExpandedState(node, !node.state.expanded, options); }; - Tree.prototype.setExpandedState = function (node, state, silent) { + Tree.prototype.setExpandedState = function (node, state, options) { - if (state) { + if (state === node.state.expanded) return; + + if (state && node.nodes) { // Expand a node node.state.expanded = true; - if (!silent) { + if (!options.silent) { this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); } } - else { + else if (!state) { // Collapse a node node.state.expanded = false; - if (!silent) { + if (!options.silent) { this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); } + + // Collapse child nodes + if (node.nodes && !options.ignoreChildren) { + $.each(node.nodes, $.proxy(function (index, node) { + this.setExpandedState(node, false, options); + }, this)); + } } }; - Tree.prototype.toggleSelectedState = function (node, silent) { - if (!node) { return; } - this.setSelectedState(node, !node.state.selected, silent); - this.render(); + Tree.prototype.toggleSelectedState = function (node, options) { + if (!node) return; + this.setSelectedState(node, !node.state.selected, options); }; - Tree.prototype.setSelectedState = function (node, state, silent) { + Tree.prototype.setSelectedState = function (node, state, options) { + + if (state === node.state.selected) return; if (state) { // If multiSelect false, unselect previously selected if (!this.options.multiSelect) { $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { - this.setSelectedState(node, false, silent); + this.setSelectedState(node, false, options); }, this)); } // Continue selecting node node.state.selected = true; - if (!silent) { - this.$element.trigger('nodeSelected', $.extend(true, {}, node) ); + if (!options.silent) { + this.$element.trigger('nodeSelected', $.extend(true, {}, node)); } } else { // Unselect node node.state.selected = false; - if (!silent) { - this.$element.trigger('nodeUnselected', $.extend(true, {}, node) ); + if (!options.silent) { + this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.toggleCheckedState = function (node, options) { + if (!node) return; + this.setCheckedState(node, !node.state.checked, options); + }; + + Tree.prototype.setCheckedState = function (node, state, options) { + + if (state === node.state.checked) return; + + if (state) { + + // Check node + node.state.checked = true; + + if (!options.silent) { + this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + } + } + else { + + // Uncheck node + node.state.checked = false; + if (!options.silent) { + this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.setDisabledState = function (node, state, options) { + + if (state === node.state.disabled) return; + + if (state) { + + // Disable node + node.state.disabled = true; + + // Disable all other states + this.setExpandedState(node, false, options); + this.setSelectedState(node, false, options); + this.setCheckedState(node, false, options); + + if (!options.silent) { + this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + } + } + else { + + // Enabled node + node.state.disabled = false; + if (!options.silent) { + this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); } } }; @@ -371,7 +505,7 @@ // structure we build the tree one node at a time Tree.prototype.buildTree = function (nodes, level) { - if (!nodes) { return; } + if (!nodes) return; level += 1; var _this = this; @@ -379,8 +513,10 @@ var treeItem = $(_this.template.item) .addClass('node-' + _this.elementId) + .addClass(node.state.checked ? 'node-checked' : '') + .addClass(node.state.disabled ? 'node-disabled': '') .addClass(node.state.selected ? 'node-selected' : '') - .addClass(node.searchResult ? 'search-result' : '') + .addClass(node.searchResult ? 'search-result' : '') .attr('data-nodeid', node.nodeId) .attr('style', _this.buildStyleOverride(node)); @@ -390,35 +526,61 @@ } // Add expand, collapse or empty spacer icons + var classList = []; if (node.nodes) { - if (!node.state.expanded) { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass('click-expand') - .addClass(_this.options.expandIcon) - ); - } - else { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass('click-collapse') - .addClass(_this.options.collapseIcon) - ); - } + classList.push('expand-icon'); + if (node.state.expanded) { + classList.push(_this.options.collapseIcon); + } + else { + classList.push(_this.options.expandIcon); + } } else { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass(_this.options.emptyIcon) - ); + classList.push(_this.options.emptyIcon); } - // Add node icon treeItem .append($(_this.template.icon) - .addClass(node.icon ? node.icon : _this.options.nodeIcon) + .addClass(classList.join(' ')) ); + + // Add node icon + if (_this.options.showIcon) { + + var classList = ['node-icon']; + + classList.push(node.icon || _this.options.nodeIcon); + if (node.state.selected) { + classList.pop(); + classList.push(node.selectedIcon || _this.options.selectedIcon || + node.icon || _this.options.nodeIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + + // Add check / unchecked icon + if (_this.options.showCheckbox) { + + var classList = ['check-icon']; + if (node.state.checked) { + classList.push(_this.options.checkedIcon); + } + else { + classList.push(_this.options.uncheckedIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + // Add text if (_this.options.enableLinks) { // Add hyperlink @@ -448,8 +610,7 @@ _this.$wrapper.append(treeItem); // Recursively add child ndoes - // console.log(node.text + ' ' + node.state.expanded); - if (node.nodes && node.state.expanded) { + if (node.nodes && node.state.expanded && !node.state.disabled) { return _this.buildTree(node.nodes, level); } }); @@ -460,6 +621,8 @@ // 2. node|data assigned color overrides Tree.prototype.buildStyleOverride = function (node) { + if (node.state.disabled) return ''; + var color = node.color; var backColor = node.backColor; @@ -472,7 +635,7 @@ } } - if (this.options.highlightSearchResults && node.searchResult) { + if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { if (this.options.searchResultColor) { color = this.options.searchResultColor; } @@ -497,12 +660,15 @@ Tree.prototype.buildStyle = function () { var style = '.node-' + this.elementId + '{'; + if (this.options.color) { style += 'color:' + this.options.color + ';'; } + if (this.options.backColor) { style += 'background-color:' + this.options.backColor + ';'; } + if (!this.options.showBorder) { style += 'border:none;'; } @@ -512,8 +678,8 @@ style += '}'; if (this.options.onhoverColor) { - style += '.node-' + this.elementId + ':hover{' + - 'background-color:' + this.options.onhoverColor + ';' + + style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this.options.onhoverColor + ';' + '}'; } @@ -524,13 +690,12 @@ list: '
      ', item: '
    • ', indent: '', - expandCollapseIcon: '', icon: '', link: '', badge: '' }; - Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.expand-collapse{width:1rem;height:1rem}.treeview span.icon{margin-left:10px;margin-right:5px}' + 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}' /** @@ -545,7 +710,7 @@ /** Returns the parent node of a given node, if valid otherwise returns undefined. @param {Object|Number} identifier - A valid node or node id - @returns {Object} parent - The parent node + @returns {Object} node - The parent node */ Tree.prototype.getParent = function (identifier) { var node = this.identifyNode(identifier); @@ -555,7 +720,7 @@ /** Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. @param {Object|Number} identifier - A valid node or node id - @returns {Array} siblings - Sibling nodes + @returns {Array} nodes - Sibling nodes */ Tree.prototype.getSiblings = function (identifier) { var node = this.identifyNode(identifier); @@ -566,37 +731,108 @@ }); }; + /** + Returns an array of selected nodes. + @returns {Array} nodes - Selected nodes + */ + Tree.prototype.getSelected = function () { + return this.findNodes('true', 'g', 'state.selected'); + }; + + /** + Returns an array of unselected nodes. + @returns {Array} nodes - Unselected nodes + */ + Tree.prototype.getUnselected = function () { + return this.findNodes('false', 'g', 'state.selected'); + }; + + /** + Returns an array of expanded nodes. + @returns {Array} nodes - Expanded nodes + */ + Tree.prototype.getExpanded = function () { + return this.findNodes('true', 'g', 'state.expanded'); + }; + + /** + Returns an array of collapsed nodes. + @returns {Array} nodes - Collapsed nodes + */ + Tree.prototype.getCollapsed = function () { + return this.findNodes('false', 'g', 'state.expanded'); + }; + + /** + Returns an array of checked nodes. + @returns {Array} nodes - Checked nodes + */ + Tree.prototype.getChecked = function () { + return this.findNodes('true', 'g', 'state.checked'); + }; + + /** + Returns an array of unchecked nodes. + @returns {Array} nodes - Unchecked nodes + */ + Tree.prototype.getUnchecked = function () { + return this.findNodes('false', 'g', 'state.checked'); + }; + + /** + Returns an array of disabled nodes. + @returns {Array} nodes - Disabled nodes + */ + Tree.prototype.getDisabled = function () { + return this.findNodes('true', 'g', 'state.disabled'); + }; + + /** + Returns an array of enabled nodes. + @returns {Array} nodes - Enabled nodes + */ + Tree.prototype.getEnabled = function () { + return this.findNodes('false', 'g', 'state.disabled'); + }; + /** Set a node state to selected - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.selectNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setSelectedState(this.identifyNode(identifier), true, silent); + Tree.prototype.selectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, true, options); + }, this)); + this.render(); }; /** Set a node state to unselected - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.unselectNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setSelectedState(this.identifyNode(identifier), false, silent); + Tree.prototype.unselectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, false, options); + }, this)); + this.render(); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.toggleNodeSelected = function (identifier, options) { - this.toggleSelectedState(this.identifyNode(identifier), - this.isSilent(options)); + Tree.prototype.toggleNodeSelected = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleSelectedState(node, options); + }, this)); + + this.render(); }; @@ -605,10 +841,9 @@ @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { - var silent = this.isSilent(options); - - $.each(this.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, false, silent); + var identifiers = this.findNodes('true', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); }, this)); this.render(); @@ -616,12 +851,14 @@ /** Collapse a given tree node - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.collapseNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setExpandedState(this.identifyNode(identifier), false, silent); + Tree.prototype.collapseNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); + }, this)); + this.render(); }; @@ -630,14 +867,15 @@ @param {optional Object} options */ Tree.prototype.expandAll = function (options) { - var silent = this.isSilent(options); + options = $.extend({}, _default.options, options); if (options && options.levels) { - this.expandLevels(this.tree, options.levels, silent); + this.expandLevels(this.tree, options.levels, options); } else { - $.each(this.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, true, silent); + var identifiers = this.findNodes('false', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); }, this)); } @@ -646,54 +884,219 @@ /** Expand a given tree node - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.expandNode = function (identifier, options) { - var silent = this.isSilent(options); - - var node = this.identifyNode(identifier); - this.setExpandedState(node, true, silent); - - if (node.nodes && (options && options.levels)) { - this.expandLevels(node.nodes, options.levels-1, silent); - } + Tree.prototype.expandNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); + if (node.nodes && (options && options.levels)) { + this.expandLevels(node.nodes, options.levels-1, options); + } + }, this)); this.render(); }; - Tree.prototype.expandLevels = function (nodes, level, silent) { + Tree.prototype.expandLevels = function (nodes, level, options) { + options = $.extend({}, _default.options, options); + $.each(nodes, $.proxy(function (index, node) { - this.setExpandedState(node, (level > 0) ? true : false) + this.setExpandedState(node, (level > 0) ? true : false, options); if (node.nodes) { - this.expandLevels(node.nodes, level-1, silent); + this.expandLevels(node.nodes, level-1, options); } }, this)); }; + /** + Reveals a given tree node, expanding the tree from node to root. + @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.revealNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + var parentNode = this.getParent(node); + while (parentNode) { + this.setExpandedState(parentNode, true, options); + parentNode = this.getParent(parentNode); + }; + }, this)); + + this.render(); + }; + /** Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.toggleNodeExpanded = function (identifier, options) { - this.toggleExpandedState(this.identifyNode(identifier), - this.isSilent(options)); + Tree.prototype.toggleNodeExpanded = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleExpandedState(node, options); + }, this)); + + this.render(); }; + + /** + Check all tree nodes + @param {optional Object} options + */ + Tree.prototype.checkAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Check a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.checkNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Uncheck all tree nodes + @param {optional Object} options + */ + Tree.prototype.uncheckAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Uncheck a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.uncheckNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes checked state; checking if unchecked, unchecking if checked. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeChecked = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleCheckedState(node, options); + }, this)); + + this.render(); + }; + + + /** + Disable all tree nodes + @param {optional Object} options + */ + Tree.prototype.disableAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Disable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.disableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Enable all tree nodes + @param {optional Object} options + */ + Tree.prototype.enableAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Enable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.enableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes disabled state; disabling is enabled, enabling if disabled. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeDisabled = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, !node.state.disabled, options); + }, this)); + + this.render(); + }; + + + /** + Common code for processing multiple identifiers + */ + Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + + options = $.extend({}, _default.options, options); + + if (!(identifiers instanceof Array)) { + identifiers = [identifiers]; + } + + $.each(identifiers, $.proxy(function (index, identifier) { + callback(this.identifyNode(identifier), options); + }, this)); + }; + + /* + Identifies a node from either a node id or object + */ Tree.prototype.identifyNode = function (identifier) { return ((typeof identifier) === 'number') ? this.nodes[identifier] : identifier; }; - Tree.prototype.isSilent = function (options) { - return (options && options.hasOwnProperty('silent')) ? - options.silent : - false; - }; - - /** Searches the tree for nodes (text) that match given criteria @param {String} pattern - A given string to match against @@ -701,8 +1104,9 @@ @return {Array} nodes - Matching nodes */ Tree.prototype.search = function (pattern, options) { + options = $.extend({}, _default.searchOptions, options); - this.clearSearch(); + this.clearSearch({ render: false }); var results = []; if (pattern && pattern.length > 0) { @@ -717,10 +1121,21 @@ } results = this.findNodes(pattern, modifier); + + // Add searchResult property to all matching nodes + // This will be used to apply custom styles + // and when identifying result to be cleared $.each(results, function (index, node) { node.searchResult = true; }) + } + // If revealResults, then render is triggered from revealNode + // otherwise we just call render. + if (options.revealResults) { + this.revealNode(results); + } + else { this.render(); } @@ -732,14 +1147,18 @@ /** Clears previous search results */ - Tree.prototype.clearSearch = function () { + Tree.prototype.clearSearch = function (options) { + + options = $.extend({}, { render: true }, options); var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { node.searchResult = false; }); - this.render(); - + if (options.render) { + this.render(); + } + this.$element.trigger('searchCleared', $.extend(true, {}, results)); }; @@ -789,10 +1208,10 @@ }; var logError = function (message) { - if(window.console) { - window.console.error(message); - } - }; + if (window.console) { + window.console.error(message); + } + }; // Prevent against multiple instantiations, // handle updates and method calls diff --git a/src/css/bootstrap-treeview.css b/src/css/bootstrap-treeview.css index 863ab3f22..23c6cf066 100644 --- a/src/css/bootstrap-treeview.css +++ b/src/css/bootstrap-treeview.css @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.css v1.0.2 + * bootstrap-treeview.css v1.2.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -26,12 +26,12 @@ margin-right: 10px; } -.treeview span.expand-collapse { - width: 1rem; - height: 1rem; -} - .treeview span.icon { - margin-left: 10px; + width: 12px; margin-right: 5px; } + +.treeview .node-disabled { + color: silver; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/js/bootstrap-treeview.js b/src/js/bootstrap-treeview.js index bdda5bc03..7a82a2eeb 100644 --- a/src/js/bootstrap-treeview.js +++ b/src/js/bootstrap-treeview.js @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.js v1.0.2 + * bootstrap-treeview.js v1.2.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -25,7 +25,9 @@ var pluginName = 'treeview'; - var defaults = { + var _default = {}; + + _default.settings = { injectStyle: true, @@ -34,7 +36,10 @@ expandIcon: 'glyphicon glyphicon-plus', collapseIcon: 'glyphicon glyphicon-minus', emptyIcon: 'glyphicon', - nodeIcon: 'glyphicon glyphicon-stop', + nodeIcon: '', + selectedIcon: '', + checkedIcon: 'glyphicon glyphicon-check', + uncheckedIcon: 'glyphicon glyphicon-unchecked', color: undefined, // '#000000', backColor: undefined, // '#FFFFFF', @@ -49,18 +54,35 @@ highlightSelected: true, highlightSearchResults: true, showBorder: true, + showIcon: true, + showCheckbox: false, showTags: false, multiSelect: false, // Event handlers + onNodeChecked: undefined, onNodeCollapsed: undefined, + onNodeDisabled: undefined, + onNodeEnabled: undefined, onNodeExpanded: undefined, onNodeSelected: undefined, + onNodeUnchecked: undefined, onNodeUnselected: undefined, onSearchComplete: undefined, onSearchCleared: undefined }; + _default.options = { + silent: false, + ignoreChildren: false + }; + + _default.searchOptions = { + ignoreCase: true, + exactMatch: false, + revealResults: true + }; + var Tree = function (element, options) { this.$element = $(element); @@ -82,6 +104,14 @@ getNode: $.proxy(this.getNode, this), getParent: $.proxy(this.getParent, this), getSiblings: $.proxy(this.getSiblings, this), + getSelected: $.proxy(this.getSelected, this), + getUnselected: $.proxy(this.getUnselected, this), + getExpanded: $.proxy(this.getExpanded, this), + getCollapsed: $.proxy(this.getCollapsed, this), + getChecked: $.proxy(this.getChecked, this), + getUnchecked: $.proxy(this.getUnchecked, this), + getDisabled: $.proxy(this.getDisabled, this), + getEnabled: $.proxy(this.getEnabled, this), // Select methods selectNode: $.proxy(this.selectNode, this), @@ -94,6 +124,21 @@ expandAll: $.proxy(this.expandAll, this), expandNode: $.proxy(this.expandNode, this), toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), + revealNode: $.proxy(this.revealNode, this), + + // Expand / collapse methods + checkAll: $.proxy(this.checkAll, this), + checkNode: $.proxy(this.checkNode, this), + uncheckAll: $.proxy(this.uncheckAll, this), + uncheckNode: $.proxy(this.uncheckNode, this), + toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), + + // Disable / enable methods + disableAll: $.proxy(this.disableAll, this), + disableNode: $.proxy(this.disableNode, this), + enableAll: $.proxy(this.enableAll, this), + enableNode: $.proxy(this.enableNode, this), + toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), // Search methods search: $.proxy(this.search, this), @@ -113,7 +158,7 @@ this.tree = $.extend(true, [], options.data); delete options.data; } - this.options = $.extend({}, defaults, options); + this.options = $.extend({}, _default.settings, options); this.destroy(); this.subscribeEvents(); @@ -144,30 +189,16 @@ Tree.prototype.unsubscribeEvents = function () { this.$element.off('click'); - - if (typeof (this.options.onNodeCollapsed) === 'function') { - this.$element.off('nodeCollapsed'); - } - - if (typeof (this.options.onNodeExpanded) === 'function') { - this.$element.off('nodeExpanded'); - } - - if (typeof (this.options.onNodeSelected) === 'function') { - this.$element.off('nodeSelected'); - } - - if (typeof (this.options.onNodeUnselected) === 'function') { - this.$element.off('nodeUnselected'); - } - - if (typeof (this.options.onSearchComplete) === 'function') { - this.$element.off('searchComplete'); - } - - if (typeof (this.options.onSearchCleared) === 'function') { - this.$element.off('searchCleared'); - } + 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 () { @@ -176,10 +207,22 @@ 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); } @@ -188,6 +231,10 @@ 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); } @@ -209,7 +256,7 @@ */ Tree.prototype.setInitialStates = function (node, level) { - if (!node.nodes) { return; } + if (!node.nodes) return; level += 1; var parent = node; @@ -227,12 +274,24 @@ node.selectable = true; } - // where provided we shouuld preserve states + // 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 (level < _this.options.levels) { + if (!node.state.disabled && + (level < _this.options.levels) && + (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } else { @@ -257,22 +316,32 @@ Tree.prototype.clickHandler = function (event) { - if (!this.options.enableLinks) { event.preventDefault(); } + if (!this.options.enableLinks) event.preventDefault(); - var target = $(event.target), - classList = target.attr('class') ? target.attr('class').split(' ') : [], - node = this.findNode(target); + var target = $(event.target); + var node = this.findNode(target); + if (!node || node.state.disabled) return; + + var classList = target.attr('class') ? target.attr('class').split(' ') : []; + if ((classList.indexOf('expand-icon') !== -1)) { - if ((classList.indexOf('click-expand') != -1) || - (classList.indexOf('click-collapse') != -1)) { - this.toggleExpandedState(node); + this.toggleExpandedState(node, _default.options); + this.render(); } - else if (node) { + else if ((classList.indexOf('check-icon') !== -1)) { + + this.toggleCheckedState(node, _default.options); + this.render(); + } + else { + if (node.selectable) { - this.toggleSelectedState(node); + this.toggleSelectedState(node, _default.options); } else { - this.toggleExpandedState(node); + this.toggleExpandedState(node, _default.options); } + + this.render(); } }; @@ -280,8 +349,8 @@ // data attribute nodeid, which is used to lookup the node in the flattened structure. Tree.prototype.findNode = function (target) { - var nodeId = target.closest('li.list-group-item').attr('data-nodeid'), - node = this.nodes[nodeId]; + 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'); @@ -289,61 +358,126 @@ return node; }; - Tree.prototype.toggleExpandedState = function (node, silent) { + Tree.prototype.toggleExpandedState = function (node, options) { if (!node) return; - this.setExpandedState(node, !node.state.expanded, silent); - this.render(); + this.setExpandedState(node, !node.state.expanded, options); }; - Tree.prototype.setExpandedState = function (node, state, silent) { + Tree.prototype.setExpandedState = function (node, state, options) { - if (state) { + if (state === node.state.expanded) return; + + if (state && node.nodes) { // Expand a node node.state.expanded = true; - if (!silent) { + if (!options.silent) { this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); } } - else { + else if (!state) { // Collapse a node node.state.expanded = false; - if (!silent) { + if (!options.silent) { this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); } + + // Collapse child nodes + if (node.nodes && !options.ignoreChildren) { + $.each(node.nodes, $.proxy(function (index, node) { + this.setExpandedState(node, false, options); + }, this)); + } } }; - Tree.prototype.toggleSelectedState = function (node, silent) { - if (!node) { return; } - this.setSelectedState(node, !node.state.selected, silent); - this.render(); + Tree.prototype.toggleSelectedState = function (node, options) { + if (!node) return; + this.setSelectedState(node, !node.state.selected, options); }; - Tree.prototype.setSelectedState = function (node, state, silent) { + Tree.prototype.setSelectedState = function (node, state, options) { + + if (state === node.state.selected) return; if (state) { // If multiSelect false, unselect previously selected if (!this.options.multiSelect) { $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { - this.setSelectedState(node, false, silent); + this.setSelectedState(node, false, options); }, this)); } // Continue selecting node node.state.selected = true; - if (!silent) { - this.$element.trigger('nodeSelected', $.extend(true, {}, node) ); + if (!options.silent) { + this.$element.trigger('nodeSelected', $.extend(true, {}, node)); } } else { // Unselect node node.state.selected = false; - if (!silent) { - this.$element.trigger('nodeUnselected', $.extend(true, {}, node) ); + if (!options.silent) { + this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.toggleCheckedState = function (node, options) { + if (!node) return; + this.setCheckedState(node, !node.state.checked, options); + }; + + Tree.prototype.setCheckedState = function (node, state, options) { + + if (state === node.state.checked) return; + + if (state) { + + // Check node + node.state.checked = true; + + if (!options.silent) { + this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + } + } + else { + + // Uncheck node + node.state.checked = false; + if (!options.silent) { + this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.setDisabledState = function (node, state, options) { + + if (state === node.state.disabled) return; + + if (state) { + + // Disable node + node.state.disabled = true; + + // Disable all other states + this.setExpandedState(node, false, options); + this.setSelectedState(node, false, options); + this.setCheckedState(node, false, options); + + if (!options.silent) { + this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + } + } + else { + + // Enabled node + node.state.disabled = false; + if (!options.silent) { + this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); } } }; @@ -371,7 +505,7 @@ // structure we build the tree one node at a time Tree.prototype.buildTree = function (nodes, level) { - if (!nodes) { return; } + if (!nodes) return; level += 1; var _this = this; @@ -379,8 +513,10 @@ var treeItem = $(_this.template.item) .addClass('node-' + _this.elementId) + .addClass(node.state.checked ? 'node-checked' : '') + .addClass(node.state.disabled ? 'node-disabled': '') .addClass(node.state.selected ? 'node-selected' : '') - .addClass(node.searchResult ? 'search-result' : '') + .addClass(node.searchResult ? 'search-result' : '') .attr('data-nodeid', node.nodeId) .attr('style', _this.buildStyleOverride(node)); @@ -390,35 +526,61 @@ } // Add expand, collapse or empty spacer icons + var classList = []; if (node.nodes) { - if (!node.state.expanded) { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass('click-expand') - .addClass(_this.options.expandIcon) - ); - } - else { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass('click-collapse') - .addClass(_this.options.collapseIcon) - ); - } + classList.push('expand-icon'); + if (node.state.expanded) { + classList.push(_this.options.collapseIcon); + } + else { + classList.push(_this.options.expandIcon); + } } else { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass(_this.options.emptyIcon) - ); + classList.push(_this.options.emptyIcon); } - // Add node icon treeItem .append($(_this.template.icon) - .addClass(node.icon ? node.icon : _this.options.nodeIcon) + .addClass(classList.join(' ')) ); + + // Add node icon + if (_this.options.showIcon) { + + var classList = ['node-icon']; + + classList.push(node.icon || _this.options.nodeIcon); + if (node.state.selected) { + classList.pop(); + classList.push(node.selectedIcon || _this.options.selectedIcon || + node.icon || _this.options.nodeIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + + // Add check / unchecked icon + if (_this.options.showCheckbox) { + + var classList = ['check-icon']; + if (node.state.checked) { + classList.push(_this.options.checkedIcon); + } + else { + classList.push(_this.options.uncheckedIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + // Add text if (_this.options.enableLinks) { // Add hyperlink @@ -448,8 +610,7 @@ _this.$wrapper.append(treeItem); // Recursively add child ndoes - // console.log(node.text + ' ' + node.state.expanded); - if (node.nodes && node.state.expanded) { + if (node.nodes && node.state.expanded && !node.state.disabled) { return _this.buildTree(node.nodes, level); } }); @@ -460,6 +621,8 @@ // 2. node|data assigned color overrides Tree.prototype.buildStyleOverride = function (node) { + if (node.state.disabled) return ''; + var color = node.color; var backColor = node.backColor; @@ -472,7 +635,7 @@ } } - if (this.options.highlightSearchResults && node.searchResult) { + if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { if (this.options.searchResultColor) { color = this.options.searchResultColor; } @@ -497,12 +660,15 @@ Tree.prototype.buildStyle = function () { var style = '.node-' + this.elementId + '{'; + if (this.options.color) { style += 'color:' + this.options.color + ';'; } + if (this.options.backColor) { style += 'background-color:' + this.options.backColor + ';'; } + if (!this.options.showBorder) { style += 'border:none;'; } @@ -512,8 +678,8 @@ style += '}'; if (this.options.onhoverColor) { - style += '.node-' + this.elementId + ':hover{' + - 'background-color:' + this.options.onhoverColor + ';' + + style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this.options.onhoverColor + ';' + '}'; } @@ -524,13 +690,12 @@ list: '
        ', item: '
      • ', indent: '', - expandCollapseIcon: '', icon: '', link: '', badge: '' }; - Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.expand-collapse{width:1rem;height:1rem}.treeview span.icon{margin-left:10px;margin-right:5px}' + 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}' /** @@ -545,7 +710,7 @@ /** Returns the parent node of a given node, if valid otherwise returns undefined. @param {Object|Number} identifier - A valid node or node id - @returns {Object} parent - The parent node + @returns {Object} node - The parent node */ Tree.prototype.getParent = function (identifier) { var node = this.identifyNode(identifier); @@ -555,7 +720,7 @@ /** Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. @param {Object|Number} identifier - A valid node or node id - @returns {Array} siblings - Sibling nodes + @returns {Array} nodes - Sibling nodes */ Tree.prototype.getSiblings = function (identifier) { var node = this.identifyNode(identifier); @@ -566,37 +731,108 @@ }); }; + /** + Returns an array of selected nodes. + @returns {Array} nodes - Selected nodes + */ + Tree.prototype.getSelected = function () { + return this.findNodes('true', 'g', 'state.selected'); + }; + + /** + Returns an array of unselected nodes. + @returns {Array} nodes - Unselected nodes + */ + Tree.prototype.getUnselected = function () { + return this.findNodes('false', 'g', 'state.selected'); + }; + + /** + Returns an array of expanded nodes. + @returns {Array} nodes - Expanded nodes + */ + Tree.prototype.getExpanded = function () { + return this.findNodes('true', 'g', 'state.expanded'); + }; + + /** + Returns an array of collapsed nodes. + @returns {Array} nodes - Collapsed nodes + */ + Tree.prototype.getCollapsed = function () { + return this.findNodes('false', 'g', 'state.expanded'); + }; + + /** + Returns an array of checked nodes. + @returns {Array} nodes - Checked nodes + */ + Tree.prototype.getChecked = function () { + return this.findNodes('true', 'g', 'state.checked'); + }; + + /** + Returns an array of unchecked nodes. + @returns {Array} nodes - Unchecked nodes + */ + Tree.prototype.getUnchecked = function () { + return this.findNodes('false', 'g', 'state.checked'); + }; + + /** + Returns an array of disabled nodes. + @returns {Array} nodes - Disabled nodes + */ + Tree.prototype.getDisabled = function () { + return this.findNodes('true', 'g', 'state.disabled'); + }; + + /** + Returns an array of enabled nodes. + @returns {Array} nodes - Enabled nodes + */ + Tree.prototype.getEnabled = function () { + return this.findNodes('false', 'g', 'state.disabled'); + }; + /** Set a node state to selected - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.selectNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setSelectedState(this.identifyNode(identifier), true, silent); + Tree.prototype.selectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, true, options); + }, this)); + this.render(); }; /** Set a node state to unselected - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.unselectNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setSelectedState(this.identifyNode(identifier), false, silent); + Tree.prototype.unselectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, false, options); + }, this)); + this.render(); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.toggleNodeSelected = function (identifier, options) { - this.toggleSelectedState(this.identifyNode(identifier), - this.isSilent(options)); + Tree.prototype.toggleNodeSelected = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleSelectedState(node, options); + }, this)); + + this.render(); }; @@ -605,10 +841,9 @@ @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { - var silent = this.isSilent(options); - - $.each(this.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, false, silent); + var identifiers = this.findNodes('true', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); }, this)); this.render(); @@ -616,12 +851,14 @@ /** Collapse a given tree node - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.collapseNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setExpandedState(this.identifyNode(identifier), false, silent); + Tree.prototype.collapseNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); + }, this)); + this.render(); }; @@ -630,14 +867,15 @@ @param {optional Object} options */ Tree.prototype.expandAll = function (options) { - var silent = this.isSilent(options); + options = $.extend({}, _default.options, options); if (options && options.levels) { - this.expandLevels(this.tree, options.levels, silent); + this.expandLevels(this.tree, options.levels, options); } else { - $.each(this.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, true, silent); + var identifiers = this.findNodes('false', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); }, this)); } @@ -646,54 +884,219 @@ /** Expand a given tree node - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.expandNode = function (identifier, options) { - var silent = this.isSilent(options); - - var node = this.identifyNode(identifier); - this.setExpandedState(node, true, silent); - - if (node.nodes && (options && options.levels)) { - this.expandLevels(node.nodes, options.levels-1, silent); - } + Tree.prototype.expandNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); + if (node.nodes && (options && options.levels)) { + this.expandLevels(node.nodes, options.levels-1, options); + } + }, this)); this.render(); }; - Tree.prototype.expandLevels = function (nodes, level, silent) { + Tree.prototype.expandLevels = function (nodes, level, options) { + options = $.extend({}, _default.options, options); + $.each(nodes, $.proxy(function (index, node) { - this.setExpandedState(node, (level > 0) ? true : false) + this.setExpandedState(node, (level > 0) ? true : false, options); if (node.nodes) { - this.expandLevels(node.nodes, level-1, silent); + this.expandLevels(node.nodes, level-1, options); } }, this)); }; + /** + Reveals a given tree node, expanding the tree from node to root. + @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.revealNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + var parentNode = this.getParent(node); + while (parentNode) { + this.setExpandedState(parentNode, true, options); + parentNode = this.getParent(parentNode); + }; + }, this)); + + this.render(); + }; + /** Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.toggleNodeExpanded = function (identifier, options) { - this.toggleExpandedState(this.identifyNode(identifier), - this.isSilent(options)); + Tree.prototype.toggleNodeExpanded = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleExpandedState(node, options); + }, this)); + + this.render(); }; + + /** + Check all tree nodes + @param {optional Object} options + */ + Tree.prototype.checkAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Check a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.checkNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Uncheck all tree nodes + @param {optional Object} options + */ + Tree.prototype.uncheckAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Uncheck a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.uncheckNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes checked state; checking if unchecked, unchecking if checked. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeChecked = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleCheckedState(node, options); + }, this)); + + this.render(); + }; + + + /** + Disable all tree nodes + @param {optional Object} options + */ + Tree.prototype.disableAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Disable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.disableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Enable all tree nodes + @param {optional Object} options + */ + Tree.prototype.enableAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Enable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.enableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes disabled state; disabling is enabled, enabling if disabled. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeDisabled = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, !node.state.disabled, options); + }, this)); + + this.render(); + }; + + + /** + Common code for processing multiple identifiers + */ + Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + + options = $.extend({}, _default.options, options); + + if (!(identifiers instanceof Array)) { + identifiers = [identifiers]; + } + + $.each(identifiers, $.proxy(function (index, identifier) { + callback(this.identifyNode(identifier), options); + }, this)); + }; + + /* + Identifies a node from either a node id or object + */ Tree.prototype.identifyNode = function (identifier) { return ((typeof identifier) === 'number') ? this.nodes[identifier] : identifier; }; - Tree.prototype.isSilent = function (options) { - return (options && options.hasOwnProperty('silent')) ? - options.silent : - false; - }; - - /** Searches the tree for nodes (text) that match given criteria @param {String} pattern - A given string to match against @@ -701,8 +1104,9 @@ @return {Array} nodes - Matching nodes */ Tree.prototype.search = function (pattern, options) { + options = $.extend({}, _default.searchOptions, options); - this.clearSearch(); + this.clearSearch({ render: false }); var results = []; if (pattern && pattern.length > 0) { @@ -717,10 +1121,21 @@ } results = this.findNodes(pattern, modifier); + + // Add searchResult property to all matching nodes + // This will be used to apply custom styles + // and when identifying result to be cleared $.each(results, function (index, node) { node.searchResult = true; }) + } + // If revealResults, then render is triggered from revealNode + // otherwise we just call render. + if (options.revealResults) { + this.revealNode(results); + } + else { this.render(); } @@ -732,14 +1147,18 @@ /** Clears previous search results */ - Tree.prototype.clearSearch = function () { + Tree.prototype.clearSearch = function (options) { + + options = $.extend({}, { render: true }, options); var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { node.searchResult = false; }); - this.render(); - + if (options.render) { + this.render(); + } + this.$element.trigger('searchCleared', $.extend(true, {}, results)); }; @@ -789,10 +1208,10 @@ }; var logError = function (message) { - if(window.console) { - window.console.error(message); - } - }; + if (window.console) { + window.console.error(message); + } + }; // Prevent against multiple instantiations, // handle updates and method calls diff --git a/tests/lib/bootstrap-treeview.css b/tests/lib/bootstrap-treeview.css index 863ab3f22..23c6cf066 100644 --- a/tests/lib/bootstrap-treeview.css +++ b/tests/lib/bootstrap-treeview.css @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.css v1.0.2 + * bootstrap-treeview.css v1.2.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -26,12 +26,12 @@ margin-right: 10px; } -.treeview span.expand-collapse { - width: 1rem; - height: 1rem; -} - .treeview span.icon { - margin-left: 10px; + width: 12px; margin-right: 5px; } + +.treeview .node-disabled { + color: silver; + cursor: not-allowed; +} \ No newline at end of file diff --git a/tests/lib/bootstrap-treeview.js b/tests/lib/bootstrap-treeview.js index bdda5bc03..7a82a2eeb 100644 --- a/tests/lib/bootstrap-treeview.js +++ b/tests/lib/bootstrap-treeview.js @@ -1,5 +1,5 @@ /* ========================================================= - * bootstrap-treeview.js v1.0.2 + * bootstrap-treeview.js v1.2.0 * ========================================================= * Copyright 2013 Jonathan Miles * Project URL : http://www.jondmiles.com/bootstrap-treeview @@ -25,7 +25,9 @@ var pluginName = 'treeview'; - var defaults = { + var _default = {}; + + _default.settings = { injectStyle: true, @@ -34,7 +36,10 @@ expandIcon: 'glyphicon glyphicon-plus', collapseIcon: 'glyphicon glyphicon-minus', emptyIcon: 'glyphicon', - nodeIcon: 'glyphicon glyphicon-stop', + nodeIcon: '', + selectedIcon: '', + checkedIcon: 'glyphicon glyphicon-check', + uncheckedIcon: 'glyphicon glyphicon-unchecked', color: undefined, // '#000000', backColor: undefined, // '#FFFFFF', @@ -49,18 +54,35 @@ highlightSelected: true, highlightSearchResults: true, showBorder: true, + showIcon: true, + showCheckbox: false, showTags: false, multiSelect: false, // Event handlers + onNodeChecked: undefined, onNodeCollapsed: undefined, + onNodeDisabled: undefined, + onNodeEnabled: undefined, onNodeExpanded: undefined, onNodeSelected: undefined, + onNodeUnchecked: undefined, onNodeUnselected: undefined, onSearchComplete: undefined, onSearchCleared: undefined }; + _default.options = { + silent: false, + ignoreChildren: false + }; + + _default.searchOptions = { + ignoreCase: true, + exactMatch: false, + revealResults: true + }; + var Tree = function (element, options) { this.$element = $(element); @@ -82,6 +104,14 @@ getNode: $.proxy(this.getNode, this), getParent: $.proxy(this.getParent, this), getSiblings: $.proxy(this.getSiblings, this), + getSelected: $.proxy(this.getSelected, this), + getUnselected: $.proxy(this.getUnselected, this), + getExpanded: $.proxy(this.getExpanded, this), + getCollapsed: $.proxy(this.getCollapsed, this), + getChecked: $.proxy(this.getChecked, this), + getUnchecked: $.proxy(this.getUnchecked, this), + getDisabled: $.proxy(this.getDisabled, this), + getEnabled: $.proxy(this.getEnabled, this), // Select methods selectNode: $.proxy(this.selectNode, this), @@ -94,6 +124,21 @@ expandAll: $.proxy(this.expandAll, this), expandNode: $.proxy(this.expandNode, this), toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), + revealNode: $.proxy(this.revealNode, this), + + // Expand / collapse methods + checkAll: $.proxy(this.checkAll, this), + checkNode: $.proxy(this.checkNode, this), + uncheckAll: $.proxy(this.uncheckAll, this), + uncheckNode: $.proxy(this.uncheckNode, this), + toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), + + // Disable / enable methods + disableAll: $.proxy(this.disableAll, this), + disableNode: $.proxy(this.disableNode, this), + enableAll: $.proxy(this.enableAll, this), + enableNode: $.proxy(this.enableNode, this), + toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), // Search methods search: $.proxy(this.search, this), @@ -113,7 +158,7 @@ this.tree = $.extend(true, [], options.data); delete options.data; } - this.options = $.extend({}, defaults, options); + this.options = $.extend({}, _default.settings, options); this.destroy(); this.subscribeEvents(); @@ -144,30 +189,16 @@ Tree.prototype.unsubscribeEvents = function () { this.$element.off('click'); - - if (typeof (this.options.onNodeCollapsed) === 'function') { - this.$element.off('nodeCollapsed'); - } - - if (typeof (this.options.onNodeExpanded) === 'function') { - this.$element.off('nodeExpanded'); - } - - if (typeof (this.options.onNodeSelected) === 'function') { - this.$element.off('nodeSelected'); - } - - if (typeof (this.options.onNodeUnselected) === 'function') { - this.$element.off('nodeUnselected'); - } - - if (typeof (this.options.onSearchComplete) === 'function') { - this.$element.off('searchComplete'); - } - - if (typeof (this.options.onSearchCleared) === 'function') { - this.$element.off('searchCleared'); - } + 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 () { @@ -176,10 +207,22 @@ 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); } @@ -188,6 +231,10 @@ 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); } @@ -209,7 +256,7 @@ */ Tree.prototype.setInitialStates = function (node, level) { - if (!node.nodes) { return; } + if (!node.nodes) return; level += 1; var parent = node; @@ -227,12 +274,24 @@ node.selectable = true; } - // where provided we shouuld preserve states + // 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 (level < _this.options.levels) { + if (!node.state.disabled && + (level < _this.options.levels) && + (node.nodes && node.nodes.length > 0)) { node.state.expanded = true; } else { @@ -257,22 +316,32 @@ Tree.prototype.clickHandler = function (event) { - if (!this.options.enableLinks) { event.preventDefault(); } + if (!this.options.enableLinks) event.preventDefault(); - var target = $(event.target), - classList = target.attr('class') ? target.attr('class').split(' ') : [], - node = this.findNode(target); + var target = $(event.target); + var node = this.findNode(target); + if (!node || node.state.disabled) return; + + var classList = target.attr('class') ? target.attr('class').split(' ') : []; + if ((classList.indexOf('expand-icon') !== -1)) { - if ((classList.indexOf('click-expand') != -1) || - (classList.indexOf('click-collapse') != -1)) { - this.toggleExpandedState(node); + this.toggleExpandedState(node, _default.options); + this.render(); } - else if (node) { + else if ((classList.indexOf('check-icon') !== -1)) { + + this.toggleCheckedState(node, _default.options); + this.render(); + } + else { + if (node.selectable) { - this.toggleSelectedState(node); + this.toggleSelectedState(node, _default.options); } else { - this.toggleExpandedState(node); + this.toggleExpandedState(node, _default.options); } + + this.render(); } }; @@ -280,8 +349,8 @@ // data attribute nodeid, which is used to lookup the node in the flattened structure. Tree.prototype.findNode = function (target) { - var nodeId = target.closest('li.list-group-item').attr('data-nodeid'), - node = this.nodes[nodeId]; + 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'); @@ -289,61 +358,126 @@ return node; }; - Tree.prototype.toggleExpandedState = function (node, silent) { + Tree.prototype.toggleExpandedState = function (node, options) { if (!node) return; - this.setExpandedState(node, !node.state.expanded, silent); - this.render(); + this.setExpandedState(node, !node.state.expanded, options); }; - Tree.prototype.setExpandedState = function (node, state, silent) { + Tree.prototype.setExpandedState = function (node, state, options) { - if (state) { + if (state === node.state.expanded) return; + + if (state && node.nodes) { // Expand a node node.state.expanded = true; - if (!silent) { + if (!options.silent) { this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); } } - else { + else if (!state) { // Collapse a node node.state.expanded = false; - if (!silent) { + if (!options.silent) { this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); } + + // Collapse child nodes + if (node.nodes && !options.ignoreChildren) { + $.each(node.nodes, $.proxy(function (index, node) { + this.setExpandedState(node, false, options); + }, this)); + } } }; - Tree.prototype.toggleSelectedState = function (node, silent) { - if (!node) { return; } - this.setSelectedState(node, !node.state.selected, silent); - this.render(); + Tree.prototype.toggleSelectedState = function (node, options) { + if (!node) return; + this.setSelectedState(node, !node.state.selected, options); }; - Tree.prototype.setSelectedState = function (node, state, silent) { + Tree.prototype.setSelectedState = function (node, state, options) { + + if (state === node.state.selected) return; if (state) { // If multiSelect false, unselect previously selected if (!this.options.multiSelect) { $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { - this.setSelectedState(node, false, silent); + this.setSelectedState(node, false, options); }, this)); } // Continue selecting node node.state.selected = true; - if (!silent) { - this.$element.trigger('nodeSelected', $.extend(true, {}, node) ); + if (!options.silent) { + this.$element.trigger('nodeSelected', $.extend(true, {}, node)); } } else { // Unselect node node.state.selected = false; - if (!silent) { - this.$element.trigger('nodeUnselected', $.extend(true, {}, node) ); + if (!options.silent) { + this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.toggleCheckedState = function (node, options) { + if (!node) return; + this.setCheckedState(node, !node.state.checked, options); + }; + + Tree.prototype.setCheckedState = function (node, state, options) { + + if (state === node.state.checked) return; + + if (state) { + + // Check node + node.state.checked = true; + + if (!options.silent) { + this.$element.trigger('nodeChecked', $.extend(true, {}, node)); + } + } + else { + + // Uncheck node + node.state.checked = false; + if (!options.silent) { + this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); + } + } + }; + + Tree.prototype.setDisabledState = function (node, state, options) { + + if (state === node.state.disabled) return; + + if (state) { + + // Disable node + node.state.disabled = true; + + // Disable all other states + this.setExpandedState(node, false, options); + this.setSelectedState(node, false, options); + this.setCheckedState(node, false, options); + + if (!options.silent) { + this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); + } + } + else { + + // Enabled node + node.state.disabled = false; + if (!options.silent) { + this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); } } }; @@ -371,7 +505,7 @@ // structure we build the tree one node at a time Tree.prototype.buildTree = function (nodes, level) { - if (!nodes) { return; } + if (!nodes) return; level += 1; var _this = this; @@ -379,8 +513,10 @@ var treeItem = $(_this.template.item) .addClass('node-' + _this.elementId) + .addClass(node.state.checked ? 'node-checked' : '') + .addClass(node.state.disabled ? 'node-disabled': '') .addClass(node.state.selected ? 'node-selected' : '') - .addClass(node.searchResult ? 'search-result' : '') + .addClass(node.searchResult ? 'search-result' : '') .attr('data-nodeid', node.nodeId) .attr('style', _this.buildStyleOverride(node)); @@ -390,35 +526,61 @@ } // Add expand, collapse or empty spacer icons + var classList = []; if (node.nodes) { - if (!node.state.expanded) { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass('click-expand') - .addClass(_this.options.expandIcon) - ); - } - else { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass('click-collapse') - .addClass(_this.options.collapseIcon) - ); - } + classList.push('expand-icon'); + if (node.state.expanded) { + classList.push(_this.options.collapseIcon); + } + else { + classList.push(_this.options.expandIcon); + } } else { - treeItem - .append($(_this.template.expandCollapseIcon) - .addClass(_this.options.emptyIcon) - ); + classList.push(_this.options.emptyIcon); } - // Add node icon treeItem .append($(_this.template.icon) - .addClass(node.icon ? node.icon : _this.options.nodeIcon) + .addClass(classList.join(' ')) ); + + // Add node icon + if (_this.options.showIcon) { + + var classList = ['node-icon']; + + classList.push(node.icon || _this.options.nodeIcon); + if (node.state.selected) { + classList.pop(); + classList.push(node.selectedIcon || _this.options.selectedIcon || + node.icon || _this.options.nodeIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + + // Add check / unchecked icon + if (_this.options.showCheckbox) { + + var classList = ['check-icon']; + if (node.state.checked) { + classList.push(_this.options.checkedIcon); + } + else { + classList.push(_this.options.uncheckedIcon); + } + + treeItem + .append($(_this.template.icon) + .addClass(classList.join(' ')) + ); + } + // Add text if (_this.options.enableLinks) { // Add hyperlink @@ -448,8 +610,7 @@ _this.$wrapper.append(treeItem); // Recursively add child ndoes - // console.log(node.text + ' ' + node.state.expanded); - if (node.nodes && node.state.expanded) { + if (node.nodes && node.state.expanded && !node.state.disabled) { return _this.buildTree(node.nodes, level); } }); @@ -460,6 +621,8 @@ // 2. node|data assigned color overrides Tree.prototype.buildStyleOverride = function (node) { + if (node.state.disabled) return ''; + var color = node.color; var backColor = node.backColor; @@ -472,7 +635,7 @@ } } - if (this.options.highlightSearchResults && node.searchResult) { + if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { if (this.options.searchResultColor) { color = this.options.searchResultColor; } @@ -497,12 +660,15 @@ Tree.prototype.buildStyle = function () { var style = '.node-' + this.elementId + '{'; + if (this.options.color) { style += 'color:' + this.options.color + ';'; } + if (this.options.backColor) { style += 'background-color:' + this.options.backColor + ';'; } + if (!this.options.showBorder) { style += 'border:none;'; } @@ -512,8 +678,8 @@ style += '}'; if (this.options.onhoverColor) { - style += '.node-' + this.elementId + ':hover{' + - 'background-color:' + this.options.onhoverColor + ';' + + style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + + 'background-color:' + this.options.onhoverColor + ';' + '}'; } @@ -524,13 +690,12 @@ list: '
          ', item: '
        • ', indent: '', - expandCollapseIcon: '', icon: '', link: '', badge: '' }; - Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.expand-collapse{width:1rem;height:1rem}.treeview span.icon{margin-left:10px;margin-right:5px}' + 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}' /** @@ -545,7 +710,7 @@ /** Returns the parent node of a given node, if valid otherwise returns undefined. @param {Object|Number} identifier - A valid node or node id - @returns {Object} parent - The parent node + @returns {Object} node - The parent node */ Tree.prototype.getParent = function (identifier) { var node = this.identifyNode(identifier); @@ -555,7 +720,7 @@ /** Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. @param {Object|Number} identifier - A valid node or node id - @returns {Array} siblings - Sibling nodes + @returns {Array} nodes - Sibling nodes */ Tree.prototype.getSiblings = function (identifier) { var node = this.identifyNode(identifier); @@ -566,37 +731,108 @@ }); }; + /** + Returns an array of selected nodes. + @returns {Array} nodes - Selected nodes + */ + Tree.prototype.getSelected = function () { + return this.findNodes('true', 'g', 'state.selected'); + }; + + /** + Returns an array of unselected nodes. + @returns {Array} nodes - Unselected nodes + */ + Tree.prototype.getUnselected = function () { + return this.findNodes('false', 'g', 'state.selected'); + }; + + /** + Returns an array of expanded nodes. + @returns {Array} nodes - Expanded nodes + */ + Tree.prototype.getExpanded = function () { + return this.findNodes('true', 'g', 'state.expanded'); + }; + + /** + Returns an array of collapsed nodes. + @returns {Array} nodes - Collapsed nodes + */ + Tree.prototype.getCollapsed = function () { + return this.findNodes('false', 'g', 'state.expanded'); + }; + + /** + Returns an array of checked nodes. + @returns {Array} nodes - Checked nodes + */ + Tree.prototype.getChecked = function () { + return this.findNodes('true', 'g', 'state.checked'); + }; + + /** + Returns an array of unchecked nodes. + @returns {Array} nodes - Unchecked nodes + */ + Tree.prototype.getUnchecked = function () { + return this.findNodes('false', 'g', 'state.checked'); + }; + + /** + Returns an array of disabled nodes. + @returns {Array} nodes - Disabled nodes + */ + Tree.prototype.getDisabled = function () { + return this.findNodes('true', 'g', 'state.disabled'); + }; + + /** + Returns an array of enabled nodes. + @returns {Array} nodes - Enabled nodes + */ + Tree.prototype.getEnabled = function () { + return this.findNodes('false', 'g', 'state.disabled'); + }; + /** Set a node state to selected - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.selectNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setSelectedState(this.identifyNode(identifier), true, silent); + Tree.prototype.selectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, true, options); + }, this)); + this.render(); }; /** Set a node state to unselected - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.unselectNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setSelectedState(this.identifyNode(identifier), false, silent); + Tree.prototype.unselectNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setSelectedState(node, false, options); + }, this)); + this.render(); }; /** Toggles a node selected state; selecting if unselected, unselecting if selected. - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.toggleNodeSelected = function (identifier, options) { - this.toggleSelectedState(this.identifyNode(identifier), - this.isSilent(options)); + Tree.prototype.toggleNodeSelected = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleSelectedState(node, options); + }, this)); + + this.render(); }; @@ -605,10 +841,9 @@ @param {optional Object} options */ Tree.prototype.collapseAll = function (options) { - var silent = this.isSilent(options); - - $.each(this.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, false, silent); + var identifiers = this.findNodes('true', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); }, this)); this.render(); @@ -616,12 +851,14 @@ /** Collapse a given tree node - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.collapseNode = function (identifier, options) { - var silent = this.isSilent(options); - this.setExpandedState(this.identifyNode(identifier), false, silent); + Tree.prototype.collapseNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, false, options); + }, this)); + this.render(); }; @@ -630,14 +867,15 @@ @param {optional Object} options */ Tree.prototype.expandAll = function (options) { - var silent = this.isSilent(options); + options = $.extend({}, _default.options, options); if (options && options.levels) { - this.expandLevels(this.tree, options.levels, silent); + this.expandLevels(this.tree, options.levels, options); } else { - $.each(this.nodes, $.proxy(function (index, node) { - this.setExpandedState(node, true, silent); + var identifiers = this.findNodes('false', 'g', 'state.expanded'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); }, this)); } @@ -646,54 +884,219 @@ /** Expand a given tree node - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.expandNode = function (identifier, options) { - var silent = this.isSilent(options); - - var node = this.identifyNode(identifier); - this.setExpandedState(node, true, silent); - - if (node.nodes && (options && options.levels)) { - this.expandLevels(node.nodes, options.levels-1, silent); - } + Tree.prototype.expandNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setExpandedState(node, true, options); + if (node.nodes && (options && options.levels)) { + this.expandLevels(node.nodes, options.levels-1, options); + } + }, this)); this.render(); }; - Tree.prototype.expandLevels = function (nodes, level, silent) { + Tree.prototype.expandLevels = function (nodes, level, options) { + options = $.extend({}, _default.options, options); + $.each(nodes, $.proxy(function (index, node) { - this.setExpandedState(node, (level > 0) ? true : false) + this.setExpandedState(node, (level > 0) ? true : false, options); if (node.nodes) { - this.expandLevels(node.nodes, level-1, silent); + this.expandLevels(node.nodes, level-1, options); } }, this)); }; + /** + Reveals a given tree node, expanding the tree from node to root. + @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.revealNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + var parentNode = this.getParent(node); + while (parentNode) { + this.setExpandedState(parentNode, true, options); + parentNode = this.getParent(parentNode); + }; + }, this)); + + this.render(); + }; + /** Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. - @param {Object|Number} identifier - A valid node or node id + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers @param {optional Object} options */ - Tree.prototype.toggleNodeExpanded = function (identifier, options) { - this.toggleExpandedState(this.identifyNode(identifier), - this.isSilent(options)); + Tree.prototype.toggleNodeExpanded = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleExpandedState(node, options); + }, this)); + + this.render(); }; + + /** + Check all tree nodes + @param {optional Object} options + */ + Tree.prototype.checkAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Check a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.checkNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Uncheck all tree nodes + @param {optional Object} options + */ + Tree.prototype.uncheckAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.checked'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Uncheck a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.uncheckNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setCheckedState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes checked state; checking if unchecked, unchecking if checked. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeChecked = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.toggleCheckedState(node, options); + }, this)); + + this.render(); + }; + + + /** + Disable all tree nodes + @param {optional Object} options + */ + Tree.prototype.disableAll = function (options) { + var identifiers = this.findNodes('false', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Disable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.disableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, true, options); + }, this)); + + this.render(); + }; + + /** + Enable all tree nodes + @param {optional Object} options + */ + Tree.prototype.enableAll = function (options) { + var identifiers = this.findNodes('true', 'g', 'state.disabled'); + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Enable a given tree node + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.enableNode = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, false, options); + }, this)); + + this.render(); + }; + + /** + Toggles a nodes disabled state; disabling is enabled, enabling if disabled. + @param {Object|Number} identifiers - A valid node, node id or array of node identifiers + @param {optional Object} options + */ + Tree.prototype.toggleNodeDisabled = function (identifiers, options) { + this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { + this.setDisabledState(node, !node.state.disabled, options); + }, this)); + + this.render(); + }; + + + /** + Common code for processing multiple identifiers + */ + Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { + + options = $.extend({}, _default.options, options); + + if (!(identifiers instanceof Array)) { + identifiers = [identifiers]; + } + + $.each(identifiers, $.proxy(function (index, identifier) { + callback(this.identifyNode(identifier), options); + }, this)); + }; + + /* + Identifies a node from either a node id or object + */ Tree.prototype.identifyNode = function (identifier) { return ((typeof identifier) === 'number') ? this.nodes[identifier] : identifier; }; - Tree.prototype.isSilent = function (options) { - return (options && options.hasOwnProperty('silent')) ? - options.silent : - false; - }; - - /** Searches the tree for nodes (text) that match given criteria @param {String} pattern - A given string to match against @@ -701,8 +1104,9 @@ @return {Array} nodes - Matching nodes */ Tree.prototype.search = function (pattern, options) { + options = $.extend({}, _default.searchOptions, options); - this.clearSearch(); + this.clearSearch({ render: false }); var results = []; if (pattern && pattern.length > 0) { @@ -717,10 +1121,21 @@ } results = this.findNodes(pattern, modifier); + + // Add searchResult property to all matching nodes + // This will be used to apply custom styles + // and when identifying result to be cleared $.each(results, function (index, node) { node.searchResult = true; }) + } + // If revealResults, then render is triggered from revealNode + // otherwise we just call render. + if (options.revealResults) { + this.revealNode(results); + } + else { this.render(); } @@ -732,14 +1147,18 @@ /** Clears previous search results */ - Tree.prototype.clearSearch = function () { + Tree.prototype.clearSearch = function (options) { + + options = $.extend({}, { render: true }, options); var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { node.searchResult = false; }); - this.render(); - + if (options.render) { + this.render(); + } + this.$element.trigger('searchCleared', $.extend(true, {}, results)); }; @@ -789,10 +1208,10 @@ }; var logError = function (message) { - if(window.console) { - window.console.error(message); - } - }; + if (window.console) { + window.console.error(message); + } + }; // Prevent against multiple instantiations, // handle updates and method calls diff --git a/tests/tests.js b/tests/tests.js index ece522cef..421b789e2 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -91,7 +91,10 @@ equal(options.expandIcon, 'glyphicon glyphicon-plus', 'expandIcon default ok'); equal(options.collapseIcon, 'glyphicon glyphicon-minus', 'collapseIcon default ok'); equal(options.emptyIcon, 'glyphicon', 'emptyIcon default ok'); - equal(options.nodeIcon, 'glyphicon glyphicon-stop', 'nodeIcon default ok'); + equal(options.nodeIcon, '', 'nodeIcon default ok'); + equal(options.selectedIcon, '', 'selectedIcon default ok'); + equal(options.checkedIcon, 'glyphicon glyphicon-check', 'checkedIcon default ok'); + equal(options.uncheckedIcon, 'glyphicon glyphicon-unchecked', 'uncheckedIcon default ok'); equal(options.color, undefined, 'color default ok'); equal(options.backColor, undefined, 'backColor default ok'); equal(options.borderColor, undefined, 'borderColor default ok'); @@ -104,11 +107,17 @@ equal(options.highlightSelected, true, 'highlightSelected default ok'); equal(options.highlightSearchResults, true, 'highlightSearchResults default ok'); equal(options.showBorder, true, 'showBorder default ok'); + equal(options.showIcon, true, 'showIcon default ok'); + equal(options.showCheckbox, false, 'showCheckbox default ok'); equal(options.showTags, false, 'showTags default ok'); equal(options.multiSelect, false, 'multiSelect default ok'); + equal(options.onNodeChecked, null, 'onNodeChecked default ok'); equal(options.onNodeCollapsed, null, 'onNodeCollapsed default ok'); + equal(options.onNodeDisabled, null, 'onNodeDisabled default ok'); + equal(options.onNodeEnabled, null, 'onNodeEnabled default ok'); equal(options.onNodeExpanded, null, 'onNodeExpanded default ok'); equal(options.onNodeSelected, null, 'onNodeSelected default ok'); + equal(options.onNodeUnchecked, null, 'onNodeUnchecked default ok'); equal(options.onNodeUnselected, null, 'onNodeUnselected default ok'); equal(options.onSearchComplete, null, 'onSearchComplete default ok'); equal(options.onSearchCleared, null, 'onSearchCleared default ok'); @@ -119,7 +128,10 @@ expandIcon: 'glyphicon glyphicon-expand', collapseIcon: 'glyphicon glyphicon-collapse', emptyIcon: 'glyphicon', - nodeIcon: 'glyphicon glyphicon-node', + nodeIcon: 'glyphicon glyphicon-stop', + selectedIcon: 'glyphicon glyphicon-selected', + checkedIcon: 'glyphicon glyphicon-checked-icon', + uncheckedIcon: 'glyphicon glyphicon-unchecked-icon', color: 'yellow', backColor: 'purple', borderColor: 'purple', @@ -132,11 +144,17 @@ highlightSelected: false, highlightSearchResults: true, showBorder: false, + showIcon: false, + showCheckbox: true, showTags: true, multiSelect: true, + onNodeChecked: function () {}, onNodeCollapsed: function () {}, + onNodeDisabled: function () {}, + onNodeEnabled: function () {}, onNodeExpanded: function () {}, onNodeSelected: function () {}, + onNodeUnchecked: function () {}, onNodeUnselected: function () {}, onSearchComplete: function () {}, onSearchCleared: function () {} @@ -148,7 +166,10 @@ equal(options.expandIcon, 'glyphicon glyphicon-expand', 'expandIcon set ok'); equal(options.collapseIcon, 'glyphicon glyphicon-collapse', 'collapseIcon set ok'); equal(options.emptyIcon, 'glyphicon', 'emptyIcon set ok'); - equal(options.nodeIcon, 'glyphicon glyphicon-node', 'nodeIcon set ok'); + equal(options.nodeIcon, 'glyphicon glyphicon-stop', 'nodeIcon set ok'); + equal(options.selectedIcon, 'glyphicon glyphicon-selected', 'selectedIcon set ok'); + equal(options.checkedIcon, 'glyphicon glyphicon-checked-icon', 'checkedIcon set ok'); + equal(options.uncheckedIcon, 'glyphicon glyphicon-unchecked-icon', 'uncheckedIcon set ok'); equal(options.color, 'yellow', 'color set ok'); equal(options.backColor, 'purple', 'backColor set ok'); equal(options.borderColor, 'purple', 'borderColor set ok'); @@ -161,11 +182,17 @@ equal(options.highlightSelected, false, 'highlightSelected set ok'); equal(options.highlightSearchResults, true, 'highlightSearchResults set ok'); equal(options.showBorder, false, 'showBorder set ok'); + equal(options.showIcon, false, 'showIcon set ok'); + equal(options.showCheckbox, true, 'showCheckbox set ok'); equal(options.showTags, true, 'showTags set ok'); equal(options.multiSelect, true, 'multiSelect set ok'); + equal(typeof options.onNodeChecked, 'function', 'onNodeChecked set ok'); equal(typeof options.onNodeCollapsed, 'function', 'onNodeCollapsed set ok'); + equal(typeof options.onNodeDisabled, 'function', 'onNodeDisabled set ok'); + equal(typeof options.onNodeEnabled, 'function', 'onNodeEnabled set ok'); equal(typeof options.onNodeExpanded, 'function', 'onNodeExpanded set ok'); equal(typeof options.onNodeSelected, 'function', 'onNodeSelected set ok'); + equal(typeof options.onNodeUnchecked, 'function', 'onNodeUnchecked set ok'); equal(typeof options.onNodeUnselected, 'function', 'onNodeUnselected set ok'); equal(typeof options.onSearchComplete, 'function', 'onSearchComplete set ok'); equal(typeof options.onSearchCleared, 'function', 'onSearchCleared set ok'); @@ -211,6 +238,7 @@ var cbWorked, onWorked = false; init({ data: data, + levels: 1, onNodeExpanded: function(/*event, date*/) { cbWorked = true; } @@ -220,7 +248,7 @@ }); var nodeCount = $('.list-group-item').length; - var el = $('.click-expand:first'); + var el = $('.expand-icon:first'); el.trigger('click'); ok(($('.list-group-item').length > nodeCount), 'Number of nodes are increased, so node must have expanded'); ok(cbWorked, 'onNodeExpanded function was called'); @@ -232,6 +260,7 @@ var cbWorked, onWorked = false; init({ data: data, + levels: 2, onNodeCollapsed: function(/*event, date*/) { cbWorked = true; } @@ -241,7 +270,7 @@ }); var nodeCount = $('.list-group-item').length; - var el = $('.click-collapse:first'); + var el = $('.expand-icon:first'); el.trigger('click'); ok(($('.list-group-item').length < nodeCount), 'Number of nodes has decreased, so node must have collapsed'); ok(cbWorked, 'onNodeCollapsed function was called'); @@ -251,7 +280,7 @@ test('Selecting a node', function () { var cbWorked, onWorked = false; - init({ + var $tree = init({ data: data, onNodeSelected: function(/*event, date*/) { cbWorked = true; @@ -260,38 +289,30 @@ .on('nodeSelected', function(/*event, date*/) { onWorked = true; }); + var options = getOptions($tree); - var el = $('.list-group-item:first'); - el.trigger('click'); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') !== -1), 'Node is correctly selected : class "node-selected" added'); - ok(($('.node-selected').length === 1), 'There is only one selected node'); - ok(cbWorked, 'onNodeSelected function was called'); - ok(onWorked, 'nodeSelected was fired'); - }); - - test('Selecting multiple nodes (multiSelect true)', function () { + // Simulate click + $('.list-group-item:first').trigger('click'); - init({ - data: data, - multiSelect: true - }); - - var $firstEl = $('.list-group-item:nth-child(1)').trigger('click'); - var $secondEl = $('.list-group-item:nth-child(2)').trigger('click'); + // Has class node-selected + ok($('.list-group-item:first').hasClass('node-selected'), 'Node is correctly selected : class "node-selected" added'); + + // Only one can be selected + ok(($('.node-selected').length === 1), 'There is only one selected node'); - $firstEl = $('.list-group-item:nth-child(1)'); - $secondEl = $('.list-group-item:nth-child(2)'); + // Has correct icon + var iconClass = options.selectedIcon || options.nodeIcon; + ok(!iconClass || $('.expand-icon:first').hasClass(iconClass), 'Node icon is correct'); - ok(($firstEl.attr('class').split(' ').indexOf('node-selected') !== -1), 'First node is correctly selected : class "node-selected" added'); - ok(($secondEl.attr('class').split(' ').indexOf('node-selected') !== -1), 'Second node is correctly selected : class "node-selected" added'); - ok(($('.node-selected').length === 2), 'There are two selected nodes'); + // Events triggered + ok(cbWorked, 'onNodeSelected function was called'); + ok(onWorked, 'nodeSelected was fired'); }); test('Unselecting a node', function () { var cbWorked, onWorked = false; - init({ + var $tree = init({ data: data, onNodeUnselected: function(/*event, date*/) { cbWorked = true; @@ -300,23 +321,48 @@ .on('nodeUnselected', function(/*event, date*/) { onWorked = true; }); + var options = getOptions($tree); // First select a node - var el = $('.list-group-item:first'); - el.trigger('click'); - - // Then test unselect by simulating another click + $('.list-group-item:first').trigger('click'); cbWorked = onWorked = false; - el = $('.list-group-item:first'); - el.trigger('click'); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') === -1), 'Node is correctly unselected : class "node-selected" removed'); + + // Simulate click + $('.list-group-item:first').trigger('click'); + + // Has class node-selected + ok(!$('.list-group-item:first').hasClass('node-selected'), 'Node is correctly unselected : class "node-selected" removed'); + + // Only one can be selected ok(($('.node-selected').length === 0), 'There are no selected nodes'); + + // Has correct icon + ok(!options.nodeIcon || $('.expand-icon:first').hasClass(options.nodeIcon), 'Node icon is correct'); + + // Events triggered ok(cbWorked, 'onNodeUnselected function was called'); ok(onWorked, 'nodeUnselected was fired'); }); - test('Clicking a non-selectable, colllapsed node expands the node', function () { + test('Selecting multiple nodes (multiSelect true)', function () { + + init({ + data: data, + multiSelect: true + }); + + var $firstEl = $('.list-group-item:nth-child(1)').trigger('click'); + var $secondEl = $('.list-group-item:nth-child(2)').trigger('click'); + + $firstEl = $('.list-group-item:nth-child(1)'); + $secondEl = $('.list-group-item:nth-child(2)'); + + ok($firstEl.hasClass('node-selected'), 'First node is correctly selected : class "node-selected" added'); + ok($secondEl.hasClass('node-selected'), 'Second node is correctly selected : class "node-selected" added'); + ok(($('.node-selected').length === 2), 'There are two selected nodes'); + }); + + test('Clicking a non-selectable, collapsed node expands the node', function () { var testData = $.extend(true, {}, data); testData[0].selectable = false; @@ -360,22 +406,81 @@ var nodeCount = $('.list-group-item').length; var el = $('.list-group-item:first'); - // console.log(el); el.trigger('click'); el = $('.list-group-item:first'); + ok(!el.hasClass('node-selected'), 'Node should not be selected'); ok(!cbCalled, 'onNodeSelected function should not be called'); ok(!onCalled, 'nodeSelected should not fire'); ok(($('.list-group-item').length < nodeCount), 'Number of nodes has decreased, so node must have collapsed'); }); + test('Checking a node', function () { + + // setup test + var cbWorked, onWorked = false; + var $tree = init({ + data: data, + showCheckbox: true, + onNodeChecked: function(/*event, date*/) { + cbWorked = true; + } + }) + .on('nodeChecked', function(/*event, date*/) { + onWorked = true; + }); + var options = getOptions($tree); + + // simulate click event on check icon + var $el = $('.check-icon:first'); + $el.trigger('click'); + + // check state is correct + $el = $('.check-icon:first'); + ok(($el.attr('class').indexOf(options.checkedIcon) !== -1), 'Node is checked : icon is correct'); + ok(cbWorked, 'onNodeChecked function was called'); + ok(onWorked, 'nodeChecked was fired'); + }); + + test('Unchecking a node', function () { + + // setup test + var cbWorked, onWorked = false; + var $tree = init({ + data: data, + showCheckbox: true, + onNodeUnchecked: function(/*event, date*/) { + cbWorked = true; + } + }) + .on('nodeUnchecked', function(/*event, date*/) { + onWorked = true; + }); + var options = getOptions($tree); + + // first check a node + var $el = $('.check-icon:first'); + $el.trigger('click'); + + // then simulate unchecking a node + cbWorked = onWorked = false; + $el = $('.check-icon:first'); + $el.trigger('click'); + + // check state is correct + $el = $('.check-icon:first'); + ok(($el.attr('class').indexOf(options.uncheckedIcon) !== -1), 'Node is unchecked : icon is correct'); + ok(cbWorked, 'onNodeUnchecked function was called'); + ok(onWorked, 'nodeUnchecked was fired'); + }); + module('Methods'); test('getNode', function () { var $tree = init({ data: data }); var nodeParent1 = $tree.treeview('getNode', 0); - equal(nodeParent1.text, 'Parent 1', 'Correct node returned : requested "Parent 1", for "Parent 1"'); + equal(nodeParent1.text, 'Parent 1', 'Correct node returned : requested "Parent 1", got "Parent 1"'); }); test('getParent', function () { @@ -412,53 +517,238 @@ ok(results, 'Correct siblings for "Child 1" [non root] : results OK'); }); - test('selectNode / unselectNode', function () { + test('getSelected', function () { + var $tree = init({ data: data }) + .treeview('selectNode', 0); + + var selectedNodes = $tree.treeview('getSelected'); + ok((selectedNodes instanceof Array), 'Result is an array'); + equal(selectedNodes.length, 1, 'Correct number of nodes returned'); + equal(selectedNodes[0].text, 'Parent 1', 'Correct node returned'); + }); + + test('getUnselected', function () { + var $tree = init({ data: data }) + .treeview('selectNode', 0); + + var unselectedNodes = $tree.treeview('getUnselected'); + ok((unselectedNodes instanceof Array), 'Result is an array'); + equal(unselectedNodes.length, 8, 'Correct number of nodes returned'); + }); + + // Assumptions: + // Default tree + expanded to 2 levels, + // means 1 node 'Parent 1' should be expanded and therefore returned + test('getExpanded', function () { var $tree = init({ data: data }); - var el; + var expandedNodes = $tree.treeview('getExpanded'); + ok((expandedNodes instanceof Array), 'Result is an array'); + equal(expandedNodes.length, 1, 'Correct number of nodes returned'); + equal(expandedNodes[0].text, 'Parent 1', 'Correct node returned'); + }); + + // Assumptions: + // Default tree + expanded to 2 levels, means only 'Parent 1' should be expanded + // as all other parent nodes have no children their state will be collapsed + // which means 8 of the 9 nodes should be returned + test('getCollapsed', function () { + var $tree = init({ data: data }); + var collapsedNodes = $tree.treeview('getCollapsed'); + ok((collapsedNodes instanceof Array), 'Result is an array'); + equal(collapsedNodes.length, 8, 'Correct number of nodes returned'); + }); + + test('getChecked', function () { + var $tree = init({ data: data, showCheckbox: true }) + .treeview('checkNode', 0); + + var checkedNodes = $tree.treeview('getChecked'); + ok((checkedNodes instanceof Array), 'Result is an array'); + equal(checkedNodes.length, 1, 'Correct number of nodes returned'); + equal(checkedNodes[0].text, 'Parent 1', 'Correct node returned'); + }); + + test('getUnchecked', function () { + var $tree = init({ data: data }) + .treeview('checkNode', 0); + + var uncheckedNodes = $tree.treeview('getUnchecked'); + ok((uncheckedNodes instanceof Array), 'Result is an array'); + equal(uncheckedNodes.length, 8, 'Correct number of nodes returned'); + }); + + test('getDisabled', function () { + var $tree = init({ data: data }) + .treeview('disableNode', 0); + + var disabledNodes = $tree.treeview('getDisabled'); + ok((disabledNodes instanceof Array), 'Result is an array'); + equal(disabledNodes.length, 1, 'Correct number of nodes returned'); + equal(disabledNodes[0].text, 'Parent 1', 'Correct node returned'); + }); + + test('getEnabled', function () { + var $tree = init({ data: data }) + .treeview('disableNode', 0); + + var enabledNodes = $tree.treeview('getEnabled'); + ok((enabledNodes instanceof Array), 'Result is an array'); + equal(enabledNodes.length, 8, 'Correct number of nodes returned'); + }); + + test('disableAll / enableAll', function () { + var $tree = init({ data: data, levels: 1 }); + + $tree.treeview('disableAll'); + equal($($tree.selector + ' ul li.node-disabled').length, 5, 'Disable all works, 9 nodes with node-disabled class'); + + $tree.treeview('enableAll'); + equal($($tree.selector + ' ul li.node-disabled').length, 0, 'Check all works, 9 nodes non with node-disabled class'); + }); + + test('disableNode / enableNode', function () { + var $tree = init({ data: data, levels: 1 }); + var nodeId = 0; + var node = $tree.treeview('getNode', 0); + + // Disable node using node id + $tree.treeview('disableNode', nodeId); + ok($('.list-group-item:first').hasClass('node-disabled'), 'Disable node (by id) : Node has class node-disabled'); + ok(($('.node-disabled').length === 1), 'Disable node (by id) : There is only one disabled node'); + + // Enable node using node id + $tree.treeview('enableNode', nodeId); + ok(!$('.list-group-item:first').hasClass('node-disabled'), 'Enable node (by id) : Node does not have class node-disabled'); + ok(($('.node-checked').length === 0), 'Enable node (by id) : There are no disabled nodes'); + + // Disable node using node + $tree.treeview('disableNode', node); + ok($('.list-group-item:first').hasClass('node-disabled'), 'Disable node (by node) : Node has class node-disabled'); + ok(($('.node-disabled').length === 1), 'Disable node (by node) : There is only one disabled node'); + + // Enable node using node + $tree.treeview('enableNode', node); + ok(!$('.list-group-item:first').hasClass('node-disabled'), 'Enable node (by node) : Node does not have class node-disabled'); + ok(($('.node-checked').length === 0), 'Enable node (by node) : There are no disabled nodes'); + }); + + test('toggleNodeDisabled', function () { + var $tree = init({ data: data, levels: 1 }); + var nodeId = 0; + var node = $tree.treeview('getNode', 0); + + // Toggle disabled using node id + $tree.treeview('toggleNodeDisabled', nodeId); + ok($('.list-group-item:first').hasClass('node-disabled'), 'Toggle node (by id) : Node has class node-disabled'); + ok(($('.node-disabled').length === 1), 'Toggle node (by id) : There is only one disabled node'); + + // Toggle disabled using node + $tree.treeview('toggleNodeDisabled', node); + ok(!$('.list-group-item:first').hasClass('node-disabled'), 'Toggle node (by node) : Node does not have class node-disabled'); + ok(($('.node-disabled').length === 0), 'Toggle node (by node) : There are no disabled nodes'); + }); + + test('checkAll / uncheckAll', function () { + var $tree = init({ data: data, levels: 3, showCheckbox: true }); + + $tree.treeview('checkAll'); + equal($($tree.selector + ' ul li.node-checked').length, 9, 'Check all works, 9 nodes with node-checked class'); + equal($($tree.selector + ' ul li .glyphicon-check').length, 9, 'Check all works, 9 nodes with glyphicon-check icon'); + + $tree.treeview('uncheckAll'); + equal($($tree.selector + ' ul li.node-checked').length, 0, 'Check all works, 9 nodes non with node-checked class'); + equal($($tree.selector + ' ul li .glyphicon-unchecked').length, 9, 'Check all works, 9 nodes with glyphicon-unchecked icon'); + }); + + test('checkNode / uncheckNode', function () { + var $tree = init({ data: data, showCheckbox: true }); + var options = getOptions($tree); + var nodeId = 0; + var node = $tree.treeview('getNode', 0); + + // Check node using node id + $tree.treeview('checkNode', nodeId); + ok($('.list-group-item:first').hasClass('node-checked'), 'Check node (by id) : Node has class node-checked'); + ok(($('.node-checked').length === 1), 'Check node (by id) : There is only one checked node'); + ok($('.check-icon:first').hasClass(options.checkedIcon), 'Check node (by id) : Node icon is correct'); + + // Uncheck node using node id + $tree.treeview('uncheckNode', nodeId); + ok(!$('.list-group-item:first').hasClass('node-checked'), 'Uncheck node (by id) : Node does not have class node-checked'); + ok(($('.node-checked').length === 0), 'Uncheck node (by id) : There are no checked nodes'); + ok($('.check-icon:first').hasClass(options.uncheckedIcon), 'Uncheck node (by id) : Node icon is correct'); + + // Check node using node + $tree.treeview('checkNode', node); + ok($('.list-group-item:first').hasClass('node-checked'), 'Check node (by node) : Node has class node-checked'); + ok(($('.node-checked').length === 1), 'Check node (by node) : There is only one checked node'); + ok($('.check-icon:first').hasClass(options.checkedIcon), 'Check node (by node) : Node icon is correct'); + + // Uncheck node using node + $tree.treeview('uncheckNode', node); + ok(!$('.list-group-item:first').hasClass('node-checked'), 'Uncheck node (by node) : Node does not have class node-checked'); + ok(($('.node-checked').length === 0), 'Uncheck node (by node) : There are no checked nodes'); + ok($('.check-icon:first').hasClass(options.uncheckedIcon), 'Uncheck node (by node) : Node icon is correct'); + }); + + test('toggleNodeChecked', function () { + var $tree = init({ data: data, showCheckbox: true }); + var options = getOptions($tree); + var nodeId = 0; + var node = $tree.treeview('getNode', 0); + + // Toggle checked using node id + $tree.treeview('toggleNodeChecked', nodeId); + ok($('.list-group-item:first').hasClass('node-checked'), 'Toggle node (by id) : Node has class node-checked'); + ok(($('.node-checked').length === 1), 'Toggle node (by id) : There is only one checked node'); + ok($('.check-icon:first').hasClass(options.checkedIcon), 'Toggle node (by id) : Node icon is correct'); + + // Toggle checked using node + $tree.treeview('toggleNodeChecked', node); + ok(!$('.list-group-item:first').hasClass('node-checked'), 'Toggle node (by node) : Node does not have class node-checked'); + ok(($('.node-checked').length === 0), 'Toggle node (by node) : There are no checked nodes'); + ok($('.check-icon:first').hasClass(options.uncheckedIcon), 'Toggle node (by node) : Node icon is correct'); + }); + + test('selectNode / unselectNode', function () { + var $tree = init({ data: data, selectedIcon: 'glyphicon glyphicon-selected' }); var nodeId = 0; var node = $tree.treeview('getNode', 0); // Select node using node id $tree.treeview('selectNode', nodeId); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') !== -1), 'Select node (by id) : Node is selected'); + ok($('.list-group-item:first').hasClass('node-selected'), 'Select node (by id) : Node has class node-selected'); ok(($('.node-selected').length === 1), 'Select node (by id) : There is only one selected node'); // Unselect node using node id $tree.treeview('unselectNode', nodeId); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') === -1), 'Select node (by id) : Node is no longer selected'); - ok(($('.node-selected').length === 0), 'Select node (by id) : There are no selected nodes'); + ok(!$('.list-group-item:first').hasClass('node-selected'), 'Unselect node (by id) : Node does not have class node-selected'); + ok(($('.node-selected').length === 0), 'Unselect node (by id) : There are no selected nodes'); // Select node using node $tree.treeview('selectNode', node); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') !== -1), 'Select node (by node) : Node is selected'); + ok($('.list-group-item:first').hasClass('node-selected'), 'Select node (by node) : Node has class node-selected'); ok(($('.node-selected').length === 1), 'Select node (by node) : There is only one selected node'); // Unselect node using node id $tree.treeview('unselectNode', node); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') === -1), 'Select node (by node) : Node is no longer selected'); - ok(($('.node-selected').length === 0), 'Select node (by node) : There are no selected nodes'); + ok(!$('.list-group-item:first').hasClass('node-selected'), 'Unselect node (by node) : Node does not have class node-selected'); + ok(($('.node-selected').length === 0), 'Unselect node (by node) : There are no selected nodes'); }); test('toggleNodeSelected', function () { var $tree = init({ data: data }); - var el; var nodeId = 0; var node = $tree.treeview('getNode', 0); // Toggle selected using node id $tree.treeview('toggleNodeSelected', nodeId); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') !== -1), 'Toggle node (by id) : Node is selected'); + ok($('.list-group-item:first').hasClass('node-selected'), 'Toggle node (by id) : Node has class node-selected'); ok(($('.node-selected').length === 1), 'Toggle node (by id) : There is only one selected node'); // Toggle selected using node $tree.treeview('toggleNodeSelected', node); - el = $('.list-group-item:first'); - ok((el.attr('class').split(' ').indexOf('node-selected') === -1), 'Toggle node (by node) : Node is unselected'); + ok(!$('.list-group-item:first').hasClass('node-selected'), 'Toggle node (by id) : Node does not have class node-selected'); ok(($('.node-selected').length === 0), 'Toggle node (by node) : There are no selected nodes'); }); @@ -515,6 +805,17 @@ equal($($tree.selector + ' ul li').length, 9, 'Expand node (levels = 2, by node) works, 9 nodes displayed'); }); + test('revealNode', function () { + var $tree = init({ data: data, levels: 1 }); + + $tree.treeview('revealNode', 1); // Child_1 + equal($($tree.selector + ' ul li').length, 7, 'Reveal node (by id) works, reveal Child 1 and 7 nodes displayed'); + + var nodeGrandchild1 = $tree.treeview('getNode', 2); // Grandchild 1 + $tree.treeview('revealNode', nodeGrandchild1); + equal($($tree.selector + ' ul li').length, 9, 'Reveal node (by node) works, reveal Grandchild 1 and 9 nodes displayed'); + }); + test('search', function () { var cbWorked, onWorked = false; var $tree = init({