├── .gitignore ├── CONTRIBUTING.md ├── src ├── scripts │ ├── module.js │ ├── filters │ │ └── ivh-treeview-as-array.js │ ├── constants │ │ ├── ivh-treeview-interpolate-end-symbol.js │ │ └── ivh-treeview-interpolate-start-symbol.js │ ├── directives │ │ ├── ivh-treeview-checkbox.js │ │ ├── ivh-treeview-toggle.js │ │ ├── ivh-treeview-children.js │ │ ├── ivh-treeview-node.js │ │ ├── ivh-treeview-checkbox-helper.js │ │ ├── ivh-treeview-twistie.js │ │ └── ivh-treeview.js │ └── services │ │ ├── ivh-treeview-compiler.js │ │ ├── ivh-treeview-bfs.js │ │ ├── ivh-treeview-options.js │ │ └── ivh-treeview-mgr.js └── styles │ ├── ivh-treeview-theme-basic.less │ └── ivh-treeview.less ├── .travis.yml ├── dist ├── ivh-treeview-theme-basic.css ├── ivh-treeview.min.css ├── ivh-treeview.css ├── ivh-treeview.min.js └── ivh-treeview.js ├── .jshintrc ├── .editorconfig ├── test └── spec │ ├── services │ ├── ivh-treeview-options.js │ ├── ivh-treeview-bfs.js │ └── ivh-treeview-mgr.js │ └── directives │ ├── ivh-treeview-custom-templates.js │ └── ivh-treeview.js ├── bower.json ├── LICENSE-MIT ├── MIGRATING.md ├── package.json ├── .jscsrc ├── gruntfile.js ├── docs └── templates-and-skins.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | test.html 4 | npm-debug.log 5 | .tmp 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Please see our consolidated [contribution 4 | guidelines](https://github.com/iVantage/Contribution-Guidelines). 5 | -------------------------------------------------------------------------------- /src/scripts/module.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The iVantage Treeview module 4 | * 5 | * @package ivh.treeview 6 | */ 7 | 8 | angular.module('ivh.treeview', []); 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 7 4 | 5 | before_script: 6 | - npm install -g bower 7 | - npm install -g grunt-cli 8 | - bower install 9 | 10 | script: 11 | - grunt 12 | -------------------------------------------------------------------------------- /src/scripts/filters/ivh-treeview-as-array.js: -------------------------------------------------------------------------------- 1 | 2 | angular.module('ivh.treeview').filter('ivhTreeviewAsArray', function() { 3 | 'use strict'; 4 | return function(arr) { 5 | if(!angular.isArray(arr) && angular.isObject(arr)) { 6 | return [arr]; 7 | } 8 | return arr; 9 | }; 10 | }); 11 | -------------------------------------------------------------------------------- /src/scripts/constants/ivh-treeview-interpolate-end-symbol.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Supports non-default interpolation symbols 4 | * 5 | * @package ivh.treeview 6 | * @copyright 2016 iVantage Health Analytics, Inc. 7 | */ 8 | 9 | angular.module('ivh.treeview').constant('ivhTreeviewInterpolateEndSymbol', '}}'); 10 | 11 | -------------------------------------------------------------------------------- /src/scripts/constants/ivh-treeview-interpolate-start-symbol.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Supports non-default interpolation symbols 4 | * 5 | * @package ivh.treeview 6 | * @copyright 2016 iVantage Health Analytics, Inc. 7 | */ 8 | 9 | angular.module('ivh.treeview').constant('ivhTreeviewInterpolateStartSymbol', '{{'); 10 | 11 | -------------------------------------------------------------------------------- /src/styles/ivh-treeview-theme-basic.less: -------------------------------------------------------------------------------- 1 | ul.ivh-treeview { 2 | list-style-type: none; 3 | padding-left: 0; 4 | 5 | ul.ivh-treeview { 6 | padding-left: 15px; 7 | } 8 | 9 | .ivh-treeview-toggle { 10 | cursor: pointer; 11 | } 12 | 13 | .ivh-treeview-node-leaf .ivh-treeview-toggle { 14 | cursor: auto; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dist/ivh-treeview-theme-basic.css: -------------------------------------------------------------------------------- 1 | ul.ivh-treeview { 2 | list-style-type: none; 3 | padding-left: 0; 4 | } 5 | ul.ivh-treeview ul.ivh-treeview { 6 | padding-left: 15px; 7 | } 8 | ul.ivh-treeview .ivh-treeview-toggle { 9 | cursor: pointer; 10 | } 11 | ul.ivh-treeview .ivh-treeview-node-leaf .ivh-treeview-toggle { 12 | cursor: auto; 13 | } 14 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": ["angular"], 3 | "browser": true, 4 | "bitwise": true, 5 | "camelcase": false, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "regexp": true, 14 | "undef": true, 15 | "unused": false, 16 | "strict": true, 17 | "trailing": true, 18 | "smarttabs": false, 19 | "laxcomma": true, 20 | "onevar": false 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /dist/ivh-treeview.min.css: -------------------------------------------------------------------------------- 1 | ul.ivh-treeview li.ivh-treeview-node-collapsed ul.ivh-treeview{display:none}ul.ivh-treeview .ivh-treeview-twistie-collapsed,ul.ivh-treeview .ivh-treeview-twistie-leaf{display:none}ul.ivh-treeview .ivh-treeview-node-collapsed .ivh-treeview-twistie-collapsed{display:inline}ul.ivh-treeview .ivh-treeview-node-collapsed .ivh-treeview-twistie-expanded{display:none}ul.ivh-treeview li.ivh-treeview-node-leaf .ivh-treeview-twistie-leaf{display:inline}ul.ivh-treeview li.ivh-treeview-node-leaf .ivh-treeview-twistie-collapsed,ul.ivh-treeview li.ivh-treeview-node-leaf .ivh-treeview-twistie-expanded{display:none} -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview-checkbox.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Wrapper for a checkbox directive 4 | * 5 | * Basically exists so folks creeting custom node templates don't need to attach 6 | * their node to this directive explicitly - i.e. keeps consistent interface 7 | * with the twistie and toggle directives. 8 | * 9 | * @package ivh.treeview 10 | * @copyright 2014 iVantage Health Analytics, Inc. 11 | */ 12 | 13 | angular.module('ivh.treeview').directive('ivhTreeviewCheckbox', [function() { 14 | 'use strict'; 15 | return { 16 | restrict: 'AE', 17 | require: '^ivhTreeview', 18 | template: '' 19 | }; 20 | }]); 21 | -------------------------------------------------------------------------------- /test/spec/services/ivh-treeview-options.js: -------------------------------------------------------------------------------- 1 | /*global jQuery, describe, beforeEach, afterEach, it, module, inject, expect */ 2 | 3 | describe('Service: ivhTreeviewOptions', function() { 4 | 'use strict'; 5 | 6 | beforeEach(module('ivh.treeview')); 7 | 8 | var ivhTreeviewOptions; 9 | 10 | beforeEach(inject(function(_ivhTreeviewOptions_) { 11 | ivhTreeviewOptions = _ivhTreeviewOptions_; 12 | })); 13 | 14 | it('should return a pristine copy every time', function() { 15 | var opts1 = ivhTreeviewOptions(); 16 | opts1.selectedAttribute = 'blargus'; 17 | 18 | var opts2 = ivhTreeviewOptions(); 19 | expect(opts2.selectedAttribute).not.toEqual(opts1.selectedAttribute); 20 | }); 21 | }); 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview-toggle.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Toggle logic for treeview nodes 4 | * 5 | * Handles expand/collapse on click. Does nothing for leaf nodes. 6 | * 7 | * @private 8 | * @package ivh.treeview 9 | * @copyright 2014 iVantage Health Analytics, Inc. 10 | */ 11 | 12 | angular.module('ivh.treeview').directive('ivhTreeviewToggle', [function() { 13 | 'use strict'; 14 | return { 15 | restrict: 'A', 16 | require: '^ivhTreeview', 17 | link: function(scope, element, attrs, trvw) { 18 | var node = scope.node; 19 | 20 | element.addClass('ivh-treeview-toggle'); 21 | 22 | element.bind('click', function() { 23 | if(!trvw.isLeaf(node)) { 24 | scope.$apply(function() { 25 | trvw.toggleExpanded(node); 26 | trvw.onToggle(node); 27 | }); 28 | } 29 | }); 30 | } 31 | }; 32 | }]); 33 | -------------------------------------------------------------------------------- /dist/ivh-treeview.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Treeview styles 3 | * 4 | * @private 5 | * @package ivh.treeview 6 | * @copyright 2014 iVantage Health Analytics, Inc. 7 | */ 8 | ul.ivh-treeview li.ivh-treeview-node-collapsed ul.ivh-treeview { 9 | display: none; 10 | } 11 | ul.ivh-treeview .ivh-treeview-twistie-leaf, 12 | ul.ivh-treeview .ivh-treeview-twistie-collapsed { 13 | display: none; 14 | } 15 | ul.ivh-treeview .ivh-treeview-node-collapsed .ivh-treeview-twistie-collapsed { 16 | display: inline; 17 | } 18 | ul.ivh-treeview .ivh-treeview-node-collapsed .ivh-treeview-twistie-expanded { 19 | display: none; 20 | } 21 | ul.ivh-treeview li.ivh-treeview-node-leaf .ivh-treeview-twistie-leaf { 22 | display: inline; 23 | } 24 | ul.ivh-treeview li.ivh-treeview-node-leaf .ivh-treeview-twistie-expanded, 25 | ul.ivh-treeview li.ivh-treeview-node-leaf .ivh-treeview-twistie-collapsed { 26 | display: none; 27 | } 28 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ivh-treeview", 3 | "version": "1.1.0", 4 | "authors": [ 5 | "jtrussell" 6 | ], 7 | "description": "Treeview for angular with filtering and checkboxes", 8 | "main": [ 9 | "dist/ivh-treeview.js", 10 | "dist/ivh-treeview.css" 11 | ], 12 | "keywords": [ 13 | "angular", 14 | "tree", 15 | "treeview" 16 | ], 17 | "license": "MIT", 18 | "homepage": "https://github.com/ivantage/angular-ivh-treeview", 19 | "ignore": [ 20 | "**/.*", 21 | "bower_components", 22 | "docs", 23 | "node_modules", 24 | "src", 25 | "test", 26 | "CONTRIBUTING.md", 27 | "gruntfile.js", 28 | "MIGRATING.md", 29 | "package.json" 30 | ], 31 | "dependencies": { 32 | "angular": "~1.2.18 || ~1.4.0 || ~1.5.0 || ~1.6.0" 33 | }, 34 | "devDependencies": { 35 | "jquery": "~2.1.1", 36 | "angular-mocks": "~1.2.18 || ~1.4.0 || ~1.5.0 || ~1.6.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview-children.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The recursive step, output child nodes for the scope node 4 | * 5 | * @package ivh.treeview 6 | * @copyright 2014 iVantage Health Analytics, Inc. 7 | */ 8 | 9 | angular.module('ivh.treeview').directive('ivhTreeviewChildren', function() { 10 | 'use strict'; 11 | return { 12 | restrict: 'AE', 13 | require: '^ivhTreeviewNode', 14 | template: [ 15 | '' 24 | ].join('\n') 25 | }; 26 | }); 27 | -------------------------------------------------------------------------------- /src/styles/ivh-treeview.less: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Treeview styles 4 | * 5 | * @private 6 | * @package ivh.treeview 7 | * @copyright 2014 iVantage Health Analytics, Inc. 8 | */ 9 | 10 | ul.ivh-treeview { 11 | //-- Hide Collapsed Nodes 12 | //----------------------- 13 | li.ivh-treeview-node-collapsed { 14 | ul.ivh-treeview { 15 | display: none; 16 | } 17 | } 18 | 19 | //-- Twisties 20 | //----------- 21 | .ivh-treeview-twistie { 22 | &-leaf, &-collapsed { 23 | display: none; 24 | } 25 | } 26 | 27 | .ivh-treeview-node-collapsed { 28 | .ivh-treeview-twistie { 29 | &-collapsed { 30 | display: inline; 31 | } 32 | &-expanded { 33 | display: none; 34 | } 35 | } 36 | } 37 | 38 | // Leaves should never have the expanded or collapsed marker, only the leaf 39 | // marker 40 | li.ivh-treeview-node-leaf { 41 | .ivh-treeview-twistie { 42 | &-leaf { 43 | display: inline; 44 | } 45 | &-expanded, &-collapsed { 46 | display: none; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015 iVantage Health Analytics, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 14 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 15 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 16 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 18 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 19 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /MIGRATING.md: -------------------------------------------------------------------------------- 1 | 2 | # Migrating 3 | 4 | This document contains notes for migrating between major version numbers of 5 | `angular-ivh-treeview`. 6 | 7 | 8 | ## Major Version 1 9 | 10 | ### Click and Change Handlers 11 | 12 | - The `ivh-treeview-click-handler` attribute was renamed to 13 | `ivh-treeview-on-toggle`. Similarly, `ivh-treeview-change-handler` was renamed 14 | to `ivh-treeview-on-cb-change`. Similarly, you should now use `onToggle` and 15 | `onCbChange` rather than `clickHandler` and `changeHandler` respectively when 16 | configuring your tree with an options hash (#45, #55). 17 | - The `ivh-treeview-on-toggle` and `ivh-treeview-on-cb-change` attributes now 18 | expect an angular expression (similar to what you might provide `ng-click`) 19 | rather than a simple callback. See the README for more details. 20 | 21 | ### Default Options Changes 22 | 23 | - The `validate` option now deafults to `true`, this means trees will be 24 | validated by default. Set this option to `false` if you prefer the previous 25 | behavior. 26 | 27 | ### Node Templates 28 | 29 | - Templates now reference the treeview controller instance as `trvw` instead of `ctrl`. 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ivh-treeview", 3 | "version": "1.1.0", 4 | "description": "Treeview for angular with filtering and checkboxes", 5 | "main": "dist/ivh-treeview.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "grunt test" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ivantage/angular-ivh-treeview.git" 15 | }, 16 | "keywords": [ 17 | "angular", 18 | "tree", 19 | "treeview" 20 | ], 21 | "author": "jtrussell", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/ivantage/angular-ivh-treeview/issues" 25 | }, 26 | "homepage": "https://github.com/ivantage/angular-ivh-treeview", 27 | "devDependencies": { 28 | "grunt": "^1.0.1", 29 | "grunt-bump": "^0.8.0", 30 | "grunt-contrib-clean": "^1.1.0", 31 | "grunt-contrib-concat": "^1.0.1", 32 | "grunt-contrib-cssmin": "^2.2.1", 33 | "grunt-contrib-jasmine": "^1.1.0", 34 | "grunt-contrib-jshint": "^1.1.0", 35 | "grunt-contrib-less": "^1.4.1", 36 | "grunt-contrib-uglify": "^3.0.1", 37 | "grunt-contrib-watch": "^1.0.0", 38 | "grunt-jscs": "^3.0.1", 39 | "matchdep": "^1.0.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview-node.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Treeview tree node directive 4 | * 5 | * @private 6 | * @package ivh.treeview 7 | * @copyright 2014 iVantage Health Analytics, Inc. 8 | */ 9 | 10 | angular.module('ivh.treeview').directive('ivhTreeviewNode', ['ivhTreeviewCompiler', function(ivhTreeviewCompiler) { 11 | 'use strict'; 12 | return { 13 | restrict: 'A', 14 | scope: { 15 | node: '=ivhTreeviewNode', 16 | depth: '=ivhTreeviewDepth' 17 | }, 18 | require: '^ivhTreeview', 19 | compile: function(tElement) { 20 | return ivhTreeviewCompiler 21 | .compile(tElement, function(scope, element, attrs, trvw) { 22 | var node = scope.node; 23 | 24 | var getChildren = scope.getChildren = function() { 25 | return trvw.children(node); 26 | }; 27 | 28 | scope.trvw = trvw; 29 | scope.childDepth = scope.depth + 1; 30 | 31 | // Expand/collapse the node as dictated by the expandToDepth property. 32 | // Note that we will respect the expanded state of this node if it has 33 | // been expanded by e.g. `ivhTreeviewMgr.expandTo` but not yet 34 | // rendered. 35 | if(!trvw.isExpanded(node)) { 36 | trvw.expand(node, trvw.isInitiallyExpanded(scope.depth)); 37 | } 38 | 39 | /** 40 | * @todo Provide a way to opt out of this 41 | */ 42 | scope.$watch(function() { 43 | return getChildren().length > 0; 44 | }, function(newVal) { 45 | if(newVal) { 46 | element.removeClass('ivh-treeview-node-leaf'); 47 | } else { 48 | element.addClass('ivh-treeview-node-leaf'); 49 | } 50 | }); 51 | }); 52 | } 53 | }; 54 | }]); 55 | 56 | -------------------------------------------------------------------------------- /src/scripts/services/ivh-treeview-compiler.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Compile helper for treeview nodes 4 | * 5 | * Defers compilation until after linking parents. Otherwise our treeview 6 | * compilation process would recurse indefinitely. 7 | * 8 | * Thanks to http://stackoverflow.com/questions/14430655/recursion-in-angular-directives 9 | * 10 | * @private 11 | * @package ivh.treeview 12 | * @copyright 2014 iVantage Health Analytics, Inc. 13 | */ 14 | 15 | angular.module('ivh.treeview').factory('ivhTreeviewCompiler', ['$compile', function($compile) { 16 | 'use strict'; 17 | return { 18 | /** 19 | * Manually compiles the element, fixing the recursion loop. 20 | * @param {Object} element The angular element or template 21 | * @param {Function} link [optional] A post-link function, or an object with function(s) registered via pre and post properties. 22 | * @returns An object containing the linking functions. 23 | */ 24 | compile: function(element, link) { 25 | // Normalize the link parameter 26 | if(angular.isFunction(link)) { 27 | link = { post: link }; 28 | } 29 | 30 | var compiledContents; 31 | return { 32 | pre: (link && link.pre) ? link.pre : null, 33 | /** 34 | * Compiles and re-adds the contents 35 | */ 36 | post: function(scope, element, attrs, trvw) { 37 | // Compile our template 38 | if(!compiledContents) { 39 | compiledContents = $compile(trvw.getNodeTpl()); 40 | } 41 | // Add the compiled template 42 | compiledContents(scope, function(clone) { 43 | element.append(clone); 44 | }); 45 | 46 | // Call the post-linking function, if any 47 | if(link && link.post) { 48 | link.post.apply(null, arguments); 49 | } 50 | } 51 | }; 52 | } 53 | }; 54 | }]); 55 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireOperatorBeforeLineBreak": [ 12 | "?", 13 | "=", 14 | "+", 15 | "-", 16 | "/", 17 | "*", 18 | "==", 19 | "===", 20 | "!=", 21 | "!==", 22 | ">", 23 | ">=", 24 | "<", 25 | "<=" 26 | ], 27 | "requireCamelCaseOrUpperCaseIdentifiers": true, 28 | "validateQuoteMarks": "'", 29 | "disallowMultipleLineStrings": true, 30 | "disallowMixedSpacesAndTabs": true, 31 | "disallowTrailingWhitespace": true, 32 | "disallowSpaceAfterPrefixUnaryOperators": true, 33 | "disallowKeywordsOnNewLine": [ 34 | "else" 35 | ], 36 | "requireSpaceAfterKeywords": [], 37 | "requireSpaceBeforeBinaryOperators": [ 38 | "=", 39 | "+=", 40 | "-=", 41 | "*=", 42 | "/=", 43 | "%=", 44 | "<<=", 45 | ">>=", 46 | ">>>=", 47 | "&=", 48 | "|=", 49 | "^=", 50 | "+=", 51 | "+", 52 | "-", 53 | "*", 54 | "/", 55 | "%", 56 | "<<", 57 | ">>", 58 | ">>>", 59 | "&", 60 | "|", 61 | "^", 62 | "&&", 63 | "||", 64 | "===", 65 | "==", 66 | ">=", 67 | "<=", 68 | "<", 69 | ">", 70 | "!=", 71 | "!==" 72 | ], 73 | "requireSpaceAfterBinaryOperators": true, 74 | "requireSpacesInConditionalExpression": true, 75 | "requireSpaceBeforeBlockStatements": true, 76 | "requireSpaceBeforeObjectValues": true, 77 | "requireSpacesInForStatement": true, 78 | "requireLineFeedAtFileEnd": true, 79 | "requireSpacesInFunctionExpression": { 80 | "beforeOpeningCurlyBrace": true 81 | }, 82 | "requireSpacesInFunctionDeclaration": { 83 | "beforeOpeningCurlyBrace": true 84 | }, 85 | "disallowSpacesInsideArrayBrackets": "all", 86 | "disallowSpacesInsideParentheses": true, 87 | "disallowNewlineBeforeBlockStatements": true 88 | } 89 | -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview-checkbox-helper.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Selection management logic for treeviews with checkboxes 4 | * 5 | * @private 6 | * @package ivh.treeview 7 | * @copyright 2014 iVantage Health Analytics, Inc. 8 | */ 9 | 10 | angular.module('ivh.treeview').directive('ivhTreeviewCheckboxHelper', [function() { 11 | 'use strict'; 12 | return { 13 | restrict: 'A', 14 | scope: { 15 | node: '=ivhTreeviewCheckboxHelper' 16 | }, 17 | require: '^ivhTreeview', 18 | link: function(scope, element, attrs, trvw) { 19 | var node = scope.node 20 | , opts = trvw.opts() 21 | , indeterminateAttr = opts.indeterminateAttribute 22 | , selectedAttr = opts.selectedAttribute; 23 | 24 | // Set initial selected state of this checkbox 25 | scope.isSelected = node[selectedAttr]; 26 | 27 | // Local access to the parent controller 28 | scope.trvw = trvw; 29 | 30 | // Enforce consistent behavior across browsers by making indeterminate 31 | // checkboxes become checked when clicked/selected using spacebar 32 | scope.resolveIndeterminateClick = function() { 33 | 34 | //intermediate state is not handled when CheckBoxes state propagation is disabled 35 | if (opts.disableCheckboxSelectionPropagation) { 36 | return; 37 | } 38 | 39 | if(node[indeterminateAttr]) { 40 | trvw.select(node, true); 41 | } 42 | }; 43 | 44 | // Update the checkbox when the node's selected status changes 45 | scope.$watch('node.' + selectedAttr, function(newVal, oldVal) { 46 | scope.isSelected = newVal; 47 | }); 48 | 49 | if (!opts.disableCheckboxSelectionPropagation) { 50 | // Update the checkbox when the node's indeterminate status changes 51 | scope.$watch('node.' + indeterminateAttr, function(newVal, oldVal) { 52 | element.find('input').prop('indeterminate', newVal); 53 | }); 54 | } 55 | }, 56 | template: [ 57 | '' 62 | ].join('\n') 63 | }; 64 | }]); 65 | 66 | -------------------------------------------------------------------------------- /src/scripts/services/ivh-treeview-bfs.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Breadth first searching for treeview data stores 4 | * 5 | * @package ivh.treeview 6 | * @copyright 2014 iVantage Health Analytics, Inc. 7 | */ 8 | 9 | angular.module('ivh.treeview').factory('ivhTreeviewBfs', ['ivhTreeviewOptions', function(ivhTreeviewOptions) { 10 | 'use strict'; 11 | 12 | var ng = angular; 13 | 14 | /** 15 | * Breadth first search of `tree` 16 | * 17 | * `opts` is optional and may override settings from `ivhTreeviewOptions.options`. 18 | * The callback `cb` will be invoked on each node in the tree as we traverse, 19 | * if it returns `false` traversal of that branch will not continue. The 20 | * callback is given the current node as the first parameter and the node 21 | * ancestors, from closest to farthest, as an array in the second parameter. 22 | * 23 | * @param {Array|Object} tree The tree data 24 | * @param {Object} opts [optional] Settings overrides 25 | * @param {Function} cb [optional] Callback to run against each node 26 | */ 27 | return function(tree, opts, cb) { 28 | if(arguments.length === 2 && ng.isFunction(opts)) { 29 | cb = opts; 30 | opts = {}; 31 | } 32 | opts = angular.extend({}, ivhTreeviewOptions(), opts); 33 | cb = cb || ng.noop; 34 | 35 | var queue = [] 36 | , childAttr = opts.childrenAttribute 37 | , next, node, parents, ix, numChildren; 38 | 39 | if(ng.isArray(tree)) { 40 | ng.forEach(tree, function(n) { 41 | // node and parents 42 | queue.push([n, []]); 43 | }); 44 | next = queue.shift(); 45 | } else { 46 | // node and parents 47 | next = [tree, []]; 48 | } 49 | 50 | while(next) { 51 | node = next[0]; 52 | parents = next[1]; 53 | // cb might return `undefined` so we have to actually check for equality 54 | // against `false` 55 | if(cb(node, parents) !== false) { 56 | if(node[childAttr] && ng.isArray(node[childAttr])) { 57 | numChildren = node[childAttr].length; 58 | for(ix = 0; ix < numChildren; ix++) { 59 | queue.push([node[childAttr][ix], [node].concat(parents)]); 60 | } 61 | } 62 | } 63 | next = queue.shift(); 64 | } 65 | }; 66 | }]); 67 | -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview-twistie.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Treeview twistie directive 4 | * 5 | * @private 6 | * @package ivh.treeview 7 | * @copyright 2014 iVantage Health Analytics, Inc. 8 | */ 9 | 10 | angular.module('ivh.treeview').directive('ivhTreeviewTwistie', ['$compile', 'ivhTreeviewOptions', function($compile, ivhTreeviewOptions) { 11 | 'use strict'; 12 | 13 | var globalOpts = ivhTreeviewOptions(); 14 | 15 | return { 16 | restrict: 'A', 17 | require: '^ivhTreeview', 18 | template: [ 19 | '', 20 | '', 21 | globalOpts.twistieCollapsedTpl, 22 | '', 23 | '', 24 | globalOpts.twistieExpandedTpl, 25 | '', 26 | '', 27 | globalOpts.twistieLeafTpl, 28 | '', 29 | '' 30 | ].join('\n'), 31 | link: function(scope, element, attrs, trvw) { 32 | 33 | if(!trvw.hasLocalTwistieTpls) { 34 | return; 35 | } 36 | 37 | var opts = trvw.opts() 38 | , $twistieContainers = element 39 | .children().eq(0) // Template root 40 | .children(); // The twistie spans 41 | 42 | angular.forEach([ 43 | // Should be in the same order as elements in template 44 | 'twistieCollapsedTpl', 45 | 'twistieExpandedTpl', 46 | 'twistieLeafTpl' 47 | ], function(tplKey, ix) { 48 | var tpl = opts[tplKey] 49 | , tplGlobal = globalOpts[tplKey]; 50 | 51 | // Do nothing if we don't have a new template 52 | if(!tpl || tpl === tplGlobal) { 53 | return; 54 | } 55 | 56 | // Super gross, the template must actually be an html string, we won't 57 | // try too hard to enforce this, just don't shoot yourself in the foot 58 | // too badly and everything will be alright. 59 | if(tpl.substr(0, 1) !== '<' || tpl.substr(-1, 1) !== '>') { 60 | tpl = '' + tpl + ''; 61 | } 62 | 63 | var $el = $compile(tpl)(scope) 64 | , $container = $twistieContainers.eq(ix); 65 | 66 | // Clean out global template and append the new one 67 | $container.html('').append($el); 68 | }); 69 | 70 | } 71 | }; 72 | }]); 73 | -------------------------------------------------------------------------------- /test/spec/services/ivh-treeview-bfs.js: -------------------------------------------------------------------------------- 1 | /*global jQuery, describe, beforeEach, afterEach, it, module, inject, expect */ 2 | 3 | describe('Service: ivhTreeviewBfs', function() { 4 | 'use strict'; 5 | 6 | beforeEach(module('ivh.treeview')); 7 | 8 | var ivhTreeviewBfs; 9 | 10 | var tree 11 | , nodes 12 | , stuff 13 | , hats 14 | , fedora 15 | , flatcap 16 | , bags 17 | , messenger 18 | , backpack; 19 | 20 | beforeEach(inject(function(_ivhTreeviewBfs_) { 21 | ivhTreeviewBfs = _ivhTreeviewBfs_; 22 | })); 23 | 24 | beforeEach(function() { 25 | tree = [{ 26 | label: 'Stuff', 27 | id: 'stuff', 28 | children: [{ 29 | label: 'Hats', 30 | id: 'hats', 31 | children: [{ 32 | label: 'Fedora', 33 | id: 'fedora' 34 | }, { 35 | label: 'Flatcap', 36 | id: 'flatcap' 37 | }] 38 | }, { 39 | label: 'Bags', 40 | id: 'bags', 41 | children: [{ 42 | label: 'Messenger', 43 | id: 'messenger' 44 | }, { 45 | label: 'Backpack', 46 | id: 'backpack' 47 | }] 48 | }] 49 | }]; 50 | 51 | stuff = tree[0]; 52 | hats = stuff.children[0]; 53 | bags = stuff.children[1]; 54 | fedora = hats.children[0]; 55 | flatcap = hats.children[1]; 56 | messenger = bags.children[0]; 57 | backpack = bags.children[1]; 58 | 59 | nodes = [hats, bags, fedora, flatcap, messenger, backpack]; 60 | }); 61 | 62 | it('should perform a breadth first traversal of the tree', function() { 63 | var visited = []; 64 | ivhTreeviewBfs(tree, function(node, parents) { 65 | visited.push(node.id); 66 | }); 67 | expect(visited).toEqual([ 68 | 'stuff', 69 | 'hats', 70 | 'bags', 71 | 'fedora', 72 | 'flatcap', 73 | 'messenger', 74 | 'backpack' 75 | ]); 76 | }); 77 | 78 | it('should stop traversal if false is returned', function() { 79 | var visited = []; 80 | ivhTreeviewBfs(tree, function(node, parents) { 81 | visited.push(node.id); 82 | return node.id !== 'hats'; 83 | }); 84 | expect(visited).toEqual([ 85 | 'stuff', 86 | 'hats', 87 | 'bags', 88 | 'messenger', 89 | 'backpack' 90 | ]); 91 | }); 92 | 93 | it('should build up a list of ancestors', function() { 94 | var hatsParents = [] 95 | , backpackParents = []; 96 | ivhTreeviewBfs(tree, function(node, parents) { 97 | if(node.id === 'hats') { 98 | parents.forEach(function(n) { 99 | hatsParents.push(n.id); 100 | }); 101 | } 102 | if(node.id === 'backpack') { 103 | parents.forEach(function(n) { 104 | backpackParents.push(n.id); 105 | }); 106 | } 107 | }); 108 | expect(hatsParents).toEqual(['stuff']); 109 | expect(backpackParents[0]).toEqual('bags'); 110 | expect(backpackParents[1]).toEqual('stuff'); 111 | }); 112 | }); 113 | 114 | 115 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | 3 | module.exports = function(grunt) { 4 | 'use strict'; 5 | 6 | // Project configuration 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | 10 | jshint: { 11 | options: { 12 | jshintrc: '.jshintrc' 13 | }, 14 | gruntfile: 'gruntfile.js', 15 | src: 'src/**/*.js', 16 | test: 'test/**/*.js' 17 | }, 18 | 19 | jscs: { 20 | options: { 21 | config: '.jscsrc' 22 | }, 23 | gruntfile: { 24 | files: { 25 | src: [ 26 | 'gruntfile.js' 27 | ] 28 | } 29 | }, 30 | spec: { 31 | files: { 32 | src: [ 33 | 'test/spec/**/*.js' 34 | ] 35 | } 36 | }, 37 | scripts: { 38 | files: { 39 | src: [ 40 | 'src/scripts/**/*.js' 41 | ] 42 | } 43 | } 44 | }, 45 | 46 | clean: { 47 | dist: 'dist' 48 | }, 49 | 50 | concat: { 51 | options: {separator: '\n'}, 52 | dist: { 53 | src: ['src/scripts/module.js', 'src/scripts/**/*.js'], 54 | dest: 'dist/ivh-treeview.js' 55 | } 56 | }, 57 | 58 | uglify: { 59 | dist: { 60 | src: 'dist/ivh-treeview.js', 61 | dest: 'dist/ivh-treeview.min.js' 62 | } 63 | }, 64 | 65 | less: { 66 | dist: { 67 | files: { 68 | 'dist/ivh-treeview.css': 'src/styles/ivh-treeview.less', 69 | 'dist/ivh-treeview-theme-basic.css': 'src/styles/ivh-treeview-theme-basic.less' 70 | } 71 | } 72 | }, 73 | 74 | cssmin: { 75 | dist: { 76 | files: { 77 | 'dist/ivh-treeview.min.css': 'dist/ivh-treeview.css' 78 | } 79 | } 80 | }, 81 | 82 | jasmine: { 83 | spec: { 84 | src: ['src/scripts/*.js', 'src/scripts/**/*.js'], 85 | options: { 86 | specs: 'test/spec/**/*.js', 87 | summary: true, 88 | vendor: [ 89 | 'bower_components/jquery/dist/jquery.js', 90 | 'bower_components/angular/angular.js', 91 | 'bower_components/angular-mocks/angular-mocks.js' 92 | ] 93 | } 94 | } 95 | }, 96 | 97 | watch: { 98 | scripts: { 99 | files: 'src/scripts/**/*.js', 100 | tasks: ['test', 'build:scripts'] 101 | }, 102 | styles: { 103 | files: 'src/styles/**/*.less', 104 | tasks: ['build:styles'] 105 | }, 106 | tests: { 107 | files: 'test/spec/**/*.js', 108 | tasks: ['test'] 109 | } 110 | }, 111 | 112 | bump: { 113 | options: { 114 | commitMessage: 'chore: Bump for release (v%VERSION%)', 115 | files: ['package.json', 'bower.json'], 116 | commitFiles: ['package.json', 'bower.json'], 117 | push: false 118 | } 119 | } 120 | }); 121 | 122 | // Load plugins 123 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 124 | 125 | grunt.registerTask('test', [ 126 | 'jshint', 127 | 'jscs', 128 | 'jasmine' 129 | ]); 130 | 131 | grunt.registerTask('build:scripts', [ 132 | 'concat', 133 | 'uglify' 134 | ]); 135 | 136 | grunt.registerTask('build:styles', [ 137 | 'less', 138 | 'cssmin' 139 | ]); 140 | 141 | grunt.registerTask('build', [ 142 | 'build:scripts', 143 | 'build:styles' 144 | ]); 145 | 146 | grunt.registerTask('default', [ 147 | 'clean', 148 | 'test', 149 | 'build' 150 | ]); 151 | 152 | }; 153 | -------------------------------------------------------------------------------- /test/spec/directives/ivh-treeview-custom-templates.js: -------------------------------------------------------------------------------- 1 | /*global jQuery, describe, beforeEach, afterEach, it, module, inject, expect */ 2 | 3 | describe('Directive: ivhTreeview + custom node templates', function() { 4 | 'use strict'; 5 | 6 | var nodeTpl, nodeTpl2, bag; 7 | 8 | beforeEach(function() { 9 | nodeTpl = [ 10 | '
', 11 | 'It is a {{trvw.label(node)}}', 12 | '
', 13 | '
' 14 | ].join('\n'); 15 | 16 | nodeTpl2 = [ 17 | '
', 18 | 'It is a {{trvw.label(node)}}', 19 | '
', 20 | '
' 21 | ].join('\n'); 22 | 23 | bag = [{ 24 | label: 'parent', 25 | children: [{ 26 | label: 'child' 27 | }] 28 | }]; 29 | }); 30 | 31 | it('should allow custom templates using the global settings', function() { 32 | module('ivh.treeview', function(ivhTreeviewOptionsProvider) { 33 | ivhTreeviewOptionsProvider.set({ 34 | nodeTpl: nodeTpl 35 | }); 36 | }); 37 | 38 | inject(function($rootScope, $compile) { 39 | var $s = $rootScope.$new(); 40 | $s.bag = bag; 41 | 42 | var $el = $compile('
')($s); 43 | $s.$apply(); 44 | 45 | expect($el.find('.spicy.custom.template').length).toBe(2); 46 | }); 47 | }); 48 | 49 | describe('non-global templates', function() { 50 | var $s, c; 51 | beforeEach(module('ivh.treeview')); 52 | beforeEach(inject(function($rootScope, $compile) { 53 | $s = $rootScope.$new(); 54 | $s.bag = bag; 55 | c = function(tpl, scp) { 56 | scp = scp || $s; 57 | var $el = $compile(tpl)(scp); 58 | scp.$apply(); 59 | return $el; 60 | }; 61 | })); 62 | 63 | it('should allow inline template definitions', function() { 64 | $s.nodeTpl2 = nodeTpl2; 65 | var $el = c('
'); 66 | expect($el.find('.spicier.custom.template').length).toBe(2); 67 | }); 68 | 69 | it('should allow templates in the options object', function() { 70 | $s.opts = { 71 | nodeTpl: nodeTpl2 72 | }; 73 | var $el = c('
'); 74 | expect($el.find('.spicier.custom.template').length).toBe(2); 75 | }); 76 | 77 | it('should use transcluded content as a node template', function() { 78 | var $el = c([ 79 | '
', 80 | '', 83 | '
' 84 | ].join('\n')); 85 | expect($el.find('.spicier.custom.template').length).toBe(2); 86 | }); 87 | 88 | it('should be able to expand to children of non-rendered nodes', inject(function($rootScope, ivhTreeviewMgr) { 89 | var $s = $rootScope.$new(); 90 | 91 | $s.bag = [{ 92 | id: 0, 93 | children: [{ 94 | id: 1, 95 | children: [{ 96 | id: 2, 97 | children: [{ 98 | id: 3 99 | }] 100 | }] 101 | }] 102 | }]; 103 | 104 | var $el = c([ 105 | '
', 106 | '', 112 | '
' 113 | ].join('\n'), $s); 114 | ivhTreeviewMgr.expandTo($s.bag, 3); 115 | $s.$apply(); 116 | expect($el.find('#3').length).toBe(1); 117 | })); 118 | 119 | }); 120 | 121 | }); 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/scripts/services/ivh-treeview-options.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Global options for ivhTreeview 4 | * 5 | * @package ivh.treeview 6 | * @copyright 2014 iVantage Health Analytics, Inc. 7 | */ 8 | 9 | angular.module('ivh.treeview').provider('ivhTreeviewOptions', [ 10 | 'ivhTreeviewInterpolateStartSymbol', 'ivhTreeviewInterpolateEndSymbol', 11 | function(ivhTreeviewInterpolateStartSymbol, ivhTreeviewInterpolateEndSymbol) { 12 | 'use strict'; 13 | 14 | var symbolStart = ivhTreeviewInterpolateStartSymbol 15 | , symbolEnd = ivhTreeviewInterpolateEndSymbol; 16 | 17 | var options = { 18 | /** 19 | * ID attribute 20 | * 21 | * For selecting nodes by identifier rather than reference 22 | */ 23 | idAttribute: 'id', 24 | 25 | /** 26 | * Collection item attribute to use for labels 27 | */ 28 | labelAttribute: 'label', 29 | 30 | /** 31 | * Collection item attribute to use for child nodes 32 | */ 33 | childrenAttribute: 'children', 34 | 35 | /** 36 | * Collection item attribute to use for selected state 37 | */ 38 | selectedAttribute: 'selected', 39 | 40 | /** 41 | * Controls whether branches are initially expanded or collapsed 42 | * 43 | * A value of `0` means the tree will be entirely collapsd (the default 44 | * state) otherwise branches will be expanded up to the specified depth. Use 45 | * `-1` to have the tree entirely expanded. 46 | */ 47 | expandToDepth: 0, 48 | 49 | /** 50 | * Whether or not to use checkboxes 51 | * 52 | * If `false` the markup to support checkboxes is not included in the 53 | * directive. 54 | */ 55 | useCheckboxes: true, 56 | 57 | /** 58 | * If set to true the checkboxes are independent on each other (no state 59 | * propagation to children and revalidation of parents' states). 60 | * If you set to true, you should set also `validate` property to `false` 61 | * and avoid explicit calling of `ivhTreeviewMgr.validate()`. 62 | */ 63 | disableCheckboxSelectionPropagation: false, 64 | 65 | /** 66 | * Whether or not directive should validate treestore on startup 67 | */ 68 | validate: true, 69 | 70 | /** 71 | * Collection item attribute to track intermediate states 72 | */ 73 | indeterminateAttribute: '__ivhTreeviewIndeterminate', 74 | 75 | /** 76 | * Collection item attribute to track expanded status 77 | */ 78 | expandedAttribute: '__ivhTreeviewExpanded', 79 | 80 | /** 81 | * Default selected state when validating 82 | */ 83 | defaultSelectedState: true, 84 | 85 | /** 86 | * Template for expanded twisties 87 | */ 88 | twistieExpandedTpl: '(-)', 89 | 90 | /** 91 | * Template for collapsed twisties 92 | */ 93 | twistieCollapsedTpl: '(+)', 94 | 95 | /** 96 | * Template for leaf twisties (i.e. no children) 97 | */ 98 | twistieLeafTpl: 'o', 99 | 100 | /** 101 | * Template for tree nodes 102 | */ 103 | nodeTpl: [ 104 | '
', 105 | '', 106 | '', 107 | '', 108 | '', 110 | '', 111 | '', 112 | '{{trvw.label(node)}}', 113 | '', 114 | '
', 115 | '
' 116 | ].join('\n') 117 | .replace(new RegExp('{{', 'g'), symbolStart) 118 | .replace(new RegExp('}}', 'g'), symbolEnd) 119 | }; 120 | 121 | /** 122 | * Update global options 123 | * 124 | * @param {Object} opts options object to override defaults with 125 | */ 126 | this.set = function(opts) { 127 | angular.extend(options, opts); 128 | }; 129 | 130 | this.$get = function() { 131 | /** 132 | * Get a copy of the global options 133 | * 134 | * @return {Object} The options object 135 | */ 136 | return function() { 137 | return angular.copy(options); 138 | }; 139 | }; 140 | }]); 141 | -------------------------------------------------------------------------------- /docs/templates-and-skins.md: -------------------------------------------------------------------------------- 1 | 2 | ## Contents 3 | 4 | - [Basic Skins](#basic-skins) 5 | - [Tree Layout](#tree-layout) 6 | - [Global Templates](#global-templates) 7 | - [Inline Templates](#inline-templates) 8 | - [Template Helper Directives](#template-helper-directives) 9 | - [Supported Template Scope Variables](#supported-template-scope-variables) 10 | 11 | 12 | ### Skins 13 | 14 | Custom node templates are most likely overkill for style tweaks. Making the look 15 | and feel of your tree match the rest of your application can often be 16 | accomplished with a bit of css and your own twisties. IVH Treeview ships with 17 | only minimal styling and you are encouraged to apply your own styles. 18 | 19 | #### Tree Layout 20 | 21 | Using the default template your tree will have the following general layout to 22 | aid in styling: 23 | 24 | ``` 25 | ul.ivh-treeview 26 | li.ivh-treeview-node[?.ivh-treeview-node-collapsed][?.ivh-treeview-node-leaf] 27 | 28 | 29 | 30 | .ivh-treeview-node-content 31 | .ivh-treeview-twistie-wrapper 32 | .ivh-treeview-twistie 33 | [?.ivh-treeview-twistie-collapsed] 34 | [?.ivh-treeview-twistie-expanded] 35 | [?.ivh-treeview-twistie-leaf] 36 | .ivh-treeview-checkbox-wrapper 37 | .ivh-treeview-checkbox 38 | .ivh-treeview-node-label 39 | ul.ivh-treeview 40 | [... more nodes] 41 | 42 | [... more nodes] 43 | ``` 44 | 45 | Where `ivh-treeview-node-collapsed` and the various twistie classnames are 46 | conditionally applied as appropriate. 47 | 48 | The top level `li` for a given node is give the classname 49 | `ivh-treeview-node-leaf` when it is a leaf node. 50 | 51 | ### Global Templates 52 | 53 | Tree node templates can be set globally using the `nodeTpl` options: 54 | 55 | ``` 56 | app.config(function(ivhTreeviewOptionsProvider) { 57 | ivhTreeviewOptionsProvider.set({ 58 | nodeTpl: '' 59 | }); 60 | }); 61 | ``` 62 | 63 | ### Inline Templates 64 | 65 | Want different node templates for different trees? This can be accomplished 66 | using inline templates. Inline templates can be specified in any of three ways: 67 | 68 | With the `ivh-treeview-node-tpl` attribute: 69 | 70 | ``` 71 |
73 | ``` 74 | 75 | ***Demo***: [Custom templates: inline](http://jsbin.com/fokunu/edit) 76 | 77 | As a property in the `ivh-treeview-options` object: 78 | 79 | ``` 80 |
82 | ``` 83 | 84 | Or as transcluded content in the treeview directive itself: 85 | 86 | ``` 87 |
88 | 101 |
102 | ``` 103 | 104 | ***Demo***: [Custom templates: transcluded](http://jsbin.com/jaqosi/edit) 105 | 106 | Note the use of the ng-template script tag wrapping the rest of the transcluded 107 | content, this wrapper is a mandatory. Also note that this form is intended to 108 | serve as a convenient and declarative way to essentially provide a template 109 | string to your treeview. The template itself does not (currently) have access a 110 | transcluded scope. 111 | 112 | 113 | ### Template Helper Directives 114 | 115 | You have access to a number of helper directives when building your node 116 | templates. These are mostly optional but should make your life a bit easier, not 117 | that all support both element and attribute level usage: 118 | 119 | - `ivh-treeview-toggle` (*attribute*) Clicking this element will expand or 120 | collapse the tree node if it is not a leaf. 121 | - `ivh-treeview-twistie` (*attribute*) Display as either an "expanded" or 122 | "collapsed" twistie as appropriate. 123 | - `ivh-treeview-checkbox` (*attribute*|*element*) A checkbox that is "plugged 124 | in" to the treeview. It will reflect your node's selected state and update 125 | parents and children appropriately out of the box. 126 | - `ivh-treeview-children` (*attribute*|*element*) The recursive step. If you 127 | want your tree to display more than one level of nodes you will need to place 128 | this some where, or have your own way of getting child nodes into the view. 129 | 130 | #### Supported Template Scope Variables 131 | 132 | **`node`** 133 | 134 | A reference to the tree node itself. Note that in general you should use 135 | controller helper methods to access node properties when possible. 136 | 137 | **`depth`** 138 | 139 | The depth of the current node in the tree. The root node will be at depth `0`, 140 | its children will be at depth `1`, etc. 141 | 142 | **`trvw`** 143 | 144 | A reference to the treeview controller with a number of useful properties and 145 | helper functions: 146 | 147 | - `trvw.select(Object node[, Boolean isSelected])`
148 | Set the seleted state of `node` to `isSelected`. The will update parent and 149 | child node selected states appropriately. `isSelected` defaults to `true`. 150 | - `trvw.isSelected(Object node) -> Boolean`
151 | Returns `true` if `node` is selected and `false` otherwise. 152 | - `trvw.toggleSelected(Object node)`
153 | Toggles the selected state of `node`. This will update parent and child note 154 | selected states appropriately. 155 | - `trvw.expand(Object node[, Boolean isExpanded])`
156 | Set the expanded state of `node` to `isExpanded`, i.e. expand or collapse 157 | `node`. `isExpanded` defaults to `true`. 158 | - `trvw.isExpanded(Object node) --> Boolean`
159 | Returns `true` if `node` is expanded and `false` otherwise. 160 | - `trvw.toggleExpanded(Object node)`
161 | Toggle the expanded state of `node`. 162 | - `trvw.isLeaf(Object node) --> Boolean`
163 | Returns `true` if `node` is a leaf node in the tree and `false` otherwise. 164 | - `trvw.label(Object node) --> String`
165 | Returns the label attribute of `node` as determined by the `labelAttribute` 166 | treeview option. 167 | - `trvw.root() --> Array|Object`
168 | Returns the tree root as handed to `ivh-treeview`. 169 | - `trvw.children(Object node) --> Array`
170 | Returns the array of children for `node`. Returns an empty array if `node` has 171 | no children or the `childrenAttribute` property value is not defined. 172 | - `trvw.opts() --> Object`
173 | Returns a merged version of the global and local options. 174 | - `trvw.isVisible(Object node) --> Boolean`
175 | Returns `true` if `node` should be considered visible under the current 176 | **filter** and `false` otherwise. Note that this only relates to treeview 177 | filters and does not take into account whether or not `node` can actually be 178 | seen as a result of expanded/collapsed parents. 179 | - `trvw.useCheckboxes() --> Boolean`
180 | Returns `true` if checkboxes should be used in the template and `false` 181 | otherwise. 182 | 183 | -------------------------------------------------------------------------------- /dist/ivh-treeview.min.js: -------------------------------------------------------------------------------- 1 | angular.module("ivh.treeview",[]),angular.module("ivh.treeview").constant("ivhTreeviewInterpolateEndSymbol","}}"),angular.module("ivh.treeview").constant("ivhTreeviewInterpolateStartSymbol","{{"),angular.module("ivh.treeview").directive("ivhTreeviewCheckboxHelper",[function(){"use strict";return{restrict:"A",scope:{node:"=ivhTreeviewCheckboxHelper"},require:"^ivhTreeview",link:function(e,t,i,n){var r=e.node,l=n.opts(),o=l.indeterminateAttribute,a=l.selectedAttribute;e.isSelected=r[a],e.trvw=n,e.resolveIndeterminateClick=function(){l.disableCheckboxSelectionPropagation||r[o]&&n.select(r,!0)},e.$watch("node."+a,function(t,i){e.isSelected=t}),l.disableCheckboxSelectionPropagation||e.$watch("node."+o,function(e,i){t.find("input").prop("indeterminate",e)})},template:[''].join("\n")}}]),angular.module("ivh.treeview").directive("ivhTreeviewCheckbox",[function(){"use strict";return{restrict:"AE",require:"^ivhTreeview",template:''}}]),angular.module("ivh.treeview").directive("ivhTreeviewChildren",function(){"use strict";return{restrict:"AE",require:"^ivhTreeviewNode",template:['"].join("\n")}}),angular.module("ivh.treeview").directive("ivhTreeviewNode",["ivhTreeviewCompiler",function(e){"use strict";return{restrict:"A",scope:{node:"=ivhTreeviewNode",depth:"=ivhTreeviewDepth"},require:"^ivhTreeview",compile:function(t){return e.compile(t,function(e,t,i,n){var r=e.node,l=e.getChildren=function(){return n.children(r)};e.trvw=n,e.childDepth=e.depth+1,n.isExpanded(r)||n.expand(r,n.isInitiallyExpanded(e.depth)),e.$watch(function(){return l().length>0},function(e){e?t.removeClass("ivh-treeview-node-leaf"):t.addClass("ivh-treeview-node-leaf")})})}}}]),angular.module("ivh.treeview").directive("ivhTreeviewToggle",[function(){"use strict";return{restrict:"A",require:"^ivhTreeview",link:function(e,t,i,n){var r=e.node;t.addClass("ivh-treeview-toggle"),t.bind("click",function(){n.isLeaf(r)||e.$apply(function(){n.toggleExpanded(r),n.onToggle(r)})})}}}]),angular.module("ivh.treeview").directive("ivhTreeviewTwistie",["$compile","ivhTreeviewOptions",function(e,t){"use strict";var i=t();return{restrict:"A",require:"^ivhTreeview",template:['','',i.twistieCollapsedTpl,"",'',i.twistieExpandedTpl,"",'',i.twistieLeafTpl,"",""].join("\n"),link:function(t,n,r,l){if(l.hasLocalTwistieTpls){var o=l.opts(),a=n.children().eq(0).children();angular.forEach(["twistieCollapsedTpl","twistieExpandedTpl","twistieLeafTpl"],function(n,r){var l=o[n],s=i[n];if(l&&l!==s){"<"===l.substr(0,1)&&">"===l.substr(-1,1)||(l=""+l+"");var c=e(l)(t);a.eq(r).html("").append(c)}})}}}}]),angular.module("ivh.treeview").directive("ivhTreeview",["ivhTreeviewMgr",function(e){"use strict";return{restrict:"A",transclude:!0,scope:{root:"=ivhTreeview",childrenAttribute:"=ivhTreeviewChildrenAttribute",defaultSelectedState:"=ivhTreeviewDefaultSelectedState",disableCheckboxSelectionPropagation:"=ivhTreeviewDisableCheckboxSelectionPropagation",expandToDepth:"=ivhTreeviewExpandToDepth",idAttribute:"=ivhTreeviewIdAttribute",indeterminateAttribute:"=ivhTreeviewIndeterminateAttribute",expandedAttribute:"=ivhTreeviewExpandedAttribute",labelAttribute:"=ivhTreeviewLabelAttribute",nodeTpl:"=ivhTreeviewNodeTpl",selectedAttribute:"=ivhTreeviewSelectedAttribute",onCbChange:"&ivhTreeviewOnCbChange",onToggle:"&ivhTreeviewOnToggle",twistieCollapsedTpl:"=ivhTreeviewTwistieCollapsedTpl",twistieExpandedTpl:"=ivhTreeviewTwistieExpandedTpl",twistieLeafTpl:"=ivhTreeviewTwistieLeafTpl",useCheckboxes:"=ivhTreeviewUseCheckboxes",validate:"=ivhTreeviewValidate",visibleAttribute:"=ivhTreeviewVisibleAttribute",userOptions:"=ivhTreeviewOptions",filter:"=ivhTreeviewFilter"},controllerAs:"trvw",controller:["$scope","$element","$attrs","$transclude","ivhTreeviewOptions","filterFilter",function(t,i,n,r,l,o){var a=angular,s=this,c=a.extend({},l(),t.userOptions);a.forEach(["childrenAttribute","defaultSelectedState","disableCheckboxSelectionPropagation","expandToDepth","idAttribute","indeterminateAttribute","expandedAttribute","labelAttribute","nodeTpl","selectedAttribute","twistieCollapsedTpl","twistieExpandedTpl","twistieLeafTpl","useCheckboxes","validate","visibleAttribute"],function(e){a.isDefined(t[e])&&(c[e]=t[e])});var u=function(e){return"ivhTreeview"+e.charAt(0).toUpperCase()+e.slice(1)};a.forEach(["onCbChange","onToggle"],function(e){n[u(e)]&&(c[e]=t[e])});var d;r(function(e,t){var i="";angular.forEach(e,function(e){i+=(e.innerHTML||"").trim()}),i.length&&(d=t,c.nodeTpl=i)}),s.opts=function(){return c};var v=t.userOptions||{};s.hasLocalTwistieTpls=!!(v.twistieCollapsedTpl||v.twistieExpandedTpl||v.twistieLeafTpl||t.twistieCollapsedTpl||t.twistieExpandedTpl||t.twistieLeafTpl),s.children=function(e){var t=e[c.childrenAttribute];return a.isArray(t)?t:[]},s.label=function(e){return e[c.labelAttribute]},s.hasFilter=function(){return a.isDefined(t.filter)},s.getFilter=function(){return t.filter||""},s.isVisible=function(e){var t=s.getFilter();if(!t||o([e],t).length)return!0;if("object"==typeof t||"function"==typeof t)for(var i=s.children(e),n=i.length;n--;)if(s.isVisible(i[n]))return!0;return!1},s.useCheckboxes=function(){return c.useCheckboxes},s.select=function(i,n){e.select(t.root,i,c,n),s.onCbChange(i,n)},s.isSelected=function(e){return e[c.selectedAttribute]},s.toggleSelected=function(e){var t=!e[c.selectedAttribute];s.select(e,t)},s.expand=function(i,n){e.expand(t.root,i,c,n)},s.isExpanded=function(e){return e[c.expandedAttribute]},s.toggleExpanded=function(e){s.expand(e,!s.isExpanded(e))},s.isInitiallyExpanded=function(e){return e<(-1===c.expandToDepth?1/0:c.expandToDepth)},s.isLeaf=function(e){return 0===s.children(e).length},s.getNodeTpl=function(){return c.nodeTpl},s.root=function(){return t.root},s.onToggle=function(e){if(c.onToggle){var i={ivhNode:e,ivhIsExpanded:s.isExpanded(e),ivhTree:t.root};c.onToggle(i)}},s.onCbChange=function(e,i){if(c.onCbChange){var n={ivhNode:e,ivhIsSelected:i,ivhTree:t.root};c.onCbChange(n)}}}],link:function(t,i,n){var r=t.trvw.opts();r.validate&&e.validate(t.root,r)},template:['"].join("\n")}}]),angular.module("ivh.treeview").filter("ivhTreeviewAsArray",function(){"use strict";return function(e){return!angular.isArray(e)&&angular.isObject(e)?[e]:e}}),angular.module("ivh.treeview").factory("ivhTreeviewBfs",["ivhTreeviewOptions",function(e){"use strict";var t=angular;return function(i,n,r){2===arguments.length&&t.isFunction(n)&&(r=n,n={}),n=angular.extend({},e(),n),r=r||t.noop;var l,o,a,s,c,u=[],d=n.childrenAttribute;for(t.isArray(i)?(t.forEach(i,function(e){u.push([e,[]])}),l=u.shift()):l=[i,[]];l;){if(o=l[0],a=l[1],!1!==r(o,a)&&o[d]&&t.isArray(o[d]))for(c=o[d].length,s=0;s2&&"boolean"==typeof u&&(d=u,u={}),u=i.extend({},n,u),d=!i.isDefined(d)||d;var v=s(c),h=!0,p=u.idAttribute;return t(e,u,function(e,n){if(h&&(v?c===e[p]:c===e)){h=!1;var r=d?o.bind(u):l.bind(u);u.disableCheckboxSelectionPropagation?r(e):(t(e,u,r),i.forEach(n,a.bind(u)))}return h}),r},r.selectAll=function(e,l,o){arguments.length>1&&"boolean"==typeof l&&(o=l,l={}),l=i.extend({},n,l),o=!i.isDefined(o)||o;var a=l.selectedAttribute,s=l.indeterminateAttribute;return t(e,l,function(e){e[a]=o,e[s]=!1}),r},r.selectEach=function(e,t,n,l){return i.forEach(t,function(t){r.select(e,t,n,l)}),r},r.deselect=function(e,t,i){return r.select(e,t,i,!1)},r.deselectAll=function(e,t){return r.selectAll(e,t,!1)},r.deselectEach=function(e,t,i){return r.selectEach(e,t,i,!1)},r.validate=function(e,l,o){if(!e)return r;arguments.length>1&&"boolean"==typeof l&&(o=l,l={}),l=i.extend({},n,l),o=i.isDefined(o)?o:l.defaultSelectedState;var a=l.selectedAttribute,s=l.indeterminateAttribute;return t(e,l,function(t,n){if(i.isDefined(t[a])&&t[a]!==o)return r.select(e,t,l,!o),!1;t[a]=o,t[s]=!1}),r},r.expand=function(e,t,l,o){arguments.length>2&&"boolean"==typeof l&&(o=l,l={}),l=i.extend({},n,l),o=!i.isDefined(o)||o;var a=s(t),u=l.expandedAttribute;return a?c(e,t,l,function(e,t){return e[u]=o,r}):(t[u]=o,r)},r.expandRecursive=function(e,l,o,a){arguments.length>2&&"boolean"==typeof o&&(a=o,o={}),l=i.isDefined(l)?l:e,o=i.extend({},n,o),a=!i.isDefined(a)||a;var u,d=s(l),v=o.expandedAttribute;return d?c(e,l,o,function(e,t){u=e}):u=l,u&&t(u,o,function(e,t){e[v]=a}),r},r.collapse=function(e,t,i){return r.expand(e,t,i,!1)},r.collapseRecursive=function(e,t,i,n){return r.expandRecursive(e,t,i,!1)},r.expandTo=function(e,t,l,o){arguments.length>2&&"boolean"==typeof l&&(o=l,l={}),l=i.extend({},n,l),o=!i.isDefined(o)||o;var a=l.expandedAttribute,s=function(e){e[a]=o};return c(e,t,l,function(e,t){return i.forEach(t,s),r})},r.collapseParents=function(e,t,i){return r.expandTo(e,t,i,!1)},r}]),angular.module("ivh.treeview").provider("ivhTreeviewOptions",["ivhTreeviewInterpolateStartSymbol","ivhTreeviewInterpolateEndSymbol",function(e,t){"use strict";var i=e,n=t,r={idAttribute:"id",labelAttribute:"label",childrenAttribute:"children",selectedAttribute:"selected",expandToDepth:0,useCheckboxes:!0,disableCheckboxSelectionPropagation:!1,validate:!0,indeterminateAttribute:"__ivhTreeviewIndeterminate",expandedAttribute:"__ivhTreeviewExpanded",defaultSelectedState:!0,twistieExpandedTpl:"(-)",twistieCollapsedTpl:"(+)",twistieLeafTpl:"o",nodeTpl:['
',"",'',"",'","",'',"{{trvw.label(node)}}","","
","
"].join("\n").replace(new RegExp("{{","g"),i).replace(new RegExp("}}","g"),n)};this.set=function(e){angular.extend(r,e)},this.$get=function(){return function(){return angular.copy(r)}}}]); -------------------------------------------------------------------------------- /test/spec/directives/ivh-treeview.js: -------------------------------------------------------------------------------- 1 | /*global jasmine, jQuery, describe, beforeEach, afterEach, it, module, inject, expect */ 2 | 3 | describe('Directive ivhTreeview', function() { 4 | 'use strict'; 5 | 6 | var ng = angular 7 | , $ = jQuery; 8 | 9 | var scope, compile, exception; 10 | 11 | beforeEach(module('ivh.treeview')); 12 | 13 | beforeEach(function() { 14 | exception = undefined; 15 | }); 16 | 17 | var tplBasic = '
'; 18 | var tplValidate = '
'; 19 | var tplExpand = '
'; 20 | var tplExpandedAttr = '
'; 21 | var tplObjRoot = '
'; 22 | var tplOptions = '
'; 23 | var tplInlineTpls = '
'; 24 | var tplChildrenAttr = '
'; 25 | 26 | var tplFilter = [ 27 | '' 31 | ].join('\n'); 32 | 33 | var tplToggleHandler = [ 34 | '' 38 | ].join('\n'); 39 | 40 | var tplCbClickHandler = [ 41 | '' 45 | ].join('\n'); 46 | 47 | beforeEach(inject(function($rootScope, $compile) { 48 | scope = $rootScope.$new(); 49 | scope.bag1 = [{ 50 | label: 'top hat', 51 | children: [{ 52 | label: 'flat cap' 53 | }, { 54 | label: 'fedora', 55 | children: [ 56 | {label: 'gatsby'}, 57 | {label: 'gatsby 2'} 58 | ] 59 | }] 60 | }, { 61 | label: 'baseball', children: [] 62 | }]; 63 | 64 | compile = function(tpl, scp) { 65 | var $el = $compile(ng.element(tpl))(scp); 66 | scp.$apply(); 67 | return $el; 68 | }; 69 | })); 70 | 71 | afterEach(inject(function($timeout) { 72 | $timeout.verifyNoPendingTasks(); 73 | })); 74 | 75 | describe('basics', function() { 76 | var $el; 77 | 78 | it('should create a tree layout', function() { 79 | $el = compile(tplBasic, scope); 80 | expect($el.find('ul ul ul li').length).toBe(2); 81 | }); 82 | 83 | it('should add label titles to tree nodes', function() { 84 | $el = compile(tplBasic, scope); 85 | expect($el.find('[title="fedora"]').length).toBe(1); 86 | }); 87 | 88 | /** 89 | * @todo Collapsing/Expanding 90 | */ 91 | 92 | it('should allow expansion by default to a given depth', function() { 93 | $el = compile(tplExpand, scope); 94 | expect($el.find('[title="top hat"]').parent('li').hasClass('ivh-treeview-node-collapsed')).toBe(false); 95 | expect($el.find('[title="fedora"]').parent('li').hasClass('ivh-treeview-node-collapsed')).toBe(true); 96 | }); 97 | 98 | it('should honor inline expanded attribute declarations', function() { 99 | $el = compile(tplExpandedAttr, scope); 100 | var fedora = scope.bag1[0].children[1] 101 | , $fedora = $el.find('[title="fedora"]'); 102 | $fedora.find('[ivh-treeview-twistie]').click(); 103 | expect($fedora.parent().hasClass('ivh-treeview-node-collapsed')).toBe(false); 104 | expect(fedora.__ivhTreeviewExpanded).toBeUndefined(); 105 | expect(fedora.expanded).toBe(true); 106 | }); 107 | 108 | it('should allow roots objects', function() { 109 | $el = compile(tplObjRoot, scope); 110 | expect($el.find('ul').first().find('ul').length).toBe(2); 111 | }); 112 | 113 | it('should update indeterminate statuses', function() { 114 | $el = compile(tplBasic, scope); 115 | $el.find('[title="fedora"] input').first().click(); 116 | scope.$apply(); 117 | expect(scope.bag1[0].__ivhTreeviewIndeterminate).toBe(true); 118 | expect($el.find('input').first().prop('indeterminate')).toBe(true); 119 | 120 | // I've noticed that deselecting a child can leave ancestors up to root 121 | // unchecked and not-indeterminate when they should be. 122 | $el.find('[title="gatsby"] input').first().click(); 123 | scope.$apply(); 124 | expect($el.find('[title="fedora"] input').first().prop('indeterminate')).toBe(true); 125 | expect(scope.bag1[0].children[1].__ivhTreeviewIndeterminate).toBe(true); 126 | expect(scope.bag1[0].children[1].selected).toBe(false); 127 | }); 128 | 129 | it('should optionally validate the tree on creation', function() { 130 | scope.bag1[0].children[1].children[0].selected = false; 131 | $el = compile(tplValidate, scope); 132 | expect($el.find('[title="top hat"]').find('input').first().prop('indeterminate')).toBe(true); 133 | }); 134 | 135 | it('should update when child nodes are added (push)', function() { 136 | $el = compile(tplBasic, scope); 137 | scope.bag1[1].children.push({label: 'five panel baseball'}); 138 | scope.$apply(); 139 | expect($el.find('[title="five panel baseball"]').length).toBe(1); 140 | expect($el.find('[title="baseball"]').parent().hasClass('ivh-treeview-node-leaf')).toBe(false); 141 | }); 142 | 143 | it('should update when child nodes are added (re-assignment)', function() { 144 | $el = compile(tplBasic, scope); 145 | scope.bag1[1].children = [{label: 'five panel baseball'}]; 146 | scope.$apply(); 147 | expect($el.find('[title="five panel baseball"]').length).toBe(1); 148 | expect($el.find('[title="baseball"]').parent().hasClass('ivh-treeview-node-leaf')).toBe(false); 149 | }); 150 | 151 | it('should allow an options object for overrides', function() { 152 | scope.customOpts = { 153 | useCheckboxes: false, 154 | twistieCollapsedTpl: '[BOOM]' 155 | }; 156 | $el = compile(tplOptions, scope); 157 | expect($el.find('input[type="checkbox"]').length).toBe(0); 158 | expect($el.find('.ivh-treeview-twistie-collapsed').eq(0).text().trim()).toBe('[BOOM]'); 159 | }); 160 | 161 | it('should allow attribute level twistie templates', function() { 162 | $el = compile(tplInlineTpls, scope); 163 | expect($el.find('.ivh-treeview-twistie-collapsed').eq(0).text().trim()).toBe('[BOOM]'); 164 | }); 165 | 166 | it('should allow a custom child attribute', function() { 167 | scope.bag1 = [{ 168 | label: 'top hat', 169 | items: [{ 170 | label: 'flat cap' 171 | }, { 172 | label: 'fedora', 173 | items: [ 174 | {label: 'gatsby'}, 175 | {label: 'gatsby 2'} 176 | ] 177 | }] 178 | }, { 179 | label: 'baseball', items: [] 180 | }]; 181 | $el = compile(tplChildrenAttr, scope); 182 | expect($el.find('[title="gatsby"]').length > 0).toBe(true); 183 | }); 184 | }); 185 | 186 | describe('filtering', function() { 187 | var $el; 188 | 189 | beforeEach(function() { 190 | $el = compile(tplFilter, scope); 191 | scope.myFilter = 'baseball'; 192 | scope.$apply(); 193 | }); 194 | 195 | it('should hide filtered out nodes', function() { 196 | expect($el.find('[title="top hat"]').is(':visible')).toBe(false); 197 | 198 | /** 199 | * @todo Why does this fail? 200 | * Elements are not in DOM 201 | */ 202 | //expect($el.find('[title="baseball"]').is(':visible')).toBe(true); 203 | }); 204 | 205 | describe('object filtering', function() { 206 | beforeEach(function() { 207 | $el = compile(tplFilter, scope); 208 | scope.myFilter = {label: 'fedora'}; 209 | scope.$apply(); 210 | }); 211 | 212 | it('should hide filtered out nodes', function() { 213 | expect($el.find('[title="baseball"]').closest('.ng-hide').length > 0).toBe(true); 214 | }); 215 | 216 | it('should show parent nodes', function() { 217 | expect($el.find('[title="top hat"]').closest('.ng-hide').length > 0).toBe(false); 218 | }); 219 | 220 | it('should show filtered nodes', function() { 221 | expect($el.find('[title="fedora"]').closest('.ng-hide').length > 0).toBe(false); 222 | }); 223 | 224 | it('should hide filtered out child nodes', function() { 225 | expect($el.find('[title="gatsby"]').closest('.ng-hide').length > 0).toBe(true); 226 | }); 227 | }); 228 | 229 | describe('function filtering', function() { 230 | beforeEach(function() { 231 | $el = compile(tplFilter, scope); 232 | scope.myFilter = function (item) { 233 | return item.label === 'fedora'; 234 | }; 235 | scope.$apply(); 236 | }); 237 | 238 | it('should hide filtered out nodes', function() { 239 | expect($el.find('[title="baseball"]').closest('.ng-hide').length > 0).toBe(true); 240 | }); 241 | 242 | it('should show parent nodes', function() { 243 | expect($el.find('[title="top hat"]').closest('.ng-hide').length > 0).toBe(false); 244 | }); 245 | 246 | it('should show filtered nodes', function() { 247 | expect($el.find('[title="fedora"]').closest('.ng-hide').length > 0).toBe(false); 248 | }); 249 | 250 | it('should hide filtered out child nodes', function() { 251 | expect($el.find('[title="gatsby"]').closest('.ng-hide').length > 0).toBe(true); 252 | }); 253 | }); 254 | }); 255 | 256 | describe('toggle handlers', function() { 257 | var $el, handlerSpy; 258 | 259 | beforeEach(function() { 260 | handlerSpy = jasmine.createSpy('handlerSpy'); 261 | scope.onNodeToggle = handlerSpy; 262 | $el = compile(tplToggleHandler, scope); 263 | }); 264 | 265 | it('should call the toggle handler once per click', function() { 266 | $el.find('[title="top hat"] [ivh-treeview-toggle]').first().click(); 267 | scope.$apply(); 268 | expect(handlerSpy.calls.count()).toEqual(1); 269 | }); 270 | 271 | it('should not call the toggle handler when a leaf is clicked', function() { 272 | $el.find('[title="gatsby"] [ivh-treeview-toggle]').first().click(); 273 | scope.$apply(); 274 | expect(handlerSpy.calls.count()).toEqual(0); 275 | }); 276 | 277 | it('should pass the clicked node to the handler', function() { 278 | $el.find('[title="top hat"] [ivh-treeview-toggle]').first().click(); 279 | scope.$apply(); 280 | expect(handlerSpy.calls.mostRecent().args[0]).toBe(scope.bag1[0]); 281 | }); 282 | 283 | it('should pass the expanded state to the change handler', function() { 284 | $el.find('[title="top hat"] [ivh-treeview-toggle]').first().click(); 285 | scope.$apply(); 286 | expect(handlerSpy.calls.mostRecent().args[1]).toBe(true); 287 | }); 288 | 289 | it('should pass the tree itself to the toggle handler', function() { 290 | $el.find('[title="top hat"] [ivh-treeview-toggle]').click(); 291 | scope.$apply(); 292 | expect(handlerSpy.calls.mostRecent().args[2]).toBe(scope.bag1); 293 | }); 294 | 295 | it('should not generate an error when there is no handler', function() { 296 | delete scope.onNodeToggle; 297 | var exception; 298 | $el = compile(tplToggleHandler, scope); 299 | try { 300 | $el.find('[title="top hat"] [ivh-treeview-toggle]').click(); 301 | } catch(_exception) { 302 | exception = _exception; 303 | } 304 | expect(exception).toBeUndefined(); 305 | }); 306 | 307 | it('should pass the clicked node and tree to the callback via an object when registered through an options hash', function() { 308 | scope.opts = { 309 | onToggle: handlerSpy 310 | }; 311 | var tpl = '
'; 312 | 313 | $el = compile(tpl, scope); 314 | $el.find('[title="top hat"] [ivh-treeview-toggle]').first().click(); 315 | scope.$apply(); 316 | 317 | expect(handlerSpy.calls.mostRecent().args[0]).toEqual({ 318 | ivhNode: scope.bag1[0], 319 | ivhIsExpanded: true, 320 | ivhTree: scope.bag1 321 | }); 322 | }); 323 | }); 324 | 325 | describe('checkbox click handlers', function() { 326 | var $el, handlerSpy; 327 | 328 | beforeEach(function() { 329 | handlerSpy = jasmine.createSpy('handlerSpy'); 330 | scope.onCbChange = handlerSpy; 331 | $el = compile(tplCbClickHandler, scope); 332 | }); 333 | 334 | it('should call the change handler when checkbox state is changed', function() { 335 | $el.find('[title="top hat"] [type=checkbox]').first().click(); 336 | scope.$apply(); 337 | expect(handlerSpy.calls.count()).toEqual(1); 338 | }); 339 | 340 | it('should pass the selected node to the handler', function() { 341 | $el.find('[title="top hat"] [type=checkbox]').first().click(); 342 | scope.$apply(); 343 | expect(handlerSpy.calls.mostRecent().args[0]).toBe(scope.bag1[0]); 344 | }); 345 | 346 | it('should pass the checkbox state to the change handler', function() { 347 | var $cb = $el.find('[title="top hat"] [type=checkbox]').first(); 348 | $cb.click(); 349 | scope.$apply(); 350 | expect(handlerSpy.calls.mostRecent().args[1]).toBe($cb.prop('checked')); 351 | }); 352 | 353 | it('should pass the tree itself to the change handler', function() { 354 | $el.find('[title="top hat"] [type=checkbox]').first().click(); 355 | scope.$apply(); 356 | expect(handlerSpy.calls.mostRecent().args[2]).toBe(scope.bag1); 357 | }); 358 | 359 | it('should not generate an error when there is no handler', function() { 360 | delete scope.onCbChange; 361 | var exception; 362 | $el = compile(tplCbClickHandler, scope); 363 | try { 364 | $el.find('[title="top hat"] [type=checkbox]').click(); 365 | } catch(_exception) { 366 | exception = _exception; 367 | } 368 | expect(exception).toBeUndefined(); 369 | }); 370 | 371 | it('should pass the clicked node, selected state, and tree to the callback via an object when registered through an options hash', function() { 372 | scope.opts = { 373 | onCbChange: handlerSpy 374 | }; 375 | var tpl = '
'; 376 | 377 | $el = compile(tpl, scope); 378 | $el.find('[title="top hat"] [type=checkbox]').first().click(); 379 | scope.$apply(); 380 | 381 | expect(handlerSpy.calls.mostRecent().args[0]).toEqual({ 382 | ivhNode: scope.bag1[0], 383 | ivhIsSelected: jasmine.any(Boolean), 384 | ivhTree: scope.bag1 385 | }); 386 | }); 387 | }); 388 | 389 | }); 390 | -------------------------------------------------------------------------------- /src/scripts/directives/ivh-treeview.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The `ivh-treeview` directive 4 | * 5 | * A filterable tree view with checkbox support. 6 | * 7 | * Example: 8 | * 9 | * ``` 10 | *
12 | * ivh-treeview-filter="myFilterText"> 13 | *
14 | * ``` 15 | * 16 | * @package ivh.treeview 17 | * @copyright 2014 iVantage Health Analytics, Inc. 18 | */ 19 | 20 | angular.module('ivh.treeview').directive('ivhTreeview', ['ivhTreeviewMgr', function(ivhTreeviewMgr) { 21 | 'use strict'; 22 | return { 23 | restrict: 'A', 24 | transclude: true, 25 | scope: { 26 | // The tree data store 27 | root: '=ivhTreeview', 28 | 29 | // Specific config options 30 | childrenAttribute: '=ivhTreeviewChildrenAttribute', 31 | defaultSelectedState: '=ivhTreeviewDefaultSelectedState', 32 | disableCheckboxSelectionPropagation: '=ivhTreeviewDisableCheckboxSelectionPropagation', 33 | expandToDepth: '=ivhTreeviewExpandToDepth', 34 | idAttribute: '=ivhTreeviewIdAttribute', 35 | indeterminateAttribute: '=ivhTreeviewIndeterminateAttribute', 36 | expandedAttribute: '=ivhTreeviewExpandedAttribute', 37 | labelAttribute: '=ivhTreeviewLabelAttribute', 38 | nodeTpl: '=ivhTreeviewNodeTpl', 39 | selectedAttribute: '=ivhTreeviewSelectedAttribute', 40 | onCbChange: '&ivhTreeviewOnCbChange', 41 | onToggle: '&ivhTreeviewOnToggle', 42 | twistieCollapsedTpl: '=ivhTreeviewTwistieCollapsedTpl', 43 | twistieExpandedTpl: '=ivhTreeviewTwistieExpandedTpl', 44 | twistieLeafTpl: '=ivhTreeviewTwistieLeafTpl', 45 | useCheckboxes: '=ivhTreeviewUseCheckboxes', 46 | validate: '=ivhTreeviewValidate', 47 | visibleAttribute: '=ivhTreeviewVisibleAttribute', 48 | 49 | // Generic options object 50 | userOptions: '=ivhTreeviewOptions', 51 | 52 | // The filter 53 | filter: '=ivhTreeviewFilter' 54 | }, 55 | controllerAs: 'trvw', 56 | controller: ['$scope', '$element', '$attrs', '$transclude', 'ivhTreeviewOptions', 'filterFilter', function($scope, $element, $attrs, $transclude, ivhTreeviewOptions, filterFilter) { 57 | var ng = angular 58 | , trvw = this; 59 | 60 | // Merge any locally set options with those registered with hte 61 | // ivhTreeviewOptions provider 62 | var localOpts = ng.extend({}, ivhTreeviewOptions(), $scope.userOptions); 63 | 64 | // Two-way bound attributes (=) can be copied over directly if they're 65 | // non-empty 66 | ng.forEach([ 67 | 'childrenAttribute', 68 | 'defaultSelectedState', 69 | 'disableCheckboxSelectionPropagation', 70 | 'expandToDepth', 71 | 'idAttribute', 72 | 'indeterminateAttribute', 73 | 'expandedAttribute', 74 | 'labelAttribute', 75 | 'nodeTpl', 76 | 'selectedAttribute', 77 | 'twistieCollapsedTpl', 78 | 'twistieExpandedTpl', 79 | 'twistieLeafTpl', 80 | 'useCheckboxes', 81 | 'validate', 82 | 'visibleAttribute' 83 | ], function(attr) { 84 | if(ng.isDefined($scope[attr])) { 85 | localOpts[attr] = $scope[attr]; 86 | } 87 | }); 88 | 89 | // Attrs with the `&` prefix will yield a defined scope entity even if 90 | // no value is specified. We must check to make sure the attribute string 91 | // is non-empty before copying over the scope value. 92 | var normedAttr = function(attrKey) { 93 | return 'ivhTreeview' + 94 | attrKey.charAt(0).toUpperCase() + 95 | attrKey.slice(1); 96 | }; 97 | 98 | ng.forEach([ 99 | 'onCbChange', 100 | 'onToggle' 101 | ], function(attr) { 102 | if($attrs[normedAttr(attr)]) { 103 | localOpts[attr] = $scope[attr]; 104 | } 105 | }); 106 | 107 | // Treat the transcluded content (if there is any) as our node template 108 | var transcludedScope; 109 | $transclude(function(clone, scope) { 110 | var transcludedNodeTpl = ''; 111 | angular.forEach(clone, function(c) { 112 | transcludedNodeTpl += (c.innerHTML || '').trim(); 113 | }); 114 | if(transcludedNodeTpl.length) { 115 | transcludedScope = scope; 116 | localOpts.nodeTpl = transcludedNodeTpl; 117 | } 118 | }); 119 | 120 | /** 121 | * Get the merged global and local options 122 | * 123 | * @return {Object} the merged options 124 | */ 125 | trvw.opts = function() { 126 | return localOpts; 127 | }; 128 | 129 | // If we didn't provide twistie templates we'll be doing a fair bit of 130 | // extra checks for no reason. Let's just inform down stream directives 131 | // whether or not they need to worry about twistie non-global templates. 132 | var userOpts = $scope.userOptions || {}; 133 | 134 | /** 135 | * Whether or not we have local twistie templates 136 | * 137 | * @private 138 | */ 139 | trvw.hasLocalTwistieTpls = !!( 140 | userOpts.twistieCollapsedTpl || 141 | userOpts.twistieExpandedTpl || 142 | userOpts.twistieLeafTpl || 143 | $scope.twistieCollapsedTpl || 144 | $scope.twistieExpandedTpl || 145 | $scope.twistieLeafTpl); 146 | 147 | /** 148 | * Get the child nodes for `node` 149 | * 150 | * Abstracts away the need to know the actual label attribute in 151 | * templates. 152 | * 153 | * @param {Object} node a tree node 154 | * @return {Array} the child nodes 155 | */ 156 | trvw.children = function(node) { 157 | var children = node[localOpts.childrenAttribute]; 158 | return ng.isArray(children) ? children : []; 159 | }; 160 | 161 | /** 162 | * Get the label for `node` 163 | * 164 | * Abstracts away the need to know the actual label attribute in 165 | * templates. 166 | * 167 | * @param {Object} node A tree node 168 | * @return {String} The node label 169 | */ 170 | trvw.label = function(node) { 171 | return node[localOpts.labelAttribute]; 172 | }; 173 | 174 | /** 175 | * Returns `true` if this treeview has a filter 176 | * 177 | * @return {Boolean} Whether on not we have a filter 178 | * @private 179 | */ 180 | trvw.hasFilter = function() { 181 | return ng.isDefined($scope.filter); 182 | }; 183 | 184 | /** 185 | * Get the treeview filter 186 | * 187 | * @return {String} The filter string 188 | * @private 189 | */ 190 | trvw.getFilter = function() { 191 | return $scope.filter || ''; 192 | }; 193 | 194 | /** 195 | * Returns `true` if current filter should hide `node`, false otherwise 196 | * 197 | * @todo Note that for object and function filters each node gets hit with 198 | * `isVisible` N-times where N is its depth in the tree. We may be able to 199 | * optimize `isVisible` in this case by: 200 | * 201 | * - On first call to `isVisible` in a given digest cycle walk the tree to 202 | * build a flat array of nodes. 203 | * - Run the array of nodes through the filter. 204 | * - Build a map (`id`/$scopeId --> true) for the nodes that survive the 205 | * filter 206 | * - On subsequent calls to `isVisible` just lookup the node id in our 207 | * map. 208 | * - Clean the map with a $timeout (?) 209 | * 210 | * In theory the result of a call to `isVisible` could change during a 211 | * digest cycle as scope variables are updated... I think calls would 212 | * happen bottom up (i.e. from "leaf" to "root") so that might not 213 | * actually be an issue. Need to investigate if this ends up feeling for 214 | * large/deep trees. 215 | * 216 | * @param {Object} node A tree node 217 | * @return {Boolean} Whether or not `node` is filtered out 218 | */ 219 | trvw.isVisible = function(node) { 220 | var filter = trvw.getFilter(); 221 | 222 | // Quick shortcut 223 | if(!filter || filterFilter([node], filter).length) { 224 | return true; 225 | } 226 | 227 | // If we have an object or function filter we have to check children 228 | // separately 229 | if(typeof filter === 'object' || typeof filter === 'function') { 230 | var children = trvw.children(node); 231 | // If any child is visible then so is this node 232 | for(var ix = children.length; ix--;) { 233 | if(trvw.isVisible(children[ix])) { 234 | return true; 235 | } 236 | } 237 | } 238 | 239 | return false; 240 | }; 241 | 242 | /** 243 | * Returns `true` if we should use checkboxes, false otherwise 244 | * 245 | * @return {Boolean} Whether or not to use checkboxes 246 | */ 247 | trvw.useCheckboxes = function() { 248 | return localOpts.useCheckboxes; 249 | }; 250 | 251 | /** 252 | * Select or deselect `node` 253 | * 254 | * Updates parent and child nodes appropriately, `isSelected` defaults to 255 | * `true`. 256 | * 257 | * @param {Object} node The node to select or deselect 258 | * @param {Boolean} isSelected Defaults to `true` 259 | */ 260 | trvw.select = function(node, isSelected) { 261 | ivhTreeviewMgr.select($scope.root, node, localOpts, isSelected); 262 | trvw.onCbChange(node, isSelected); 263 | }; 264 | 265 | /** 266 | * Get the selected state of `node` 267 | * 268 | * @param {Object} node The node to get the selected state of 269 | * @return {Boolean} `true` if `node` is selected 270 | */ 271 | trvw.isSelected = function(node) { 272 | return node[localOpts.selectedAttribute]; 273 | }; 274 | 275 | /** 276 | * Toggle the selected state of `node` 277 | * 278 | * Updates parent and child node selected states appropriately. 279 | * 280 | * @param {Object} node The node to update 281 | */ 282 | trvw.toggleSelected = function(node) { 283 | var isSelected = !node[localOpts.selectedAttribute]; 284 | trvw.select(node, isSelected); 285 | }; 286 | 287 | /** 288 | * Expand or collapse a given node 289 | * 290 | * `isExpanded` is optional and defaults to `true`. 291 | * 292 | * @param {Object} node The node to expand/collapse 293 | * @param {Boolean} isExpanded Whether to expand (`true`) or collapse 294 | */ 295 | trvw.expand = function(node, isExpanded) { 296 | ivhTreeviewMgr.expand($scope.root, node, localOpts, isExpanded); 297 | }; 298 | 299 | /** 300 | * Get the expanded state of a given node 301 | * 302 | * @param {Object} node The node to check the expanded state of 303 | * @return {Boolean} 304 | */ 305 | trvw.isExpanded = function(node) { 306 | return node[localOpts.expandedAttribute]; 307 | }; 308 | 309 | /** 310 | * Toggle the expanded state of a given node 311 | * 312 | * @param {Object} node The node to toggle 313 | */ 314 | trvw.toggleExpanded = function(node) { 315 | trvw.expand(node, !trvw.isExpanded(node)); 316 | }; 317 | 318 | /** 319 | * Whether or not nodes at `depth` should be expanded by default 320 | * 321 | * Use -1 to fully expand the tree by default. 322 | * 323 | * @param {Integer} depth The depth to expand to 324 | * @return {Boolean} Whether or not nodes at `depth` should be expanded 325 | * @private 326 | */ 327 | trvw.isInitiallyExpanded = function(depth) { 328 | var expandTo = localOpts.expandToDepth === -1 ? 329 | Infinity : localOpts.expandToDepth; 330 | return depth < expandTo; 331 | }; 332 | 333 | /** 334 | * Returns `true` if `node` is a leaf node 335 | * 336 | * @param {Object} node The node to check 337 | * @return {Boolean} `true` if `node` is a leaf 338 | */ 339 | trvw.isLeaf = function(node) { 340 | return trvw.children(node).length === 0; 341 | }; 342 | 343 | /** 344 | * Get the tree node template 345 | * 346 | * @return {String} The node template 347 | * @private 348 | */ 349 | trvw.getNodeTpl = function() { 350 | return localOpts.nodeTpl; 351 | }; 352 | 353 | /** 354 | * Get the root of the tree 355 | * 356 | * Mostly a helper for custom templates 357 | * 358 | * @return {Object|Array} The tree root 359 | * @private 360 | */ 361 | trvw.root = function() { 362 | return $scope.root; 363 | }; 364 | 365 | /** 366 | * Call the registered toggle handler 367 | * 368 | * Handler will get a reference to `node` and the root of the tree. 369 | * 370 | * @param {Object} node Tree node to pass to the handler 371 | * @private 372 | */ 373 | trvw.onToggle = function(node) { 374 | if(localOpts.onToggle) { 375 | var locals = { 376 | ivhNode: node, 377 | ivhIsExpanded: trvw.isExpanded(node), 378 | ivhTree: $scope.root 379 | }; 380 | localOpts.onToggle(locals); 381 | } 382 | }; 383 | 384 | /** 385 | * Call the registered selection change handler 386 | * 387 | * Handler will get a reference to `node`, the new selected state of 388 | * `node, and the root of the tree. 389 | * 390 | * @param {Object} node Tree node to pass to the handler 391 | * @param {Boolean} isSelected Selected state for `node` 392 | * @private 393 | */ 394 | trvw.onCbChange = function(node, isSelected) { 395 | if(localOpts.onCbChange) { 396 | var locals = { 397 | ivhNode: node, 398 | ivhIsSelected: isSelected, 399 | ivhTree: $scope.root 400 | }; 401 | localOpts.onCbChange(locals); 402 | } 403 | }; 404 | }], 405 | link: function(scope, element, attrs) { 406 | var opts = scope.trvw.opts(); 407 | 408 | // Allow opt-in validate on startup 409 | if(opts.validate) { 410 | ivhTreeviewMgr.validate(scope.root, opts); 411 | } 412 | }, 413 | template: [ 414 | '
    ', 415 | '
  • ', 421 | '
  • ', 422 | '
' 423 | ].join('\n') 424 | }; 425 | }]); 426 | -------------------------------------------------------------------------------- /src/scripts/services/ivh-treeview-mgr.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Manager for treeview data stores 4 | * 5 | * Used to assist treeview operations, e.g. selecting or validating a tree-like 6 | * collection. 7 | * 8 | * @package ivh.treeview 9 | * @copyright 2014 iVantage Health Analytics, Inc. 10 | */ 11 | 12 | angular.module('ivh.treeview') 13 | .factory('ivhTreeviewMgr', ['ivhTreeviewOptions', 'ivhTreeviewBfs', function(ivhTreeviewOptions, ivhTreeviewBfs) { 14 | 'use strict'; 15 | 16 | var ng = angular 17 | , options = ivhTreeviewOptions() 18 | , exports = {}; 19 | 20 | // The make* methods and validateParent need to be bound to an options 21 | // object 22 | var makeDeselected = function(node) { 23 | node[this.selectedAttribute] = false; 24 | node[this.indeterminateAttribute] = false; 25 | }; 26 | 27 | var makeSelected = function(node) { 28 | node[this.selectedAttribute] = true; 29 | node[this.indeterminateAttribute] = false; 30 | }; 31 | 32 | var validateParent = function(node) { 33 | var children = node[this.childrenAttribute] 34 | , selectedAttr = this.selectedAttribute 35 | , indeterminateAttr = this.indeterminateAttribute 36 | , numSelected = 0 37 | , numIndeterminate = 0; 38 | ng.forEach(children, function(n, ix) { 39 | if(n[selectedAttr]) { 40 | numSelected++; 41 | } else { 42 | if(n[indeterminateAttr]) { 43 | numIndeterminate++; 44 | } 45 | } 46 | }); 47 | 48 | if(0 === numSelected && 0 === numIndeterminate) { 49 | node[selectedAttr] = false; 50 | node[indeterminateAttr] = false; 51 | } else if(numSelected === children.length) { 52 | node[selectedAttr] = true; 53 | node[indeterminateAttr] = false; 54 | } else { 55 | node[selectedAttr] = false; 56 | node[indeterminateAttr] = true; 57 | } 58 | }; 59 | 60 | var isId = function(val) { 61 | return ng.isString(val) || ng.isNumber(val); 62 | }; 63 | 64 | var findNode = function(tree, node, opts, cb) { 65 | var useId = isId(node) 66 | , proceed = true 67 | , idAttr = opts.idAttribute; 68 | 69 | // Our return values 70 | var foundNode = null 71 | , foundParents = []; 72 | 73 | ivhTreeviewBfs(tree, opts, function(n, p) { 74 | var isNode = proceed && (useId ? 75 | node === n[idAttr] : 76 | node === n); 77 | 78 | if(isNode) { 79 | // I've been looking for you all my life 80 | proceed = false; 81 | foundNode = n; 82 | foundParents = p; 83 | } 84 | 85 | return proceed; 86 | }); 87 | 88 | return cb(foundNode, foundParents); 89 | }; 90 | 91 | /** 92 | * Select (or deselect) a tree node 93 | * 94 | * This method will update the rest of the tree to account for your change. 95 | * 96 | * You may alternatively pass an id as `node`, in which case the tree will 97 | * be searched for your item. 98 | * 99 | * @param {Object|Array} tree The tree data 100 | * @param {Object|String} node The node (or id) to (de)select 101 | * @param {Object} opts [optional] Options to override default options with 102 | * @param {Boolean} isSelected [optional] Whether or not to select `node`, defaults to `true` 103 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 104 | */ 105 | exports.select = function(tree, node, opts, isSelected) { 106 | if(arguments.length > 2) { 107 | if(typeof opts === 'boolean') { 108 | isSelected = opts; 109 | opts = {}; 110 | } 111 | } 112 | opts = ng.extend({}, options, opts); 113 | isSelected = ng.isDefined(isSelected) ? isSelected : true; 114 | 115 | var useId = isId(node) 116 | , proceed = true 117 | , idAttr = opts.idAttribute; 118 | 119 | ivhTreeviewBfs(tree, opts, function(n, p) { 120 | var isNode = proceed && (useId ? 121 | node === n[idAttr] : 122 | node === n); 123 | 124 | if(isNode) { 125 | // I've been looking for you all my life 126 | proceed = false; 127 | 128 | var cb = isSelected ? 129 | makeSelected.bind(opts) : 130 | makeDeselected.bind(opts); 131 | 132 | if (opts.disableCheckboxSelectionPropagation) { 133 | cb(n); 134 | } else { 135 | ivhTreeviewBfs(n, opts, cb); 136 | ng.forEach(p, validateParent.bind(opts)); 137 | } 138 | } 139 | 140 | return proceed; 141 | }); 142 | 143 | return exports; 144 | }; 145 | 146 | /** 147 | * Select all nodes in a tree 148 | * 149 | * `opts` will default to an empty object, `isSelected` defaults to `true`. 150 | * 151 | * @param {Object|Array} tree The tree data 152 | * @param {Object} opts [optional] Default options overrides 153 | * @param {Boolean} isSelected [optional] Whether or not to select items 154 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 155 | */ 156 | exports.selectAll = function(tree, opts, isSelected) { 157 | if(arguments.length > 1) { 158 | if(typeof opts === 'boolean') { 159 | isSelected = opts; 160 | opts = {}; 161 | } 162 | } 163 | 164 | opts = ng.extend({}, options, opts); 165 | isSelected = ng.isDefined(isSelected) ? isSelected : true; 166 | 167 | var selectedAttr = opts.selectedAttribute 168 | , indeterminateAttr = opts.indeterminateAttribute; 169 | 170 | ivhTreeviewBfs(tree, opts, function(node) { 171 | node[selectedAttr] = isSelected; 172 | node[indeterminateAttr] = false; 173 | }); 174 | 175 | return exports; 176 | }; 177 | 178 | /** 179 | * Select or deselect each of the passed items 180 | * 181 | * Eventually it would be nice if this did something more intelligent than 182 | * just calling `select` on each item in the array... 183 | * 184 | * @param {Object|Array} tree The tree data 185 | * @param {Array} nodes The array of nodes or node ids 186 | * @param {Object} opts [optional] Default options overrides 187 | * @param {Boolean} isSelected [optional] Whether or not to select items 188 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 189 | */ 190 | exports.selectEach = function(tree, nodes, opts, isSelected) { 191 | /** 192 | * @todo Surely we can do something better than this... 193 | */ 194 | ng.forEach(nodes, function(node) { 195 | exports.select(tree, node, opts, isSelected); 196 | }); 197 | return exports; 198 | }; 199 | 200 | /** 201 | * Deselect a tree node 202 | * 203 | * Delegates to `ivhTreeviewMgr.select` with `isSelected` set to `false`. 204 | * 205 | * @param {Object|Array} tree The tree data 206 | * @param {Object|String} node The node (or id) to (de)select 207 | * @param {Object} opts [optional] Options to override default options with 208 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 209 | */ 210 | exports.deselect = function(tree, node, opts) { 211 | return exports.select(tree, node, opts, false); 212 | }; 213 | 214 | /** 215 | * Deselect all nodes in a tree 216 | * 217 | * Delegates to `ivhTreeviewMgr.selectAll` with `isSelected` set to `false`. 218 | * 219 | * @param {Object|Array} tree The tree data 220 | * @param {Object} opts [optional] Default options overrides 221 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 222 | */ 223 | exports.deselectAll = function(tree, opts) { 224 | return exports.selectAll(tree, opts, false); 225 | }; 226 | 227 | /** 228 | * Deselect each of the passed items 229 | * 230 | * Delegates to `ivhTreeviewMgr.selectEach` with `isSelected` set to 231 | * `false`. 232 | * 233 | * @param {Object|Array} tree The tree data 234 | * @param {Array} nodes The array of nodes or node ids 235 | * @param {Object} opts [optional] Default options overrides 236 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 237 | */ 238 | exports.deselectEach = function(tree, nodes, opts) { 239 | return exports.selectEach(tree, nodes, opts, false); 240 | }; 241 | 242 | /** 243 | * Validate tree for parent/child selection consistency 244 | * 245 | * Assumes `bias` as default selected state. The first element with 246 | * `node.select !== bias` will be assumed correct. For example, if `bias` is 247 | * `true` (the default) we'll traverse the tree until we come to an 248 | * unselected node at which point we stop and deselect each of that node's 249 | * children (and their children, etc.). 250 | * 251 | * Indeterminate states will also be resolved. 252 | * 253 | * @param {Object|Array} tree The tree data 254 | * @param {Object} opts [optional] Options to override default options with 255 | * @param {Boolean} bias [optional] Default selected state 256 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 257 | */ 258 | exports.validate = function(tree, opts, bias) { 259 | if(!tree) { 260 | // Guard against uninitialized trees 261 | return exports; 262 | } 263 | 264 | if(arguments.length > 1) { 265 | if(typeof opts === 'boolean') { 266 | bias = opts; 267 | opts = {}; 268 | } 269 | } 270 | opts = ng.extend({}, options, opts); 271 | bias = ng.isDefined(bias) ? bias : opts.defaultSelectedState; 272 | 273 | var selectedAttr = opts.selectedAttribute 274 | , indeterminateAttr = opts.indeterminateAttribute; 275 | 276 | ivhTreeviewBfs(tree, opts, function(node, parents) { 277 | if(ng.isDefined(node[selectedAttr]) && node[selectedAttr] !== bias) { 278 | exports.select(tree, node, opts, !bias); 279 | return false; 280 | } else { 281 | node[selectedAttr] = bias; 282 | node[indeterminateAttr] = false; 283 | } 284 | }); 285 | 286 | return exports; 287 | }; 288 | 289 | /** 290 | * Expand/collapse a given tree node 291 | * 292 | * `node` may be either an actual tree node object or a node id. 293 | * 294 | * `opts` may override any of the defaults set by `ivhTreeviewOptions`. 295 | * 296 | * @param {Object|Array} tree The tree data 297 | * @param {Object|String} node The node (or id) to expand/collapse 298 | * @param {Object} opts [optional] Options to override default options with 299 | * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true` 300 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 301 | */ 302 | exports.expand = function(tree, node, opts, isExpanded) { 303 | if(arguments.length > 2) { 304 | if(typeof opts === 'boolean') { 305 | isExpanded = opts; 306 | opts = {}; 307 | } 308 | } 309 | opts = ng.extend({}, options, opts); 310 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true; 311 | 312 | var useId = isId(node) 313 | , expandedAttr = opts.expandedAttribute; 314 | 315 | if(!useId) { 316 | // No need to do any searching if we already have the node in hand 317 | node[expandedAttr] = isExpanded; 318 | return exports; 319 | } 320 | 321 | return findNode(tree, node, opts, function(n, p) { 322 | n[expandedAttr] = isExpanded; 323 | return exports; 324 | }); 325 | }; 326 | 327 | /** 328 | * Expand/collapse a given tree node and its children 329 | * 330 | * `node` may be either an actual tree node object or a node id. You may 331 | * leave off `node` entirely to expand/collapse the entire tree, however, if 332 | * you specify a value for `opts` or `isExpanded` you must provide a value 333 | * for `node`. 334 | * 335 | * `opts` may override any of the defaults set by `ivhTreeviewOptions`. 336 | * 337 | * @param {Object|Array} tree The tree data 338 | * @param {Object|String} node [optional*] The node (or id) to expand/collapse recursively 339 | * @param {Object} opts [optional] Options to override default options with 340 | * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true` 341 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 342 | */ 343 | exports.expandRecursive = function(tree, node, opts, isExpanded) { 344 | if(arguments.length > 2) { 345 | if(typeof opts === 'boolean') { 346 | isExpanded = opts; 347 | opts = {}; 348 | } 349 | } 350 | node = ng.isDefined(node) ? node : tree; 351 | opts = ng.extend({}, options, opts); 352 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true; 353 | 354 | var useId = isId(node) 355 | , expandedAttr = opts.expandedAttribute 356 | , branch; 357 | 358 | // If we have an ID first resolve it to an actual node in the tree 359 | if(useId) { 360 | findNode(tree, node, opts, function(n, p) { 361 | branch = n; 362 | }); 363 | } else { 364 | branch = node; 365 | } 366 | 367 | if(branch) { 368 | ivhTreeviewBfs(branch, opts, function(n, p) { 369 | n[expandedAttr] = isExpanded; 370 | }); 371 | } 372 | 373 | return exports; 374 | }; 375 | 376 | /** 377 | * Collapse a given tree node 378 | * 379 | * Delegates to `exports.expand` with `isExpanded` set to `false`. 380 | * 381 | * @param {Object|Array} tree The tree data 382 | * @param {Object|String} node The node (or id) to collapse 383 | * @param {Object} opts [optional] Options to override default options with 384 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 385 | */ 386 | exports.collapse = function(tree, node, opts) { 387 | return exports.expand(tree, node, opts, false); 388 | }; 389 | 390 | /** 391 | * Collapse a given tree node and its children 392 | * 393 | * Delegates to `exports.expandRecursive` with `isExpanded` set to `false`. 394 | * 395 | * @param {Object|Array} tree The tree data 396 | * @param {Object|String} node The node (or id) to expand/collapse recursively 397 | * @param {Object} opts [optional] Options to override default options with 398 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 399 | */ 400 | exports.collapseRecursive = function(tree, node, opts, isExpanded) { 401 | return exports.expandRecursive(tree, node, opts, false); 402 | }; 403 | 404 | /** 405 | * Expand[/collapse] all parents of a given node, i.e. "reveal" the node 406 | * 407 | * @param {Object|Array} tree The tree data 408 | * @param {Object|String} node The node (or id) to expand to 409 | * @param {Object} opts [optional] Options to override default options with 410 | * @param {Boolean} isExpanded [optional] Whether or not to expand parent nodes 411 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 412 | */ 413 | exports.expandTo = function(tree, node, opts, isExpanded) { 414 | if(arguments.length > 2) { 415 | if(typeof opts === 'boolean') { 416 | isExpanded = opts; 417 | opts = {}; 418 | } 419 | } 420 | opts = ng.extend({}, options, opts); 421 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true; 422 | 423 | var expandedAttr = opts.expandedAttribute; 424 | 425 | var expandCollapseNode = function(n) { 426 | n[expandedAttr] = isExpanded; 427 | }; 428 | 429 | // Even if wer were given the actual node and not its ID we must still 430 | // traverse the tree to find that node's parents. 431 | return findNode(tree, node, opts, function(n, p) { 432 | ng.forEach(p, expandCollapseNode); 433 | return exports; 434 | }); 435 | }; 436 | 437 | /** 438 | * Collapse all parents of a give node 439 | * 440 | * Delegates to `exports.expandTo` with `isExpanded` set to `false`. 441 | * 442 | * @param {Object|Array} tree The tree data 443 | * @param {Object|String} node The node (or id) to expand to 444 | * @param {Object} opts [optional] Options to override default options with 445 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 446 | */ 447 | exports.collapseParents = function(tree, node, opts) { 448 | return exports.expandTo(tree, node, opts, false); 449 | }; 450 | 451 | return exports; 452 | } 453 | ]); 454 | -------------------------------------------------------------------------------- /test/spec/services/ivh-treeview-mgr.js: -------------------------------------------------------------------------------- 1 | /*global jQuery, describe, beforeEach, afterEach, it, module, inject, expect */ 2 | 3 | describe('Service: ivhTreeviewMgr', function() { 4 | 'use strict'; 5 | 6 | beforeEach(module('ivh.treeview')); 7 | 8 | var ivhTreeviewMgr; 9 | 10 | var tree 11 | , nodes 12 | , hatNodes 13 | , bagNodes 14 | , stuff 15 | , hats 16 | , fedora 17 | , flatcap 18 | , bags 19 | , messenger 20 | , backpack; 21 | 22 | beforeEach(inject(function(_ivhTreeviewMgr_) { 23 | ivhTreeviewMgr = _ivhTreeviewMgr_; 24 | })); 25 | 26 | beforeEach(function() { 27 | tree = [{ 28 | label: 'Stuff', 29 | id: 'stuff', 30 | children: [{ 31 | label: 'Hats', 32 | id: 'hats', 33 | children: [{ 34 | label: 'Fedora', 35 | id: 'fedora' 36 | }, { 37 | label: 'Flatcap', 38 | id: 'flatcap' 39 | }] 40 | }, { 41 | label: 'Bags', 42 | id: 'bags', 43 | children: [{ 44 | label: 'Messenger', 45 | id: 'messenger' 46 | }, { 47 | label: 'Backpack', 48 | id: 'backpack' 49 | }] 50 | }] 51 | }]; 52 | 53 | stuff = tree[0]; 54 | hats = stuff.children[0]; 55 | bags = stuff.children[1]; 56 | fedora = hats.children[0]; 57 | flatcap = hats.children[1]; 58 | messenger = bags.children[0]; 59 | backpack = bags.children[1]; 60 | 61 | nodes = [hats, bags, fedora, flatcap, messenger, backpack]; 62 | hatNodes = [hats, fedora, flatcap]; 63 | bagNodes = [bags, messenger, backpack]; 64 | }); 65 | 66 | describe('#select', function() { 67 | 68 | it('should select all child nodes', function() { 69 | ivhTreeviewMgr.select(tree, hats); 70 | expect(fedora.selected).toBe(true); 71 | expect(flatcap.selected).toBe(true); 72 | }); 73 | 74 | it('should select nodes by id', function() { 75 | ivhTreeviewMgr.select(tree, 'hats'); 76 | expect(fedora.selected).toBe(true); 77 | expect(flatcap.selected).toBe(true); 78 | }); 79 | 80 | it('should allow numeric ids', function() { 81 | var t = {id: 1, label: 'One'}; 82 | ivhTreeviewMgr.select(t, 1); 83 | expect(t.selected).toBe(true); 84 | }); 85 | 86 | it('should make parents indeterminate if there are unselected siblings', function() { 87 | ivhTreeviewMgr.select(tree, fedora); 88 | expect(stuff.__ivhTreeviewIndeterminate).toBe(true); 89 | expect(stuff.selected).toBe(false); // Indeterminte nodes are not selected 90 | expect(hats.__ivhTreeviewIndeterminate).toBe(true); 91 | expect(hats.selected).toBe(false); // Indeterminte nodes are not selected 92 | }); 93 | 94 | }); 95 | 96 | describe('#select (disabled propagation)', function() { 97 | 98 | it('should not affect child nodes', function() { 99 | var options = { 100 | disableCheckboxSelectionPropagation: true 101 | }; 102 | ivhTreeviewMgr.select(tree, hats, options); 103 | expect(fedora.selected).toBeFalsy(); 104 | expect(flatcap.selected).toBeFalsy(); 105 | }); 106 | 107 | it('should NOT affect parents (indeterminate state is not used at all)', function() { 108 | var options = { 109 | disableCheckboxSelectionPropagation: true 110 | }; 111 | ivhTreeviewMgr.select(tree, fedora, options); 112 | 113 | // Indeterminte state is not handled with disableCheckboxSelectionPropagation option set 114 | expect(stuff.__ivhTreeviewIndeterminate).toBeFalsy(); 115 | expect(stuff.selected).toBeFalsy(); 116 | expect(hats.__ivhTreeviewIndeterminate).toBeFalsy(); 117 | expect(hats.selected).toBeFalsy(); 118 | }); 119 | 120 | }); 121 | 122 | describe('#selectAll', function() { 123 | 124 | it('should select all nodes in a tree', function() { 125 | ivhTreeviewMgr.selectAll(tree); 126 | nodes.forEach(function(n) { 127 | expect(n.selected).toBe(true); 128 | expect(n.__ivhTreeviewIndeterminate).toBe(false); 129 | }); 130 | }); 131 | 132 | }); 133 | 134 | describe('#selectEach', function() { 135 | 136 | it('should select with an array of node references', function() { 137 | ivhTreeviewMgr.selectEach(tree, [flatcap, bags]); 138 | [flatcap, bags, messenger, backpack].forEach(function(n) { 139 | expect(n.selected).toBe(true); 140 | }); 141 | [stuff, hats, fedora].forEach(function(n) { 142 | expect(n.selected).not.toBe(true); 143 | }); 144 | }); 145 | 146 | it('should select with an array of node ids', function() { 147 | ivhTreeviewMgr.selectEach(tree, ['flatcap', 'bags']); 148 | [flatcap, bags, messenger, backpack].forEach(function(n) { 149 | expect(n.selected).toBe(true); 150 | }); 151 | [stuff, hats, fedora].forEach(function(n) { 152 | expect(n.selected).not.toBe(true); 153 | }); 154 | }); 155 | 156 | }); 157 | 158 | describe('#deselect', function() { 159 | 160 | beforeEach(function() { 161 | angular.forEach(nodes, function(n) { 162 | n.selected = true; 163 | }); 164 | }); 165 | 166 | it('should deselect all child nodes', function() { 167 | ivhTreeviewMgr.deselect(tree, hats); 168 | expect(fedora.selected).toBe(false); 169 | expect(flatcap.selected).toBe(false); 170 | }); 171 | 172 | it('should make parents indeterminate if there are selected siblings', function() { 173 | ivhTreeviewMgr.deselect(tree, hats); 174 | expect(stuff.__ivhTreeviewIndeterminate).toBe(true); 175 | expect(stuff.selected).toBe(false); // Indeterminte nodes are not selected 176 | }); 177 | 178 | }); 179 | 180 | describe('#deselect (disabled propagation)', function() { 181 | 182 | beforeEach(function() { 183 | angular.forEach(nodes, function(n) { 184 | n.selected = true; 185 | }); 186 | stuff.selected = true; 187 | }); 188 | 189 | var options = { 190 | disableCheckboxSelectionPropagation: true 191 | }; 192 | 193 | it('should deselect only hats, child nodes remain selected', function() { 194 | ivhTreeviewMgr.deselect(tree, hats, options); 195 | expect(hats.selected).toBe(false); 196 | expect(fedora.selected).toBe(true); 197 | expect(flatcap.selected).toBe(true); 198 | }); 199 | 200 | it('should not affect parents state', function() { 201 | ivhTreeviewMgr.deselect(tree, hats, options); 202 | expect(stuff.selected).toBe(true); 203 | }); 204 | 205 | }); 206 | 207 | describe('#deselectAll', function() { 208 | beforeEach(function() { 209 | nodes.forEach(function(n) { 210 | n.selected = true; 211 | }); 212 | }); 213 | 214 | it('should deselect all nodes in a tree', function() { 215 | ivhTreeviewMgr.deselectAll(tree); 216 | nodes.forEach(function(n) { 217 | expect(n.selected).toBe(false); 218 | expect(n.__ivhTreeviewIndeterminate).toBe(false); 219 | }); 220 | }); 221 | 222 | }); 223 | 224 | describe('#deselectEach', function() { 225 | beforeEach(function() { 226 | angular.forEach(nodes, function(n) { 227 | n.selected = true; 228 | }); 229 | }); 230 | 231 | it('should deselect with an array of node references', function() { 232 | ivhTreeviewMgr.deselectEach(tree, [flatcap, bags]); 233 | [stuff, hats, flatcap, bags, messenger, backpack].forEach(function(n) { 234 | expect(n.selected).toBe(false); 235 | }); 236 | [fedora].forEach(function(n) { 237 | expect(n.selected).toBe(true); 238 | }); 239 | }); 240 | 241 | it('should deselect with an array of node ids', function() { 242 | ivhTreeviewMgr.deselectEach(tree, ['flatcap', 'bags']); 243 | [stuff, hats, flatcap, bags, messenger, backpack].forEach(function(n) { 244 | expect(n.selected).toBe(false); 245 | }); 246 | [fedora].forEach(function(n) { 247 | expect(n.selected).toBe(true); 248 | }); 249 | }); 250 | 251 | }); 252 | 253 | describe('#validate', function() { 254 | 255 | it('should assume selected state by default', function() { 256 | angular.forEach(nodes, function(n) { 257 | n.selected = true; 258 | }); 259 | hats.selected = false; 260 | ivhTreeviewMgr.validate(tree); 261 | 262 | expect(stuff.selected).toBe(false); 263 | expect(stuff.__ivhTreeviewIndeterminate).toBe(true); 264 | 265 | expect(hats.selected).toBe(false); 266 | expect(hats.__ivhTreeviewIndeterminate).toBe(false); 267 | 268 | expect(bags.selected).toBe(true); 269 | expect(bags.__ivhTreeviewIndeterminate).toBe(false); 270 | 271 | expect(fedora.selected).toBe(false); 272 | expect(fedora.__ivhTreeviewIndeterminate).toBe(false); 273 | 274 | expect(flatcap.selected).toBe(false); 275 | expect(flatcap.__ivhTreeviewIndeterminate).toBe(false); 276 | 277 | expect(messenger.selected).toBe(true); 278 | expect(messenger.__ivhTreeviewIndeterminate).toBe(false); 279 | 280 | expect(backpack.selected).toBe(true); 281 | expect(backpack.__ivhTreeviewIndeterminate).toBe(false); 282 | }); 283 | 284 | it('should not throw when validating empty/null trees', function() { 285 | var fn = function() { 286 | ivhTreeviewMgr.validate(null); 287 | }; 288 | expect(fn).not.toThrow(); 289 | }); 290 | 291 | }); 292 | 293 | describe('#expand', function() { 294 | 295 | it('should be able to expand a single node', function() { 296 | angular.forEach(nodes, function(n) { 297 | n.__ivhTreeviewExpanded = false; 298 | }); 299 | 300 | ivhTreeviewMgr.expand(tree, bags); 301 | 302 | angular.forEach(nodes, function(n) { 303 | expect(n.__ivhTreeviewExpanded).toBe(n === bags); 304 | }); 305 | }); 306 | 307 | it('should be able to expand a single node by id', function() { 308 | angular.forEach(nodes, function(n) { 309 | n.__ivhTreeviewExpanded = false; 310 | }); 311 | 312 | ivhTreeviewMgr.expand(tree, 'bags'); 313 | 314 | angular.forEach(nodes, function(n) { 315 | expect(n.__ivhTreeviewExpanded).toBe(n === bags); 316 | }); 317 | }); 318 | 319 | it('should return ivhTreeviewMgr for chaining', function() { 320 | expect(ivhTreeviewMgr.expand(tree, bags)).toBe(ivhTreeviewMgr); 321 | }); 322 | 323 | it('should honor local options for the is-expanded attribute', function() { 324 | ivhTreeviewMgr.expand(tree, bags, {expandedAttribute: 'expanded'}, true); 325 | expect(bags.expanded).toBe(true); 326 | expect(bags.__ivhTreeviewExpanded).toBeUndefined(); 327 | }); 328 | 329 | }); 330 | 331 | describe('#expandRecursive', function() { 332 | 333 | it('should be able to expand a node and all its children', function() { 334 | angular.forEach(nodes, function(n) { 335 | n.__ivhTreeviewExpanded = false; 336 | }); 337 | 338 | ivhTreeviewMgr.expandRecursive(tree, bags); 339 | 340 | angular.forEach(bagNodes, function(n) { 341 | expect(n.__ivhTreeviewExpanded).toBe(true); 342 | }); 343 | angular.forEach(hatNodes, function(n) { 344 | expect(n.__ivhTreeviewExpanded).toBe(false); 345 | }); 346 | }); 347 | 348 | it('should be able to expand a node and all its children by id', function() { 349 | angular.forEach(nodes, function(n) { 350 | n.__ivhTreeviewExpanded = false; 351 | }); 352 | 353 | ivhTreeviewMgr.expandRecursive(tree, 'bags'); 354 | 355 | angular.forEach(bagNodes, function(n) { 356 | expect(n.__ivhTreeviewExpanded).toBe(true); 357 | }); 358 | angular.forEach(hatNodes, function(n) { 359 | expect(n.__ivhTreeviewExpanded).toBe(false); 360 | }); 361 | }); 362 | 363 | it('should be able to expand the entire tree', function() { 364 | angular.forEach(nodes, function(n) { 365 | n.__ivhTreeviewExpanded = false; 366 | }); 367 | 368 | ivhTreeviewMgr.expandRecursive(tree); 369 | 370 | angular.forEach(nodes, function(n) { 371 | expect(n.__ivhTreeviewExpanded).toBe(true); 372 | }); 373 | }); 374 | 375 | it('should return ivhTreeviewMgr for chaining', function() { 376 | expect(ivhTreeviewMgr.expandRecursive(tree, hats)).toBe(ivhTreeviewMgr); 377 | }); 378 | 379 | }); 380 | 381 | describe('#collapse', function() { 382 | 383 | it('should be able to callapse a single node', function() { 384 | angular.forEach(nodes, function(n) { 385 | n.__ivhTreeviewExpanded = true; 386 | }); 387 | 388 | ivhTreeviewMgr.collapse(tree, bags); 389 | 390 | angular.forEach(nodes, function(n) { 391 | expect(n.__ivhTreeviewExpanded).toBe(n !== bags); 392 | }); 393 | }); 394 | 395 | it('should be able to callapse a single node by id', function() { 396 | angular.forEach(nodes, function(n) { 397 | n.__ivhTreeviewExpanded = true; 398 | }); 399 | 400 | ivhTreeviewMgr.collapse(tree, 'bags'); 401 | 402 | angular.forEach(nodes, function(n) { 403 | expect(n.__ivhTreeviewExpanded).toBe(n !== bags); 404 | }); 405 | }); 406 | 407 | it('should return ivhTreeviewMgr for chaining', function() { 408 | expect(ivhTreeviewMgr.collapse(tree, bags)).toBe(ivhTreeviewMgr); 409 | }); 410 | 411 | }); 412 | 413 | describe('#collapseRecursive', function() { 414 | 415 | it('should be able to collapse a node and all its children', function() { 416 | angular.forEach(nodes, function(n) { 417 | n.__ivhTreeviewExpanded = true; 418 | }); 419 | 420 | ivhTreeviewMgr.collapseRecursive(tree, bags); 421 | 422 | angular.forEach(bagNodes, function(n) { 423 | expect(n.__ivhTreeviewExpanded).toBe(false); 424 | }); 425 | angular.forEach(hatNodes, function(n) { 426 | expect(n.__ivhTreeviewExpanded).toBe(true); 427 | }); 428 | }); 429 | 430 | it('should be able to collapse a node and all its children by id', function() { 431 | angular.forEach(nodes, function(n) { 432 | n.__ivhTreeviewExpanded = true; 433 | }); 434 | 435 | ivhTreeviewMgr.collapseRecursive(tree, 'bags'); 436 | 437 | angular.forEach(bagNodes, function(n) { 438 | expect(n.__ivhTreeviewExpanded).toBe(false); 439 | }); 440 | angular.forEach(hatNodes, function(n) { 441 | expect(n.__ivhTreeviewExpanded).toBe(true); 442 | }); 443 | }); 444 | 445 | it('should be able to collapse the entire tree', function() { 446 | angular.forEach(nodes, function(n) { 447 | n.__ivhTreeviewExpanded = true; 448 | }); 449 | 450 | ivhTreeviewMgr.collapseRecursive(tree); 451 | 452 | angular.forEach(nodes, function(n) { 453 | expect(n.__ivhTreeviewExpanded).toBe(false); 454 | }); 455 | }); 456 | 457 | it('should return ivhTreeviewMgr for chaining', function() { 458 | expect(ivhTreeviewMgr.collapseRecursive(tree, hats)).toBe(ivhTreeviewMgr); 459 | }); 460 | 461 | }); 462 | 463 | describe('#expandTo', function() { 464 | 465 | it('should be able to expand all *parents* of a given node', function() { 466 | angular.forEach(nodes, function(n) { 467 | n.__ivhTreeviewExpanded = false; 468 | }); 469 | 470 | ivhTreeviewMgr.expandTo(tree, fedora); 471 | 472 | var parents = [stuff, hats]; 473 | 474 | angular.forEach(nodes.concat([stuff]), function(n) { 475 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) > -1); 476 | }); 477 | }); 478 | 479 | it('should be able to expand all *parents* of a given node by id', function() { 480 | angular.forEach(nodes, function(n) { 481 | n.__ivhTreeviewExpanded = false; 482 | }); 483 | 484 | ivhTreeviewMgr.expandTo(tree, 'fedora'); 485 | 486 | var parents = [stuff, hats]; 487 | 488 | angular.forEach(nodes.concat([stuff]), function(n) { 489 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) > -1); 490 | }); 491 | }); 492 | 493 | it('should return ivhTreeviewMgr for chaining', function() { 494 | expect(ivhTreeviewMgr.expandTo(fedora)).toBe(ivhTreeviewMgr); 495 | }); 496 | 497 | }); 498 | 499 | describe('#collapseParents', function() { 500 | 501 | it('should be able to collapse all *parents* of a given node', function() { 502 | angular.forEach(nodes, function(n) { 503 | n.__ivhTreeviewExpanded = true; 504 | }); 505 | 506 | ivhTreeviewMgr.collapseParents(tree, fedora); 507 | 508 | var parents = [stuff, hats]; 509 | 510 | angular.forEach(nodes.concat([stuff]), function(n) { 511 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) === -1); 512 | }); 513 | }); 514 | 515 | it('should be able to collapse all *parents* of a given node by id', function() { 516 | angular.forEach(nodes, function(n) { 517 | n.__ivhTreeviewExpanded = true; 518 | }); 519 | 520 | ivhTreeviewMgr.collapseParents(tree, 'fedora'); 521 | 522 | var parents = [stuff, hats]; 523 | 524 | angular.forEach(nodes.concat([stuff]), function(n) { 525 | expect(n.__ivhTreeviewExpanded).toBe(parents.indexOf(n) === -1); 526 | }); 527 | }); 528 | 529 | it('should return ivhTreeviewMgr for chaining', function() { 530 | expect(ivhTreeviewMgr.collapseParents(fedora)).toBe(ivhTreeviewMgr); 531 | }); 532 | 533 | }); 534 | 535 | }); 536 | 537 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular IVH Treeview 2 | 3 | [ ![Build Status][travis-img] ][travis-link] 4 | 5 | > A treeview for AngularJS with filtering, checkbox support, custom templates, 6 | > and more. 7 | 8 | ## Contents 9 | 10 | - [Getting Started](#getting-started) 11 | - [Example Usage](#example-usage) 12 | - [Options](#options) 13 | - [Filtering](#filtering) 14 | - [Expanded by Default](#expanded-by-default) 15 | - [Default Selected State](#default-selected-state) 16 | - [Validate on Startup](#validate-on-startup) 17 | - [Twisties](#twisties) 18 | - [Templates and Skins](#templates-and-skins) 19 | - [Toggle Handlers](#toggle-handlers) 20 | - [Select/Deselect Handlers](#selectdeselect-handlers) 21 | - [All the Options](#all-the-options) 22 | - [Treeview Manager Service](#treeview-manager-service) 23 | - [`ivhTreeviewMgr.select(tree, node[, opts][, isSelected])`](#ivhtreeviewmgrselecttree-node-opts-isselected) 24 | - [`ivhTreeviewMgr.selectAll(tree[, opts][, isSelected])`](#ivhtreeviewmgrselectalltree-opts-isselected) 25 | - [`ivhTreeviewMgr.selectEach(tree, nodes[, opts][, isSelected])`](#ivhtreeviewmgrselecteachtree-nodes-opts-isselected) 26 | - [`ivhTreeviewMgr.deselect(tree, node[, opts])`](#ivhtreeviewmgrdeselecttree-node-opts) 27 | - [`ivhTreeviewMgr.deselectAll(tree[, opts])`](#ivhtreeviewmgrdeselectalltree-opts) 28 | - [`ivhTreeviewMgr.deselectEach(tree, nodes[, opts])`](#ivhtreeviewmgrdeselecteachtree-nodes-opts) 29 | - [`ivhTreeviewMgr.expand(tree, node[, opts][, isExpanded])`](#ivhtreeviewmgrexpandtree-node-opts-isexpanded) 30 | - [`ivhTreeviewMgr.expandRecursive(tree[, node[, opts][,isExpanded]])`](#ivhtreeviewmgrexpandrecursivetree-node-opts-isexpanded) 31 | - [`ivhTreeviewMgr.expandTo(tree, node[, opts][, isExpanded])`](#ivhtreeviewmgrexpandtotree-node-opts-isexpanded) 32 | - [`ivhTreeviewMgr.collapse(tree, node[, opts])`](#ivhtreeviewmgrcollapsetree-node-opts) 33 | - [`ivhTreeviewMgr.collapseRecursive(tree[, node[, opts]])`](#ivhtreeviewmgrcollapserecursivetree-node-opts) 34 | - [`ivhTreeviewMgr.collapseParents(tree, node[, opts])`](#ivhtreeviewmgrcollapseparentstree-node-opts) 35 | - [`ivhTreeviewMgr.validate(tree[, opts][, bias])`](#ivhtreeviewmgrvalidatetree-opts-bias) 36 | - [Dynamic Changes](#dynamic-changes) 37 | - [Tree Traversal](#tree-traversal) 38 | - [`ivhTreeviewBfs(tree[, opts][, cb])`](#ivhtreeviewbfstree-opts-cb) 39 | - [Optimizations and Known Limitations](#optimizations-and-known-limitations) 40 | - [Reporting Issues](#reporting-issues-and-getting-help) 41 | - [Contributing](#contributing) 42 | - [Release History](#release-history) 43 | - [License](#license) 44 | 45 | 46 | ## Getting Started 47 | 48 | IVH Treeview can be installed with bower and npm: 49 | 50 | ``` 51 | bower install angular-ivh-treeview 52 | # or 53 | npm install angular-ivh-treeview 54 | ``` 55 | 56 | Once installed, include the following files in your app: 57 | 58 | - `dist/ivh-treeview.js` 59 | - `dist/ivh-treeview.css` 60 | - `dist/ivh-treeview-theme-basic.css` (optional minimalist theme) 61 | 62 | And add the `ivh.treeview` module to your main Angular module: 63 | 64 | ```javascript 65 | angular.module('myApp', [ 66 | 'ivh.treeview' 67 | // other module dependencies... 68 | ]); 69 | ``` 70 | 71 | You're now ready to use the `ivh-treeview` directive, `ivhTreeviewMgr` service, 72 | and `ivhTreeviewBfs` service. 73 | 74 | ## Example Usage 75 | 76 | In your controller... 77 | 78 | ```javascript 79 | app.controller('MyCtrl', function() { 80 | this.bag = [{ 81 | label: 'Glasses', 82 | value: 'glasses', 83 | children: [{ 84 | label: 'Top Hat', 85 | value: 'top_hat' 86 | },{ 87 | label: 'Curly Mustache', 88 | value: 'mustachio' 89 | }] 90 | }]; 91 | 92 | this.awesomeCallback = function(node, tree) { 93 | // Do something with node or tree 94 | }; 95 | 96 | this.otherAwesomeCallback = function(node, isSelected, tree) { 97 | // Do soemthing with node or tree based on isSelected 98 | } 99 | }); 100 | ``` 101 | 102 | In your view... 103 | 104 | ```html 105 |
106 | 107 | 108 |
111 |
112 |
113 | ``` 114 | 115 | ## Options 116 | 117 | IVH Treeview is pretty configurable. By default it expects your elements to have 118 | `label` and `children` properties for node display text and child nodes 119 | respectively. It'll also make use of a `selected` attribute to manage selected 120 | states. If you would like to pick out nodes by ID rather than reference it'll 121 | also use an `id` attribute. Those attributes can all be changed, for example: 122 | 123 | ```html 124 |
125 |
130 |
131 | ``` 132 | 133 | IVH Treeview attaches checkboxes to each item in your tree for a hierarchical 134 | selection model. If you'd rather not have these checkboxes use 135 | `ivh-treeview-use-checkboxes="false"`: 136 | 137 | ```html 138 |
139 |
141 |
142 | ``` 143 | 144 | There's also a provider if you'd like to change the global defaults: 145 | 146 | ```javascript 147 | app.config(function(ivhTreeviewOptionsProvider) { 148 | ivhTreeviewOptionsProvider.set({ 149 | idAttribute: 'id', 150 | labelAttribute: 'label', 151 | childrenAttribute: 'children', 152 | selectedAttribute: 'selected', 153 | useCheckboxes: true, 154 | disableCheckboxSelectionPropagation: false, 155 | expandToDepth: 0, 156 | indeterminateAttribute: '__ivhTreeviewIndeterminate', 157 | expandedAttribute: '__ivhTreeviewExpanded', 158 | defaultSelectedState: true, 159 | validate: true, 160 | twistieExpandedTpl: '(-)', 161 | twistieCollapsedTpl: '(+)', 162 | twistieLeafTpl: 'o', 163 | nodeTpl: '...' 164 | }); 165 | }); 166 | ``` 167 | 168 | Note that you can also use the `ivhTreeviewOptions` service to inspect global 169 | options at runtime. For an explanation of each option see the comments in the 170 | [source for ivhTreeviewOptions][trvw-opts]. 171 | 172 | ```javascript 173 | app.controller('MyCtrl', function(ivhTreeviewOptions) { 174 | var opts = ivhTreeviewOptions(); 175 | 176 | // opts.idAttribute === 'id' 177 | // opts.labelAttribute === 'label' 178 | // opts.childrenAttribute === 'children' 179 | // opts.selectedAttribute === 'selected' 180 | // opts.useCheckboxes === true 181 | // opts.disableCheckboxSelectionPropagation === false 182 | // opts.expandToDepth === 0 183 | // opts.indeterminateAttribute === '__ivhTreeviewIndeterminate' 184 | // opts.expandedAttribute === '__ivhTreeviewExpanded' 185 | // opts.defaultSelectedState === true 186 | // opts.validate === true 187 | // opts.twistieExpandedTpl === '(-)' 188 | // opts.twistieCollapsedTpl === '(+)' 189 | // opts.twistieLeafTpl === 'o' 190 | // opts.nodeTpl =(eh)= '...' 191 | }); 192 | 193 | ``` 194 | 195 | 196 | ### Filtering 197 | 198 | We support filtering through the `ivh-treeview-filter` attribute, this value is 199 | supplied to Angular's `filterFilter` and applied to each node individually. 200 | 201 | IVH Treeview uses `ngHide` to hide filtered out nodes. If you would like to 202 | customize the hide/show behavior of nodes as they are filtered in and out of 203 | view (e.g. with `ngAnimate`) you can target elements with elements with the 204 | `.ivh-treeview-node` class: 205 | 206 | ```css 207 | /* with e.g. keyframe animations */ 208 | .ivh-treeview-node.ng-enter { 209 | animation: my-enter-animation 0.5s linear; 210 | } 211 | 212 | .ivh-treeview-node.ng-leave { 213 | animation: my-leave-animation 0.5s linear; 214 | } 215 | 216 | /* or class based animations */ 217 | .ivh-treeview-node.ng-hide { 218 | transition: 0.5s linear all; 219 | opacity: 0; 220 | } 221 | 222 | /* alternatively, just strike-through filtered out nodes */ 223 | .ivh-treeview-node.ng-hide { 224 | display: block !important; 225 | } 226 | 227 | .ivh-treeview-node.ng-hide .ivh-treeview-node-label { 228 | color: red; 229 | text-decoration: line-through; 230 | } 231 | ``` 232 | 233 | ***Demo***: [Filtering](http://jsbin.com/zitiri/edit?html,output) 234 | 235 | ### Expanded by Default 236 | 237 | If you want the tree to start out expanded to a certain depth use the 238 | `ivh-treeview-expand-to-depth` attribute: 239 | 240 | ```html 241 |
242 |
246 |
247 | ``` 248 | 249 | You can also use the `ivhTreeviewOptionsProvider` to set a global default. 250 | 251 | If you want the tree *entirely* expanded use a depth of `-1`. Providing a depth 252 | greater than your tree's maximum depth will cause the entire tree to be 253 | initially expanded. 254 | 255 | ***Demo***: [Expand to depth on 256 | load](http://jsbin.com/ruxedo/edit?html,js,output) 257 | 258 | ### Default Selected State 259 | 260 | When using checkboxes you can have a default selected state of `true` or 261 | `false`. The default selected state is used when validating your tree data with 262 | `ivhTreeviewMgr.validate` which will assume this state if none is specified, 263 | i.e. any node without a selected state will assume the default state. 264 | Futhermore, when `ivhTreeviewMgr.validate` finds a node whose selected state 265 | differs from the default it will assign the same state to each of that node's 266 | childred, parent nodes are updated accordingly. 267 | 268 | Use `ivh-treeview-default-selected-state` attribute or `defaultSelectedState` 269 | option to set this property. 270 | 271 | ***Demo***: [Default selected state and validate on 272 | startup](http://jsbin.com/pajeze/2/edit) 273 | 274 | ### Validate on Startup 275 | 276 | `ivh.treeview` will not assume control of your model on startup if you do not 277 | want it to. You can opt out of validation on startup by setting 278 | `ivh-treeview-validate="false"` at the attribute level or by globally setting 279 | the `validate` property in `ivhTreeviewOptionsProvider`. 280 | 281 | ***Demo***: [Default selected state and validate on 282 | startup](http://jsbin.com/pajeze/2/edit) 283 | 284 | ### Twisties 285 | 286 | The basic twisties that ship with this `ivh.treeview` are little more than ASCII 287 | art. You're encouraged to use your own twistie templates. For example, if you've 288 | got bootstrap on your page you might do something like this: 289 | 290 | ```javascript 291 | ivhTreeviewOptionsProvider.set({ 292 | twistieCollapsedTpl: '', 293 | twistieExpandedTpl: '', 294 | twistieLeafTpl: '●' 295 | }); 296 | ``` 297 | 298 | If you need different twistie templates for different treeview elements you can 299 | assign these templates at the attribute level: 300 | 301 | ```html 302 |
305 |
306 | ``` 307 | 308 | Alternatively, you can pass them as part of a [full configuration 309 | object](https://github.com/iVantage/angular-ivh-treeview#all-the-options). 310 | 311 | 312 | ***Demo***: [Custom twisties](http://jsbin.com/gizofu/edit?html,js,output) 313 | 314 | ### Templates and Skins 315 | 316 | IVH Treeview allows you to fully customize your tree nodes. See 317 | [docs/templates-and-skins.md](docs/templates-and-skins.md) for demos and 318 | details. 319 | 320 | ### Toggle Handlers 321 | 322 | Want to register a callback for whenever a user expands or collapses a node? Use 323 | the `ivh-treeview-on-toggle` attribute. Your expression will be evaluated with 324 | the following local variables: `ivhNode`, the node that was toggled; `ivhTree`, 325 | the tree it belongs to; `ivhIsExpanded`, whether or not the node is now 326 | expanded. 327 | 328 | ```html 329 |
330 |
333 |
334 | ``` 335 | 336 | You may also supply a toggle handler as a function (rather than an angular 337 | expression) using `ivh-treeview-options` or by setting a global `onToggle` 338 | option. In this case the function will be passed a single object with `ivhNode` 339 | and `ivhTree` properties. 340 | 341 | ***Demo***: [Toggle Handler](http://jsbin.com/xegari/edit) 342 | 343 | ### Select/Deselect Handlers 344 | 345 | Want to be notified any time a checkbox changes state as the result of a click? 346 | Use the `ivh-treeview-on-cb-change` attribute. Your expression will be evaluated 347 | whenever a node checkbox changes state with the following local variables: 348 | `ivhNode`, the node whose selected state changed; `ivhIsSelected`, the new 349 | selected state of the node; and `ivhTree`, the tree `ivhNode` belongs to. 350 | 351 | You may also supply a selected handler as a function (rather than an angular 352 | expression) using `ivh-treeview-options` or by setting a global `onCbChange` 353 | option. In this case the function will be passed a single object with `ivhNode`, 354 | `ivhIsSelected`, and `ivhTree` properties. 355 | 356 | Note that programmatic changes to a node's selected state (including selection 357 | change propagation) will not trigger this callback. It is only run for the 358 | actual node clicked on by a user. 359 | 360 | ```html 361 |
362 |
365 |
366 | ``` 367 | 368 | ***Demo***: [Select/Deselect Handler](http://jsbin.com/febexe/edit) 369 | 370 | 371 | ## All the Options 372 | 373 | If passing a configuration object is more your style than inlining everything in 374 | the view, that's OK too. 375 | 376 | In your fancy controller... 377 | 378 | ```javascript 379 | this.customOpts = { 380 | useCheckboxes: false, 381 | onToggle: this.awesomeCallback 382 | }; 383 | ``` 384 | 385 | In your view... 386 | 387 | ```html 388 |
391 |
392 | ``` 393 | 394 | Any option that can be set with `ivhTreeviewOptionsProvider` can be overriden 395 | here. 396 | 397 | 398 | ## Treeview Manager Service 399 | 400 | `ivh.treeview` supplies a service, `ivhTreeviewMgr`, for interacting with your 401 | tree data directly. 402 | 403 | #### `ivhTreeviewMgr.select(tree, node[, opts][, isSelected])` 404 | 405 | Select (or deselect) an item in `tree`, `node` can be either a reference to the 406 | actual tree node or its ID. 407 | 408 | We'll use settings registered with `ivhTreeviewOptions` by default, but you can 409 | override any of them with the optional `opts` parameter. 410 | 411 | `isSelected` is also optional and defaults to `true` (i.e. the node will be 412 | selected). 413 | 414 | When an item is selected each of its children are also selected and the 415 | indeterminate state of each of the node's parents is validated. 416 | 417 | ***Demo***: [Programmatic select/deselect](http://jsbin.com/kotohu/edit) 418 | 419 | #### `ivhTreeviewMgr.selectAll(tree[, opts][, isSelected])` 420 | 421 | Like `ivhTreeviewMgr.select` except every node in `tree` is either selected or 422 | deselected. 423 | 424 | ***Demo***: [Programmatic selectAll/deselectAll](http://jsbin.com/buhife/edit) 425 | 426 | #### `ivhTreeviewMgr.selectEach(tree, nodes[, opts][, isSelected])` 427 | 428 | Like `ivhTreeviewMgr.select` except an array of nodes (or node IDs) is used. 429 | Each node in `tree` corresponding to one of the passed `nodes` will be selected 430 | or deselected. 431 | 432 | ***Demo***: [Programmatic selectEach/deselectEach](http://jsbin.com/burigo/edit) 433 | 434 | #### `ivhTreeviewMgr.deselect(tree, node[, opts])` 435 | 436 | A convenience method, delegates to `ivhTreeviewMgr.select` with `isSelected` set 437 | to `false`. 438 | 439 | ***Demo***: [Programmatic select/deselect](http://jsbin.com/kotohu/edit) 440 | 441 | #### `ivhTreeviewMgr.deselectAll(tree[, opts])` 442 | 443 | A convenience method, delegates to `ivhTreeviewMgr.selectAll` with `isSelected` 444 | set to `false`. 445 | 446 | ***Demo***: [Programmatic selectAll/deselectAll](http://jsbin.com/buhife/edit) 447 | 448 | #### `ivhTreeviewMgr.deselectEach(tree, nodes[, opts])` 449 | 450 | A convenience method, delegates to `ivhTreeviewMgr.selectEach` with `isSelected` 451 | set to `false`. 452 | 453 | ***Demo***: [Programmatic selectEach/deselectEach](http://jsbin.com/burigo/edit) 454 | 455 | #### `ivhTreeviewMgr.expand(tree, node[, opts][, isExpanded])` 456 | 457 | Expand (or collapse) a given `node` in `tree`, again `node` may be an actual 458 | object reference or an ID. 459 | 460 | We'll use settings registered with `ivhTreeviewOptions` by default, but you can 461 | override any of them with the optional `opts` parameter. 462 | 463 | By default this method will expand the node in question, you may pass `false` as 464 | the last parameter though to collapse the node. Or, just use 465 | `ivhTreeviewMgr.collapse`. 466 | 467 | ***Demo***: [Programmatic expand/collapse](http://jsbin.com/degofo/edit?html,js,output) 468 | 469 | #### `ivhTreeviewMgr.expandRecursive(tree[, node[, opts][, isExpanded]])` 470 | 471 | Expand (or collapse) `node` and all its child nodes. Note that you may omit the 472 | `node` parameter (i.e. expand/collapse the entire tree) but only when all other 473 | option parameters are also omitted. 474 | 475 | ***Demo***: [Programmatic recursive expand/collapse](http://jsbin.com/wugege/edit) 476 | 477 | #### `ivhTreeviewMgr.expandTo(tree, node[, opts][, isExpanded])` 478 | 479 | Expand (or collapse) all parents of `node`. This may be used to "reveal" a 480 | nested node or to recursively collapse all parents of a node. 481 | 482 | ***Demo***: [Programmatic reveal/hide](http://jsbin.com/musodi/edit) 483 | 484 | #### `ivhTreeviewMgr.collapse(tree, node[, opts])` 485 | 486 | A convenience method, delegates to `ivhTreeviewMgr.expand` with `isExpanded` 487 | set to `false`. 488 | 489 | #### `ivhTreeviewMgr.collapseRecursive(tree[, node[, opts]])` 490 | 491 | A convenience method, delegates to `ivhTreeviewMgr.expandRecursive` with 492 | `isExpanded` set to `false`, 493 | 494 | ***Demo***: [Programmatic recursive expand/collapse](http://jsbin.com/wugege/edit) 495 | 496 | #### `ivhTreeviewMgr.collapseParents(tree, node[, opts])` 497 | 498 | A convenience method, delegates to `ivhTreeviewMgr.expandTo` with `isExpanded` 499 | set to `false`. 500 | 501 | ***Demo***: [Programmatic reveal/hide](http://jsbin.com/musodi/edit) 502 | 503 | #### `ivhTreeviewMgr.validate(tree[, opts][, bias])` 504 | 505 | Validate a `tree` data store, `bias` is a convenient redundancy for 506 | `opts.defaultSelectedState`. 507 | 508 | When validating tree data we look for the first node in each branch which has a 509 | selected state defined that differs from `opts.defaultSelectedState` (or 510 | `bias`). Each of that node's children are updated to match the differing node 511 | and parent indeterminate states are updated. 512 | 513 | ***Demo***: [Programmatic select/deselect](http://jsbin.com/bexedi/edit) 514 | 515 | ## Dynamic Changes 516 | 517 | Adding and removing tree nodes on the fly is supported. Just keep in mind that 518 | added nodes do not automatically inherit selected states (i.e. checkbox states) 519 | from their parent nodes. Similarly, adding new child nodes does not cause parent 520 | nodes to automatically validate their own selected states. You will typically 521 | want to use `ivhTreeviewMgr.validate` or `ivhTreeviewMgr.select` after adding 522 | new nodes to your tree: 523 | 524 | ```javascript 525 | // References to the tree, parent node, and children... 526 | var tree = getTree() 527 | , parent = getParent() 528 | , newNodes = [{label: 'Hello'},{label: 'World'}]; 529 | 530 | // Attach new children to parent node 531 | parent.children = newNodes; 532 | 533 | // Force revalidate on tree given parent node's selected status 534 | ivhTreeviewMgr.select(myTree, parent, parent.selected); 535 | ``` 536 | 537 | ## Tree Traversal 538 | 539 | The internal tree traversal service is exposed as `ivhTreeviewBfs` (bfs --> 540 | breadth first search). 541 | 542 | #### `ivhTreeviewBfs(tree[, opts][, cb])` 543 | 544 | We perform a breadth first traversal of `tree` applying the function `cb` to 545 | each node as it is reached. `cb` is passed two parameters, the node itself and 546 | an array of parents nodes ordered nearest to farthest. If the `cb` returns 547 | `false` traversal of that branch is stopped. 548 | 549 | Note that even if `false` is returned each of `nodes` siblings will still be 550 | traversed. Essentially none of `nodes` children will be added to traversal 551 | queue. All other branches in `tree` will be traversed as normal. 552 | 553 | In other words returning `false` tells `ivhTreeviewBfs` to go no deeper in the 554 | current branch only. 555 | 556 | ***Demo***: [`ivhTreeviewBfs` in 557 | action](http://jsbin.com/wofunu/1/edit?html,js,output) 558 | 559 | 560 | ## Optimizations and Known Limitations 561 | 562 | ### Performance at Scale 563 | 564 | The default node template assumes a reasonable number of tree nodes. As your 565 | tree grows (3k-10k+ nodes) you will likely notice a significant dip in 566 | performance. This can be mitigated by using a custom template with a few easy 567 | tweaks. 568 | 569 | **Only process visible nodes** by adding an `ng-if` to the 570 | `ivh-treeview-children` element. This small change will result in significant 571 | performance boosts for large trees as now only the visible nodes (i.e. nodes 572 | with all parents expanded) will be processed. This change will likely be added 573 | to the default template in version 1.1. 574 | 575 | **Use Angular's bind-once syntx in a custom template**. The default template 576 | supports angular@1.2.x and so does not leverage the native double-colon syntax 577 | to make one time bindings. By binding once where possible you can trim a large 578 | number of watches from your trees. 579 | 580 | ### Known Issues 581 | 582 | - Creating multiple treeviews within an ngRepeat loops creates an issue where 583 | each treeview accesses the same controller instance after initial load. See 584 | issue #113. 585 | - We use Angular's `filterFilter` for filtering, by default this compares your 586 | filter string with at all object attributes. This directive attaches an 587 | attribute to your tree nodes to track its selected state (e.g. `selected: 588 | false`). If you want your filter to ignore the selection tracking attribute 589 | use an object or function filter. See issue #151. 590 | 591 | ## Reporting Issues and Getting Help 592 | 593 | When reporting an issue please take a moment to reproduce your setup by 594 | modifying our [starter template](http://jsbin.com/wecafa/2/edit). Only make as 595 | many changes as necessary to demonstrate your issue but do comment your added 596 | code. 597 | 598 | Please use Stack Overflow for general questions and help with implementation. 599 | 600 | 601 | ## Contributing 602 | 603 | Please see our consolidated [contribution 604 | guidelines](https://github.com/iVantage/Contribution-Guidelines). 605 | 606 | 607 | ## Release History 608 | 609 | - 2015-11-29 v1.0.2 Allow numeric ids as well as string ids 610 | - 2015-09-23 v1.0.0 Use expressions rather than callbacks for change/toggle 611 | handlers, update default template. See MIGRATING doc for breaking changes. 612 | - 2015-05-06 v0.10.0 Make node templates customizable 613 | - 2015-02-10 v0.9.0 All options are set-able via attributes or config object 614 | - 2015-01-02 v0.8.0 Add ability to expand/collapse nodes programmatically 615 | - 2014-09-21 v0.6.0 Tree accepts nodes added on the fly 616 | - 2014-09-09 v0.3.0 Complete refactor. Directive no longer propagates changes 617 | automatically on programmatic changes, use ivhTreeviewMgr. 618 | - 2014-08-25 v0.2.0 Allow for initial expansion 619 | - 2014-06-20 v0.1.0 Initial release 620 | 621 | 622 | ## License 623 | 624 | [MIT license][license], copyright iVantage Health Analytics, Inc. 625 | 626 | [license]: https://raw.github.com/iVantage/angular-ivh-treeview/master/LICENSE-MIT 627 | [bootstrap]: http://getbootstrap.com/ 628 | [travis-img]: https://travis-ci.org/iVantage/angular-ivh-treeview.svg?branch=master 629 | [travis-link]: https://travis-ci.org/iVantage/angular-ivh-treeview 630 | [trvw-opts]: https://github.com/iVantage/angular-ivh-treeview/blob/master/src/scripts/services/ivh-treeview-options.js#L13-L103 631 | -------------------------------------------------------------------------------- /dist/ivh-treeview.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * The iVantage Treeview module 4 | * 5 | * @package ivh.treeview 6 | */ 7 | 8 | angular.module('ivh.treeview', []); 9 | 10 | 11 | /** 12 | * Supports non-default interpolation symbols 13 | * 14 | * @package ivh.treeview 15 | * @copyright 2016 iVantage Health Analytics, Inc. 16 | */ 17 | 18 | angular.module('ivh.treeview').constant('ivhTreeviewInterpolateEndSymbol', '}}'); 19 | 20 | 21 | 22 | /** 23 | * Supports non-default interpolation symbols 24 | * 25 | * @package ivh.treeview 26 | * @copyright 2016 iVantage Health Analytics, Inc. 27 | */ 28 | 29 | angular.module('ivh.treeview').constant('ivhTreeviewInterpolateStartSymbol', '{{'); 30 | 31 | 32 | 33 | /** 34 | * Selection management logic for treeviews with checkboxes 35 | * 36 | * @private 37 | * @package ivh.treeview 38 | * @copyright 2014 iVantage Health Analytics, Inc. 39 | */ 40 | 41 | angular.module('ivh.treeview').directive('ivhTreeviewCheckboxHelper', [function() { 42 | 'use strict'; 43 | return { 44 | restrict: 'A', 45 | scope: { 46 | node: '=ivhTreeviewCheckboxHelper' 47 | }, 48 | require: '^ivhTreeview', 49 | link: function(scope, element, attrs, trvw) { 50 | var node = scope.node 51 | , opts = trvw.opts() 52 | , indeterminateAttr = opts.indeterminateAttribute 53 | , selectedAttr = opts.selectedAttribute; 54 | 55 | // Set initial selected state of this checkbox 56 | scope.isSelected = node[selectedAttr]; 57 | 58 | // Local access to the parent controller 59 | scope.trvw = trvw; 60 | 61 | // Enforce consistent behavior across browsers by making indeterminate 62 | // checkboxes become checked when clicked/selected using spacebar 63 | scope.resolveIndeterminateClick = function() { 64 | 65 | //intermediate state is not handled when CheckBoxes state propagation is disabled 66 | if (opts.disableCheckboxSelectionPropagation) { 67 | return; 68 | } 69 | 70 | if(node[indeterminateAttr]) { 71 | trvw.select(node, true); 72 | } 73 | }; 74 | 75 | // Update the checkbox when the node's selected status changes 76 | scope.$watch('node.' + selectedAttr, function(newVal, oldVal) { 77 | scope.isSelected = newVal; 78 | }); 79 | 80 | if (!opts.disableCheckboxSelectionPropagation) { 81 | // Update the checkbox when the node's indeterminate status changes 82 | scope.$watch('node.' + indeterminateAttr, function(newVal, oldVal) { 83 | element.find('input').prop('indeterminate', newVal); 84 | }); 85 | } 86 | }, 87 | template: [ 88 | '' 93 | ].join('\n') 94 | }; 95 | }]); 96 | 97 | 98 | 99 | /** 100 | * Wrapper for a checkbox directive 101 | * 102 | * Basically exists so folks creeting custom node templates don't need to attach 103 | * their node to this directive explicitly - i.e. keeps consistent interface 104 | * with the twistie and toggle directives. 105 | * 106 | * @package ivh.treeview 107 | * @copyright 2014 iVantage Health Analytics, Inc. 108 | */ 109 | 110 | angular.module('ivh.treeview').directive('ivhTreeviewCheckbox', [function() { 111 | 'use strict'; 112 | return { 113 | restrict: 'AE', 114 | require: '^ivhTreeview', 115 | template: '' 116 | }; 117 | }]); 118 | 119 | 120 | /** 121 | * The recursive step, output child nodes for the scope node 122 | * 123 | * @package ivh.treeview 124 | * @copyright 2014 iVantage Health Analytics, Inc. 125 | */ 126 | 127 | angular.module('ivh.treeview').directive('ivhTreeviewChildren', function() { 128 | 'use strict'; 129 | return { 130 | restrict: 'AE', 131 | require: '^ivhTreeviewNode', 132 | template: [ 133 | '
    ', 134 | '
  • ', 140 | '
  • ', 141 | '
' 142 | ].join('\n') 143 | }; 144 | }); 145 | 146 | 147 | /** 148 | * Treeview tree node directive 149 | * 150 | * @private 151 | * @package ivh.treeview 152 | * @copyright 2014 iVantage Health Analytics, Inc. 153 | */ 154 | 155 | angular.module('ivh.treeview').directive('ivhTreeviewNode', ['ivhTreeviewCompiler', function(ivhTreeviewCompiler) { 156 | 'use strict'; 157 | return { 158 | restrict: 'A', 159 | scope: { 160 | node: '=ivhTreeviewNode', 161 | depth: '=ivhTreeviewDepth' 162 | }, 163 | require: '^ivhTreeview', 164 | compile: function(tElement) { 165 | return ivhTreeviewCompiler 166 | .compile(tElement, function(scope, element, attrs, trvw) { 167 | var node = scope.node; 168 | 169 | var getChildren = scope.getChildren = function() { 170 | return trvw.children(node); 171 | }; 172 | 173 | scope.trvw = trvw; 174 | scope.childDepth = scope.depth + 1; 175 | 176 | // Expand/collapse the node as dictated by the expandToDepth property. 177 | // Note that we will respect the expanded state of this node if it has 178 | // been expanded by e.g. `ivhTreeviewMgr.expandTo` but not yet 179 | // rendered. 180 | if(!trvw.isExpanded(node)) { 181 | trvw.expand(node, trvw.isInitiallyExpanded(scope.depth)); 182 | } 183 | 184 | /** 185 | * @todo Provide a way to opt out of this 186 | */ 187 | scope.$watch(function() { 188 | return getChildren().length > 0; 189 | }, function(newVal) { 190 | if(newVal) { 191 | element.removeClass('ivh-treeview-node-leaf'); 192 | } else { 193 | element.addClass('ivh-treeview-node-leaf'); 194 | } 195 | }); 196 | }); 197 | } 198 | }; 199 | }]); 200 | 201 | 202 | 203 | /** 204 | * Toggle logic for treeview nodes 205 | * 206 | * Handles expand/collapse on click. Does nothing for leaf nodes. 207 | * 208 | * @private 209 | * @package ivh.treeview 210 | * @copyright 2014 iVantage Health Analytics, Inc. 211 | */ 212 | 213 | angular.module('ivh.treeview').directive('ivhTreeviewToggle', [function() { 214 | 'use strict'; 215 | return { 216 | restrict: 'A', 217 | require: '^ivhTreeview', 218 | link: function(scope, element, attrs, trvw) { 219 | var node = scope.node; 220 | 221 | element.addClass('ivh-treeview-toggle'); 222 | 223 | element.bind('click', function() { 224 | if(!trvw.isLeaf(node)) { 225 | scope.$apply(function() { 226 | trvw.toggleExpanded(node); 227 | trvw.onToggle(node); 228 | }); 229 | } 230 | }); 231 | } 232 | }; 233 | }]); 234 | 235 | 236 | /** 237 | * Treeview twistie directive 238 | * 239 | * @private 240 | * @package ivh.treeview 241 | * @copyright 2014 iVantage Health Analytics, Inc. 242 | */ 243 | 244 | angular.module('ivh.treeview').directive('ivhTreeviewTwistie', ['$compile', 'ivhTreeviewOptions', function($compile, ivhTreeviewOptions) { 245 | 'use strict'; 246 | 247 | var globalOpts = ivhTreeviewOptions(); 248 | 249 | return { 250 | restrict: 'A', 251 | require: '^ivhTreeview', 252 | template: [ 253 | '', 254 | '', 255 | globalOpts.twistieCollapsedTpl, 256 | '', 257 | '', 258 | globalOpts.twistieExpandedTpl, 259 | '', 260 | '', 261 | globalOpts.twistieLeafTpl, 262 | '', 263 | '' 264 | ].join('\n'), 265 | link: function(scope, element, attrs, trvw) { 266 | 267 | if(!trvw.hasLocalTwistieTpls) { 268 | return; 269 | } 270 | 271 | var opts = trvw.opts() 272 | , $twistieContainers = element 273 | .children().eq(0) // Template root 274 | .children(); // The twistie spans 275 | 276 | angular.forEach([ 277 | // Should be in the same order as elements in template 278 | 'twistieCollapsedTpl', 279 | 'twistieExpandedTpl', 280 | 'twistieLeafTpl' 281 | ], function(tplKey, ix) { 282 | var tpl = opts[tplKey] 283 | , tplGlobal = globalOpts[tplKey]; 284 | 285 | // Do nothing if we don't have a new template 286 | if(!tpl || tpl === tplGlobal) { 287 | return; 288 | } 289 | 290 | // Super gross, the template must actually be an html string, we won't 291 | // try too hard to enforce this, just don't shoot yourself in the foot 292 | // too badly and everything will be alright. 293 | if(tpl.substr(0, 1) !== '<' || tpl.substr(-1, 1) !== '>') { 294 | tpl = '' + tpl + ''; 295 | } 296 | 297 | var $el = $compile(tpl)(scope) 298 | , $container = $twistieContainers.eq(ix); 299 | 300 | // Clean out global template and append the new one 301 | $container.html('').append($el); 302 | }); 303 | 304 | } 305 | }; 306 | }]); 307 | 308 | 309 | /** 310 | * The `ivh-treeview` directive 311 | * 312 | * A filterable tree view with checkbox support. 313 | * 314 | * Example: 315 | * 316 | * ``` 317 | *
319 | * ivh-treeview-filter="myFilterText"> 320 | *
321 | * ``` 322 | * 323 | * @package ivh.treeview 324 | * @copyright 2014 iVantage Health Analytics, Inc. 325 | */ 326 | 327 | angular.module('ivh.treeview').directive('ivhTreeview', ['ivhTreeviewMgr', function(ivhTreeviewMgr) { 328 | 'use strict'; 329 | return { 330 | restrict: 'A', 331 | transclude: true, 332 | scope: { 333 | // The tree data store 334 | root: '=ivhTreeview', 335 | 336 | // Specific config options 337 | childrenAttribute: '=ivhTreeviewChildrenAttribute', 338 | defaultSelectedState: '=ivhTreeviewDefaultSelectedState', 339 | disableCheckboxSelectionPropagation: '=ivhTreeviewDisableCheckboxSelectionPropagation', 340 | expandToDepth: '=ivhTreeviewExpandToDepth', 341 | idAttribute: '=ivhTreeviewIdAttribute', 342 | indeterminateAttribute: '=ivhTreeviewIndeterminateAttribute', 343 | expandedAttribute: '=ivhTreeviewExpandedAttribute', 344 | labelAttribute: '=ivhTreeviewLabelAttribute', 345 | nodeTpl: '=ivhTreeviewNodeTpl', 346 | selectedAttribute: '=ivhTreeviewSelectedAttribute', 347 | onCbChange: '&ivhTreeviewOnCbChange', 348 | onToggle: '&ivhTreeviewOnToggle', 349 | twistieCollapsedTpl: '=ivhTreeviewTwistieCollapsedTpl', 350 | twistieExpandedTpl: '=ivhTreeviewTwistieExpandedTpl', 351 | twistieLeafTpl: '=ivhTreeviewTwistieLeafTpl', 352 | useCheckboxes: '=ivhTreeviewUseCheckboxes', 353 | validate: '=ivhTreeviewValidate', 354 | visibleAttribute: '=ivhTreeviewVisibleAttribute', 355 | 356 | // Generic options object 357 | userOptions: '=ivhTreeviewOptions', 358 | 359 | // The filter 360 | filter: '=ivhTreeviewFilter' 361 | }, 362 | controllerAs: 'trvw', 363 | controller: ['$scope', '$element', '$attrs', '$transclude', 'ivhTreeviewOptions', 'filterFilter', function($scope, $element, $attrs, $transclude, ivhTreeviewOptions, filterFilter) { 364 | var ng = angular 365 | , trvw = this; 366 | 367 | // Merge any locally set options with those registered with hte 368 | // ivhTreeviewOptions provider 369 | var localOpts = ng.extend({}, ivhTreeviewOptions(), $scope.userOptions); 370 | 371 | // Two-way bound attributes (=) can be copied over directly if they're 372 | // non-empty 373 | ng.forEach([ 374 | 'childrenAttribute', 375 | 'defaultSelectedState', 376 | 'disableCheckboxSelectionPropagation', 377 | 'expandToDepth', 378 | 'idAttribute', 379 | 'indeterminateAttribute', 380 | 'expandedAttribute', 381 | 'labelAttribute', 382 | 'nodeTpl', 383 | 'selectedAttribute', 384 | 'twistieCollapsedTpl', 385 | 'twistieExpandedTpl', 386 | 'twistieLeafTpl', 387 | 'useCheckboxes', 388 | 'validate', 389 | 'visibleAttribute' 390 | ], function(attr) { 391 | if(ng.isDefined($scope[attr])) { 392 | localOpts[attr] = $scope[attr]; 393 | } 394 | }); 395 | 396 | // Attrs with the `&` prefix will yield a defined scope entity even if 397 | // no value is specified. We must check to make sure the attribute string 398 | // is non-empty before copying over the scope value. 399 | var normedAttr = function(attrKey) { 400 | return 'ivhTreeview' + 401 | attrKey.charAt(0).toUpperCase() + 402 | attrKey.slice(1); 403 | }; 404 | 405 | ng.forEach([ 406 | 'onCbChange', 407 | 'onToggle' 408 | ], function(attr) { 409 | if($attrs[normedAttr(attr)]) { 410 | localOpts[attr] = $scope[attr]; 411 | } 412 | }); 413 | 414 | // Treat the transcluded content (if there is any) as our node template 415 | var transcludedScope; 416 | $transclude(function(clone, scope) { 417 | var transcludedNodeTpl = ''; 418 | angular.forEach(clone, function(c) { 419 | transcludedNodeTpl += (c.innerHTML || '').trim(); 420 | }); 421 | if(transcludedNodeTpl.length) { 422 | transcludedScope = scope; 423 | localOpts.nodeTpl = transcludedNodeTpl; 424 | } 425 | }); 426 | 427 | /** 428 | * Get the merged global and local options 429 | * 430 | * @return {Object} the merged options 431 | */ 432 | trvw.opts = function() { 433 | return localOpts; 434 | }; 435 | 436 | // If we didn't provide twistie templates we'll be doing a fair bit of 437 | // extra checks for no reason. Let's just inform down stream directives 438 | // whether or not they need to worry about twistie non-global templates. 439 | var userOpts = $scope.userOptions || {}; 440 | 441 | /** 442 | * Whether or not we have local twistie templates 443 | * 444 | * @private 445 | */ 446 | trvw.hasLocalTwistieTpls = !!( 447 | userOpts.twistieCollapsedTpl || 448 | userOpts.twistieExpandedTpl || 449 | userOpts.twistieLeafTpl || 450 | $scope.twistieCollapsedTpl || 451 | $scope.twistieExpandedTpl || 452 | $scope.twistieLeafTpl); 453 | 454 | /** 455 | * Get the child nodes for `node` 456 | * 457 | * Abstracts away the need to know the actual label attribute in 458 | * templates. 459 | * 460 | * @param {Object} node a tree node 461 | * @return {Array} the child nodes 462 | */ 463 | trvw.children = function(node) { 464 | var children = node[localOpts.childrenAttribute]; 465 | return ng.isArray(children) ? children : []; 466 | }; 467 | 468 | /** 469 | * Get the label for `node` 470 | * 471 | * Abstracts away the need to know the actual label attribute in 472 | * templates. 473 | * 474 | * @param {Object} node A tree node 475 | * @return {String} The node label 476 | */ 477 | trvw.label = function(node) { 478 | return node[localOpts.labelAttribute]; 479 | }; 480 | 481 | /** 482 | * Returns `true` if this treeview has a filter 483 | * 484 | * @return {Boolean} Whether on not we have a filter 485 | * @private 486 | */ 487 | trvw.hasFilter = function() { 488 | return ng.isDefined($scope.filter); 489 | }; 490 | 491 | /** 492 | * Get the treeview filter 493 | * 494 | * @return {String} The filter string 495 | * @private 496 | */ 497 | trvw.getFilter = function() { 498 | return $scope.filter || ''; 499 | }; 500 | 501 | /** 502 | * Returns `true` if current filter should hide `node`, false otherwise 503 | * 504 | * @todo Note that for object and function filters each node gets hit with 505 | * `isVisible` N-times where N is its depth in the tree. We may be able to 506 | * optimize `isVisible` in this case by: 507 | * 508 | * - On first call to `isVisible` in a given digest cycle walk the tree to 509 | * build a flat array of nodes. 510 | * - Run the array of nodes through the filter. 511 | * - Build a map (`id`/$scopeId --> true) for the nodes that survive the 512 | * filter 513 | * - On subsequent calls to `isVisible` just lookup the node id in our 514 | * map. 515 | * - Clean the map with a $timeout (?) 516 | * 517 | * In theory the result of a call to `isVisible` could change during a 518 | * digest cycle as scope variables are updated... I think calls would 519 | * happen bottom up (i.e. from "leaf" to "root") so that might not 520 | * actually be an issue. Need to investigate if this ends up feeling for 521 | * large/deep trees. 522 | * 523 | * @param {Object} node A tree node 524 | * @return {Boolean} Whether or not `node` is filtered out 525 | */ 526 | trvw.isVisible = function(node) { 527 | var filter = trvw.getFilter(); 528 | 529 | // Quick shortcut 530 | if(!filter || filterFilter([node], filter).length) { 531 | return true; 532 | } 533 | 534 | // If we have an object or function filter we have to check children 535 | // separately 536 | if(typeof filter === 'object' || typeof filter === 'function') { 537 | var children = trvw.children(node); 538 | // If any child is visible then so is this node 539 | for(var ix = children.length; ix--;) { 540 | if(trvw.isVisible(children[ix])) { 541 | return true; 542 | } 543 | } 544 | } 545 | 546 | return false; 547 | }; 548 | 549 | /** 550 | * Returns `true` if we should use checkboxes, false otherwise 551 | * 552 | * @return {Boolean} Whether or not to use checkboxes 553 | */ 554 | trvw.useCheckboxes = function() { 555 | return localOpts.useCheckboxes; 556 | }; 557 | 558 | /** 559 | * Select or deselect `node` 560 | * 561 | * Updates parent and child nodes appropriately, `isSelected` defaults to 562 | * `true`. 563 | * 564 | * @param {Object} node The node to select or deselect 565 | * @param {Boolean} isSelected Defaults to `true` 566 | */ 567 | trvw.select = function(node, isSelected) { 568 | ivhTreeviewMgr.select($scope.root, node, localOpts, isSelected); 569 | trvw.onCbChange(node, isSelected); 570 | }; 571 | 572 | /** 573 | * Get the selected state of `node` 574 | * 575 | * @param {Object} node The node to get the selected state of 576 | * @return {Boolean} `true` if `node` is selected 577 | */ 578 | trvw.isSelected = function(node) { 579 | return node[localOpts.selectedAttribute]; 580 | }; 581 | 582 | /** 583 | * Toggle the selected state of `node` 584 | * 585 | * Updates parent and child node selected states appropriately. 586 | * 587 | * @param {Object} node The node to update 588 | */ 589 | trvw.toggleSelected = function(node) { 590 | var isSelected = !node[localOpts.selectedAttribute]; 591 | trvw.select(node, isSelected); 592 | }; 593 | 594 | /** 595 | * Expand or collapse a given node 596 | * 597 | * `isExpanded` is optional and defaults to `true`. 598 | * 599 | * @param {Object} node The node to expand/collapse 600 | * @param {Boolean} isExpanded Whether to expand (`true`) or collapse 601 | */ 602 | trvw.expand = function(node, isExpanded) { 603 | ivhTreeviewMgr.expand($scope.root, node, localOpts, isExpanded); 604 | }; 605 | 606 | /** 607 | * Get the expanded state of a given node 608 | * 609 | * @param {Object} node The node to check the expanded state of 610 | * @return {Boolean} 611 | */ 612 | trvw.isExpanded = function(node) { 613 | return node[localOpts.expandedAttribute]; 614 | }; 615 | 616 | /** 617 | * Toggle the expanded state of a given node 618 | * 619 | * @param {Object} node The node to toggle 620 | */ 621 | trvw.toggleExpanded = function(node) { 622 | trvw.expand(node, !trvw.isExpanded(node)); 623 | }; 624 | 625 | /** 626 | * Whether or not nodes at `depth` should be expanded by default 627 | * 628 | * Use -1 to fully expand the tree by default. 629 | * 630 | * @param {Integer} depth The depth to expand to 631 | * @return {Boolean} Whether or not nodes at `depth` should be expanded 632 | * @private 633 | */ 634 | trvw.isInitiallyExpanded = function(depth) { 635 | var expandTo = localOpts.expandToDepth === -1 ? 636 | Infinity : localOpts.expandToDepth; 637 | return depth < expandTo; 638 | }; 639 | 640 | /** 641 | * Returns `true` if `node` is a leaf node 642 | * 643 | * @param {Object} node The node to check 644 | * @return {Boolean} `true` if `node` is a leaf 645 | */ 646 | trvw.isLeaf = function(node) { 647 | return trvw.children(node).length === 0; 648 | }; 649 | 650 | /** 651 | * Get the tree node template 652 | * 653 | * @return {String} The node template 654 | * @private 655 | */ 656 | trvw.getNodeTpl = function() { 657 | return localOpts.nodeTpl; 658 | }; 659 | 660 | /** 661 | * Get the root of the tree 662 | * 663 | * Mostly a helper for custom templates 664 | * 665 | * @return {Object|Array} The tree root 666 | * @private 667 | */ 668 | trvw.root = function() { 669 | return $scope.root; 670 | }; 671 | 672 | /** 673 | * Call the registered toggle handler 674 | * 675 | * Handler will get a reference to `node` and the root of the tree. 676 | * 677 | * @param {Object} node Tree node to pass to the handler 678 | * @private 679 | */ 680 | trvw.onToggle = function(node) { 681 | if(localOpts.onToggle) { 682 | var locals = { 683 | ivhNode: node, 684 | ivhIsExpanded: trvw.isExpanded(node), 685 | ivhTree: $scope.root 686 | }; 687 | localOpts.onToggle(locals); 688 | } 689 | }; 690 | 691 | /** 692 | * Call the registered selection change handler 693 | * 694 | * Handler will get a reference to `node`, the new selected state of 695 | * `node, and the root of the tree. 696 | * 697 | * @param {Object} node Tree node to pass to the handler 698 | * @param {Boolean} isSelected Selected state for `node` 699 | * @private 700 | */ 701 | trvw.onCbChange = function(node, isSelected) { 702 | if(localOpts.onCbChange) { 703 | var locals = { 704 | ivhNode: node, 705 | ivhIsSelected: isSelected, 706 | ivhTree: $scope.root 707 | }; 708 | localOpts.onCbChange(locals); 709 | } 710 | }; 711 | }], 712 | link: function(scope, element, attrs) { 713 | var opts = scope.trvw.opts(); 714 | 715 | // Allow opt-in validate on startup 716 | if(opts.validate) { 717 | ivhTreeviewMgr.validate(scope.root, opts); 718 | } 719 | }, 720 | template: [ 721 | '
    ', 722 | '
  • ', 728 | '
  • ', 729 | '
' 730 | ].join('\n') 731 | }; 732 | }]); 733 | 734 | 735 | angular.module('ivh.treeview').filter('ivhTreeviewAsArray', function() { 736 | 'use strict'; 737 | return function(arr) { 738 | if(!angular.isArray(arr) && angular.isObject(arr)) { 739 | return [arr]; 740 | } 741 | return arr; 742 | }; 743 | }); 744 | 745 | 746 | /** 747 | * Breadth first searching for treeview data stores 748 | * 749 | * @package ivh.treeview 750 | * @copyright 2014 iVantage Health Analytics, Inc. 751 | */ 752 | 753 | angular.module('ivh.treeview').factory('ivhTreeviewBfs', ['ivhTreeviewOptions', function(ivhTreeviewOptions) { 754 | 'use strict'; 755 | 756 | var ng = angular; 757 | 758 | /** 759 | * Breadth first search of `tree` 760 | * 761 | * `opts` is optional and may override settings from `ivhTreeviewOptions.options`. 762 | * The callback `cb` will be invoked on each node in the tree as we traverse, 763 | * if it returns `false` traversal of that branch will not continue. The 764 | * callback is given the current node as the first parameter and the node 765 | * ancestors, from closest to farthest, as an array in the second parameter. 766 | * 767 | * @param {Array|Object} tree The tree data 768 | * @param {Object} opts [optional] Settings overrides 769 | * @param {Function} cb [optional] Callback to run against each node 770 | */ 771 | return function(tree, opts, cb) { 772 | if(arguments.length === 2 && ng.isFunction(opts)) { 773 | cb = opts; 774 | opts = {}; 775 | } 776 | opts = angular.extend({}, ivhTreeviewOptions(), opts); 777 | cb = cb || ng.noop; 778 | 779 | var queue = [] 780 | , childAttr = opts.childrenAttribute 781 | , next, node, parents, ix, numChildren; 782 | 783 | if(ng.isArray(tree)) { 784 | ng.forEach(tree, function(n) { 785 | // node and parents 786 | queue.push([n, []]); 787 | }); 788 | next = queue.shift(); 789 | } else { 790 | // node and parents 791 | next = [tree, []]; 792 | } 793 | 794 | while(next) { 795 | node = next[0]; 796 | parents = next[1]; 797 | // cb might return `undefined` so we have to actually check for equality 798 | // against `false` 799 | if(cb(node, parents) !== false) { 800 | if(node[childAttr] && ng.isArray(node[childAttr])) { 801 | numChildren = node[childAttr].length; 802 | for(ix = 0; ix < numChildren; ix++) { 803 | queue.push([node[childAttr][ix], [node].concat(parents)]); 804 | } 805 | } 806 | } 807 | next = queue.shift(); 808 | } 809 | }; 810 | }]); 811 | 812 | 813 | /** 814 | * Compile helper for treeview nodes 815 | * 816 | * Defers compilation until after linking parents. Otherwise our treeview 817 | * compilation process would recurse indefinitely. 818 | * 819 | * Thanks to http://stackoverflow.com/questions/14430655/recursion-in-angular-directives 820 | * 821 | * @private 822 | * @package ivh.treeview 823 | * @copyright 2014 iVantage Health Analytics, Inc. 824 | */ 825 | 826 | angular.module('ivh.treeview').factory('ivhTreeviewCompiler', ['$compile', function($compile) { 827 | 'use strict'; 828 | return { 829 | /** 830 | * Manually compiles the element, fixing the recursion loop. 831 | * @param {Object} element The angular element or template 832 | * @param {Function} link [optional] A post-link function, or an object with function(s) registered via pre and post properties. 833 | * @returns An object containing the linking functions. 834 | */ 835 | compile: function(element, link) { 836 | // Normalize the link parameter 837 | if(angular.isFunction(link)) { 838 | link = { post: link }; 839 | } 840 | 841 | var compiledContents; 842 | return { 843 | pre: (link && link.pre) ? link.pre : null, 844 | /** 845 | * Compiles and re-adds the contents 846 | */ 847 | post: function(scope, element, attrs, trvw) { 848 | // Compile our template 849 | if(!compiledContents) { 850 | compiledContents = $compile(trvw.getNodeTpl()); 851 | } 852 | // Add the compiled template 853 | compiledContents(scope, function(clone) { 854 | element.append(clone); 855 | }); 856 | 857 | // Call the post-linking function, if any 858 | if(link && link.post) { 859 | link.post.apply(null, arguments); 860 | } 861 | } 862 | }; 863 | } 864 | }; 865 | }]); 866 | 867 | 868 | /** 869 | * Manager for treeview data stores 870 | * 871 | * Used to assist treeview operations, e.g. selecting or validating a tree-like 872 | * collection. 873 | * 874 | * @package ivh.treeview 875 | * @copyright 2014 iVantage Health Analytics, Inc. 876 | */ 877 | 878 | angular.module('ivh.treeview') 879 | .factory('ivhTreeviewMgr', ['ivhTreeviewOptions', 'ivhTreeviewBfs', function(ivhTreeviewOptions, ivhTreeviewBfs) { 880 | 'use strict'; 881 | 882 | var ng = angular 883 | , options = ivhTreeviewOptions() 884 | , exports = {}; 885 | 886 | // The make* methods and validateParent need to be bound to an options 887 | // object 888 | var makeDeselected = function(node) { 889 | node[this.selectedAttribute] = false; 890 | node[this.indeterminateAttribute] = false; 891 | }; 892 | 893 | var makeSelected = function(node) { 894 | node[this.selectedAttribute] = true; 895 | node[this.indeterminateAttribute] = false; 896 | }; 897 | 898 | var validateParent = function(node) { 899 | var children = node[this.childrenAttribute] 900 | , selectedAttr = this.selectedAttribute 901 | , indeterminateAttr = this.indeterminateAttribute 902 | , numSelected = 0 903 | , numIndeterminate = 0; 904 | ng.forEach(children, function(n, ix) { 905 | if(n[selectedAttr]) { 906 | numSelected++; 907 | } else { 908 | if(n[indeterminateAttr]) { 909 | numIndeterminate++; 910 | } 911 | } 912 | }); 913 | 914 | if(0 === numSelected && 0 === numIndeterminate) { 915 | node[selectedAttr] = false; 916 | node[indeterminateAttr] = false; 917 | } else if(numSelected === children.length) { 918 | node[selectedAttr] = true; 919 | node[indeterminateAttr] = false; 920 | } else { 921 | node[selectedAttr] = false; 922 | node[indeterminateAttr] = true; 923 | } 924 | }; 925 | 926 | var isId = function(val) { 927 | return ng.isString(val) || ng.isNumber(val); 928 | }; 929 | 930 | var findNode = function(tree, node, opts, cb) { 931 | var useId = isId(node) 932 | , proceed = true 933 | , idAttr = opts.idAttribute; 934 | 935 | // Our return values 936 | var foundNode = null 937 | , foundParents = []; 938 | 939 | ivhTreeviewBfs(tree, opts, function(n, p) { 940 | var isNode = proceed && (useId ? 941 | node === n[idAttr] : 942 | node === n); 943 | 944 | if(isNode) { 945 | // I've been looking for you all my life 946 | proceed = false; 947 | foundNode = n; 948 | foundParents = p; 949 | } 950 | 951 | return proceed; 952 | }); 953 | 954 | return cb(foundNode, foundParents); 955 | }; 956 | 957 | /** 958 | * Select (or deselect) a tree node 959 | * 960 | * This method will update the rest of the tree to account for your change. 961 | * 962 | * You may alternatively pass an id as `node`, in which case the tree will 963 | * be searched for your item. 964 | * 965 | * @param {Object|Array} tree The tree data 966 | * @param {Object|String} node The node (or id) to (de)select 967 | * @param {Object} opts [optional] Options to override default options with 968 | * @param {Boolean} isSelected [optional] Whether or not to select `node`, defaults to `true` 969 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 970 | */ 971 | exports.select = function(tree, node, opts, isSelected) { 972 | if(arguments.length > 2) { 973 | if(typeof opts === 'boolean') { 974 | isSelected = opts; 975 | opts = {}; 976 | } 977 | } 978 | opts = ng.extend({}, options, opts); 979 | isSelected = ng.isDefined(isSelected) ? isSelected : true; 980 | 981 | var useId = isId(node) 982 | , proceed = true 983 | , idAttr = opts.idAttribute; 984 | 985 | ivhTreeviewBfs(tree, opts, function(n, p) { 986 | var isNode = proceed && (useId ? 987 | node === n[idAttr] : 988 | node === n); 989 | 990 | if(isNode) { 991 | // I've been looking for you all my life 992 | proceed = false; 993 | 994 | var cb = isSelected ? 995 | makeSelected.bind(opts) : 996 | makeDeselected.bind(opts); 997 | 998 | if (opts.disableCheckboxSelectionPropagation) { 999 | cb(n); 1000 | } else { 1001 | ivhTreeviewBfs(n, opts, cb); 1002 | ng.forEach(p, validateParent.bind(opts)); 1003 | } 1004 | } 1005 | 1006 | return proceed; 1007 | }); 1008 | 1009 | return exports; 1010 | }; 1011 | 1012 | /** 1013 | * Select all nodes in a tree 1014 | * 1015 | * `opts` will default to an empty object, `isSelected` defaults to `true`. 1016 | * 1017 | * @param {Object|Array} tree The tree data 1018 | * @param {Object} opts [optional] Default options overrides 1019 | * @param {Boolean} isSelected [optional] Whether or not to select items 1020 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1021 | */ 1022 | exports.selectAll = function(tree, opts, isSelected) { 1023 | if(arguments.length > 1) { 1024 | if(typeof opts === 'boolean') { 1025 | isSelected = opts; 1026 | opts = {}; 1027 | } 1028 | } 1029 | 1030 | opts = ng.extend({}, options, opts); 1031 | isSelected = ng.isDefined(isSelected) ? isSelected : true; 1032 | 1033 | var selectedAttr = opts.selectedAttribute 1034 | , indeterminateAttr = opts.indeterminateAttribute; 1035 | 1036 | ivhTreeviewBfs(tree, opts, function(node) { 1037 | node[selectedAttr] = isSelected; 1038 | node[indeterminateAttr] = false; 1039 | }); 1040 | 1041 | return exports; 1042 | }; 1043 | 1044 | /** 1045 | * Select or deselect each of the passed items 1046 | * 1047 | * Eventually it would be nice if this did something more intelligent than 1048 | * just calling `select` on each item in the array... 1049 | * 1050 | * @param {Object|Array} tree The tree data 1051 | * @param {Array} nodes The array of nodes or node ids 1052 | * @param {Object} opts [optional] Default options overrides 1053 | * @param {Boolean} isSelected [optional] Whether or not to select items 1054 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1055 | */ 1056 | exports.selectEach = function(tree, nodes, opts, isSelected) { 1057 | /** 1058 | * @todo Surely we can do something better than this... 1059 | */ 1060 | ng.forEach(nodes, function(node) { 1061 | exports.select(tree, node, opts, isSelected); 1062 | }); 1063 | return exports; 1064 | }; 1065 | 1066 | /** 1067 | * Deselect a tree node 1068 | * 1069 | * Delegates to `ivhTreeviewMgr.select` with `isSelected` set to `false`. 1070 | * 1071 | * @param {Object|Array} tree The tree data 1072 | * @param {Object|String} node The node (or id) to (de)select 1073 | * @param {Object} opts [optional] Options to override default options with 1074 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1075 | */ 1076 | exports.deselect = function(tree, node, opts) { 1077 | return exports.select(tree, node, opts, false); 1078 | }; 1079 | 1080 | /** 1081 | * Deselect all nodes in a tree 1082 | * 1083 | * Delegates to `ivhTreeviewMgr.selectAll` with `isSelected` set to `false`. 1084 | * 1085 | * @param {Object|Array} tree The tree data 1086 | * @param {Object} opts [optional] Default options overrides 1087 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1088 | */ 1089 | exports.deselectAll = function(tree, opts) { 1090 | return exports.selectAll(tree, opts, false); 1091 | }; 1092 | 1093 | /** 1094 | * Deselect each of the passed items 1095 | * 1096 | * Delegates to `ivhTreeviewMgr.selectEach` with `isSelected` set to 1097 | * `false`. 1098 | * 1099 | * @param {Object|Array} tree The tree data 1100 | * @param {Array} nodes The array of nodes or node ids 1101 | * @param {Object} opts [optional] Default options overrides 1102 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1103 | */ 1104 | exports.deselectEach = function(tree, nodes, opts) { 1105 | return exports.selectEach(tree, nodes, opts, false); 1106 | }; 1107 | 1108 | /** 1109 | * Validate tree for parent/child selection consistency 1110 | * 1111 | * Assumes `bias` as default selected state. The first element with 1112 | * `node.select !== bias` will be assumed correct. For example, if `bias` is 1113 | * `true` (the default) we'll traverse the tree until we come to an 1114 | * unselected node at which point we stop and deselect each of that node's 1115 | * children (and their children, etc.). 1116 | * 1117 | * Indeterminate states will also be resolved. 1118 | * 1119 | * @param {Object|Array} tree The tree data 1120 | * @param {Object} opts [optional] Options to override default options with 1121 | * @param {Boolean} bias [optional] Default selected state 1122 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1123 | */ 1124 | exports.validate = function(tree, opts, bias) { 1125 | if(!tree) { 1126 | // Guard against uninitialized trees 1127 | return exports; 1128 | } 1129 | 1130 | if(arguments.length > 1) { 1131 | if(typeof opts === 'boolean') { 1132 | bias = opts; 1133 | opts = {}; 1134 | } 1135 | } 1136 | opts = ng.extend({}, options, opts); 1137 | bias = ng.isDefined(bias) ? bias : opts.defaultSelectedState; 1138 | 1139 | var selectedAttr = opts.selectedAttribute 1140 | , indeterminateAttr = opts.indeterminateAttribute; 1141 | 1142 | ivhTreeviewBfs(tree, opts, function(node, parents) { 1143 | if(ng.isDefined(node[selectedAttr]) && node[selectedAttr] !== bias) { 1144 | exports.select(tree, node, opts, !bias); 1145 | return false; 1146 | } else { 1147 | node[selectedAttr] = bias; 1148 | node[indeterminateAttr] = false; 1149 | } 1150 | }); 1151 | 1152 | return exports; 1153 | }; 1154 | 1155 | /** 1156 | * Expand/collapse a given tree node 1157 | * 1158 | * `node` may be either an actual tree node object or a node id. 1159 | * 1160 | * `opts` may override any of the defaults set by `ivhTreeviewOptions`. 1161 | * 1162 | * @param {Object|Array} tree The tree data 1163 | * @param {Object|String} node The node (or id) to expand/collapse 1164 | * @param {Object} opts [optional] Options to override default options with 1165 | * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true` 1166 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1167 | */ 1168 | exports.expand = function(tree, node, opts, isExpanded) { 1169 | if(arguments.length > 2) { 1170 | if(typeof opts === 'boolean') { 1171 | isExpanded = opts; 1172 | opts = {}; 1173 | } 1174 | } 1175 | opts = ng.extend({}, options, opts); 1176 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true; 1177 | 1178 | var useId = isId(node) 1179 | , expandedAttr = opts.expandedAttribute; 1180 | 1181 | if(!useId) { 1182 | // No need to do any searching if we already have the node in hand 1183 | node[expandedAttr] = isExpanded; 1184 | return exports; 1185 | } 1186 | 1187 | return findNode(tree, node, opts, function(n, p) { 1188 | n[expandedAttr] = isExpanded; 1189 | return exports; 1190 | }); 1191 | }; 1192 | 1193 | /** 1194 | * Expand/collapse a given tree node and its children 1195 | * 1196 | * `node` may be either an actual tree node object or a node id. You may 1197 | * leave off `node` entirely to expand/collapse the entire tree, however, if 1198 | * you specify a value for `opts` or `isExpanded` you must provide a value 1199 | * for `node`. 1200 | * 1201 | * `opts` may override any of the defaults set by `ivhTreeviewOptions`. 1202 | * 1203 | * @param {Object|Array} tree The tree data 1204 | * @param {Object|String} node [optional*] The node (or id) to expand/collapse recursively 1205 | * @param {Object} opts [optional] Options to override default options with 1206 | * @param {Boolean} isExpanded [optional] Whether or not to expand `node`, defaults to `true` 1207 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1208 | */ 1209 | exports.expandRecursive = function(tree, node, opts, isExpanded) { 1210 | if(arguments.length > 2) { 1211 | if(typeof opts === 'boolean') { 1212 | isExpanded = opts; 1213 | opts = {}; 1214 | } 1215 | } 1216 | node = ng.isDefined(node) ? node : tree; 1217 | opts = ng.extend({}, options, opts); 1218 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true; 1219 | 1220 | var useId = isId(node) 1221 | , expandedAttr = opts.expandedAttribute 1222 | , branch; 1223 | 1224 | // If we have an ID first resolve it to an actual node in the tree 1225 | if(useId) { 1226 | findNode(tree, node, opts, function(n, p) { 1227 | branch = n; 1228 | }); 1229 | } else { 1230 | branch = node; 1231 | } 1232 | 1233 | if(branch) { 1234 | ivhTreeviewBfs(branch, opts, function(n, p) { 1235 | n[expandedAttr] = isExpanded; 1236 | }); 1237 | } 1238 | 1239 | return exports; 1240 | }; 1241 | 1242 | /** 1243 | * Collapse a given tree node 1244 | * 1245 | * Delegates to `exports.expand` with `isExpanded` set to `false`. 1246 | * 1247 | * @param {Object|Array} tree The tree data 1248 | * @param {Object|String} node The node (or id) to collapse 1249 | * @param {Object} opts [optional] Options to override default options with 1250 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1251 | */ 1252 | exports.collapse = function(tree, node, opts) { 1253 | return exports.expand(tree, node, opts, false); 1254 | }; 1255 | 1256 | /** 1257 | * Collapse a given tree node and its children 1258 | * 1259 | * Delegates to `exports.expandRecursive` with `isExpanded` set to `false`. 1260 | * 1261 | * @param {Object|Array} tree The tree data 1262 | * @param {Object|String} node The node (or id) to expand/collapse recursively 1263 | * @param {Object} opts [optional] Options to override default options with 1264 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1265 | */ 1266 | exports.collapseRecursive = function(tree, node, opts, isExpanded) { 1267 | return exports.expandRecursive(tree, node, opts, false); 1268 | }; 1269 | 1270 | /** 1271 | * Expand[/collapse] all parents of a given node, i.e. "reveal" the node 1272 | * 1273 | * @param {Object|Array} tree The tree data 1274 | * @param {Object|String} node The node (or id) to expand to 1275 | * @param {Object} opts [optional] Options to override default options with 1276 | * @param {Boolean} isExpanded [optional] Whether or not to expand parent nodes 1277 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1278 | */ 1279 | exports.expandTo = function(tree, node, opts, isExpanded) { 1280 | if(arguments.length > 2) { 1281 | if(typeof opts === 'boolean') { 1282 | isExpanded = opts; 1283 | opts = {}; 1284 | } 1285 | } 1286 | opts = ng.extend({}, options, opts); 1287 | isExpanded = ng.isDefined(isExpanded) ? isExpanded : true; 1288 | 1289 | var expandedAttr = opts.expandedAttribute; 1290 | 1291 | var expandCollapseNode = function(n) { 1292 | n[expandedAttr] = isExpanded; 1293 | }; 1294 | 1295 | // Even if wer were given the actual node and not its ID we must still 1296 | // traverse the tree to find that node's parents. 1297 | return findNode(tree, node, opts, function(n, p) { 1298 | ng.forEach(p, expandCollapseNode); 1299 | return exports; 1300 | }); 1301 | }; 1302 | 1303 | /** 1304 | * Collapse all parents of a give node 1305 | * 1306 | * Delegates to `exports.expandTo` with `isExpanded` set to `false`. 1307 | * 1308 | * @param {Object|Array} tree The tree data 1309 | * @param {Object|String} node The node (or id) to expand to 1310 | * @param {Object} opts [optional] Options to override default options with 1311 | * @return {Object} Returns the ivhTreeviewMgr instance for chaining 1312 | */ 1313 | exports.collapseParents = function(tree, node, opts) { 1314 | return exports.expandTo(tree, node, opts, false); 1315 | }; 1316 | 1317 | return exports; 1318 | } 1319 | ]); 1320 | 1321 | 1322 | /** 1323 | * Global options for ivhTreeview 1324 | * 1325 | * @package ivh.treeview 1326 | * @copyright 2014 iVantage Health Analytics, Inc. 1327 | */ 1328 | 1329 | angular.module('ivh.treeview').provider('ivhTreeviewOptions', [ 1330 | 'ivhTreeviewInterpolateStartSymbol', 'ivhTreeviewInterpolateEndSymbol', 1331 | function(ivhTreeviewInterpolateStartSymbol, ivhTreeviewInterpolateEndSymbol) { 1332 | 'use strict'; 1333 | 1334 | var symbolStart = ivhTreeviewInterpolateStartSymbol 1335 | , symbolEnd = ivhTreeviewInterpolateEndSymbol; 1336 | 1337 | var options = { 1338 | /** 1339 | * ID attribute 1340 | * 1341 | * For selecting nodes by identifier rather than reference 1342 | */ 1343 | idAttribute: 'id', 1344 | 1345 | /** 1346 | * Collection item attribute to use for labels 1347 | */ 1348 | labelAttribute: 'label', 1349 | 1350 | /** 1351 | * Collection item attribute to use for child nodes 1352 | */ 1353 | childrenAttribute: 'children', 1354 | 1355 | /** 1356 | * Collection item attribute to use for selected state 1357 | */ 1358 | selectedAttribute: 'selected', 1359 | 1360 | /** 1361 | * Controls whether branches are initially expanded or collapsed 1362 | * 1363 | * A value of `0` means the tree will be entirely collapsd (the default 1364 | * state) otherwise branches will be expanded up to the specified depth. Use 1365 | * `-1` to have the tree entirely expanded. 1366 | */ 1367 | expandToDepth: 0, 1368 | 1369 | /** 1370 | * Whether or not to use checkboxes 1371 | * 1372 | * If `false` the markup to support checkboxes is not included in the 1373 | * directive. 1374 | */ 1375 | useCheckboxes: true, 1376 | 1377 | /** 1378 | * If set to true the checkboxes are independent on each other (no state 1379 | * propagation to children and revalidation of parents' states). 1380 | * If you set to true, you should set also `validate` property to `false` 1381 | * and avoid explicit calling of `ivhTreeviewMgr.validate()`. 1382 | */ 1383 | disableCheckboxSelectionPropagation: false, 1384 | 1385 | /** 1386 | * Whether or not directive should validate treestore on startup 1387 | */ 1388 | validate: true, 1389 | 1390 | /** 1391 | * Collection item attribute to track intermediate states 1392 | */ 1393 | indeterminateAttribute: '__ivhTreeviewIndeterminate', 1394 | 1395 | /** 1396 | * Collection item attribute to track expanded status 1397 | */ 1398 | expandedAttribute: '__ivhTreeviewExpanded', 1399 | 1400 | /** 1401 | * Default selected state when validating 1402 | */ 1403 | defaultSelectedState: true, 1404 | 1405 | /** 1406 | * Template for expanded twisties 1407 | */ 1408 | twistieExpandedTpl: '(-)', 1409 | 1410 | /** 1411 | * Template for collapsed twisties 1412 | */ 1413 | twistieCollapsedTpl: '(+)', 1414 | 1415 | /** 1416 | * Template for leaf twisties (i.e. no children) 1417 | */ 1418 | twistieLeafTpl: 'o', 1419 | 1420 | /** 1421 | * Template for tree nodes 1422 | */ 1423 | nodeTpl: [ 1424 | '
', 1425 | '', 1426 | '', 1427 | '', 1428 | '', 1430 | '', 1431 | '', 1432 | '{{trvw.label(node)}}', 1433 | '', 1434 | '
', 1435 | '
' 1436 | ].join('\n') 1437 | .replace(new RegExp('{{', 'g'), symbolStart) 1438 | .replace(new RegExp('}}', 'g'), symbolEnd) 1439 | }; 1440 | 1441 | /** 1442 | * Update global options 1443 | * 1444 | * @param {Object} opts options object to override defaults with 1445 | */ 1446 | this.set = function(opts) { 1447 | angular.extend(options, opts); 1448 | }; 1449 | 1450 | this.$get = function() { 1451 | /** 1452 | * Get a copy of the global options 1453 | * 1454 | * @return {Object} The options object 1455 | */ 1456 | return function() { 1457 | return angular.copy(options); 1458 | }; 1459 | }; 1460 | }]); 1461 | --------------------------------------------------------------------------------