├── .bowerrc ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── app.js ├── bower.json ├── dist ├── bootstrap-treeview.min.css └── bootstrap-treeview.min.js ├── package.json ├── public ├── css │ └── bootstrap-treeview.css ├── example-dom.html ├── index.html └── js │ └── bootstrap-treeview.js ├── screenshot └── default.PNG ├── src ├── css │ └── bootstrap-treeview.css └── js │ └── bootstrap-treeview.js └── tests ├── README.md ├── lib ├── blanket.min.js ├── bootstrap-treeview.css ├── bootstrap-treeview.js ├── jquery.js ├── qunit-1.12.0.css └── qunit-1.12.0.js ├── tests.html └── tests.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "public/bower_components" 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/bower_components/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // Details: https://github.com/victorporof/Sublime-JSHint#using-your-own-jshintrc-options 3 | // Example: https://github.com/jshint/jshint/blob/master/examples/.jshintrc 4 | // Documentation: http://www.jshint.com/docs/ 5 | "browser": true, 6 | "esnext": true, 7 | "globals": { $: false, jQuery: false, "console": false}, 8 | "globalstrict": true, 9 | "quotmark": "single", 10 | "smarttabs": true, 11 | "trailing": true, 12 | "undef": true, 13 | "unused": true 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "0.11" 5 | - "0.10" 6 | 7 | before_script: 8 | - npm install -g grunt-cli 9 | - npm install 10 | - bower install 11 | 12 | script: grunt test --verbose --force -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## v1.2.0 - 9th May, 2015 6 | 7 | ### New Features 8 | 9 | - Disable nodes, allow a tree node to disabled (not selectable, expandable or checkable) 10 | 11 | - Added node state property `disabled` to set a node initial state 12 | 13 | - Methods `disableAll`, `disableNode`, `enableAll`, `enableNode` and `toggleNodeDisabled` added to control state programmatically 14 | 15 | - Events `nodeDisabled` and `nodeEnabled` 16 | 17 | - Checkable nodes, allows a tree node to be checked or unchecked. 18 | 19 | - Added node state property `checked` to set a node initial state 20 | 21 | - Pass option `{showCheckbox: true}` to initialize tree view with checkboxes 22 | 23 | - Use options `checkedIcon` and `uncheckedIcon` to configure checkbox icons 24 | 25 | - Methods `checkAll`, `checkNode`, `uncheckAll`, `uncheckNode` and `toggleNodeChecked` to control state programmatically 26 | 27 | - Events `nodeChecked` and `nodeUnchecked` 28 | 29 | - New option + node property `selectedIcon` to support displaying different icons when a node is selected. 30 | 31 | - New search option `{ revealResults : true | false }` which when set to true will automatically expand the tree view to reveal matching nodes 32 | 33 | - New method `revealNode` which expands the tree view to reveal a given node 34 | 35 | - New methods to retrieve nodes by state : `getSelected`, `getUnselected`, `getExpanded`, `getCollapsed`, `getChecked`, `getUnchecked`, `getDisabled` and `getEnabled` 36 | 37 | 38 | ### Changes 39 | - Removed nodeIcon by default, by popular demand. Use `{nodeIcon: 'glyphicon glyphicon-stop'}` in initial options to add a node icon. 40 | 41 | - Search behaviour, by default search will the expand tree view and reveal results. Alternatively pass `{revealResults:false}` 42 | 43 | - Method collapseNode accepts new option `{ ignoreChildren: true | false }`. The default is false, passing true will leave child nodes uncollapsed 44 | 45 | 46 | ### Bug Fixes 47 | - Remove unnecessary render in clearSearch when called from search 48 | 49 | - Child nodes should collapse by default on collapseNode 50 | 51 | - Incorrect expand collapse icon displayed when nodes array is empty 52 | 53 | 54 | 55 | 56 | ## v1.1.0 - 29th March, 2015 57 | 58 | ### New Features 59 | 60 | - Added node state properties `expanded` and `selected` so a node's intial state can be set 61 | 62 | - New get methods `getNode`, `getParent` and `getSiblings` for retrieving nodes and their immediate relations 63 | 64 | - New select methods `selectNode`, `unselectNode` and `toggleNodeSelected` 65 | 66 | - Adding `nodeUnselected` event 67 | 68 | - New global option `multiSelect` which allows multiple nodes to hold the selected state, default is false 69 | 70 | - New expand collapse methods `expandAll`, `collapseAll`, `expandNode`, `collapseNode` and `toggleNodeExpanded` 71 | 72 | - Adding events `nodeExpanded` and `nodeCollapsed` 73 | 74 | - New methods `search` and `clearSearch` which allow you to query the tree view for nodes based on a `text` value 75 | 76 | - Adding events `searchComplete` and `searchCleared` 77 | 78 | - New global options `highlightSearchResults`, `searchResultColor` and `searchResultBackColor` for configuring how search results are displayed 79 | 80 | 81 | 82 | 83 | ## v1.0.2 - 6th February, 2015 84 | 85 | ### Changes 86 | - jQuery dependency version updated in Bower 87 | 88 | ### Bug Fixes 89 | - Events not unbound when re-initialised 90 | 91 | - CSS selectors too general, affecting other page elements 92 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), // the package file to use 4 | 5 | uglify: { 6 | files: { 7 | expand: true, 8 | flatten: true, 9 | src: 'src/js/*.js', 10 | dest: 'dist', 11 | ext: '.min.js' 12 | } 13 | }, 14 | 15 | cssmin: { 16 | minify: { 17 | expand: true, 18 | cwd: 'src/css', 19 | src: ['*.css', '!*.min.css'], 20 | dest: 'dist', 21 | ext: '.min.css' 22 | } 23 | }, 24 | 25 | qunit: { 26 | all: ['tests/*.html'] 27 | }, 28 | 29 | watch: { 30 | files: ['tests/*.js', 'tests/*.html', 'src/**'], 31 | tasks: ['default'] 32 | }, 33 | 34 | copy: { 35 | main: { 36 | files: [ 37 | // copy dist to tests 38 | // { expand: true, cwd: 'dist', src: '*', dest: 'tests/lib/' }, 39 | { expand: true, cwd: 'src/css', src: '*', dest: 'tests/lib/' }, 40 | { expand: true, cwd: 'src/js', src: '*', dest: 'tests/lib/' }, 41 | // copy latest libs to tests 42 | { expand: true, cwd: 'public/bower_components/jquery', src: 'jquery.js', dest: 'tests/lib/' }, 43 | { expand: true, cwd: 'public/bower_components/bootstrap-datepicker/js', src: 'bootstrap-datepicker.js', dest: 'tests/lib/' }, 44 | // copy src to example 45 | { expand: true, cwd: 'src/css', src: '*', dest: 'public/css/' }, 46 | { expand: true, cwd: 'src/js', src: '*', dest: 'public/js/' } 47 | ] 48 | } 49 | } 50 | }); 51 | 52 | // load up your plugins 53 | grunt.loadNpmTasks('grunt-contrib-uglify'); 54 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 55 | grunt.loadNpmTasks('grunt-contrib-qunit'); 56 | grunt.loadNpmTasks('grunt-contrib-watch'); 57 | grunt.loadNpmTasks('grunt-contrib-copy'); 58 | 59 | // register one or more task lists (you should ALWAYS have a "default" task list) 60 | grunt.registerTask('default', ['uglify','cssmin', 'copy', 'qunit', 'watch']); 61 | grunt.registerTask('test', 'qunit'); 62 | }; 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bootstrap Tree View 2 | 3 | --- 4 | 5 | ![Bower version](https://img.shields.io/bower/v/bootstrap-treeview.svg?style=flat) 6 | [![npm version](https://img.shields.io/npm/v/bootstrap-treeview.svg?style=flat)](https://www.npmjs.com/package/bootstrap-treeview) 7 | [![Build Status](https://img.shields.io/travis/jonmiles/bootstrap-treeview/master.svg?style=flat)](https://travis-ci.org/jonmiles/bootstrap-treeview) 8 | 9 | A simple and elegant solution to displaying hierarchical tree structures (i.e. a Tree View) while leveraging the best that Twitter Bootstrap has to offer. 10 | 11 | ![Bootstrap Tree View Default](https://raw.github.com/jonmiles/bootstrap-treeview/master/screenshot/default.PNG) 12 | 13 | ## Dependencies 14 | 15 | Where provided these are the actual versions bootstrap-treeview has been tested against. 16 | 17 | - [Bootstrap v3.3.4 (>= 3.0.0)](http://getbootstrap.com/) 18 | - [jQuery v2.1.3 (>= 1.9.0)](http://jquery.com/) 19 | 20 | 21 | ## Getting Started 22 | 23 | ### Install 24 | 25 | You can install using bower (recommended): 26 | 27 | ```javascript 28 | $ bower install bootstrap-treeview 29 | ``` 30 | 31 | or using npm: 32 | 33 | ```javascript 34 | $ npm install bootstrap-treeview 35 | ``` 36 | 37 | or [download](https://github.com/jonmiles/bootstrap-treeview/releases/tag/v1.2.0) manually. 38 | 39 | 40 | 41 | ### Usage 42 | 43 | Add the following resources for the bootstrap-treeview to function correctly. 44 | 45 | ```html 46 | 47 | 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | The component will bind to any existing DOM element. 55 | 56 | ```html 57 |
58 | ``` 59 | 60 | Basic usage may look something like this. 61 | 62 | ```javascript 63 | function getTree() { 64 | // Some logic to retrieve, or generate tree structure 65 | return data; 66 | } 67 | 68 | $('#tree').treeview({data: getTree()}); 69 | ``` 70 | 71 | 72 | ## Data Structure 73 | 74 | In order to define the hierarchical structure needed for the tree it's necessary to provide a nested array of JavaScript objects. 75 | 76 | Example 77 | 78 | ```javascript 79 | var tree = [ 80 | { 81 | text: "Parent 1", 82 | nodes: [ 83 | { 84 | text: "Child 1", 85 | nodes: [ 86 | { 87 | text: "Grandchild 1" 88 | }, 89 | { 90 | text: "Grandchild 2" 91 | } 92 | ] 93 | }, 94 | { 95 | text: "Child 2" 96 | } 97 | ] 98 | }, 99 | { 100 | text: "Parent 2" 101 | }, 102 | { 103 | text: "Parent 3" 104 | }, 105 | { 106 | text: "Parent 4" 107 | }, 108 | { 109 | text: "Parent 5" 110 | } 111 | ]; 112 | ``` 113 | 114 | At the lowest level a tree node is a represented as a simple JavaScript object. This one required property `text` will build you a tree. 115 | 116 | ```javascript 117 | { 118 | text: "Node 1" 119 | } 120 | ``` 121 | 122 | If you want to do more, here's the full node specification 123 | 124 | ```javascript 125 | { 126 | text: "Node 1", 127 | icon: "glyphicon glyphicon-stop", 128 | selectedIcon: "glyphicon glyphicon-stop", 129 | color: "#000000", 130 | backColor: "#FFFFFF", 131 | href: "#node-1", 132 | selectable: true, 133 | state: { 134 | checked: true, 135 | disabled: true, 136 | expanded: true, 137 | selected: true 138 | }, 139 | tags: ['available'], 140 | nodes: [ 141 | {}, 142 | ... 143 | ] 144 | } 145 | ``` 146 | 147 | ### Node Properties 148 | 149 | The following properties are defined to allow node level overrides, such as node specific icons, colours and tags. 150 | 151 | #### text 152 | `String` `Mandatory` 153 | 154 | The text value displayed for a given tree node, typically to the right of the nodes icon. 155 | 156 | #### icon 157 | `String` `Optional` 158 | 159 | The icon displayed on a given node, typically to the left of the text. 160 | 161 | For simplicity we directly leverage [Bootstraps Glyphicons support](http://getbootstrap.com/components/#glyphicons) and as such you should provide both the base class and individual icon class separated by a space. 162 | 163 | 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. 164 | 165 | #### selectedIcon 166 | `String` `Optional` 167 | 168 | The icon displayed on a given node when selected, typically to the left of the text. 169 | 170 | #### color 171 | `String` `Optional` 172 | 173 | The foreground color used on a given node, overrides global color option. 174 | 175 | #### backColor 176 | `String` `Optional` 177 | 178 | The background color used on a given node, overrides global color option. 179 | 180 | #### href 181 | `String` `Optional` 182 | 183 | Used in conjunction with global enableLinks option to specify anchor tag URL on a given node. 184 | 185 | #### selectable 186 | `Boolean` `Default: true` 187 | 188 | Whether or not a node is selectable in the tree. False indicates the node should act as an expansion heading and will not fire selection events. 189 | 190 | #### state 191 | `Object` `Optional` 192 | Describes a node's initial state. 193 | 194 | #### state.checked 195 | `Boolean` `Default: false` 196 | 197 | Whether or not a node is checked, represented by a checkbox style glyphicon. 198 | 199 | #### state.disabled 200 | `Boolean` `Default: false` 201 | 202 | Whether or not a node is disabled (not selectable, expandable or checkable). 203 | 204 | #### state.expanded 205 | `Boolean` `Default: false` 206 | 207 | Whether or not a node is expanded i.e. open. Takes precedence over global option levels. 208 | 209 | #### state.selected 210 | `Boolean` `Default: false` 211 | 212 | Whether or not a node is selected. 213 | 214 | #### tags 215 | `Array of Strings` `Optional` 216 | 217 | Used in conjunction with global showTags option to add additional information to the right of each node; using [Bootstrap Badges](http://getbootstrap.com/components/#badges) 218 | 219 | ### Extendible 220 | 221 | You can extend the node object by adding any number of additional key value pairs that you require for your application. Remember this is the object which will be passed around during selection events. 222 | 223 | 224 | 225 | ## Options 226 | 227 | Options allow you to customise the treeview's default appearance and behaviour. They are passed to the plugin on initialization, as an object. 228 | 229 | ```javascript 230 | // Example: initializing the treeview 231 | // expanded to 5 levels 232 | // with a background color of green 233 | $('#tree').treeview({ 234 | data: data, // data is not optional 235 | levels: 5, 236 | backColor: 'green' 237 | }); 238 | ``` 239 | You can pass a new options object to the treeview at any time but this will have the effect of re-initializing the treeview. 240 | 241 | ### List of Options 242 | 243 | The following is a list of all available options. 244 | 245 | #### data 246 | Array of Objects. No default, expects data 247 | 248 | This is the core data to be displayed by the tree view. 249 | 250 | #### backColor 251 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: inherits from Bootstrap.css. 252 | 253 | Sets the default background color used by all nodes, except when overridden on a per node basis in data. 254 | 255 | #### borderColor 256 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: inherits from Bootstrap.css. 257 | 258 | Sets the border color for the component; set showBorder to false if you don't want a visible border. 259 | 260 | #### checkedIcon 261 | String, class names(s). Default: "glyphicon glyphicon-check" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 262 | 263 | Sets the icon to be as a checked checkbox, used in conjunction with showCheckbox. 264 | 265 | #### collapseIcon 266 | String, class name(s). Default: "glyphicon glyphicon-minus" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 267 | 268 | Sets the icon to be used on a collapsible tree node. 269 | 270 | #### color 271 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: inherits from Bootstrap.css. 272 | 273 | Sets the default foreground color used by all nodes, except when overridden on a per node basis in data. 274 | 275 | #### emptyIcon 276 | String, class name(s). Default: "glyphicon" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 277 | 278 | Sets the icon to be used on a tree node with no child nodes. 279 | 280 | #### enableLinks 281 | Boolean. Default: false 282 | 283 | Whether or not to present node text as a hyperlink. The href value of which must be provided in the data structure on a per node basis. 284 | 285 | #### expandIcon 286 | String, class name(s). Default: "glyphicon glyphicon-plus" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 287 | 288 | Sets the icon to be used on an expandable tree node. 289 | 290 | #### highlightSearchResults 291 | Boolean. Default: true 292 | 293 | Whether or not to highlight search results. 294 | 295 | #### highlightSelected 296 | Boolean. Default: true 297 | 298 | Whether or not to highlight the selected node. 299 | 300 | #### levels 301 | Integer. Default: 2 302 | 303 | Sets the number of hierarchical levels deep the tree will be expanded to by default. 304 | 305 | #### multiSelect 306 | Boolean. Default: false 307 | 308 | Whether or not multiple nodes can be selected at the same time. 309 | 310 | #### nodeIcon 311 | String, class name(s). Default: "glyphicon glyphicon-stop" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 312 | 313 | Sets the default icon to be used on all nodes, except when overridden on a per node basis in data. 314 | 315 | #### onhoverColor 316 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: '#F5F5F5'. 317 | 318 | Sets the default background color activated when the users cursor hovers over a node. 319 | 320 | #### selectedIcon 321 | String, class name(s). Default: "glyphicon glyphicon-stop" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 322 | 323 | Sets the default icon to be used on all selected nodes, except when overridden on a per node basis in data. 324 | 325 | #### searchResultBackColor 326 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: undefined, inherits. 327 | 328 | Sets the background color of the selected node. 329 | 330 | #### searchResultColor 331 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: '#D9534F'. 332 | 333 | Sets the foreground color of the selected node. 334 | 335 | #### selectedBackColor 336 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: '#428bca'. 337 | 338 | Sets the background color of the selected node. 339 | 340 | #### selectedColor 341 | String, [any legal color value](http://www.w3schools.com/cssref/css_colors_legal.asp). Default: '#FFFFFF'. 342 | 343 | Sets the foreground color of the selected node. 344 | 345 | #### showBorder 346 | Boolean. Default: true 347 | 348 | Whether or not to display a border around nodes. 349 | 350 | #### showCheckbox 351 | Boolean. Default: false 352 | 353 | Whether or not to display checkboxes on nodes. 354 | 355 | #### showIcon 356 | Boolean. Default: true 357 | 358 | Whether or not to display a nodes icon. 359 | 360 | #### showTags 361 | Boolean. Default: false 362 | 363 | 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. 364 | 365 | #### uncheckedIcon 366 | String, class names(s). Default: "glyphicon glyphicon-unchecked" as defined by [Bootstrap Glyphicons](http://getbootstrap.com/components/#glyphicons) 367 | 368 | Sets the icon to be as an unchecked checkbox, used in conjunction with showCheckbox. 369 | 370 | 371 | ## Methods 372 | 373 | Methods provide a way of interacting with the plugin programmatically. For example, expanding a node is possible via the expandNode method. 374 | 375 | You can invoke methods in one of two ways, using either: 376 | 377 | #### 1. The plugin's wrapper 378 | 379 | The plugin's wrapper works as a proxy for accessing the underlying methods. 380 | 381 | ```javascript 382 | $('#tree').treeview('methodName', args) 383 | ``` 384 | > Limitation, multiple arguments must be passed as an array of arguments. 385 | 386 | #### 2. The treeview directly 387 | 388 | You can get an instance of the treeview using one of the two following methods. 389 | 390 | ```javascript 391 | // This special method returns an instance of the treeview. 392 | $('#tree').treeview(true) 393 | .methodName(args); 394 | 395 | // The instance is also saved in the DOM elements data, 396 | // and accessible using the plugin's id 'treeview'. 397 | $('#tree').data('treeview') 398 | .methodName(args); 399 | ``` 400 | > A better approach, if you plan a lot of interaction. 401 | 402 | ### List of Methods 403 | 404 | The following is a list of all available methods. 405 | 406 | #### checkAll(options) 407 | 408 | Checks all tree nodes 409 | 410 | ```javascript 411 | $('#tree').treeview('checkAll', { silent: true }); 412 | ``` 413 | 414 | Triggers `nodeChecked` event; pass silent to suppress events. 415 | 416 | #### checkNode(node | nodeId, options) 417 | 418 | Checks a given tree node, accepts node or nodeId. 419 | 420 | ```javascript 421 | $('#tree').treeview('checkNode', [ nodeId, { silent: true } ]); 422 | ``` 423 | 424 | Triggers `nodeChecked` event; pass silent to suppress events. 425 | 426 | #### clearSearch() 427 | 428 | Clear the tree view of any previous search results e.g. remove their highlighted state. 429 | 430 | ```javascript 431 | $('#tree').treeview('clearSearch'); 432 | ``` 433 | 434 | Triggers `searchCleared` event 435 | 436 | #### collapseAll(options) 437 | 438 | Collapse all tree nodes, collapsing the entire tree. 439 | 440 | ```javascript 441 | $('#tree').treeview('collapseAll', { silent: true }); 442 | ``` 443 | 444 | Triggers `nodeCollapsed` event; pass silent to suppress events. 445 | 446 | #### collapseNode(node | nodeId, options) 447 | 448 | Collapse a given tree node and it's child nodes. If you don't want to collapse the child nodes, pass option `{ ignoreChildren: true }`. 449 | 450 | ```javascript 451 | $('#tree').treeview('collapseNode', [ nodeId, { silent: true, ignoreChildren: false } ]); 452 | ``` 453 | 454 | Triggers `nodeCollapsed` event; pass silent to suppress events. 455 | 456 | #### disableAll(options) 457 | 458 | Disable all tree nodes 459 | 460 | ```javascript 461 | $('#tree').treeview('disableAll', { silent: true }); 462 | ``` 463 | 464 | Triggers `nodeDisabled` event; pass silent to suppress events. 465 | 466 | #### disableNode(node | nodeId, options) 467 | 468 | Disable a given tree node, accepts node or nodeId. 469 | 470 | ```javascript 471 | $('#tree').treeview('disableNode', [ nodeId, { silent: true } ]); 472 | ``` 473 | 474 | Triggers `nodeDisabled` event; pass silent to suppress events. 475 | 476 | #### enableAll(options) 477 | 478 | Enable all tree nodes 479 | 480 | ```javascript 481 | $('#tree').treeview('enableAll', { silent: true }); 482 | ``` 483 | 484 | Triggers `nodeEnabled` event; pass silent to suppress events. 485 | 486 | #### enableNode(node | nodeId, options) 487 | 488 | Enable a given tree node, accepts node or nodeId. 489 | 490 | ```javascript 491 | $('#tree').treeview('enableNode', [ nodeId, { silent: true } ]); 492 | ``` 493 | 494 | Triggers `nodeEnabled` event; pass silent to suppress events. 495 | 496 | #### expandAll(options) 497 | 498 | Expand all tree nodes. Optionally can be expanded to any given number of levels. 499 | 500 | ```javascript 501 | $('#tree').treeview('expandAll', { levels: 2, silent: true }); 502 | ``` 503 | 504 | Triggers `nodeExpanded` event; pass silent to suppress events. 505 | 506 | #### expandNode(node | nodeId, options) 507 | 508 | Expand a given tree node, accepts node or nodeId. Optionally can be expanded to any given number of levels. 509 | 510 | ```javascript 511 | $('#tree').treeview('expandNode', [ nodeId, { levels: 2, silent: true } ]); 512 | ``` 513 | 514 | Triggers `nodeExpanded` event; pass silent to suppress events. 515 | 516 | #### getCollapsed() 517 | 518 | Returns an array of collapsed nodes e.g. state.expanded = false. 519 | 520 | ```javascript 521 | $('#tree').treeview('getCollapsed', nodeId); 522 | ``` 523 | 524 | #### getDisabled() 525 | 526 | Returns an array of disabled nodes e.g. state.disabled = true. 527 | 528 | ```javascript 529 | $('#tree').treeview('getDisabled', nodeId); 530 | ``` 531 | 532 | #### getEnabled() 533 | 534 | Returns an array of enabled nodes e.g. state.disabled = false. 535 | 536 | ```javascript 537 | $('#tree').treeview('getEnabled', nodeId); 538 | ``` 539 | 540 | #### getExpanded() 541 | 542 | Returns an array of expanded nodes e.g. state.expanded = true. 543 | 544 | ```javascript 545 | $('#tree').treeview('getExpanded', nodeId); 546 | ``` 547 | 548 | #### getNode(nodeId) 549 | 550 | Returns a single node object that matches the given node id. 551 | 552 | ```javascript 553 | $('#tree').treeview('getNode', nodeId); 554 | ``` 555 | 556 | #### getParent(node | nodeId) 557 | 558 | Returns the parent node of a given node, if valid otherwise returns undefined. 559 | 560 | ```javascript 561 | $('#tree').treeview('getParent', node); 562 | ``` 563 | 564 | #### getSelected() 565 | 566 | Returns an array of selected nodes e.g. state.selected = true. 567 | 568 | ```javascript 569 | $('#tree').treeview('getSelected', nodeId); 570 | ``` 571 | 572 | #### getSiblings(node | nodeId) 573 | 574 | Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. 575 | 576 | ```javascript 577 | $('#tree').treeview('getSiblings', node); 578 | ``` 579 | 580 | #### getUnselected() 581 | 582 | Returns an array of unselected nodes e.g. state.selected = false. 583 | 584 | ```javascript 585 | $('#tree').treeview('getUnselected', nodeId); 586 | ``` 587 | 588 | #### remove() 589 | 590 | Removes the tree view component. Removing attached events, internal attached objects, and added HTML elements. 591 | 592 | ```javascript 593 | $('#tree').treeview('remove'); 594 | ``` 595 | 596 | #### revealNode(node | nodeId, options) 597 | 598 | Reveals a given tree node, expanding the tree from node to root. 599 | 600 | ```javascript 601 | $('#tree').treeview('revealNode', [ nodeId, { silent: true } ]); 602 | ``` 603 | 604 | Triggers `nodeExpanded` event; pass silent to suppress events. 605 | 606 | #### search(pattern, options) 607 | 608 | Searches the tree view for nodes that match a given string, highlighting them in the tree. 609 | 610 | Returns an array of matching nodes. 611 | 612 | ```javascript 613 | $('#tree').treeview('search', [ 'Parent', { 614 | ignoreCase: true, // case insensitive 615 | exactMatch: false, // like or equals 616 | revealResults: true, // reveal matching nodes 617 | }]); 618 | ``` 619 | 620 | Triggers `searchComplete` event 621 | 622 | #### selectNode(node | nodeId, options) 623 | 624 | Selects a given tree node, accepts node or nodeId. 625 | 626 | ```javascript 627 | $('#tree').treeview('selectNode', [ nodeId, { silent: true } ]); 628 | ``` 629 | 630 | Triggers `nodeSelected` event; pass silent to suppress events. 631 | 632 | #### toggleNodeChecked(node | nodeId, options) 633 | 634 | Toggles a nodes checked state; checking if unchecked, unchecking if checked. 635 | 636 | ```javascript 637 | $('#tree').treeview('toggleNodeChecked', [ nodeId, { silent: true } ]); 638 | ``` 639 | 640 | Triggers either `nodeChecked` or `nodeUnchecked` event; pass silent to suppress events. 641 | 642 | #### toggleNodeDisabled(node | nodeId, options) 643 | 644 | Toggles a nodes disabled state; disabling if enabled, enabling if disabled. 645 | 646 | ```javascript 647 | $('#tree').treeview('toggleNodeDisabled', [ nodeId, { silent: true } ]); 648 | ``` 649 | 650 | Triggers either `nodeDisabled` or `nodeEnabled` event; pass silent to suppress events. 651 | 652 | #### toggleNodeExpanded(node | nodeId, options) 653 | 654 | Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. 655 | 656 | ```javascript 657 | $('#tree').treeview('toggleNodeExpanded', [ nodeId, { silent: true } ]); 658 | ``` 659 | 660 | Triggers either `nodeExpanded` or `nodeCollapsed` event; pass silent to suppress events. 661 | 662 | #### toggleNodeSelected(node | nodeId, options) 663 | 664 | Toggles a node selected state; selecting if unselected, unselecting if selected. 665 | 666 | ```javascript 667 | $('#tree').treeview('toggleNodeSelected', [ nodeId, { silent: true } ]); 668 | ``` 669 | 670 | Triggers either `nodeSelected` or `nodeUnselected` event; pass silent to suppress events. 671 | 672 | #### uncheckAll(options) 673 | 674 | Uncheck all tree nodes. 675 | 676 | ```javascript 677 | $('#tree').treeview('uncheckAll', { silent: true }); 678 | ``` 679 | 680 | Triggers `nodeUnchecked` event; pass silent to suppress events. 681 | 682 | #### uncheckNode(node | nodeId, options) 683 | 684 | Uncheck a given tree node, accepts node or nodeId. 685 | 686 | ```javascript 687 | $('#tree').treeview('uncheckNode', [ nodeId, { silent: true } ]); 688 | ``` 689 | 690 | Triggers `nodeUnchecked` event; pass silent to suppress events. 691 | 692 | #### unselectNode(node | nodeId, options) 693 | 694 | Unselects a given tree node, accepts node or nodeId. 695 | 696 | ```javascript 697 | $('#tree').treeview('unselectNode', [ nodeId, { silent: true } ]); 698 | ``` 699 | 700 | Triggers `nodeUnselected` event; pass silent to suppress events. 701 | 702 | ## Events 703 | 704 | Events are provided so that your application can respond to changes in the treeview's state. For example, if you want to update a display when a node is selected use the `nodeSelected` event. 705 | 706 | You can bind to any event defined below by either using an options callback handler, or the standard jQuery .on method. 707 | 708 | Example using options callback handler: 709 | 710 | ```javascript 711 | $('#tree').treeview({ 712 | // The naming convention for callback's is to prepend with `on` 713 | // and capitalize the first letter of the event name 714 | // e.g. nodeSelected -> onNodeSelected 715 | onNodeSelected: function(event, data) { 716 | // Your logic goes here 717 | }); 718 | ``` 719 | 720 | and using jQuery .on method 721 | 722 | ```javascript 723 | $('#tree').on('nodeSelected', function(event, data) { 724 | // Your logic goes here 725 | }); 726 | ``` 727 | 728 | ### List of Events 729 | 730 | `nodeChecked (event, node)` - A node is checked. 731 | 732 | `nodeCollapsed (event, node)` - A node is collapsed. 733 | 734 | `nodeDisabled (event, node)` - A node is disabled. 735 | 736 | `nodeEnabled (event, node)` - A node is enabled. 737 | 738 | `nodeExpanded (event, node)` - A node is expanded. 739 | 740 | `nodeSelected (event, node)` - A node is selected. 741 | 742 | `nodeUnchecked (event, node)` - A node is unchecked. 743 | 744 | `nodeUnselected (event, node)` - A node is unselected. 745 | 746 | `searchComplete (event, results)` - After a search completes 747 | 748 | `searchCleared (event, results)` - After search results are cleared 749 | 750 | 751 | 752 | ## Copyright and Licensing 753 | Copyright 2013 Jonathan Miles 754 | 755 | Licensed under the Apache License, Version 2.0 (the "License"); 756 | you may not use this file except in compliance with the License. 757 | You may obtain a copy of the License at 758 | 759 | Unless required by applicable law or agreed to in writing, software 760 | distributed under the License is distributed on an "AS IS" BASIS, 761 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 762 | See the License for the specific language governing permissions and 763 | limitations under the License. 764 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'); 7 | var http = require('http'); 8 | var path = require('path'); 9 | 10 | var app = express(); 11 | 12 | // all environments 13 | app.set('port', process.env.PORT || 3000); 14 | app.use(express.favicon()); 15 | app.use(express.logger('dev')); 16 | app.use(express.json()); 17 | app.use(express.urlencoded()); 18 | app.use(express.methodOverride()); 19 | app.use(express.static(path.join(__dirname, '/public'))); 20 | app.use(express.static(path.join(__dirname, '/tests'))); 21 | 22 | // development only 23 | if ('development' == app.get('env')) { 24 | app.use(express.errorHandler()); 25 | } 26 | 27 | http.createServer(app).listen(app.get('port'), function(){ 28 | console.log('Express server listening on port ' + app.get('port')); 29 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-treeview", 3 | "description": "Tree View for Twitter Bootstrap", 4 | "version": "1.2.0", 5 | "homepage": "https://github.com/jonmiles/bootstrap-treeview", 6 | "main": [ 7 | "dist/bootstrap-treeview.min.js", 8 | "dist/bootstrap-treeview.min.css" 9 | ], 10 | "keywords": [ 11 | "twitter", 12 | "bootstrap", 13 | "tree", 14 | "treeview", 15 | "tree-view", 16 | "navigation", 17 | "javascript", 18 | "jquery", 19 | "jquery-plugin" 20 | ], 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "dependencies": { 29 | "jquery": ">= 1.9.0", 30 | "bootstrap": ">= 3.0.0" 31 | }, 32 | "devDependencies": {} 33 | } 34 | -------------------------------------------------------------------------------- /dist/bootstrap-treeview.min.css: -------------------------------------------------------------------------------- 1 | .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} -------------------------------------------------------------------------------- /dist/bootstrap-treeview.min.js: -------------------------------------------------------------------------------- 1 | !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); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap-treeview", 3 | "description": "Tree View for Twitter Bootstrap", 4 | "version": "1.2.0", 5 | "homepage": "https://github.com/jonmiles/bootstrap-treeview", 6 | "author": { 7 | "name": "Jonathan Miles" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/jonmiles/bootstrap-treeview.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/jonmiles/bootstrap-treeview/issues" 15 | }, 16 | "licenses": [ 17 | { 18 | "type": "Apache", 19 | "url": "https://github.com/jonmiles/bootstrap-treeview/blob/master/LICENSE" 20 | } 21 | ], 22 | "main": [ 23 | "dist/bootstrap-treeview.min.js", 24 | "dist/bootstrap-treeview.min.css" 25 | ], 26 | "scripts": { 27 | "install": "bower install", 28 | "start": "node app", 29 | "test": "grunt test" 30 | }, 31 | "engines": { 32 | "node": ">= 0.10.0" 33 | }, 34 | "dependencies": { 35 | "express": "3.4.x", 36 | "ejs": "2.2.x", 37 | "phantomjs": "1.9.x" 38 | }, 39 | "devDependencies": { 40 | "bower": "1.3.x", 41 | "grunt": "0.4.x", 42 | "grunt-contrib-uglify": "0.7.x", 43 | "grunt-contrib-cssmin": "0.12.x", 44 | "grunt-contrib-qunit": "0.5.x", 45 | "grunt-contrib-watch": "0.6.x", 46 | "grunt-contrib-copy": "0.7.x" 47 | }, 48 | "keywords": [ 49 | "twitter", 50 | "bootstrap", 51 | "tree", 52 | "treeview", 53 | "tree-view", 54 | "navigation", 55 | "javascript", 56 | "jquery", 57 | "jquery-plugin" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /public/css/bootstrap-treeview.css: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-treeview.css v1.2.0 3 | * ========================================================= 4 | * Copyright 2013 Jonathan Miles 5 | * Project URL : http://www.jondmiles.com/bootstrap-treeview 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | .treeview .list-group-item { 21 | cursor: pointer; 22 | } 23 | 24 | .treeview span.indent { 25 | margin-left: 10px; 26 | margin-right: 10px; 27 | } 28 | 29 | .treeview span.icon { 30 | width: 12px; 31 | margin-right: 5px; 32 | } 33 | 34 | .treeview .node-disabled { 35 | color: silver; 36 | cursor: not-allowed; 37 | } -------------------------------------------------------------------------------- /public/example-dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bootstrap Tree View 5 | 6 | 7 | 8 | 9 |
    10 |

    Bootstrap Tree View - DOM Tree

    11 |
    12 |
    13 |
    14 | 15 |
    16 |
    17 |
    18 |
    19 | 20 | 21 | 59 | 60 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Bootstrap Tree View 5 | 6 | 7 | 8 | 9 |
    10 |

    Bootstrap Tree View

    11 |
    12 |
    13 |
    14 |

    Default

    15 |
    16 |
    17 |
    18 |

    Collapsed

    19 |
    20 |
    21 |
    22 |

    Expanded

    23 |
    24 |
    25 |
    26 |
    27 |
    28 |

    Blue Theme

    29 |
    30 |
    31 |
    32 |

    Custom Icons

    33 |
    34 |
    35 |
    36 |

    Tags as Badges

    37 |
    38 |
    39 |
    40 |
    41 |
    42 |

    No Border

    43 |
    44 |
    45 |
    46 |

    Colourful

    47 |
    48 |
    49 |
    50 |

    Node Overrides

    51 |
    52 |
    53 |
    54 |
    55 |
    56 |

    Link enabled, or

    57 |
    58 |
    59 |
    60 | 61 |
    62 |
    63 | 64 |
    65 |
    66 |
    67 |
    68 |

    Searchable Tree

    69 |
    70 |

    Input

    71 | 72 |
    73 | 74 | 75 |
    76 |
    77 | 81 |
    82 |
    83 | 87 |
    88 |
    89 | 93 |
    94 | 95 | 96 | 97 |
    98 |
    99 |

    Tree

    100 |
    101 |
    102 |
    103 |

    Results

    104 |
    105 |
    106 |
    107 |
    108 |
    109 |

    Selectable Tree

    110 |
    111 |

    Input

    112 |
    113 | 114 | 115 |
    116 |
    117 | 121 |
    122 |
    123 | 127 |
    128 |
    129 | 130 |
    131 |
    132 | 133 |
    134 |
    135 | 136 |
    137 |
    138 |
    139 |

    Tree

    140 |
    141 |
    142 |
    143 |

    Events

    144 |
    145 |
    146 |
    147 |
    148 |
    149 |

    Expandible Tree

    150 |
    151 |

    Input

    152 |
    153 | 154 | 155 |
    156 |
    157 | 161 |
    162 |
    163 |
    164 | 165 |
    166 |
    167 | 171 |
    172 |
    173 |
    174 | 175 |
    176 |
    177 | 178 |
    179 |
    180 |
    181 |
    182 | 183 |
    184 |
    185 | 189 |
    190 |
    191 | 192 |
    193 |
    194 |

    Tree

    195 |
    196 |
    197 |
    198 |

    Events

    199 |
    200 |
    201 |
    202 |
    203 |
    204 |

    Checkable Tree

    205 |
    206 |

    Input

    207 |
    208 | 209 | 210 |
    211 |
    212 | 216 |
    217 |
    218 |
    219 | 220 |
    221 |
    222 |
    223 | 224 |
    225 |
    226 | 227 |
    228 |
    229 |
    230 |
    231 | 232 |
    233 |
    234 | 235 |
    236 |
    237 |

    Tree

    238 |
    239 |
    240 |
    241 |

    Events

    242 |
    243 |
    244 |
    245 |
    246 |
    247 |

    Disabled Tree

    248 |
    249 |

    Input

    250 |
    251 | 252 | 253 |
    254 |
    255 | 259 |
    260 |
    261 |
    262 | 263 |
    264 |
    265 |
    266 | 267 |
    268 |
    269 | 270 |
    271 |
    272 |
    273 |
    274 | 275 |
    276 |
    277 | 278 |
    279 |
    280 |

    Tree

    281 |
    282 |
    283 |
    284 |

    Events

    285 |
    286 |
    287 |
    288 |
    289 |
    290 |

    Data

    291 |
    292 |

    JSON Data

    293 |
    294 |
    295 |
    296 |

    297 |
    298 |
    299 |
    300 |

    301 |
    302 |
    303 |
    304 |
    305 |
    306 |
    307 |
    308 |
    309 | 310 | 311 | 762 | 763 | 764 | -------------------------------------------------------------------------------- /public/js/bootstrap-treeview.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-treeview.js v1.2.0 3 | * ========================================================= 4 | * Copyright 2013 Jonathan Miles 5 | * Project URL : http://www.jondmiles.com/bootstrap-treeview 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | ;(function ($, window, document, undefined) { 21 | 22 | /*global jQuery, console*/ 23 | 24 | 'use strict'; 25 | 26 | var pluginName = 'treeview'; 27 | 28 | var _default = {}; 29 | 30 | _default.settings = { 31 | 32 | injectStyle: true, 33 | 34 | levels: 2, 35 | 36 | expandIcon: 'glyphicon glyphicon-plus', 37 | collapseIcon: 'glyphicon glyphicon-minus', 38 | emptyIcon: 'glyphicon', 39 | nodeIcon: '', 40 | selectedIcon: '', 41 | checkedIcon: 'glyphicon glyphicon-check', 42 | uncheckedIcon: 'glyphicon glyphicon-unchecked', 43 | 44 | color: undefined, // '#000000', 45 | backColor: undefined, // '#FFFFFF', 46 | borderColor: undefined, // '#dddddd', 47 | onhoverColor: '#F5F5F5', 48 | selectedColor: '#FFFFFF', 49 | selectedBackColor: '#428bca', 50 | searchResultColor: '#D9534F', 51 | searchResultBackColor: undefined, //'#FFFFFF', 52 | 53 | enableLinks: false, 54 | highlightSelected: true, 55 | highlightSearchResults: true, 56 | showBorder: true, 57 | showIcon: true, 58 | showCheckbox: false, 59 | showTags: false, 60 | multiSelect: false, 61 | 62 | // Event handlers 63 | onNodeChecked: undefined, 64 | onNodeCollapsed: undefined, 65 | onNodeDisabled: undefined, 66 | onNodeEnabled: undefined, 67 | onNodeExpanded: undefined, 68 | onNodeSelected: undefined, 69 | onNodeUnchecked: undefined, 70 | onNodeUnselected: undefined, 71 | onSearchComplete: undefined, 72 | onSearchCleared: undefined 73 | }; 74 | 75 | _default.options = { 76 | silent: false, 77 | ignoreChildren: false 78 | }; 79 | 80 | _default.searchOptions = { 81 | ignoreCase: true, 82 | exactMatch: false, 83 | revealResults: true 84 | }; 85 | 86 | var Tree = function (element, options) { 87 | 88 | this.$element = $(element); 89 | this.elementId = element.id; 90 | this.styleId = this.elementId + '-style'; 91 | 92 | this.init(options); 93 | 94 | return { 95 | 96 | // Options (public access) 97 | options: this.options, 98 | 99 | // Initialize / destroy methods 100 | init: $.proxy(this.init, this), 101 | remove: $.proxy(this.remove, this), 102 | 103 | // Get methods 104 | getNode: $.proxy(this.getNode, this), 105 | getParent: $.proxy(this.getParent, this), 106 | getSiblings: $.proxy(this.getSiblings, this), 107 | getSelected: $.proxy(this.getSelected, this), 108 | getUnselected: $.proxy(this.getUnselected, this), 109 | getExpanded: $.proxy(this.getExpanded, this), 110 | getCollapsed: $.proxy(this.getCollapsed, this), 111 | getChecked: $.proxy(this.getChecked, this), 112 | getUnchecked: $.proxy(this.getUnchecked, this), 113 | getDisabled: $.proxy(this.getDisabled, this), 114 | getEnabled: $.proxy(this.getEnabled, this), 115 | 116 | // Select methods 117 | selectNode: $.proxy(this.selectNode, this), 118 | unselectNode: $.proxy(this.unselectNode, this), 119 | toggleNodeSelected: $.proxy(this.toggleNodeSelected, this), 120 | 121 | // Expand / collapse methods 122 | collapseAll: $.proxy(this.collapseAll, this), 123 | collapseNode: $.proxy(this.collapseNode, this), 124 | expandAll: $.proxy(this.expandAll, this), 125 | expandNode: $.proxy(this.expandNode, this), 126 | toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), 127 | revealNode: $.proxy(this.revealNode, this), 128 | 129 | // Expand / collapse methods 130 | checkAll: $.proxy(this.checkAll, this), 131 | checkNode: $.proxy(this.checkNode, this), 132 | uncheckAll: $.proxy(this.uncheckAll, this), 133 | uncheckNode: $.proxy(this.uncheckNode, this), 134 | toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), 135 | 136 | // Disable / enable methods 137 | disableAll: $.proxy(this.disableAll, this), 138 | disableNode: $.proxy(this.disableNode, this), 139 | enableAll: $.proxy(this.enableAll, this), 140 | enableNode: $.proxy(this.enableNode, this), 141 | toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), 142 | 143 | // Search methods 144 | search: $.proxy(this.search, this), 145 | clearSearch: $.proxy(this.clearSearch, this) 146 | }; 147 | }; 148 | 149 | Tree.prototype.init = function (options) { 150 | 151 | this.tree = []; 152 | this.nodes = []; 153 | 154 | if (options.data) { 155 | if (typeof options.data === 'string') { 156 | options.data = $.parseJSON(options.data); 157 | } 158 | this.tree = $.extend(true, [], options.data); 159 | delete options.data; 160 | } 161 | this.options = $.extend({}, _default.settings, options); 162 | 163 | this.destroy(); 164 | this.subscribeEvents(); 165 | this.setInitialStates({ nodes: this.tree }, 0); 166 | this.render(); 167 | }; 168 | 169 | Tree.prototype.remove = function () { 170 | this.destroy(); 171 | $.removeData(this, pluginName); 172 | $('#' + this.styleId).remove(); 173 | }; 174 | 175 | Tree.prototype.destroy = function () { 176 | 177 | if (!this.initialized) return; 178 | 179 | this.$wrapper.remove(); 180 | this.$wrapper = null; 181 | 182 | // Switch off events 183 | this.unsubscribeEvents(); 184 | 185 | // Reset this.initialized flag 186 | this.initialized = false; 187 | }; 188 | 189 | Tree.prototype.unsubscribeEvents = function () { 190 | 191 | this.$element.off('click'); 192 | this.$element.off('nodeChecked'); 193 | this.$element.off('nodeCollapsed'); 194 | this.$element.off('nodeDisabled'); 195 | this.$element.off('nodeEnabled'); 196 | this.$element.off('nodeExpanded'); 197 | this.$element.off('nodeSelected'); 198 | this.$element.off('nodeUnchecked'); 199 | this.$element.off('nodeUnselected'); 200 | this.$element.off('searchComplete'); 201 | this.$element.off('searchCleared'); 202 | }; 203 | 204 | Tree.prototype.subscribeEvents = function () { 205 | 206 | this.unsubscribeEvents(); 207 | 208 | this.$element.on('click', $.proxy(this.clickHandler, this)); 209 | 210 | if (typeof (this.options.onNodeChecked) === 'function') { 211 | this.$element.on('nodeChecked', this.options.onNodeChecked); 212 | } 213 | 214 | if (typeof (this.options.onNodeCollapsed) === 'function') { 215 | this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); 216 | } 217 | 218 | if (typeof (this.options.onNodeDisabled) === 'function') { 219 | this.$element.on('nodeDisabled', this.options.onNodeDisabled); 220 | } 221 | 222 | if (typeof (this.options.onNodeEnabled) === 'function') { 223 | this.$element.on('nodeEnabled', this.options.onNodeEnabled); 224 | } 225 | 226 | if (typeof (this.options.onNodeExpanded) === 'function') { 227 | this.$element.on('nodeExpanded', this.options.onNodeExpanded); 228 | } 229 | 230 | if (typeof (this.options.onNodeSelected) === 'function') { 231 | this.$element.on('nodeSelected', this.options.onNodeSelected); 232 | } 233 | 234 | if (typeof (this.options.onNodeUnchecked) === 'function') { 235 | this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); 236 | } 237 | 238 | if (typeof (this.options.onNodeUnselected) === 'function') { 239 | this.$element.on('nodeUnselected', this.options.onNodeUnselected); 240 | } 241 | 242 | if (typeof (this.options.onSearchComplete) === 'function') { 243 | this.$element.on('searchComplete', this.options.onSearchComplete); 244 | } 245 | 246 | if (typeof (this.options.onSearchCleared) === 'function') { 247 | this.$element.on('searchCleared', this.options.onSearchCleared); 248 | } 249 | }; 250 | 251 | /* 252 | Recurse the tree structure and ensure all nodes have 253 | valid initial states. User defined states will be preserved. 254 | For performance we also take this opportunity to 255 | index nodes in a flattened structure 256 | */ 257 | Tree.prototype.setInitialStates = function (node, level) { 258 | 259 | if (!node.nodes) return; 260 | level += 1; 261 | 262 | var parent = node; 263 | var _this = this; 264 | $.each(node.nodes, function checkStates(index, node) { 265 | 266 | // nodeId : unique, incremental identifier 267 | node.nodeId = _this.nodes.length; 268 | 269 | // parentId : transversing up the tree 270 | node.parentId = parent.nodeId; 271 | 272 | // if not provided set selectable default value 273 | if (!node.hasOwnProperty('selectable')) { 274 | node.selectable = true; 275 | } 276 | 277 | // where provided we should preserve states 278 | node.state = node.state || {}; 279 | 280 | // set checked state; unless set always false 281 | if (!node.state.hasOwnProperty('checked')) { 282 | node.state.checked = false; 283 | } 284 | 285 | // set enabled state; unless set always false 286 | if (!node.state.hasOwnProperty('disabled')) { 287 | node.state.disabled = false; 288 | } 289 | 290 | // set expanded state; if not provided based on levels 291 | if (!node.state.hasOwnProperty('expanded')) { 292 | if (!node.state.disabled && 293 | (level < _this.options.levels) && 294 | (node.nodes && node.nodes.length > 0)) { 295 | node.state.expanded = true; 296 | } 297 | else { 298 | node.state.expanded = false; 299 | } 300 | } 301 | 302 | // set selected state; unless set always false 303 | if (!node.state.hasOwnProperty('selected')) { 304 | node.state.selected = false; 305 | } 306 | 307 | // index nodes in a flattened structure for use later 308 | _this.nodes.push(node); 309 | 310 | // recurse child nodes and transverse the tree 311 | if (node.nodes) { 312 | _this.setInitialStates(node, level); 313 | } 314 | }); 315 | }; 316 | 317 | Tree.prototype.clickHandler = function (event) { 318 | 319 | if (!this.options.enableLinks) event.preventDefault(); 320 | 321 | var target = $(event.target); 322 | var node = this.findNode(target); 323 | if (!node || node.state.disabled) return; 324 | 325 | var classList = target.attr('class') ? target.attr('class').split(' ') : []; 326 | if ((classList.indexOf('expand-icon') !== -1)) { 327 | 328 | this.toggleExpandedState(node, _default.options); 329 | this.render(); 330 | } 331 | else if ((classList.indexOf('check-icon') !== -1)) { 332 | 333 | this.toggleCheckedState(node, _default.options); 334 | this.render(); 335 | } 336 | else { 337 | 338 | if (node.selectable) { 339 | this.toggleSelectedState(node, _default.options); 340 | } else { 341 | this.toggleExpandedState(node, _default.options); 342 | } 343 | 344 | this.render(); 345 | } 346 | }; 347 | 348 | // Looks up the DOM for the closest parent list item to retrieve the 349 | // data attribute nodeid, which is used to lookup the node in the flattened structure. 350 | Tree.prototype.findNode = function (target) { 351 | 352 | var nodeId = target.closest('li.list-group-item').attr('data-nodeid'); 353 | var node = this.nodes[nodeId]; 354 | 355 | if (!node) { 356 | console.log('Error: node does not exist'); 357 | } 358 | return node; 359 | }; 360 | 361 | Tree.prototype.toggleExpandedState = function (node, options) { 362 | if (!node) return; 363 | this.setExpandedState(node, !node.state.expanded, options); 364 | }; 365 | 366 | Tree.prototype.setExpandedState = function (node, state, options) { 367 | 368 | if (state === node.state.expanded) return; 369 | 370 | if (state && node.nodes) { 371 | 372 | // Expand a node 373 | node.state.expanded = true; 374 | if (!options.silent) { 375 | this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); 376 | } 377 | } 378 | else if (!state) { 379 | 380 | // Collapse a node 381 | node.state.expanded = false; 382 | if (!options.silent) { 383 | this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); 384 | } 385 | 386 | // Collapse child nodes 387 | if (node.nodes && !options.ignoreChildren) { 388 | $.each(node.nodes, $.proxy(function (index, node) { 389 | this.setExpandedState(node, false, options); 390 | }, this)); 391 | } 392 | } 393 | }; 394 | 395 | Tree.prototype.toggleSelectedState = function (node, options) { 396 | if (!node) return; 397 | this.setSelectedState(node, !node.state.selected, options); 398 | }; 399 | 400 | Tree.prototype.setSelectedState = function (node, state, options) { 401 | 402 | if (state === node.state.selected) return; 403 | 404 | if (state) { 405 | 406 | // If multiSelect false, unselect previously selected 407 | if (!this.options.multiSelect) { 408 | $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { 409 | this.setSelectedState(node, false, options); 410 | }, this)); 411 | } 412 | 413 | // Continue selecting node 414 | node.state.selected = true; 415 | if (!options.silent) { 416 | this.$element.trigger('nodeSelected', $.extend(true, {}, node)); 417 | } 418 | } 419 | else { 420 | 421 | // Unselect node 422 | node.state.selected = false; 423 | if (!options.silent) { 424 | this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); 425 | } 426 | } 427 | }; 428 | 429 | Tree.prototype.toggleCheckedState = function (node, options) { 430 | if (!node) return; 431 | this.setCheckedState(node, !node.state.checked, options); 432 | }; 433 | 434 | Tree.prototype.setCheckedState = function (node, state, options) { 435 | 436 | if (state === node.state.checked) return; 437 | 438 | if (state) { 439 | 440 | // Check node 441 | node.state.checked = true; 442 | 443 | if (!options.silent) { 444 | this.$element.trigger('nodeChecked', $.extend(true, {}, node)); 445 | } 446 | } 447 | else { 448 | 449 | // Uncheck node 450 | node.state.checked = false; 451 | if (!options.silent) { 452 | this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); 453 | } 454 | } 455 | }; 456 | 457 | Tree.prototype.setDisabledState = function (node, state, options) { 458 | 459 | if (state === node.state.disabled) return; 460 | 461 | if (state) { 462 | 463 | // Disable node 464 | node.state.disabled = true; 465 | 466 | // Disable all other states 467 | this.setExpandedState(node, false, options); 468 | this.setSelectedState(node, false, options); 469 | this.setCheckedState(node, false, options); 470 | 471 | if (!options.silent) { 472 | this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); 473 | } 474 | } 475 | else { 476 | 477 | // Enabled node 478 | node.state.disabled = false; 479 | if (!options.silent) { 480 | this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); 481 | } 482 | } 483 | }; 484 | 485 | Tree.prototype.render = function () { 486 | 487 | if (!this.initialized) { 488 | 489 | // Setup first time only components 490 | this.$element.addClass(pluginName); 491 | this.$wrapper = $(this.template.list); 492 | 493 | this.injectStyle(); 494 | 495 | this.initialized = true; 496 | } 497 | 498 | this.$element.empty().append(this.$wrapper.empty()); 499 | 500 | // Build tree 501 | this.buildTree(this.tree, 0); 502 | }; 503 | 504 | // Starting from the root node, and recursing down the 505 | // structure we build the tree one node at a time 506 | Tree.prototype.buildTree = function (nodes, level) { 507 | 508 | if (!nodes) return; 509 | level += 1; 510 | 511 | var _this = this; 512 | $.each(nodes, function addNodes(id, node) { 513 | 514 | var treeItem = $(_this.template.item) 515 | .addClass('node-' + _this.elementId) 516 | .addClass(node.state.checked ? 'node-checked' : '') 517 | .addClass(node.state.disabled ? 'node-disabled': '') 518 | .addClass(node.state.selected ? 'node-selected' : '') 519 | .addClass(node.searchResult ? 'search-result' : '') 520 | .attr('data-nodeid', node.nodeId) 521 | .attr('style', _this.buildStyleOverride(node)); 522 | 523 | // Add indent/spacer to mimic tree structure 524 | for (var i = 0; i < (level - 1); i++) { 525 | treeItem.append(_this.template.indent); 526 | } 527 | 528 | // Add expand, collapse or empty spacer icons 529 | var classList = []; 530 | if (node.nodes) { 531 | classList.push('expand-icon'); 532 | if (node.state.expanded) { 533 | classList.push(_this.options.collapseIcon); 534 | } 535 | else { 536 | classList.push(_this.options.expandIcon); 537 | } 538 | } 539 | else { 540 | classList.push(_this.options.emptyIcon); 541 | } 542 | 543 | treeItem 544 | .append($(_this.template.icon) 545 | .addClass(classList.join(' ')) 546 | ); 547 | 548 | 549 | // Add node icon 550 | if (_this.options.showIcon) { 551 | 552 | var classList = ['node-icon']; 553 | 554 | classList.push(node.icon || _this.options.nodeIcon); 555 | if (node.state.selected) { 556 | classList.pop(); 557 | classList.push(node.selectedIcon || _this.options.selectedIcon || 558 | node.icon || _this.options.nodeIcon); 559 | } 560 | 561 | treeItem 562 | .append($(_this.template.icon) 563 | .addClass(classList.join(' ')) 564 | ); 565 | } 566 | 567 | // Add check / unchecked icon 568 | if (_this.options.showCheckbox) { 569 | 570 | var classList = ['check-icon']; 571 | if (node.state.checked) { 572 | classList.push(_this.options.checkedIcon); 573 | } 574 | else { 575 | classList.push(_this.options.uncheckedIcon); 576 | } 577 | 578 | treeItem 579 | .append($(_this.template.icon) 580 | .addClass(classList.join(' ')) 581 | ); 582 | } 583 | 584 | // Add text 585 | if (_this.options.enableLinks) { 586 | // Add hyperlink 587 | treeItem 588 | .append($(_this.template.link) 589 | .attr('href', node.href) 590 | .append(node.text) 591 | ); 592 | } 593 | else { 594 | // otherwise just text 595 | treeItem 596 | .append(node.text); 597 | } 598 | 599 | // Add tags as badges 600 | if (_this.options.showTags && node.tags) { 601 | $.each(node.tags, function addTag(id, tag) { 602 | treeItem 603 | .append($(_this.template.badge) 604 | .append(tag) 605 | ); 606 | }); 607 | } 608 | 609 | // Add item to the tree 610 | _this.$wrapper.append(treeItem); 611 | 612 | // Recursively add child ndoes 613 | if (node.nodes && node.state.expanded && !node.state.disabled) { 614 | return _this.buildTree(node.nodes, level); 615 | } 616 | }); 617 | }; 618 | 619 | // Define any node level style override for 620 | // 1. selectedNode 621 | // 2. node|data assigned color overrides 622 | Tree.prototype.buildStyleOverride = function (node) { 623 | 624 | if (node.state.disabled) return ''; 625 | 626 | var color = node.color; 627 | var backColor = node.backColor; 628 | 629 | if (this.options.highlightSelected && node.state.selected) { 630 | if (this.options.selectedColor) { 631 | color = this.options.selectedColor; 632 | } 633 | if (this.options.selectedBackColor) { 634 | backColor = this.options.selectedBackColor; 635 | } 636 | } 637 | 638 | if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { 639 | if (this.options.searchResultColor) { 640 | color = this.options.searchResultColor; 641 | } 642 | if (this.options.searchResultBackColor) { 643 | backColor = this.options.searchResultBackColor; 644 | } 645 | } 646 | 647 | return 'color:' + color + 648 | ';background-color:' + backColor + ';'; 649 | }; 650 | 651 | // Add inline style into head 652 | Tree.prototype.injectStyle = function () { 653 | 654 | if (this.options.injectStyle && !document.getElementById(this.styleId)) { 655 | $('').appendTo('head'); 656 | } 657 | }; 658 | 659 | // Construct trees style based on user options 660 | Tree.prototype.buildStyle = function () { 661 | 662 | var style = '.node-' + this.elementId + '{'; 663 | 664 | if (this.options.color) { 665 | style += 'color:' + this.options.color + ';'; 666 | } 667 | 668 | if (this.options.backColor) { 669 | style += 'background-color:' + this.options.backColor + ';'; 670 | } 671 | 672 | if (!this.options.showBorder) { 673 | style += 'border:none;'; 674 | } 675 | else if (this.options.borderColor) { 676 | style += 'border:1px solid ' + this.options.borderColor + ';'; 677 | } 678 | style += '}'; 679 | 680 | if (this.options.onhoverColor) { 681 | style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + 682 | 'background-color:' + this.options.onhoverColor + ';' + 683 | '}'; 684 | } 685 | 686 | return this.css + style; 687 | }; 688 | 689 | Tree.prototype.template = { 690 | list: '
      ', 691 | item: '
    • ', 692 | indent: '', 693 | icon: '', 694 | link: '', 695 | badge: '' 696 | }; 697 | 698 | 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}' 699 | 700 | 701 | /** 702 | Returns a single node object that matches the given node id. 703 | @param {Number} nodeId - A node's unique identifier 704 | @return {Object} node - Matching node 705 | */ 706 | Tree.prototype.getNode = function (nodeId) { 707 | return this.nodes[nodeId]; 708 | }; 709 | 710 | /** 711 | Returns the parent node of a given node, if valid otherwise returns undefined. 712 | @param {Object|Number} identifier - A valid node or node id 713 | @returns {Object} node - The parent node 714 | */ 715 | Tree.prototype.getParent = function (identifier) { 716 | var node = this.identifyNode(identifier); 717 | return this.nodes[node.parentId]; 718 | }; 719 | 720 | /** 721 | Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. 722 | @param {Object|Number} identifier - A valid node or node id 723 | @returns {Array} nodes - Sibling nodes 724 | */ 725 | Tree.prototype.getSiblings = function (identifier) { 726 | var node = this.identifyNode(identifier); 727 | var parent = this.getParent(node); 728 | var nodes = parent ? parent.nodes : this.tree; 729 | return nodes.filter(function (obj) { 730 | return obj.nodeId !== node.nodeId; 731 | }); 732 | }; 733 | 734 | /** 735 | Returns an array of selected nodes. 736 | @returns {Array} nodes - Selected nodes 737 | */ 738 | Tree.prototype.getSelected = function () { 739 | return this.findNodes('true', 'g', 'state.selected'); 740 | }; 741 | 742 | /** 743 | Returns an array of unselected nodes. 744 | @returns {Array} nodes - Unselected nodes 745 | */ 746 | Tree.prototype.getUnselected = function () { 747 | return this.findNodes('false', 'g', 'state.selected'); 748 | }; 749 | 750 | /** 751 | Returns an array of expanded nodes. 752 | @returns {Array} nodes - Expanded nodes 753 | */ 754 | Tree.prototype.getExpanded = function () { 755 | return this.findNodes('true', 'g', 'state.expanded'); 756 | }; 757 | 758 | /** 759 | Returns an array of collapsed nodes. 760 | @returns {Array} nodes - Collapsed nodes 761 | */ 762 | Tree.prototype.getCollapsed = function () { 763 | return this.findNodes('false', 'g', 'state.expanded'); 764 | }; 765 | 766 | /** 767 | Returns an array of checked nodes. 768 | @returns {Array} nodes - Checked nodes 769 | */ 770 | Tree.prototype.getChecked = function () { 771 | return this.findNodes('true', 'g', 'state.checked'); 772 | }; 773 | 774 | /** 775 | Returns an array of unchecked nodes. 776 | @returns {Array} nodes - Unchecked nodes 777 | */ 778 | Tree.prototype.getUnchecked = function () { 779 | return this.findNodes('false', 'g', 'state.checked'); 780 | }; 781 | 782 | /** 783 | Returns an array of disabled nodes. 784 | @returns {Array} nodes - Disabled nodes 785 | */ 786 | Tree.prototype.getDisabled = function () { 787 | return this.findNodes('true', 'g', 'state.disabled'); 788 | }; 789 | 790 | /** 791 | Returns an array of enabled nodes. 792 | @returns {Array} nodes - Enabled nodes 793 | */ 794 | Tree.prototype.getEnabled = function () { 795 | return this.findNodes('false', 'g', 'state.disabled'); 796 | }; 797 | 798 | 799 | /** 800 | Set a node state to selected 801 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 802 | @param {optional Object} options 803 | */ 804 | Tree.prototype.selectNode = function (identifiers, options) { 805 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 806 | this.setSelectedState(node, true, options); 807 | }, this)); 808 | 809 | this.render(); 810 | }; 811 | 812 | /** 813 | Set a node state to unselected 814 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 815 | @param {optional Object} options 816 | */ 817 | Tree.prototype.unselectNode = function (identifiers, options) { 818 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 819 | this.setSelectedState(node, false, options); 820 | }, this)); 821 | 822 | this.render(); 823 | }; 824 | 825 | /** 826 | Toggles a node selected state; selecting if unselected, unselecting if selected. 827 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 828 | @param {optional Object} options 829 | */ 830 | Tree.prototype.toggleNodeSelected = function (identifiers, options) { 831 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 832 | this.toggleSelectedState(node, options); 833 | }, this)); 834 | 835 | this.render(); 836 | }; 837 | 838 | 839 | /** 840 | Collapse all tree nodes 841 | @param {optional Object} options 842 | */ 843 | Tree.prototype.collapseAll = function (options) { 844 | var identifiers = this.findNodes('true', 'g', 'state.expanded'); 845 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 846 | this.setExpandedState(node, false, options); 847 | }, this)); 848 | 849 | this.render(); 850 | }; 851 | 852 | /** 853 | Collapse a given tree node 854 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 855 | @param {optional Object} options 856 | */ 857 | Tree.prototype.collapseNode = function (identifiers, options) { 858 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 859 | this.setExpandedState(node, false, options); 860 | }, this)); 861 | 862 | this.render(); 863 | }; 864 | 865 | /** 866 | Expand all tree nodes 867 | @param {optional Object} options 868 | */ 869 | Tree.prototype.expandAll = function (options) { 870 | options = $.extend({}, _default.options, options); 871 | 872 | if (options && options.levels) { 873 | this.expandLevels(this.tree, options.levels, options); 874 | } 875 | else { 876 | var identifiers = this.findNodes('false', 'g', 'state.expanded'); 877 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 878 | this.setExpandedState(node, true, options); 879 | }, this)); 880 | } 881 | 882 | this.render(); 883 | }; 884 | 885 | /** 886 | Expand a given tree node 887 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 888 | @param {optional Object} options 889 | */ 890 | Tree.prototype.expandNode = function (identifiers, options) { 891 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 892 | this.setExpandedState(node, true, options); 893 | if (node.nodes && (options && options.levels)) { 894 | this.expandLevels(node.nodes, options.levels-1, options); 895 | } 896 | }, this)); 897 | 898 | this.render(); 899 | }; 900 | 901 | Tree.prototype.expandLevels = function (nodes, level, options) { 902 | options = $.extend({}, _default.options, options); 903 | 904 | $.each(nodes, $.proxy(function (index, node) { 905 | this.setExpandedState(node, (level > 0) ? true : false, options); 906 | if (node.nodes) { 907 | this.expandLevels(node.nodes, level-1, options); 908 | } 909 | }, this)); 910 | }; 911 | 912 | /** 913 | Reveals a given tree node, expanding the tree from node to root. 914 | @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers 915 | @param {optional Object} options 916 | */ 917 | Tree.prototype.revealNode = function (identifiers, options) { 918 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 919 | var parentNode = this.getParent(node); 920 | while (parentNode) { 921 | this.setExpandedState(parentNode, true, options); 922 | parentNode = this.getParent(parentNode); 923 | }; 924 | }, this)); 925 | 926 | this.render(); 927 | }; 928 | 929 | /** 930 | Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. 931 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 932 | @param {optional Object} options 933 | */ 934 | Tree.prototype.toggleNodeExpanded = function (identifiers, options) { 935 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 936 | this.toggleExpandedState(node, options); 937 | }, this)); 938 | 939 | this.render(); 940 | }; 941 | 942 | 943 | /** 944 | Check all tree nodes 945 | @param {optional Object} options 946 | */ 947 | Tree.prototype.checkAll = function (options) { 948 | var identifiers = this.findNodes('false', 'g', 'state.checked'); 949 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 950 | this.setCheckedState(node, true, options); 951 | }, this)); 952 | 953 | this.render(); 954 | }; 955 | 956 | /** 957 | Check a given tree node 958 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 959 | @param {optional Object} options 960 | */ 961 | Tree.prototype.checkNode = function (identifiers, options) { 962 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 963 | this.setCheckedState(node, true, options); 964 | }, this)); 965 | 966 | this.render(); 967 | }; 968 | 969 | /** 970 | Uncheck all tree nodes 971 | @param {optional Object} options 972 | */ 973 | Tree.prototype.uncheckAll = function (options) { 974 | var identifiers = this.findNodes('true', 'g', 'state.checked'); 975 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 976 | this.setCheckedState(node, false, options); 977 | }, this)); 978 | 979 | this.render(); 980 | }; 981 | 982 | /** 983 | Uncheck a given tree node 984 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 985 | @param {optional Object} options 986 | */ 987 | Tree.prototype.uncheckNode = function (identifiers, options) { 988 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 989 | this.setCheckedState(node, false, options); 990 | }, this)); 991 | 992 | this.render(); 993 | }; 994 | 995 | /** 996 | Toggles a nodes checked state; checking if unchecked, unchecking if checked. 997 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 998 | @param {optional Object} options 999 | */ 1000 | Tree.prototype.toggleNodeChecked = function (identifiers, options) { 1001 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1002 | this.toggleCheckedState(node, options); 1003 | }, this)); 1004 | 1005 | this.render(); 1006 | }; 1007 | 1008 | 1009 | /** 1010 | Disable all tree nodes 1011 | @param {optional Object} options 1012 | */ 1013 | Tree.prototype.disableAll = function (options) { 1014 | var identifiers = this.findNodes('false', 'g', 'state.disabled'); 1015 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1016 | this.setDisabledState(node, true, options); 1017 | }, this)); 1018 | 1019 | this.render(); 1020 | }; 1021 | 1022 | /** 1023 | Disable a given tree node 1024 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1025 | @param {optional Object} options 1026 | */ 1027 | Tree.prototype.disableNode = function (identifiers, options) { 1028 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1029 | this.setDisabledState(node, true, options); 1030 | }, this)); 1031 | 1032 | this.render(); 1033 | }; 1034 | 1035 | /** 1036 | Enable all tree nodes 1037 | @param {optional Object} options 1038 | */ 1039 | Tree.prototype.enableAll = function (options) { 1040 | var identifiers = this.findNodes('true', 'g', 'state.disabled'); 1041 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1042 | this.setDisabledState(node, false, options); 1043 | }, this)); 1044 | 1045 | this.render(); 1046 | }; 1047 | 1048 | /** 1049 | Enable a given tree node 1050 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1051 | @param {optional Object} options 1052 | */ 1053 | Tree.prototype.enableNode = function (identifiers, options) { 1054 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1055 | this.setDisabledState(node, false, options); 1056 | }, this)); 1057 | 1058 | this.render(); 1059 | }; 1060 | 1061 | /** 1062 | Toggles a nodes disabled state; disabling is enabled, enabling if disabled. 1063 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1064 | @param {optional Object} options 1065 | */ 1066 | Tree.prototype.toggleNodeDisabled = function (identifiers, options) { 1067 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1068 | this.setDisabledState(node, !node.state.disabled, options); 1069 | }, this)); 1070 | 1071 | this.render(); 1072 | }; 1073 | 1074 | 1075 | /** 1076 | Common code for processing multiple identifiers 1077 | */ 1078 | Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { 1079 | 1080 | options = $.extend({}, _default.options, options); 1081 | 1082 | if (!(identifiers instanceof Array)) { 1083 | identifiers = [identifiers]; 1084 | } 1085 | 1086 | $.each(identifiers, $.proxy(function (index, identifier) { 1087 | callback(this.identifyNode(identifier), options); 1088 | }, this)); 1089 | }; 1090 | 1091 | /* 1092 | Identifies a node from either a node id or object 1093 | */ 1094 | Tree.prototype.identifyNode = function (identifier) { 1095 | return ((typeof identifier) === 'number') ? 1096 | this.nodes[identifier] : 1097 | identifier; 1098 | }; 1099 | 1100 | /** 1101 | Searches the tree for nodes (text) that match given criteria 1102 | @param {String} pattern - A given string to match against 1103 | @param {optional Object} options - Search criteria options 1104 | @return {Array} nodes - Matching nodes 1105 | */ 1106 | Tree.prototype.search = function (pattern, options) { 1107 | options = $.extend({}, _default.searchOptions, options); 1108 | 1109 | this.clearSearch({ render: false }); 1110 | 1111 | var results = []; 1112 | if (pattern && pattern.length > 0) { 1113 | 1114 | if (options.exactMatch) { 1115 | pattern = '^' + pattern + '$'; 1116 | } 1117 | 1118 | var modifier = 'g'; 1119 | if (options.ignoreCase) { 1120 | modifier += 'i'; 1121 | } 1122 | 1123 | results = this.findNodes(pattern, modifier); 1124 | 1125 | // Add searchResult property to all matching nodes 1126 | // This will be used to apply custom styles 1127 | // and when identifying result to be cleared 1128 | $.each(results, function (index, node) { 1129 | node.searchResult = true; 1130 | }) 1131 | } 1132 | 1133 | // If revealResults, then render is triggered from revealNode 1134 | // otherwise we just call render. 1135 | if (options.revealResults) { 1136 | this.revealNode(results); 1137 | } 1138 | else { 1139 | this.render(); 1140 | } 1141 | 1142 | this.$element.trigger('searchComplete', $.extend(true, {}, results)); 1143 | 1144 | return results; 1145 | }; 1146 | 1147 | /** 1148 | Clears previous search results 1149 | */ 1150 | Tree.prototype.clearSearch = function (options) { 1151 | 1152 | options = $.extend({}, { render: true }, options); 1153 | 1154 | var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { 1155 | node.searchResult = false; 1156 | }); 1157 | 1158 | if (options.render) { 1159 | this.render(); 1160 | } 1161 | 1162 | this.$element.trigger('searchCleared', $.extend(true, {}, results)); 1163 | }; 1164 | 1165 | /** 1166 | Find nodes that match a given criteria 1167 | @param {String} pattern - A given string to match against 1168 | @param {optional String} modifier - Valid RegEx modifiers 1169 | @param {optional String} attribute - Attribute to compare pattern against 1170 | @return {Array} nodes - Nodes that match your criteria 1171 | */ 1172 | Tree.prototype.findNodes = function (pattern, modifier, attribute) { 1173 | 1174 | modifier = modifier || 'g'; 1175 | attribute = attribute || 'text'; 1176 | 1177 | var _this = this; 1178 | return $.grep(this.nodes, function (node) { 1179 | var val = _this.getNodeValue(node, attribute); 1180 | if (typeof val === 'string') { 1181 | return val.match(new RegExp(pattern, modifier)); 1182 | } 1183 | }); 1184 | }; 1185 | 1186 | /** 1187 | Recursive find for retrieving nested attributes values 1188 | All values are return as strings, unless invalid 1189 | @param {Object} obj - Typically a node, could be any object 1190 | @param {String} attr - Identifies an object property using dot notation 1191 | @return {String} value - Matching attributes string representation 1192 | */ 1193 | Tree.prototype.getNodeValue = function (obj, attr) { 1194 | var index = attr.indexOf('.'); 1195 | if (index > 0) { 1196 | var _obj = obj[attr.substring(0, index)]; 1197 | var _attr = attr.substring(index + 1, attr.length); 1198 | return this.getNodeValue(_obj, _attr); 1199 | } 1200 | else { 1201 | if (obj.hasOwnProperty(attr)) { 1202 | return obj[attr].toString(); 1203 | } 1204 | else { 1205 | return undefined; 1206 | } 1207 | } 1208 | }; 1209 | 1210 | var logError = function (message) { 1211 | if (window.console) { 1212 | window.console.error(message); 1213 | } 1214 | }; 1215 | 1216 | // Prevent against multiple instantiations, 1217 | // handle updates and method calls 1218 | $.fn[pluginName] = function (options, args) { 1219 | 1220 | var result; 1221 | 1222 | this.each(function () { 1223 | var _this = $.data(this, pluginName); 1224 | if (typeof options === 'string') { 1225 | if (!_this) { 1226 | logError('Not initialized, can not call method : ' + options); 1227 | } 1228 | else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') { 1229 | logError('No such method : ' + options); 1230 | } 1231 | else { 1232 | if (!(args instanceof Array)) { 1233 | args = [ args ]; 1234 | } 1235 | result = _this[options].apply(_this, args); 1236 | } 1237 | } 1238 | else if (typeof options === 'boolean') { 1239 | result = _this; 1240 | } 1241 | else { 1242 | $.data(this, pluginName, new Tree(this, $.extend(true, {}, options))); 1243 | } 1244 | }); 1245 | 1246 | return result || this; 1247 | }; 1248 | 1249 | })(jQuery, window, document); 1250 | -------------------------------------------------------------------------------- /screenshot/default.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jonmiles/bootstrap-treeview/542f57eab636e14417216e95fa7f7049bbf3d69f/screenshot/default.PNG -------------------------------------------------------------------------------- /src/css/bootstrap-treeview.css: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-treeview.css v1.2.0 3 | * ========================================================= 4 | * Copyright 2013 Jonathan Miles 5 | * Project URL : http://www.jondmiles.com/bootstrap-treeview 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | .treeview .list-group-item { 21 | cursor: pointer; 22 | } 23 | 24 | .treeview span.indent { 25 | margin-left: 10px; 26 | margin-right: 10px; 27 | } 28 | 29 | .treeview span.icon { 30 | width: 12px; 31 | margin-right: 5px; 32 | } 33 | 34 | .treeview .node-disabled { 35 | color: silver; 36 | cursor: not-allowed; 37 | } -------------------------------------------------------------------------------- /src/js/bootstrap-treeview.js: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-treeview.js v1.2.0 3 | * ========================================================= 4 | * Copyright 2013 Jonathan Miles 5 | * Project URL : http://www.jondmiles.com/bootstrap-treeview 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | ;(function ($, window, document, undefined) { 21 | 22 | /*global jQuery, console*/ 23 | 24 | 'use strict'; 25 | 26 | var pluginName = 'treeview'; 27 | 28 | var _default = {}; 29 | 30 | _default.settings = { 31 | 32 | injectStyle: true, 33 | 34 | levels: 2, 35 | 36 | expandIcon: 'glyphicon glyphicon-plus', 37 | collapseIcon: 'glyphicon glyphicon-minus', 38 | emptyIcon: 'glyphicon', 39 | nodeIcon: '', 40 | selectedIcon: '', 41 | checkedIcon: 'glyphicon glyphicon-check', 42 | uncheckedIcon: 'glyphicon glyphicon-unchecked', 43 | 44 | color: undefined, // '#000000', 45 | backColor: undefined, // '#FFFFFF', 46 | borderColor: undefined, // '#dddddd', 47 | onhoverColor: '#F5F5F5', 48 | selectedColor: '#FFFFFF', 49 | selectedBackColor: '#428bca', 50 | searchResultColor: '#D9534F', 51 | searchResultBackColor: undefined, //'#FFFFFF', 52 | 53 | enableLinks: false, 54 | highlightSelected: true, 55 | highlightSearchResults: true, 56 | showBorder: true, 57 | showIcon: true, 58 | showCheckbox: false, 59 | showTags: false, 60 | multiSelect: false, 61 | 62 | // Event handlers 63 | onNodeChecked: undefined, 64 | onNodeCollapsed: undefined, 65 | onNodeDisabled: undefined, 66 | onNodeEnabled: undefined, 67 | onNodeExpanded: undefined, 68 | onNodeSelected: undefined, 69 | onNodeUnchecked: undefined, 70 | onNodeUnselected: undefined, 71 | onSearchComplete: undefined, 72 | onSearchCleared: undefined 73 | }; 74 | 75 | _default.options = { 76 | silent: false, 77 | ignoreChildren: false 78 | }; 79 | 80 | _default.searchOptions = { 81 | ignoreCase: true, 82 | exactMatch: false, 83 | revealResults: true 84 | }; 85 | 86 | var Tree = function (element, options) { 87 | 88 | this.$element = $(element); 89 | this.elementId = element.id; 90 | this.styleId = this.elementId + '-style'; 91 | 92 | this.init(options); 93 | 94 | return { 95 | 96 | // Options (public access) 97 | options: this.options, 98 | 99 | // Initialize / destroy methods 100 | init: $.proxy(this.init, this), 101 | remove: $.proxy(this.remove, this), 102 | 103 | // Get methods 104 | getNode: $.proxy(this.getNode, this), 105 | getParent: $.proxy(this.getParent, this), 106 | getSiblings: $.proxy(this.getSiblings, this), 107 | getSelected: $.proxy(this.getSelected, this), 108 | getUnselected: $.proxy(this.getUnselected, this), 109 | getExpanded: $.proxy(this.getExpanded, this), 110 | getCollapsed: $.proxy(this.getCollapsed, this), 111 | getChecked: $.proxy(this.getChecked, this), 112 | getUnchecked: $.proxy(this.getUnchecked, this), 113 | getDisabled: $.proxy(this.getDisabled, this), 114 | getEnabled: $.proxy(this.getEnabled, this), 115 | 116 | // Select methods 117 | selectNode: $.proxy(this.selectNode, this), 118 | unselectNode: $.proxy(this.unselectNode, this), 119 | toggleNodeSelected: $.proxy(this.toggleNodeSelected, this), 120 | 121 | // Expand / collapse methods 122 | collapseAll: $.proxy(this.collapseAll, this), 123 | collapseNode: $.proxy(this.collapseNode, this), 124 | expandAll: $.proxy(this.expandAll, this), 125 | expandNode: $.proxy(this.expandNode, this), 126 | toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this), 127 | revealNode: $.proxy(this.revealNode, this), 128 | 129 | // Expand / collapse methods 130 | checkAll: $.proxy(this.checkAll, this), 131 | checkNode: $.proxy(this.checkNode, this), 132 | uncheckAll: $.proxy(this.uncheckAll, this), 133 | uncheckNode: $.proxy(this.uncheckNode, this), 134 | toggleNodeChecked: $.proxy(this.toggleNodeChecked, this), 135 | 136 | // Disable / enable methods 137 | disableAll: $.proxy(this.disableAll, this), 138 | disableNode: $.proxy(this.disableNode, this), 139 | enableAll: $.proxy(this.enableAll, this), 140 | enableNode: $.proxy(this.enableNode, this), 141 | toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this), 142 | 143 | // Search methods 144 | search: $.proxy(this.search, this), 145 | clearSearch: $.proxy(this.clearSearch, this) 146 | }; 147 | }; 148 | 149 | Tree.prototype.init = function (options) { 150 | 151 | this.tree = []; 152 | this.nodes = []; 153 | 154 | if (options.data) { 155 | if (typeof options.data === 'string') { 156 | options.data = $.parseJSON(options.data); 157 | } 158 | this.tree = $.extend(true, [], options.data); 159 | delete options.data; 160 | } 161 | this.options = $.extend({}, _default.settings, options); 162 | 163 | this.destroy(); 164 | this.subscribeEvents(); 165 | this.setInitialStates({ nodes: this.tree }, 0); 166 | this.render(); 167 | }; 168 | 169 | Tree.prototype.remove = function () { 170 | this.destroy(); 171 | $.removeData(this, pluginName); 172 | $('#' + this.styleId).remove(); 173 | }; 174 | 175 | Tree.prototype.destroy = function () { 176 | 177 | if (!this.initialized) return; 178 | 179 | this.$wrapper.remove(); 180 | this.$wrapper = null; 181 | 182 | // Switch off events 183 | this.unsubscribeEvents(); 184 | 185 | // Reset this.initialized flag 186 | this.initialized = false; 187 | }; 188 | 189 | Tree.prototype.unsubscribeEvents = function () { 190 | 191 | this.$element.off('click'); 192 | this.$element.off('nodeChecked'); 193 | this.$element.off('nodeCollapsed'); 194 | this.$element.off('nodeDisabled'); 195 | this.$element.off('nodeEnabled'); 196 | this.$element.off('nodeExpanded'); 197 | this.$element.off('nodeSelected'); 198 | this.$element.off('nodeUnchecked'); 199 | this.$element.off('nodeUnselected'); 200 | this.$element.off('searchComplete'); 201 | this.$element.off('searchCleared'); 202 | }; 203 | 204 | Tree.prototype.subscribeEvents = function () { 205 | 206 | this.unsubscribeEvents(); 207 | 208 | this.$element.on('click', $.proxy(this.clickHandler, this)); 209 | 210 | if (typeof (this.options.onNodeChecked) === 'function') { 211 | this.$element.on('nodeChecked', this.options.onNodeChecked); 212 | } 213 | 214 | if (typeof (this.options.onNodeCollapsed) === 'function') { 215 | this.$element.on('nodeCollapsed', this.options.onNodeCollapsed); 216 | } 217 | 218 | if (typeof (this.options.onNodeDisabled) === 'function') { 219 | this.$element.on('nodeDisabled', this.options.onNodeDisabled); 220 | } 221 | 222 | if (typeof (this.options.onNodeEnabled) === 'function') { 223 | this.$element.on('nodeEnabled', this.options.onNodeEnabled); 224 | } 225 | 226 | if (typeof (this.options.onNodeExpanded) === 'function') { 227 | this.$element.on('nodeExpanded', this.options.onNodeExpanded); 228 | } 229 | 230 | if (typeof (this.options.onNodeSelected) === 'function') { 231 | this.$element.on('nodeSelected', this.options.onNodeSelected); 232 | } 233 | 234 | if (typeof (this.options.onNodeUnchecked) === 'function') { 235 | this.$element.on('nodeUnchecked', this.options.onNodeUnchecked); 236 | } 237 | 238 | if (typeof (this.options.onNodeUnselected) === 'function') { 239 | this.$element.on('nodeUnselected', this.options.onNodeUnselected); 240 | } 241 | 242 | if (typeof (this.options.onSearchComplete) === 'function') { 243 | this.$element.on('searchComplete', this.options.onSearchComplete); 244 | } 245 | 246 | if (typeof (this.options.onSearchCleared) === 'function') { 247 | this.$element.on('searchCleared', this.options.onSearchCleared); 248 | } 249 | }; 250 | 251 | /* 252 | Recurse the tree structure and ensure all nodes have 253 | valid initial states. User defined states will be preserved. 254 | For performance we also take this opportunity to 255 | index nodes in a flattened structure 256 | */ 257 | Tree.prototype.setInitialStates = function (node, level) { 258 | 259 | if (!node.nodes) return; 260 | level += 1; 261 | 262 | var parent = node; 263 | var _this = this; 264 | $.each(node.nodes, function checkStates(index, node) { 265 | 266 | // nodeId : unique, incremental identifier 267 | node.nodeId = _this.nodes.length; 268 | 269 | // parentId : transversing up the tree 270 | node.parentId = parent.nodeId; 271 | 272 | // if not provided set selectable default value 273 | if (!node.hasOwnProperty('selectable')) { 274 | node.selectable = true; 275 | } 276 | 277 | // where provided we should preserve states 278 | node.state = node.state || {}; 279 | 280 | // set checked state; unless set always false 281 | if (!node.state.hasOwnProperty('checked')) { 282 | node.state.checked = false; 283 | } 284 | 285 | // set enabled state; unless set always false 286 | if (!node.state.hasOwnProperty('disabled')) { 287 | node.state.disabled = false; 288 | } 289 | 290 | // set expanded state; if not provided based on levels 291 | if (!node.state.hasOwnProperty('expanded')) { 292 | if (!node.state.disabled && 293 | (level < _this.options.levels) && 294 | (node.nodes && node.nodes.length > 0)) { 295 | node.state.expanded = true; 296 | } 297 | else { 298 | node.state.expanded = false; 299 | } 300 | } 301 | 302 | // set selected state; unless set always false 303 | if (!node.state.hasOwnProperty('selected')) { 304 | node.state.selected = false; 305 | } 306 | 307 | // index nodes in a flattened structure for use later 308 | _this.nodes.push(node); 309 | 310 | // recurse child nodes and transverse the tree 311 | if (node.nodes) { 312 | _this.setInitialStates(node, level); 313 | } 314 | }); 315 | }; 316 | 317 | Tree.prototype.clickHandler = function (event) { 318 | 319 | if (!this.options.enableLinks) event.preventDefault(); 320 | 321 | var target = $(event.target); 322 | var node = this.findNode(target); 323 | if (!node || node.state.disabled) return; 324 | 325 | var classList = target.attr('class') ? target.attr('class').split(' ') : []; 326 | if ((classList.indexOf('expand-icon') !== -1)) { 327 | 328 | this.toggleExpandedState(node, _default.options); 329 | this.render(); 330 | } 331 | else if ((classList.indexOf('check-icon') !== -1)) { 332 | 333 | this.toggleCheckedState(node, _default.options); 334 | this.render(); 335 | } 336 | else { 337 | 338 | if (node.selectable) { 339 | this.toggleSelectedState(node, _default.options); 340 | } else { 341 | this.toggleExpandedState(node, _default.options); 342 | } 343 | 344 | this.render(); 345 | } 346 | }; 347 | 348 | // Looks up the DOM for the closest parent list item to retrieve the 349 | // data attribute nodeid, which is used to lookup the node in the flattened structure. 350 | Tree.prototype.findNode = function (target) { 351 | 352 | var nodeId = target.closest('li.list-group-item').attr('data-nodeid'); 353 | var node = this.nodes[nodeId]; 354 | 355 | if (!node) { 356 | console.log('Error: node does not exist'); 357 | } 358 | return node; 359 | }; 360 | 361 | Tree.prototype.toggleExpandedState = function (node, options) { 362 | if (!node) return; 363 | this.setExpandedState(node, !node.state.expanded, options); 364 | }; 365 | 366 | Tree.prototype.setExpandedState = function (node, state, options) { 367 | 368 | if (state === node.state.expanded) return; 369 | 370 | if (state && node.nodes) { 371 | 372 | // Expand a node 373 | node.state.expanded = true; 374 | if (!options.silent) { 375 | this.$element.trigger('nodeExpanded', $.extend(true, {}, node)); 376 | } 377 | } 378 | else if (!state) { 379 | 380 | // Collapse a node 381 | node.state.expanded = false; 382 | if (!options.silent) { 383 | this.$element.trigger('nodeCollapsed', $.extend(true, {}, node)); 384 | } 385 | 386 | // Collapse child nodes 387 | if (node.nodes && !options.ignoreChildren) { 388 | $.each(node.nodes, $.proxy(function (index, node) { 389 | this.setExpandedState(node, false, options); 390 | }, this)); 391 | } 392 | } 393 | }; 394 | 395 | Tree.prototype.toggleSelectedState = function (node, options) { 396 | if (!node) return; 397 | this.setSelectedState(node, !node.state.selected, options); 398 | }; 399 | 400 | Tree.prototype.setSelectedState = function (node, state, options) { 401 | 402 | if (state === node.state.selected) return; 403 | 404 | if (state) { 405 | 406 | // If multiSelect false, unselect previously selected 407 | if (!this.options.multiSelect) { 408 | $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) { 409 | this.setSelectedState(node, false, options); 410 | }, this)); 411 | } 412 | 413 | // Continue selecting node 414 | node.state.selected = true; 415 | if (!options.silent) { 416 | this.$element.trigger('nodeSelected', $.extend(true, {}, node)); 417 | } 418 | } 419 | else { 420 | 421 | // Unselect node 422 | node.state.selected = false; 423 | if (!options.silent) { 424 | this.$element.trigger('nodeUnselected', $.extend(true, {}, node)); 425 | } 426 | } 427 | }; 428 | 429 | Tree.prototype.toggleCheckedState = function (node, options) { 430 | if (!node) return; 431 | this.setCheckedState(node, !node.state.checked, options); 432 | }; 433 | 434 | Tree.prototype.setCheckedState = function (node, state, options) { 435 | 436 | if (state === node.state.checked) return; 437 | 438 | if (state) { 439 | 440 | // Check node 441 | node.state.checked = true; 442 | 443 | if (!options.silent) { 444 | this.$element.trigger('nodeChecked', $.extend(true, {}, node)); 445 | } 446 | } 447 | else { 448 | 449 | // Uncheck node 450 | node.state.checked = false; 451 | if (!options.silent) { 452 | this.$element.trigger('nodeUnchecked', $.extend(true, {}, node)); 453 | } 454 | } 455 | }; 456 | 457 | Tree.prototype.setDisabledState = function (node, state, options) { 458 | 459 | if (state === node.state.disabled) return; 460 | 461 | if (state) { 462 | 463 | // Disable node 464 | node.state.disabled = true; 465 | 466 | // Disable all other states 467 | this.setExpandedState(node, false, options); 468 | this.setSelectedState(node, false, options); 469 | this.setCheckedState(node, false, options); 470 | 471 | if (!options.silent) { 472 | this.$element.trigger('nodeDisabled', $.extend(true, {}, node)); 473 | } 474 | } 475 | else { 476 | 477 | // Enabled node 478 | node.state.disabled = false; 479 | if (!options.silent) { 480 | this.$element.trigger('nodeEnabled', $.extend(true, {}, node)); 481 | } 482 | } 483 | }; 484 | 485 | Tree.prototype.render = function () { 486 | 487 | if (!this.initialized) { 488 | 489 | // Setup first time only components 490 | this.$element.addClass(pluginName); 491 | this.$wrapper = $(this.template.list); 492 | 493 | this.injectStyle(); 494 | 495 | this.initialized = true; 496 | } 497 | 498 | this.$element.empty().append(this.$wrapper.empty()); 499 | 500 | // Build tree 501 | this.buildTree(this.tree, 0); 502 | }; 503 | 504 | // Starting from the root node, and recursing down the 505 | // structure we build the tree one node at a time 506 | Tree.prototype.buildTree = function (nodes, level) { 507 | 508 | if (!nodes) return; 509 | level += 1; 510 | 511 | var _this = this; 512 | $.each(nodes, function addNodes(id, node) { 513 | 514 | var treeItem = $(_this.template.item) 515 | .addClass('node-' + _this.elementId) 516 | .addClass(node.state.checked ? 'node-checked' : '') 517 | .addClass(node.state.disabled ? 'node-disabled': '') 518 | .addClass(node.state.selected ? 'node-selected' : '') 519 | .addClass(node.searchResult ? 'search-result' : '') 520 | .attr('data-nodeid', node.nodeId) 521 | .attr('style', _this.buildStyleOverride(node)); 522 | 523 | // Add indent/spacer to mimic tree structure 524 | for (var i = 0; i < (level - 1); i++) { 525 | treeItem.append(_this.template.indent); 526 | } 527 | 528 | // Add expand, collapse or empty spacer icons 529 | var classList = []; 530 | if (node.nodes) { 531 | classList.push('expand-icon'); 532 | if (node.state.expanded) { 533 | classList.push(_this.options.collapseIcon); 534 | } 535 | else { 536 | classList.push(_this.options.expandIcon); 537 | } 538 | } 539 | else { 540 | classList.push(_this.options.emptyIcon); 541 | } 542 | 543 | treeItem 544 | .append($(_this.template.icon) 545 | .addClass(classList.join(' ')) 546 | ); 547 | 548 | 549 | // Add node icon 550 | if (_this.options.showIcon) { 551 | 552 | var classList = ['node-icon']; 553 | 554 | classList.push(node.icon || _this.options.nodeIcon); 555 | if (node.state.selected) { 556 | classList.pop(); 557 | classList.push(node.selectedIcon || _this.options.selectedIcon || 558 | node.icon || _this.options.nodeIcon); 559 | } 560 | 561 | treeItem 562 | .append($(_this.template.icon) 563 | .addClass(classList.join(' ')) 564 | ); 565 | } 566 | 567 | // Add check / unchecked icon 568 | if (_this.options.showCheckbox) { 569 | 570 | var classList = ['check-icon']; 571 | if (node.state.checked) { 572 | classList.push(_this.options.checkedIcon); 573 | } 574 | else { 575 | classList.push(_this.options.uncheckedIcon); 576 | } 577 | 578 | treeItem 579 | .append($(_this.template.icon) 580 | .addClass(classList.join(' ')) 581 | ); 582 | } 583 | 584 | // Add text 585 | if (_this.options.enableLinks) { 586 | // Add hyperlink 587 | treeItem 588 | .append($(_this.template.link) 589 | .attr('href', node.href) 590 | .append(node.text) 591 | ); 592 | } 593 | else { 594 | // otherwise just text 595 | treeItem 596 | .append(node.text); 597 | } 598 | 599 | // Add tags as badges 600 | if (_this.options.showTags && node.tags) { 601 | $.each(node.tags, function addTag(id, tag) { 602 | treeItem 603 | .append($(_this.template.badge) 604 | .append(tag) 605 | ); 606 | }); 607 | } 608 | 609 | // Add item to the tree 610 | _this.$wrapper.append(treeItem); 611 | 612 | // Recursively add child ndoes 613 | if (node.nodes && node.state.expanded && !node.state.disabled) { 614 | return _this.buildTree(node.nodes, level); 615 | } 616 | }); 617 | }; 618 | 619 | // Define any node level style override for 620 | // 1. selectedNode 621 | // 2. node|data assigned color overrides 622 | Tree.prototype.buildStyleOverride = function (node) { 623 | 624 | if (node.state.disabled) return ''; 625 | 626 | var color = node.color; 627 | var backColor = node.backColor; 628 | 629 | if (this.options.highlightSelected && node.state.selected) { 630 | if (this.options.selectedColor) { 631 | color = this.options.selectedColor; 632 | } 633 | if (this.options.selectedBackColor) { 634 | backColor = this.options.selectedBackColor; 635 | } 636 | } 637 | 638 | if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) { 639 | if (this.options.searchResultColor) { 640 | color = this.options.searchResultColor; 641 | } 642 | if (this.options.searchResultBackColor) { 643 | backColor = this.options.searchResultBackColor; 644 | } 645 | } 646 | 647 | return 'color:' + color + 648 | ';background-color:' + backColor + ';'; 649 | }; 650 | 651 | // Add inline style into head 652 | Tree.prototype.injectStyle = function () { 653 | 654 | if (this.options.injectStyle && !document.getElementById(this.styleId)) { 655 | $('').appendTo('head'); 656 | } 657 | }; 658 | 659 | // Construct trees style based on user options 660 | Tree.prototype.buildStyle = function () { 661 | 662 | var style = '.node-' + this.elementId + '{'; 663 | 664 | if (this.options.color) { 665 | style += 'color:' + this.options.color + ';'; 666 | } 667 | 668 | if (this.options.backColor) { 669 | style += 'background-color:' + this.options.backColor + ';'; 670 | } 671 | 672 | if (!this.options.showBorder) { 673 | style += 'border:none;'; 674 | } 675 | else if (this.options.borderColor) { 676 | style += 'border:1px solid ' + this.options.borderColor + ';'; 677 | } 678 | style += '}'; 679 | 680 | if (this.options.onhoverColor) { 681 | style += '.node-' + this.elementId + ':not(.node-disabled):hover{' + 682 | 'background-color:' + this.options.onhoverColor + ';' + 683 | '}'; 684 | } 685 | 686 | return this.css + style; 687 | }; 688 | 689 | Tree.prototype.template = { 690 | list: '
        ', 691 | item: '
      • ', 692 | indent: '', 693 | icon: '', 694 | link: '', 695 | badge: '' 696 | }; 697 | 698 | 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}' 699 | 700 | 701 | /** 702 | Returns a single node object that matches the given node id. 703 | @param {Number} nodeId - A node's unique identifier 704 | @return {Object} node - Matching node 705 | */ 706 | Tree.prototype.getNode = function (nodeId) { 707 | return this.nodes[nodeId]; 708 | }; 709 | 710 | /** 711 | Returns the parent node of a given node, if valid otherwise returns undefined. 712 | @param {Object|Number} identifier - A valid node or node id 713 | @returns {Object} node - The parent node 714 | */ 715 | Tree.prototype.getParent = function (identifier) { 716 | var node = this.identifyNode(identifier); 717 | return this.nodes[node.parentId]; 718 | }; 719 | 720 | /** 721 | Returns an array of sibling nodes for a given node, if valid otherwise returns undefined. 722 | @param {Object|Number} identifier - A valid node or node id 723 | @returns {Array} nodes - Sibling nodes 724 | */ 725 | Tree.prototype.getSiblings = function (identifier) { 726 | var node = this.identifyNode(identifier); 727 | var parent = this.getParent(node); 728 | var nodes = parent ? parent.nodes : this.tree; 729 | return nodes.filter(function (obj) { 730 | return obj.nodeId !== node.nodeId; 731 | }); 732 | }; 733 | 734 | /** 735 | Returns an array of selected nodes. 736 | @returns {Array} nodes - Selected nodes 737 | */ 738 | Tree.prototype.getSelected = function () { 739 | return this.findNodes('true', 'g', 'state.selected'); 740 | }; 741 | 742 | /** 743 | Returns an array of unselected nodes. 744 | @returns {Array} nodes - Unselected nodes 745 | */ 746 | Tree.prototype.getUnselected = function () { 747 | return this.findNodes('false', 'g', 'state.selected'); 748 | }; 749 | 750 | /** 751 | Returns an array of expanded nodes. 752 | @returns {Array} nodes - Expanded nodes 753 | */ 754 | Tree.prototype.getExpanded = function () { 755 | return this.findNodes('true', 'g', 'state.expanded'); 756 | }; 757 | 758 | /** 759 | Returns an array of collapsed nodes. 760 | @returns {Array} nodes - Collapsed nodes 761 | */ 762 | Tree.prototype.getCollapsed = function () { 763 | return this.findNodes('false', 'g', 'state.expanded'); 764 | }; 765 | 766 | /** 767 | Returns an array of checked nodes. 768 | @returns {Array} nodes - Checked nodes 769 | */ 770 | Tree.prototype.getChecked = function () { 771 | return this.findNodes('true', 'g', 'state.checked'); 772 | }; 773 | 774 | /** 775 | Returns an array of unchecked nodes. 776 | @returns {Array} nodes - Unchecked nodes 777 | */ 778 | Tree.prototype.getUnchecked = function () { 779 | return this.findNodes('false', 'g', 'state.checked'); 780 | }; 781 | 782 | /** 783 | Returns an array of disabled nodes. 784 | @returns {Array} nodes - Disabled nodes 785 | */ 786 | Tree.prototype.getDisabled = function () { 787 | return this.findNodes('true', 'g', 'state.disabled'); 788 | }; 789 | 790 | /** 791 | Returns an array of enabled nodes. 792 | @returns {Array} nodes - Enabled nodes 793 | */ 794 | Tree.prototype.getEnabled = function () { 795 | return this.findNodes('false', 'g', 'state.disabled'); 796 | }; 797 | 798 | 799 | /** 800 | Set a node state to selected 801 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 802 | @param {optional Object} options 803 | */ 804 | Tree.prototype.selectNode = function (identifiers, options) { 805 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 806 | this.setSelectedState(node, true, options); 807 | }, this)); 808 | 809 | this.render(); 810 | }; 811 | 812 | /** 813 | Set a node state to unselected 814 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 815 | @param {optional Object} options 816 | */ 817 | Tree.prototype.unselectNode = function (identifiers, options) { 818 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 819 | this.setSelectedState(node, false, options); 820 | }, this)); 821 | 822 | this.render(); 823 | }; 824 | 825 | /** 826 | Toggles a node selected state; selecting if unselected, unselecting if selected. 827 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 828 | @param {optional Object} options 829 | */ 830 | Tree.prototype.toggleNodeSelected = function (identifiers, options) { 831 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 832 | this.toggleSelectedState(node, options); 833 | }, this)); 834 | 835 | this.render(); 836 | }; 837 | 838 | 839 | /** 840 | Collapse all tree nodes 841 | @param {optional Object} options 842 | */ 843 | Tree.prototype.collapseAll = function (options) { 844 | var identifiers = this.findNodes('true', 'g', 'state.expanded'); 845 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 846 | this.setExpandedState(node, false, options); 847 | }, this)); 848 | 849 | this.render(); 850 | }; 851 | 852 | /** 853 | Collapse a given tree node 854 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 855 | @param {optional Object} options 856 | */ 857 | Tree.prototype.collapseNode = function (identifiers, options) { 858 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 859 | this.setExpandedState(node, false, options); 860 | }, this)); 861 | 862 | this.render(); 863 | }; 864 | 865 | /** 866 | Expand all tree nodes 867 | @param {optional Object} options 868 | */ 869 | Tree.prototype.expandAll = function (options) { 870 | options = $.extend({}, _default.options, options); 871 | 872 | if (options && options.levels) { 873 | this.expandLevels(this.tree, options.levels, options); 874 | } 875 | else { 876 | var identifiers = this.findNodes('false', 'g', 'state.expanded'); 877 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 878 | this.setExpandedState(node, true, options); 879 | }, this)); 880 | } 881 | 882 | this.render(); 883 | }; 884 | 885 | /** 886 | Expand a given tree node 887 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 888 | @param {optional Object} options 889 | */ 890 | Tree.prototype.expandNode = function (identifiers, options) { 891 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 892 | this.setExpandedState(node, true, options); 893 | if (node.nodes && (options && options.levels)) { 894 | this.expandLevels(node.nodes, options.levels-1, options); 895 | } 896 | }, this)); 897 | 898 | this.render(); 899 | }; 900 | 901 | Tree.prototype.expandLevels = function (nodes, level, options) { 902 | options = $.extend({}, _default.options, options); 903 | 904 | $.each(nodes, $.proxy(function (index, node) { 905 | this.setExpandedState(node, (level > 0) ? true : false, options); 906 | if (node.nodes) { 907 | this.expandLevels(node.nodes, level-1, options); 908 | } 909 | }, this)); 910 | }; 911 | 912 | /** 913 | Reveals a given tree node, expanding the tree from node to root. 914 | @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers 915 | @param {optional Object} options 916 | */ 917 | Tree.prototype.revealNode = function (identifiers, options) { 918 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 919 | var parentNode = this.getParent(node); 920 | while (parentNode) { 921 | this.setExpandedState(parentNode, true, options); 922 | parentNode = this.getParent(parentNode); 923 | }; 924 | }, this)); 925 | 926 | this.render(); 927 | }; 928 | 929 | /** 930 | Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed. 931 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 932 | @param {optional Object} options 933 | */ 934 | Tree.prototype.toggleNodeExpanded = function (identifiers, options) { 935 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 936 | this.toggleExpandedState(node, options); 937 | }, this)); 938 | 939 | this.render(); 940 | }; 941 | 942 | 943 | /** 944 | Check all tree nodes 945 | @param {optional Object} options 946 | */ 947 | Tree.prototype.checkAll = function (options) { 948 | var identifiers = this.findNodes('false', 'g', 'state.checked'); 949 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 950 | this.setCheckedState(node, true, options); 951 | }, this)); 952 | 953 | this.render(); 954 | }; 955 | 956 | /** 957 | Check a given tree node 958 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 959 | @param {optional Object} options 960 | */ 961 | Tree.prototype.checkNode = function (identifiers, options) { 962 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 963 | this.setCheckedState(node, true, options); 964 | }, this)); 965 | 966 | this.render(); 967 | }; 968 | 969 | /** 970 | Uncheck all tree nodes 971 | @param {optional Object} options 972 | */ 973 | Tree.prototype.uncheckAll = function (options) { 974 | var identifiers = this.findNodes('true', 'g', 'state.checked'); 975 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 976 | this.setCheckedState(node, false, options); 977 | }, this)); 978 | 979 | this.render(); 980 | }; 981 | 982 | /** 983 | Uncheck a given tree node 984 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 985 | @param {optional Object} options 986 | */ 987 | Tree.prototype.uncheckNode = function (identifiers, options) { 988 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 989 | this.setCheckedState(node, false, options); 990 | }, this)); 991 | 992 | this.render(); 993 | }; 994 | 995 | /** 996 | Toggles a nodes checked state; checking if unchecked, unchecking if checked. 997 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 998 | @param {optional Object} options 999 | */ 1000 | Tree.prototype.toggleNodeChecked = function (identifiers, options) { 1001 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1002 | this.toggleCheckedState(node, options); 1003 | }, this)); 1004 | 1005 | this.render(); 1006 | }; 1007 | 1008 | 1009 | /** 1010 | Disable all tree nodes 1011 | @param {optional Object} options 1012 | */ 1013 | Tree.prototype.disableAll = function (options) { 1014 | var identifiers = this.findNodes('false', 'g', 'state.disabled'); 1015 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1016 | this.setDisabledState(node, true, options); 1017 | }, this)); 1018 | 1019 | this.render(); 1020 | }; 1021 | 1022 | /** 1023 | Disable a given tree node 1024 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1025 | @param {optional Object} options 1026 | */ 1027 | Tree.prototype.disableNode = function (identifiers, options) { 1028 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1029 | this.setDisabledState(node, true, options); 1030 | }, this)); 1031 | 1032 | this.render(); 1033 | }; 1034 | 1035 | /** 1036 | Enable all tree nodes 1037 | @param {optional Object} options 1038 | */ 1039 | Tree.prototype.enableAll = function (options) { 1040 | var identifiers = this.findNodes('true', 'g', 'state.disabled'); 1041 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1042 | this.setDisabledState(node, false, options); 1043 | }, this)); 1044 | 1045 | this.render(); 1046 | }; 1047 | 1048 | /** 1049 | Enable a given tree node 1050 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1051 | @param {optional Object} options 1052 | */ 1053 | Tree.prototype.enableNode = function (identifiers, options) { 1054 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1055 | this.setDisabledState(node, false, options); 1056 | }, this)); 1057 | 1058 | this.render(); 1059 | }; 1060 | 1061 | /** 1062 | Toggles a nodes disabled state; disabling is enabled, enabling if disabled. 1063 | @param {Object|Number} identifiers - A valid node, node id or array of node identifiers 1064 | @param {optional Object} options 1065 | */ 1066 | Tree.prototype.toggleNodeDisabled = function (identifiers, options) { 1067 | this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) { 1068 | this.setDisabledState(node, !node.state.disabled, options); 1069 | }, this)); 1070 | 1071 | this.render(); 1072 | }; 1073 | 1074 | 1075 | /** 1076 | Common code for processing multiple identifiers 1077 | */ 1078 | Tree.prototype.forEachIdentifier = function (identifiers, options, callback) { 1079 | 1080 | options = $.extend({}, _default.options, options); 1081 | 1082 | if (!(identifiers instanceof Array)) { 1083 | identifiers = [identifiers]; 1084 | } 1085 | 1086 | $.each(identifiers, $.proxy(function (index, identifier) { 1087 | callback(this.identifyNode(identifier), options); 1088 | }, this)); 1089 | }; 1090 | 1091 | /* 1092 | Identifies a node from either a node id or object 1093 | */ 1094 | Tree.prototype.identifyNode = function (identifier) { 1095 | return ((typeof identifier) === 'number') ? 1096 | this.nodes[identifier] : 1097 | identifier; 1098 | }; 1099 | 1100 | /** 1101 | Searches the tree for nodes (text) that match given criteria 1102 | @param {String} pattern - A given string to match against 1103 | @param {optional Object} options - Search criteria options 1104 | @return {Array} nodes - Matching nodes 1105 | */ 1106 | Tree.prototype.search = function (pattern, options) { 1107 | options = $.extend({}, _default.searchOptions, options); 1108 | 1109 | this.clearSearch({ render: false }); 1110 | 1111 | var results = []; 1112 | if (pattern && pattern.length > 0) { 1113 | 1114 | if (options.exactMatch) { 1115 | pattern = '^' + pattern + '$'; 1116 | } 1117 | 1118 | var modifier = 'g'; 1119 | if (options.ignoreCase) { 1120 | modifier += 'i'; 1121 | } 1122 | 1123 | results = this.findNodes(pattern, modifier); 1124 | 1125 | // Add searchResult property to all matching nodes 1126 | // This will be used to apply custom styles 1127 | // and when identifying result to be cleared 1128 | $.each(results, function (index, node) { 1129 | node.searchResult = true; 1130 | }) 1131 | } 1132 | 1133 | // If revealResults, then render is triggered from revealNode 1134 | // otherwise we just call render. 1135 | if (options.revealResults) { 1136 | this.revealNode(results); 1137 | } 1138 | else { 1139 | this.render(); 1140 | } 1141 | 1142 | this.$element.trigger('searchComplete', $.extend(true, {}, results)); 1143 | 1144 | return results; 1145 | }; 1146 | 1147 | /** 1148 | Clears previous search results 1149 | */ 1150 | Tree.prototype.clearSearch = function (options) { 1151 | 1152 | options = $.extend({}, { render: true }, options); 1153 | 1154 | var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) { 1155 | node.searchResult = false; 1156 | }); 1157 | 1158 | if (options.render) { 1159 | this.render(); 1160 | } 1161 | 1162 | this.$element.trigger('searchCleared', $.extend(true, {}, results)); 1163 | }; 1164 | 1165 | /** 1166 | Find nodes that match a given criteria 1167 | @param {String} pattern - A given string to match against 1168 | @param {optional String} modifier - Valid RegEx modifiers 1169 | @param {optional String} attribute - Attribute to compare pattern against 1170 | @return {Array} nodes - Nodes that match your criteria 1171 | */ 1172 | Tree.prototype.findNodes = function (pattern, modifier, attribute) { 1173 | 1174 | modifier = modifier || 'g'; 1175 | attribute = attribute || 'text'; 1176 | 1177 | var _this = this; 1178 | return $.grep(this.nodes, function (node) { 1179 | var val = _this.getNodeValue(node, attribute); 1180 | if (typeof val === 'string') { 1181 | return val.match(new RegExp(pattern, modifier)); 1182 | } 1183 | }); 1184 | }; 1185 | 1186 | /** 1187 | Recursive find for retrieving nested attributes values 1188 | All values are return as strings, unless invalid 1189 | @param {Object} obj - Typically a node, could be any object 1190 | @param {String} attr - Identifies an object property using dot notation 1191 | @return {String} value - Matching attributes string representation 1192 | */ 1193 | Tree.prototype.getNodeValue = function (obj, attr) { 1194 | var index = attr.indexOf('.'); 1195 | if (index > 0) { 1196 | var _obj = obj[attr.substring(0, index)]; 1197 | var _attr = attr.substring(index + 1, attr.length); 1198 | return this.getNodeValue(_obj, _attr); 1199 | } 1200 | else { 1201 | if (obj.hasOwnProperty(attr)) { 1202 | return obj[attr].toString(); 1203 | } 1204 | else { 1205 | return undefined; 1206 | } 1207 | } 1208 | }; 1209 | 1210 | var logError = function (message) { 1211 | if (window.console) { 1212 | window.console.error(message); 1213 | } 1214 | }; 1215 | 1216 | // Prevent against multiple instantiations, 1217 | // handle updates and method calls 1218 | $.fn[pluginName] = function (options, args) { 1219 | 1220 | var result; 1221 | 1222 | this.each(function () { 1223 | var _this = $.data(this, pluginName); 1224 | if (typeof options === 'string') { 1225 | if (!_this) { 1226 | logError('Not initialized, can not call method : ' + options); 1227 | } 1228 | else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') { 1229 | logError('No such method : ' + options); 1230 | } 1231 | else { 1232 | if (!(args instanceof Array)) { 1233 | args = [ args ]; 1234 | } 1235 | result = _this[options].apply(_this, args); 1236 | } 1237 | } 1238 | else if (typeof options === 'boolean') { 1239 | result = _this; 1240 | } 1241 | else { 1242 | $.data(this, pluginName, new Tree(this, $.extend(true, {}, options))); 1243 | } 1244 | }); 1245 | 1246 | return result || this; 1247 | }; 1248 | 1249 | })(jQuery, window, document); 1250 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Tests are run using [QUnit](https://qunitjs.com/) and [PhantomJS](http://phantomjs.org/) 4 | 5 | To run the tests yourself. 6 | 7 | ```javascript 8 | $ npm install 9 | $ npm test 10 | ``` 11 | 12 | To view the test report. 13 | 14 | ```javascript 15 | $ npm install 16 | $ npm start 17 | ``` 18 | 19 | Then open a browser and navigate to [http://localhost:3000/tests.html](http://localhost:3000/tests.html) 20 | -------------------------------------------------------------------------------- /tests/lib/bootstrap-treeview.css: -------------------------------------------------------------------------------- 1 | /* ========================================================= 2 | * bootstrap-treeview.css v1.2.0 3 | * ========================================================= 4 | * Copyright 2013 Jonathan Miles 5 | * Project URL : http://www.jondmiles.com/bootstrap-treeview 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ========================================================= */ 19 | 20 | .treeview .list-group-item { 21 | cursor: pointer; 22 | } 23 | 24 | .treeview span.indent { 25 | margin-left: 10px; 26 | margin-right: 10px; 27 | } 28 | 29 | .treeview span.icon { 30 | width: 12px; 31 | margin-right: 5px; 32 | } 33 | 34 | .treeview .node-disabled { 35 | color: silver; 36 | cursor: not-allowed; 37 | } -------------------------------------------------------------------------------- /tests/lib/qunit-1.12.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /tests/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | bootstrap-treeview.js Tests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
        22 |
        23 | 24 | 25 |
        26 |
        27 | 28 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | /* global $, module, test, equal, ok */ 2 | 3 | ;(function () { 4 | 5 | 'use strict'; 6 | 7 | function init(options) { 8 | return $('#treeview').treeview(options); 9 | } 10 | 11 | function getOptions(el) { 12 | return el.data().treeview.options; 13 | } 14 | 15 | var data = [ 16 | { 17 | text: 'Parent 1', 18 | nodes: [ 19 | { 20 | text: 'Child 1', 21 | nodes: [ 22 | { 23 | text: 'Grandchild 1' 24 | }, 25 | { 26 | text: 'Grandchild 2' 27 | } 28 | ] 29 | }, 30 | { 31 | text: 'Child 2' 32 | } 33 | ] 34 | }, 35 | { 36 | text: 'Parent 2' 37 | }, 38 | { 39 | text: 'Parent 3' 40 | }, 41 | { 42 | text: 'Parent 4' 43 | }, 44 | { 45 | text: 'Parent 5' 46 | } 47 | ]; 48 | 49 | var json = '[' + 50 | '{' + 51 | '"text": "Parent 1",' + 52 | '"nodes": [' + 53 | '{' + 54 | '"text": "Child 1",' + 55 | '"nodes": [' + 56 | '{' + 57 | '"text": "Grandchild 1"' + 58 | '},' + 59 | '{' + 60 | '"text": "Grandchild 2"' + 61 | '}' + 62 | ']' + 63 | '},' + 64 | '{' + 65 | '"text": "Child 2"' + 66 | '}' + 67 | ']' + 68 | '},' + 69 | '{' + 70 | '"text": "Parent 2"' + 71 | '},' + 72 | '{' + 73 | '"text": "Parent 3"' + 74 | '},' + 75 | '{' + 76 | '"text": "Parent 4"' + 77 | '},' + 78 | '{' + 79 | '"text": "Parent 5"' + 80 | '}' + 81 | ']'; 82 | 83 | module('Options'); 84 | 85 | test('Options setup', function () { 86 | // First test defaults option values 87 | var el = init(), 88 | options = getOptions(el); 89 | ok(options, 'Defaults created ok'); 90 | equal(options.levels, 2, 'levels default ok'); 91 | equal(options.expandIcon, 'glyphicon glyphicon-plus', 'expandIcon default ok'); 92 | equal(options.collapseIcon, 'glyphicon glyphicon-minus', 'collapseIcon default ok'); 93 | equal(options.emptyIcon, 'glyphicon', 'emptyIcon default ok'); 94 | equal(options.nodeIcon, '', 'nodeIcon default ok'); 95 | equal(options.selectedIcon, '', 'selectedIcon default ok'); 96 | equal(options.checkedIcon, 'glyphicon glyphicon-check', 'checkedIcon default ok'); 97 | equal(options.uncheckedIcon, 'glyphicon glyphicon-unchecked', 'uncheckedIcon default ok'); 98 | equal(options.color, undefined, 'color default ok'); 99 | equal(options.backColor, undefined, 'backColor default ok'); 100 | equal(options.borderColor, undefined, 'borderColor default ok'); 101 | equal(options.onhoverColor, '#F5F5F5', 'onhoverColor default ok'); 102 | equal(options.selectedColor, '#FFFFFF', 'selectedColor default ok'); 103 | equal(options.selectedBackColor, '#428bca', 'selectedBackColor default ok'); 104 | equal(options.searchResultColor, '#D9534F', 'searchResultColor default ok'); 105 | equal(options.searchResultBackColor, undefined, 'searchResultBackColor default ok'); 106 | equal(options.enableLinks, false, 'enableLinks default ok'); 107 | equal(options.highlightSelected, true, 'highlightSelected default ok'); 108 | equal(options.highlightSearchResults, true, 'highlightSearchResults default ok'); 109 | equal(options.showBorder, true, 'showBorder default ok'); 110 | equal(options.showIcon, true, 'showIcon default ok'); 111 | equal(options.showCheckbox, false, 'showCheckbox default ok'); 112 | equal(options.showTags, false, 'showTags default ok'); 113 | equal(options.multiSelect, false, 'multiSelect default ok'); 114 | equal(options.onNodeChecked, null, 'onNodeChecked default ok'); 115 | equal(options.onNodeCollapsed, null, 'onNodeCollapsed default ok'); 116 | equal(options.onNodeDisabled, null, 'onNodeDisabled default ok'); 117 | equal(options.onNodeEnabled, null, 'onNodeEnabled default ok'); 118 | equal(options.onNodeExpanded, null, 'onNodeExpanded default ok'); 119 | equal(options.onNodeSelected, null, 'onNodeSelected default ok'); 120 | equal(options.onNodeUnchecked, null, 'onNodeUnchecked default ok'); 121 | equal(options.onNodeUnselected, null, 'onNodeUnselected default ok'); 122 | equal(options.onSearchComplete, null, 'onSearchComplete default ok'); 123 | equal(options.onSearchCleared, null, 'onSearchCleared default ok'); 124 | 125 | // Then test user options are correctly set 126 | var opts = { 127 | levels: 99, 128 | expandIcon: 'glyphicon glyphicon-expand', 129 | collapseIcon: 'glyphicon glyphicon-collapse', 130 | emptyIcon: 'glyphicon', 131 | nodeIcon: 'glyphicon glyphicon-stop', 132 | selectedIcon: 'glyphicon glyphicon-selected', 133 | checkedIcon: 'glyphicon glyphicon-checked-icon', 134 | uncheckedIcon: 'glyphicon glyphicon-unchecked-icon', 135 | color: 'yellow', 136 | backColor: 'purple', 137 | borderColor: 'purple', 138 | onhoverColor: 'orange', 139 | selectedColor: 'yellow', 140 | selectedBackColor: 'darkorange', 141 | searchResultColor: 'yellow', 142 | searchResultBackColor: 'darkorange', 143 | enableLinks: true, 144 | highlightSelected: false, 145 | highlightSearchResults: true, 146 | showBorder: false, 147 | showIcon: false, 148 | showCheckbox: true, 149 | showTags: true, 150 | multiSelect: true, 151 | onNodeChecked: function () {}, 152 | onNodeCollapsed: function () {}, 153 | onNodeDisabled: function () {}, 154 | onNodeEnabled: function () {}, 155 | onNodeExpanded: function () {}, 156 | onNodeSelected: function () {}, 157 | onNodeUnchecked: function () {}, 158 | onNodeUnselected: function () {}, 159 | onSearchComplete: function () {}, 160 | onSearchCleared: function () {} 161 | }; 162 | 163 | options = getOptions(init(opts)); 164 | ok(options, 'User options created ok'); 165 | equal(options.levels, 99, 'levels set ok'); 166 | equal(options.expandIcon, 'glyphicon glyphicon-expand', 'expandIcon set ok'); 167 | equal(options.collapseIcon, 'glyphicon glyphicon-collapse', 'collapseIcon set ok'); 168 | equal(options.emptyIcon, 'glyphicon', 'emptyIcon set ok'); 169 | equal(options.nodeIcon, 'glyphicon glyphicon-stop', 'nodeIcon set ok'); 170 | equal(options.selectedIcon, 'glyphicon glyphicon-selected', 'selectedIcon set ok'); 171 | equal(options.checkedIcon, 'glyphicon glyphicon-checked-icon', 'checkedIcon set ok'); 172 | equal(options.uncheckedIcon, 'glyphicon glyphicon-unchecked-icon', 'uncheckedIcon set ok'); 173 | equal(options.color, 'yellow', 'color set ok'); 174 | equal(options.backColor, 'purple', 'backColor set ok'); 175 | equal(options.borderColor, 'purple', 'borderColor set ok'); 176 | equal(options.onhoverColor, 'orange', 'onhoverColor set ok'); 177 | equal(options.selectedColor, 'yellow', 'selectedColor set ok'); 178 | equal(options.selectedBackColor, 'darkorange', 'selectedBackColor set ok'); 179 | equal(options.searchResultColor, 'yellow', 'searchResultColor set ok'); 180 | equal(options.searchResultBackColor, 'darkorange', 'searchResultBackColor set ok'); 181 | equal(options.enableLinks, true, 'enableLinks set ok'); 182 | equal(options.highlightSelected, false, 'highlightSelected set ok'); 183 | equal(options.highlightSearchResults, true, 'highlightSearchResults set ok'); 184 | equal(options.showBorder, false, 'showBorder set ok'); 185 | equal(options.showIcon, false, 'showIcon set ok'); 186 | equal(options.showCheckbox, true, 'showCheckbox set ok'); 187 | equal(options.showTags, true, 'showTags set ok'); 188 | equal(options.multiSelect, true, 'multiSelect set ok'); 189 | equal(typeof options.onNodeChecked, 'function', 'onNodeChecked set ok'); 190 | equal(typeof options.onNodeCollapsed, 'function', 'onNodeCollapsed set ok'); 191 | equal(typeof options.onNodeDisabled, 'function', 'onNodeDisabled set ok'); 192 | equal(typeof options.onNodeEnabled, 'function', 'onNodeEnabled set ok'); 193 | equal(typeof options.onNodeExpanded, 'function', 'onNodeExpanded set ok'); 194 | equal(typeof options.onNodeSelected, 'function', 'onNodeSelected set ok'); 195 | equal(typeof options.onNodeUnchecked, 'function', 'onNodeUnchecked set ok'); 196 | equal(typeof options.onNodeUnselected, 'function', 'onNodeUnselected set ok'); 197 | equal(typeof options.onSearchComplete, 'function', 'onSearchComplete set ok'); 198 | equal(typeof options.onSearchCleared, 'function', 'onSearchCleared set ok'); 199 | }); 200 | 201 | test('Links enabled', function () { 202 | init({enableLinks:true, data:data}); 203 | ok($('.list-group-item:first').children('a').length, 'Links are enabled'); 204 | 205 | }); 206 | 207 | 208 | module('Data'); 209 | 210 | test('Accepts JSON', function () { 211 | var el = init({levels:1,data:json}); 212 | equal($(el.selector + ' ul li').length, 5, 'Correct number of root nodes'); 213 | 214 | }); 215 | 216 | 217 | module('Behaviour'); 218 | 219 | test('Is chainable', function () { 220 | var el = init(); 221 | equal(el.addClass('test').attr('class'), 'treeview test', 'Is chainable'); 222 | }); 223 | 224 | test('Correct initial levels shown', function () { 225 | 226 | var el = init({levels:1,data:data}); 227 | equal($(el.selector + ' ul li').length, 5, 'Correctly display 5 root nodes when levels set to 1'); 228 | 229 | el = init({levels:2,data:data}); 230 | equal($(el.selector + ' ul li').length, 7, 'Correctly display 5 root and 2 child nodes when levels set to 2'); 231 | 232 | el = init({levels:3,data:data}); 233 | equal($(el.selector + ' ul li').length, 9, 'Correctly display 5 root, 2 children and 2 grand children nodes when levels set to 3'); 234 | }); 235 | 236 | test('Expanding a node', function () { 237 | 238 | var cbWorked, onWorked = false; 239 | init({ 240 | data: data, 241 | levels: 1, 242 | onNodeExpanded: function(/*event, date*/) { 243 | cbWorked = true; 244 | } 245 | }) 246 | .on('nodeExpanded', function(/*event, date*/) { 247 | onWorked = true; 248 | }); 249 | 250 | var nodeCount = $('.list-group-item').length; 251 | var el = $('.expand-icon:first'); 252 | el.trigger('click'); 253 | ok(($('.list-group-item').length > nodeCount), 'Number of nodes are increased, so node must have expanded'); 254 | ok(cbWorked, 'onNodeExpanded function was called'); 255 | ok(onWorked, 'nodeExpanded was fired'); 256 | }); 257 | 258 | test('Collapsing a node', function () { 259 | 260 | var cbWorked, onWorked = false; 261 | init({ 262 | data: data, 263 | levels: 2, 264 | onNodeCollapsed: function(/*event, date*/) { 265 | cbWorked = true; 266 | } 267 | }) 268 | .on('nodeCollapsed', function(/*event, date*/) { 269 | onWorked = true; 270 | }); 271 | 272 | var nodeCount = $('.list-group-item').length; 273 | var el = $('.expand-icon:first'); 274 | el.trigger('click'); 275 | ok(($('.list-group-item').length < nodeCount), 'Number of nodes has decreased, so node must have collapsed'); 276 | ok(cbWorked, 'onNodeCollapsed function was called'); 277 | ok(onWorked, 'nodeCollapsed was fired'); 278 | }); 279 | 280 | test('Selecting a node', function () { 281 | 282 | var cbWorked, onWorked = false; 283 | var $tree = init({ 284 | data: data, 285 | onNodeSelected: function(/*event, date*/) { 286 | cbWorked = true; 287 | } 288 | }) 289 | .on('nodeSelected', function(/*event, date*/) { 290 | onWorked = true; 291 | }); 292 | var options = getOptions($tree); 293 | 294 | // Simulate click 295 | $('.list-group-item:first').trigger('click'); 296 | 297 | // Has class node-selected 298 | ok($('.list-group-item:first').hasClass('node-selected'), 'Node is correctly selected : class "node-selected" added'); 299 | 300 | // Only one can be selected 301 | ok(($('.node-selected').length === 1), 'There is only one selected node'); 302 | 303 | // Has correct icon 304 | var iconClass = options.selectedIcon || options.nodeIcon; 305 | ok(!iconClass || $('.expand-icon:first').hasClass(iconClass), 'Node icon is correct'); 306 | 307 | // Events triggered 308 | ok(cbWorked, 'onNodeSelected function was called'); 309 | ok(onWorked, 'nodeSelected was fired'); 310 | }); 311 | 312 | test('Unselecting a node', function () { 313 | 314 | var cbWorked, onWorked = false; 315 | var $tree = init({ 316 | data: data, 317 | onNodeUnselected: function(/*event, date*/) { 318 | cbWorked = true; 319 | } 320 | }) 321 | .on('nodeUnselected', function(/*event, date*/) { 322 | onWorked = true; 323 | }); 324 | var options = getOptions($tree); 325 | 326 | // First select a node 327 | $('.list-group-item:first').trigger('click'); 328 | cbWorked = onWorked = false; 329 | 330 | // Simulate click 331 | $('.list-group-item:first').trigger('click'); 332 | 333 | // Has class node-selected 334 | ok(!$('.list-group-item:first').hasClass('node-selected'), 'Node is correctly unselected : class "node-selected" removed'); 335 | 336 | // Only one can be selected 337 | ok(($('.node-selected').length === 0), 'There are no selected nodes'); 338 | 339 | // Has correct icon 340 | ok(!options.nodeIcon || $('.expand-icon:first').hasClass(options.nodeIcon), 'Node icon is correct'); 341 | 342 | // Events triggered 343 | ok(cbWorked, 'onNodeUnselected function was called'); 344 | ok(onWorked, 'nodeUnselected was fired'); 345 | }); 346 | 347 | test('Selecting multiple nodes (multiSelect true)', function () { 348 | 349 | init({ 350 | data: data, 351 | multiSelect: true 352 | }); 353 | 354 | var $firstEl = $('.list-group-item:nth-child(1)').trigger('click'); 355 | var $secondEl = $('.list-group-item:nth-child(2)').trigger('click'); 356 | 357 | $firstEl = $('.list-group-item:nth-child(1)'); 358 | $secondEl = $('.list-group-item:nth-child(2)'); 359 | 360 | ok($firstEl.hasClass('node-selected'), 'First node is correctly selected : class "node-selected" added'); 361 | ok($secondEl.hasClass('node-selected'), 'Second node is correctly selected : class "node-selected" added'); 362 | ok(($('.node-selected').length === 2), 'There are two selected nodes'); 363 | }); 364 | 365 | test('Clicking a non-selectable, collapsed node expands the node', function () { 366 | var testData = $.extend(true, {}, data); 367 | testData[0].selectable = false; 368 | 369 | var cbCalled, onCalled = false; 370 | init({ 371 | levels: 1, 372 | data: testData, 373 | onNodeSelected: function(/*event, date*/) { 374 | cbCalled = true; 375 | } 376 | }) 377 | .on('nodeSelected', function(/*event, date*/) { 378 | onCalled = true; 379 | }); 380 | 381 | var nodeCount = $('.list-group-item').length; 382 | var el = $('.list-group-item:first'); 383 | el.trigger('click'); 384 | el = $('.list-group-item:first'); 385 | ok(!el.hasClass('node-selected'), 'Node should not be selected'); 386 | ok(!cbCalled, 'onNodeSelected function should not be called'); 387 | ok(!onCalled, 'nodeSelected should not fire'); 388 | ok(($('.list-group-item').length > nodeCount), 'Number of nodes are increased, so node must have expanded'); 389 | }); 390 | 391 | test('Clicking a non-selectable, expanded node collapses the node', function () { 392 | var testData = $.extend(true, {}, data); 393 | testData[0].selectable = false; 394 | 395 | var cbCalled, onCalled = false; 396 | init({ 397 | levels: 2, 398 | data: testData, 399 | onNodeSelected: function(/*event, date*/) { 400 | cbCalled = true; 401 | } 402 | }) 403 | .on('nodeSelected', function(/*event, date*/) { 404 | onCalled = true; 405 | }); 406 | 407 | var nodeCount = $('.list-group-item').length; 408 | var el = $('.list-group-item:first'); 409 | el.trigger('click'); 410 | el = $('.list-group-item:first'); 411 | 412 | ok(!el.hasClass('node-selected'), 'Node should not be selected'); 413 | ok(!cbCalled, 'onNodeSelected function should not be called'); 414 | ok(!onCalled, 'nodeSelected should not fire'); 415 | ok(($('.list-group-item').length < nodeCount), 'Number of nodes has decreased, so node must have collapsed'); 416 | }); 417 | 418 | test('Checking a node', function () { 419 | 420 | // setup test 421 | var cbWorked, onWorked = false; 422 | var $tree = init({ 423 | data: data, 424 | showCheckbox: true, 425 | onNodeChecked: function(/*event, date*/) { 426 | cbWorked = true; 427 | } 428 | }) 429 | .on('nodeChecked', function(/*event, date*/) { 430 | onWorked = true; 431 | }); 432 | var options = getOptions($tree); 433 | 434 | // simulate click event on check icon 435 | var $el = $('.check-icon:first'); 436 | $el.trigger('click'); 437 | 438 | // check state is correct 439 | $el = $('.check-icon:first'); 440 | ok(($el.attr('class').indexOf(options.checkedIcon) !== -1), 'Node is checked : icon is correct'); 441 | ok(cbWorked, 'onNodeChecked function was called'); 442 | ok(onWorked, 'nodeChecked was fired'); 443 | }); 444 | 445 | test('Unchecking a node', function () { 446 | 447 | // setup test 448 | var cbWorked, onWorked = false; 449 | var $tree = init({ 450 | data: data, 451 | showCheckbox: true, 452 | onNodeUnchecked: function(/*event, date*/) { 453 | cbWorked = true; 454 | } 455 | }) 456 | .on('nodeUnchecked', function(/*event, date*/) { 457 | onWorked = true; 458 | }); 459 | var options = getOptions($tree); 460 | 461 | // first check a node 462 | var $el = $('.check-icon:first'); 463 | $el.trigger('click'); 464 | 465 | // then simulate unchecking a node 466 | cbWorked = onWorked = false; 467 | $el = $('.check-icon:first'); 468 | $el.trigger('click'); 469 | 470 | // check state is correct 471 | $el = $('.check-icon:first'); 472 | ok(($el.attr('class').indexOf(options.uncheckedIcon) !== -1), 'Node is unchecked : icon is correct'); 473 | ok(cbWorked, 'onNodeUnchecked function was called'); 474 | ok(onWorked, 'nodeUnchecked was fired'); 475 | }); 476 | 477 | 478 | module('Methods'); 479 | 480 | test('getNode', function () { 481 | var $tree = init({ data: data }); 482 | var nodeParent1 = $tree.treeview('getNode', 0); 483 | equal(nodeParent1.text, 'Parent 1', 'Correct node returned : requested "Parent 1", got "Parent 1"'); 484 | }); 485 | 486 | test('getParent', function () { 487 | var $tree = init({ data: data }); 488 | var nodeChild1 = $tree.treeview('getNode', 1); 489 | var parentNode = $tree.treeview('getParent', nodeChild1); 490 | equal(parentNode.text, 'Parent 1', 'Correct node returned : requested parent of "Child 1", got "Parent 1"'); 491 | }); 492 | 493 | test('getSiblings', function () { 494 | var $tree = init({ data: data }); 495 | 496 | // Test root level, internally uses the this.tree 497 | var nodeParent1 = $tree.treeview('getNode', 0); 498 | var nodeParent1Siblings = $tree.treeview('getSiblings', nodeParent1); 499 | var isArray = (nodeParent1Siblings instanceof Array); 500 | var countOK = nodeParent1Siblings.length === 4; 501 | var resultsOK = nodeParent1Siblings[0].text === 'Parent 2'; 502 | resultsOK = resultsOK && nodeParent1Siblings[1].text === 'Parent 3'; 503 | resultsOK = resultsOK && nodeParent1Siblings[2].text === 'Parent 4'; 504 | resultsOK = resultsOK && nodeParent1Siblings[3].text === 'Parent 5'; 505 | ok(isArray, 'Correct siblings for "Parent 1" [root] : is array'); 506 | ok(countOK, 'Correct siblings for "Parent 1" [root] : count OK'); 507 | ok(resultsOK, 'Correct siblings for "Parent 1" [root] : results OK'); 508 | 509 | // Test non root level, internally uses getParent.nodes 510 | var nodeChild1 = $tree.treeview('getNode', 1); 511 | var nodeChild1Siblings = $tree.treeview('getSiblings', nodeChild1); 512 | var isArray = (nodeChild1Siblings instanceof Array); 513 | var countOK = nodeChild1Siblings.length === 1; 514 | var results = nodeChild1Siblings[0].text === 'Child 2' 515 | ok(isArray, 'Correct siblings for "Child 1" [non root] : is array'); 516 | ok(countOK, 'Correct siblings for "Child 1" [non root] : count OK'); 517 | ok(results, 'Correct siblings for "Child 1" [non root] : results OK'); 518 | }); 519 | 520 | test('getSelected', function () { 521 | var $tree = init({ data: data }) 522 | .treeview('selectNode', 0); 523 | 524 | var selectedNodes = $tree.treeview('getSelected'); 525 | ok((selectedNodes instanceof Array), 'Result is an array'); 526 | equal(selectedNodes.length, 1, 'Correct number of nodes returned'); 527 | equal(selectedNodes[0].text, 'Parent 1', 'Correct node returned'); 528 | }); 529 | 530 | test('getUnselected', function () { 531 | var $tree = init({ data: data }) 532 | .treeview('selectNode', 0); 533 | 534 | var unselectedNodes = $tree.treeview('getUnselected'); 535 | ok((unselectedNodes instanceof Array), 'Result is an array'); 536 | equal(unselectedNodes.length, 8, 'Correct number of nodes returned'); 537 | }); 538 | 539 | // Assumptions: 540 | // Default tree + expanded to 2 levels, 541 | // means 1 node 'Parent 1' should be expanded and therefore returned 542 | test('getExpanded', function () { 543 | var $tree = init({ data: data }); 544 | var expandedNodes = $tree.treeview('getExpanded'); 545 | ok((expandedNodes instanceof Array), 'Result is an array'); 546 | equal(expandedNodes.length, 1, 'Correct number of nodes returned'); 547 | equal(expandedNodes[0].text, 'Parent 1', 'Correct node returned'); 548 | }); 549 | 550 | // Assumptions: 551 | // Default tree + expanded to 2 levels, means only 'Parent 1' should be expanded 552 | // as all other parent nodes have no children their state will be collapsed 553 | // which means 8 of the 9 nodes should be returned 554 | test('getCollapsed', function () { 555 | var $tree = init({ data: data }); 556 | var collapsedNodes = $tree.treeview('getCollapsed'); 557 | ok((collapsedNodes instanceof Array), 'Result is an array'); 558 | equal(collapsedNodes.length, 8, 'Correct number of nodes returned'); 559 | }); 560 | 561 | test('getChecked', function () { 562 | var $tree = init({ data: data, showCheckbox: true }) 563 | .treeview('checkNode', 0); 564 | 565 | var checkedNodes = $tree.treeview('getChecked'); 566 | ok((checkedNodes instanceof Array), 'Result is an array'); 567 | equal(checkedNodes.length, 1, 'Correct number of nodes returned'); 568 | equal(checkedNodes[0].text, 'Parent 1', 'Correct node returned'); 569 | }); 570 | 571 | test('getUnchecked', function () { 572 | var $tree = init({ data: data }) 573 | .treeview('checkNode', 0); 574 | 575 | var uncheckedNodes = $tree.treeview('getUnchecked'); 576 | ok((uncheckedNodes instanceof Array), 'Result is an array'); 577 | equal(uncheckedNodes.length, 8, 'Correct number of nodes returned'); 578 | }); 579 | 580 | test('getDisabled', function () { 581 | var $tree = init({ data: data }) 582 | .treeview('disableNode', 0); 583 | 584 | var disabledNodes = $tree.treeview('getDisabled'); 585 | ok((disabledNodes instanceof Array), 'Result is an array'); 586 | equal(disabledNodes.length, 1, 'Correct number of nodes returned'); 587 | equal(disabledNodes[0].text, 'Parent 1', 'Correct node returned'); 588 | }); 589 | 590 | test('getEnabled', function () { 591 | var $tree = init({ data: data }) 592 | .treeview('disableNode', 0); 593 | 594 | var enabledNodes = $tree.treeview('getEnabled'); 595 | ok((enabledNodes instanceof Array), 'Result is an array'); 596 | equal(enabledNodes.length, 8, 'Correct number of nodes returned'); 597 | }); 598 | 599 | test('disableAll / enableAll', function () { 600 | var $tree = init({ data: data, levels: 1 }); 601 | 602 | $tree.treeview('disableAll'); 603 | equal($($tree.selector + ' ul li.node-disabled').length, 5, 'Disable all works, 9 nodes with node-disabled class'); 604 | 605 | $tree.treeview('enableAll'); 606 | equal($($tree.selector + ' ul li.node-disabled').length, 0, 'Check all works, 9 nodes non with node-disabled class'); 607 | }); 608 | 609 | test('disableNode / enableNode', function () { 610 | var $tree = init({ data: data, levels: 1 }); 611 | var nodeId = 0; 612 | var node = $tree.treeview('getNode', 0); 613 | 614 | // Disable node using node id 615 | $tree.treeview('disableNode', nodeId); 616 | ok($('.list-group-item:first').hasClass('node-disabled'), 'Disable node (by id) : Node has class node-disabled'); 617 | ok(($('.node-disabled').length === 1), 'Disable node (by id) : There is only one disabled node'); 618 | 619 | // Enable node using node id 620 | $tree.treeview('enableNode', nodeId); 621 | ok(!$('.list-group-item:first').hasClass('node-disabled'), 'Enable node (by id) : Node does not have class node-disabled'); 622 | ok(($('.node-checked').length === 0), 'Enable node (by id) : There are no disabled nodes'); 623 | 624 | // Disable node using node 625 | $tree.treeview('disableNode', node); 626 | ok($('.list-group-item:first').hasClass('node-disabled'), 'Disable node (by node) : Node has class node-disabled'); 627 | ok(($('.node-disabled').length === 1), 'Disable node (by node) : There is only one disabled node'); 628 | 629 | // Enable node using node 630 | $tree.treeview('enableNode', node); 631 | ok(!$('.list-group-item:first').hasClass('node-disabled'), 'Enable node (by node) : Node does not have class node-disabled'); 632 | ok(($('.node-checked').length === 0), 'Enable node (by node) : There are no disabled nodes'); 633 | }); 634 | 635 | test('toggleNodeDisabled', function () { 636 | var $tree = init({ data: data, levels: 1 }); 637 | var nodeId = 0; 638 | var node = $tree.treeview('getNode', 0); 639 | 640 | // Toggle disabled using node id 641 | $tree.treeview('toggleNodeDisabled', nodeId); 642 | ok($('.list-group-item:first').hasClass('node-disabled'), 'Toggle node (by id) : Node has class node-disabled'); 643 | ok(($('.node-disabled').length === 1), 'Toggle node (by id) : There is only one disabled node'); 644 | 645 | // Toggle disabled using node 646 | $tree.treeview('toggleNodeDisabled', node); 647 | ok(!$('.list-group-item:first').hasClass('node-disabled'), 'Toggle node (by node) : Node does not have class node-disabled'); 648 | ok(($('.node-disabled').length === 0), 'Toggle node (by node) : There are no disabled nodes'); 649 | }); 650 | 651 | test('checkAll / uncheckAll', function () { 652 | var $tree = init({ data: data, levels: 3, showCheckbox: true }); 653 | 654 | $tree.treeview('checkAll'); 655 | equal($($tree.selector + ' ul li.node-checked').length, 9, 'Check all works, 9 nodes with node-checked class'); 656 | equal($($tree.selector + ' ul li .glyphicon-check').length, 9, 'Check all works, 9 nodes with glyphicon-check icon'); 657 | 658 | $tree.treeview('uncheckAll'); 659 | equal($($tree.selector + ' ul li.node-checked').length, 0, 'Check all works, 9 nodes non with node-checked class'); 660 | equal($($tree.selector + ' ul li .glyphicon-unchecked').length, 9, 'Check all works, 9 nodes with glyphicon-unchecked icon'); 661 | }); 662 | 663 | test('checkNode / uncheckNode', function () { 664 | var $tree = init({ data: data, showCheckbox: true }); 665 | var options = getOptions($tree); 666 | var nodeId = 0; 667 | var node = $tree.treeview('getNode', 0); 668 | 669 | // Check node using node id 670 | $tree.treeview('checkNode', nodeId); 671 | ok($('.list-group-item:first').hasClass('node-checked'), 'Check node (by id) : Node has class node-checked'); 672 | ok(($('.node-checked').length === 1), 'Check node (by id) : There is only one checked node'); 673 | ok($('.check-icon:first').hasClass(options.checkedIcon), 'Check node (by id) : Node icon is correct'); 674 | 675 | // Uncheck node using node id 676 | $tree.treeview('uncheckNode', nodeId); 677 | ok(!$('.list-group-item:first').hasClass('node-checked'), 'Uncheck node (by id) : Node does not have class node-checked'); 678 | ok(($('.node-checked').length === 0), 'Uncheck node (by id) : There are no checked nodes'); 679 | ok($('.check-icon:first').hasClass(options.uncheckedIcon), 'Uncheck node (by id) : Node icon is correct'); 680 | 681 | // Check node using node 682 | $tree.treeview('checkNode', node); 683 | ok($('.list-group-item:first').hasClass('node-checked'), 'Check node (by node) : Node has class node-checked'); 684 | ok(($('.node-checked').length === 1), 'Check node (by node) : There is only one checked node'); 685 | ok($('.check-icon:first').hasClass(options.checkedIcon), 'Check node (by node) : Node icon is correct'); 686 | 687 | // Uncheck node using node 688 | $tree.treeview('uncheckNode', node); 689 | ok(!$('.list-group-item:first').hasClass('node-checked'), 'Uncheck node (by node) : Node does not have class node-checked'); 690 | ok(($('.node-checked').length === 0), 'Uncheck node (by node) : There are no checked nodes'); 691 | ok($('.check-icon:first').hasClass(options.uncheckedIcon), 'Uncheck node (by node) : Node icon is correct'); 692 | }); 693 | 694 | test('toggleNodeChecked', function () { 695 | var $tree = init({ data: data, showCheckbox: true }); 696 | var options = getOptions($tree); 697 | var nodeId = 0; 698 | var node = $tree.treeview('getNode', 0); 699 | 700 | // Toggle checked using node id 701 | $tree.treeview('toggleNodeChecked', nodeId); 702 | ok($('.list-group-item:first').hasClass('node-checked'), 'Toggle node (by id) : Node has class node-checked'); 703 | ok(($('.node-checked').length === 1), 'Toggle node (by id) : There is only one checked node'); 704 | ok($('.check-icon:first').hasClass(options.checkedIcon), 'Toggle node (by id) : Node icon is correct'); 705 | 706 | // Toggle checked using node 707 | $tree.treeview('toggleNodeChecked', node); 708 | ok(!$('.list-group-item:first').hasClass('node-checked'), 'Toggle node (by node) : Node does not have class node-checked'); 709 | ok(($('.node-checked').length === 0), 'Toggle node (by node) : There are no checked nodes'); 710 | ok($('.check-icon:first').hasClass(options.uncheckedIcon), 'Toggle node (by node) : Node icon is correct'); 711 | }); 712 | 713 | test('selectNode / unselectNode', function () { 714 | var $tree = init({ data: data, selectedIcon: 'glyphicon glyphicon-selected' }); 715 | var nodeId = 0; 716 | var node = $tree.treeview('getNode', 0); 717 | 718 | // Select node using node id 719 | $tree.treeview('selectNode', nodeId); 720 | ok($('.list-group-item:first').hasClass('node-selected'), 'Select node (by id) : Node has class node-selected'); 721 | ok(($('.node-selected').length === 1), 'Select node (by id) : There is only one selected node'); 722 | 723 | // Unselect node using node id 724 | $tree.treeview('unselectNode', nodeId); 725 | ok(!$('.list-group-item:first').hasClass('node-selected'), 'Unselect node (by id) : Node does not have class node-selected'); 726 | ok(($('.node-selected').length === 0), 'Unselect node (by id) : There are no selected nodes'); 727 | 728 | // Select node using node 729 | $tree.treeview('selectNode', node); 730 | ok($('.list-group-item:first').hasClass('node-selected'), 'Select node (by node) : Node has class node-selected'); 731 | ok(($('.node-selected').length === 1), 'Select node (by node) : There is only one selected node'); 732 | 733 | // Unselect node using node id 734 | $tree.treeview('unselectNode', node); 735 | ok(!$('.list-group-item:first').hasClass('node-selected'), 'Unselect node (by node) : Node does not have class node-selected'); 736 | ok(($('.node-selected').length === 0), 'Unselect node (by node) : There are no selected nodes'); 737 | }); 738 | 739 | test('toggleNodeSelected', function () { 740 | var $tree = init({ data: data }); 741 | var nodeId = 0; 742 | var node = $tree.treeview('getNode', 0); 743 | 744 | // Toggle selected using node id 745 | $tree.treeview('toggleNodeSelected', nodeId); 746 | ok($('.list-group-item:first').hasClass('node-selected'), 'Toggle node (by id) : Node has class node-selected'); 747 | ok(($('.node-selected').length === 1), 'Toggle node (by id) : There is only one selected node'); 748 | 749 | // Toggle selected using node 750 | $tree.treeview('toggleNodeSelected', node); 751 | ok(!$('.list-group-item:first').hasClass('node-selected'), 'Toggle node (by id) : Node does not have class node-selected'); 752 | ok(($('.node-selected').length === 0), 'Toggle node (by node) : There are no selected nodes'); 753 | }); 754 | 755 | test('expandAll / collapseAll', function () { 756 | var $tree = init({ data: data, levels: 1 }); 757 | equal($($tree.selector + ' ul li').length, 5, 'Starts in collapsed state, 5 root nodes displayed'); 758 | 759 | $tree.treeview('expandAll'); 760 | equal($($tree.selector + ' ul li').length, 9, 'Expand all works, all 9 nodes displayed'); 761 | 762 | $tree.treeview('collapseAll'); 763 | equal($($tree.selector + ' ul li').length, 5, 'Collapse all works, 5 original root nodes displayed'); 764 | 765 | $tree.treeview('expandAll', { levels: 1 }); 766 | equal($($tree.selector + ' ul li').length, 7, 'Expand all (levels = 1) works, correctly displayed 7 nodes'); 767 | }); 768 | 769 | test('expandNode / collapseNode / toggleExpanded', function () { 770 | var $tree = init({ data: data, levels: 1 }); 771 | equal($($tree.selector + ' ul li').length, 5, 'Starts in collapsed state, 5 root nodes displayed'); 772 | 773 | $tree.treeview('expandNode', 0); 774 | equal($($tree.selector + ' ul li').length, 7, 'Expand node (by id) works, 7 nodes displayed'); 775 | 776 | $tree.treeview('collapseNode', 0); 777 | equal($($tree.selector + ' ul li').length, 5, 'Collapse node (by id) works, 5 original nodes displayed'); 778 | 779 | $tree.treeview('toggleNodeExpanded', 0); 780 | equal($($tree.selector + ' ul li').length, 7, 'Toggle node (by id) works, 7 nodes displayed'); 781 | 782 | $tree.treeview('toggleNodeExpanded', 0); 783 | equal($($tree.selector + ' ul li').length, 5, 'Toggle node (by id) works, 5 original nodes displayed'); 784 | 785 | $tree.treeview('expandNode', [ 0, { levels: 2 } ]); 786 | equal($($tree.selector + ' ul li').length, 9, 'Expand node (levels = 2, by id) works, 9 nodes displayed'); 787 | 788 | $tree = init({ data: data, levels: 1 }); 789 | equal($($tree.selector + ' ul li').length, 5, 'Reset to collapsed state, 5 root nodes displayed'); 790 | 791 | var nodeParent1 = $tree.treeview('getNode', 0); 792 | $tree.treeview('expandNode', nodeParent1); 793 | equal($($tree.selector + ' ul li').length, 7, 'Expand node (by node) works, 7 nodes displayed'); 794 | 795 | $tree.treeview('collapseNode', nodeParent1); 796 | equal($($tree.selector + ' ul li').length, 5, 'Collapse node (by node) works, 5 original nodes displayed'); 797 | 798 | $tree.treeview('toggleNodeExpanded', nodeParent1); 799 | equal($($tree.selector + ' ul li').length, 7, 'Toggle node (by node) works, 7 nodes displayed'); 800 | 801 | $tree.treeview('toggleNodeExpanded', nodeParent1); 802 | equal($($tree.selector + ' ul li').length, 5, 'Toggle node (by node) works, 5 original nodes displayed'); 803 | 804 | $tree.treeview('expandNode', [ nodeParent1, { levels: 2 } ]); 805 | equal($($tree.selector + ' ul li').length, 9, 'Expand node (levels = 2, by node) works, 9 nodes displayed'); 806 | }); 807 | 808 | test('revealNode', function () { 809 | var $tree = init({ data: data, levels: 1 }); 810 | 811 | $tree.treeview('revealNode', 1); // Child_1 812 | equal($($tree.selector + ' ul li').length, 7, 'Reveal node (by id) works, reveal Child 1 and 7 nodes displayed'); 813 | 814 | var nodeGrandchild1 = $tree.treeview('getNode', 2); // Grandchild 1 815 | $tree.treeview('revealNode', nodeGrandchild1); 816 | equal($($tree.selector + ' ul li').length, 9, 'Reveal node (by node) works, reveal Grandchild 1 and 9 nodes displayed'); 817 | }); 818 | 819 | test('search', function () { 820 | var cbWorked, onWorked = false; 821 | var $tree = init({ 822 | data: data, 823 | onSearchComplete: function(/*event, results*/) { 824 | cbWorked = true; 825 | } 826 | }) 827 | .on('searchComplete', function(/*event, results*/) { 828 | onWorked = true; 829 | }); 830 | 831 | // Case sensitive, exact match 832 | var result = $tree.treeview('search', [ 'Parent 1', { ignoreCase: false, exactMatch: true } ]); 833 | equal(result.length, 1, 'Search "Parent 1" case sensitive, exact match - returns 1 result'); 834 | 835 | // Case sensitive, like 836 | result = $tree.treeview('search', [ 'Parent', { ignoreCase: false, exactMatch: false } ]); 837 | equal(result.length, 5, 'Search "Parent" case sensitive, exact match - returns 5 results'); 838 | 839 | // Case insensitive, exact match 840 | result = $tree.treeview('search', [ 'parent 1', { ignoreCase: true, exactMatch: true } ]); 841 | equal(result.length, 1, 'Search "parent 1" case insensitive, exact match - returns 1 result'); 842 | 843 | // Case insensitive, like 844 | result = $tree.treeview('search', [ 'parent', { ignoreCase: true, exactMatch: false } ]); 845 | equal(result.length, 5, 'Search "parent" case insensitive, exact match - returns 5 results') 846 | 847 | // Check events fire 848 | ok(cbWorked, 'onSearchComplete function was called'); 849 | ok(onWorked, 'searchComplete was fired'); 850 | }); 851 | 852 | test('clearSearch', function () { 853 | var cbWorked, onWorked = false; 854 | var $tree = init({ 855 | data: data, 856 | onSearchCleared: function(/*event, results*/) { 857 | cbWorked = true; 858 | } 859 | }) 860 | .on('searchCleared', function(/*event, results*/) { 861 | onWorked = true; 862 | }); 863 | 864 | // Check results are cleared 865 | $tree.treeview('search', [ 'Parent 1', { ignoreCase: false, exactMatch: true } ]); 866 | equal($tree.find('.search-result').length, 1, 'Search results highlighted'); 867 | $tree.treeview('clearSearch'); 868 | equal($tree.find('.search-result').length, 0, 'Search results cleared'); 869 | 870 | // Check events fire 871 | ok(cbWorked, 'onSearchCleared function was called'); 872 | ok(onWorked, 'searchCleared was fired'); 873 | }); 874 | 875 | }()); 876 | --------------------------------------------------------------------------------