├── .gitattributes ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE ├── bower.json ├── dist ├── angular-bootstrap-multiselect-templates.js ├── angular-bootstrap-multiselect.js └── angular-bootstrap-multiselect.min.js ├── index.js ├── package.json ├── readme.md ├── src ├── multiselect.html └── multiselect.js └── test ├── e2e ├── multiselect-async-datasources-e2e-test.js ├── multiselect-objectmodels-e2e-test.js ├── multiselect-stringmodels-e2e-test.js ├── protractor.conf.js ├── test-async-datasources.html ├── test-objectmodels.html └── test-stringmodels.html └── unit ├── karma.conf.js ├── multiselect-objectmodels-test.js └── multiselect-stringmodels-test.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specific to this project 2 | test/e2e/screenshots 3 | 4 | 5 | # Numerous always-ignore extensions 6 | *.diff 7 | *.err 8 | *.orig 9 | *.log 10 | *.rej 11 | *.swo 12 | *.swp 13 | *.zip 14 | *.vi 15 | *~ 16 | 17 | # OS or Editor folders 18 | .DS_Store 19 | ._* 20 | Thumbs.db 21 | .cache 22 | .project 23 | .settings 24 | .tmproj 25 | *.esproj 26 | nbproject 27 | *.sublime-project 28 | *.sublime-workspace 29 | .idea 30 | .bowerrc 31 | 32 | # Folders to ignore 33 | node_modules 34 | bower_components 35 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireOperatorBeforeLineBreak": true, 12 | "validateIndentation": 4, 13 | "validateQuoteMarks": "'", 14 | 15 | "disallowMixedSpacesAndTabs": true, 16 | "disallowTrailingWhitespace": true, 17 | "disallowSpaceAfterPrefixUnaryOperators": true, 18 | "disallowMultipleVarDecl": true, 19 | 20 | "requireSpaceAfterKeywords": [ 21 | "if", 22 | "else", 23 | "for", 24 | "while", 25 | "do", 26 | "switch", 27 | "return", 28 | "try", 29 | "catch" 30 | ], 31 | "requireSpaceBeforeBinaryOperators": [ 32 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 33 | "&=", "|=", "^=", "+=", 34 | 35 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 36 | "|", "^", "&&", "||", "===", "==", ">=", 37 | "<=", "<", ">", "!=", "!==" 38 | ], 39 | "requireSpaceAfterBinaryOperators": true, 40 | "requireSpacesInConditionalExpression": true, 41 | "requireSpaceBeforeBlockStatements": true, 42 | "requireLineFeedAtFileEnd": true, 43 | "disallowSpacesInsideObjectBrackets": "all", 44 | "disallowSpacesInsideArrayBrackets": "all", 45 | "disallowSpacesInsideParentheses": true, 46 | 47 | 48 | "jsDoc": { 49 | "checkParamNames": true, 50 | "requireParamTypes": true 51 | }, 52 | 53 | "disallowNewlineBeforeBlockStatements": true 54 | } 55 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": true, 3 | "browser": true, 4 | "eqeqeq": false, 5 | "eqnull": true, 6 | "es3": true, 7 | "expr": true, 8 | "jquery": true, 9 | "latedef": true, 10 | "laxbreak": true, 11 | "nonbsp": true, 12 | "strict": true, 13 | "undef": true, 14 | "unused": true, 15 | "predef": ["angular", "_"] 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | before_install: 5 | - npm install -g grunt-cli bower 6 | - bower install 7 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | grunt.loadNpmTasks('grunt-contrib-clean'); 4 | grunt.loadNpmTasks('grunt-contrib-jshint'); 5 | grunt.loadNpmTasks('grunt-contrib-concat'); 6 | grunt.loadNpmTasks("grunt-contrib-watch"); 7 | grunt.loadNpmTasks('grunt-jscs'); 8 | grunt.loadNpmTasks('grunt-contrib-uglify'); 9 | grunt.loadNpmTasks('grunt-karma'); 10 | grunt.loadNpmTasks('grunt-maven-tasks'); 11 | grunt.loadNpmTasks('grunt-html2js'); 12 | grunt.loadNpmTasks('grunt-contrib-connect'); 13 | grunt.loadNpmTasks('grunt-protractor-runner'); 14 | grunt.loadNpmTasks('grunt-bootlint'); 15 | grunt.loadNpmTasks('grunt-ng-annotate'); 16 | grunt.loadNpmTasks('grunt-bump'); 17 | 18 | // Project configuration. 19 | grunt.initConfig({ 20 | 21 | pkg: grunt.file.readJSON('package.json'), 22 | 23 | clean: { 24 | options: { 25 | force: true 26 | }, 27 | dist: ['dist'] 28 | }, 29 | 30 | ngAnnotate: { 31 | add: { 32 | options: { 33 | singleQuotes: true 34 | }, 35 | files: { 36 | 'dist/angular-bootstrap-multiselect.js': 'dist/angular-bootstrap-multiselect.js' 37 | } 38 | } 39 | }, 40 | 41 | bootlint: { 42 | options: { 43 | stoponerror: true, 44 | relaxerror: ['E001', 'W001', 'W002', 'W003', 'W005'] 45 | }, 46 | files: ['src/**/*.html'] 47 | }, 48 | 49 | jshint: { 50 | options: { 51 | jshintrc: '.jshintrc' 52 | }, 53 | sources: { 54 | src: ['src/**/*.js'] 55 | } 56 | }, 57 | 58 | jscs: { 59 | options: { 60 | config: '.jscsrc' 61 | }, 62 | src: { 63 | src: '<%= jshint.sources.src %>' 64 | } 65 | }, 66 | 67 | html2js: { 68 | options: { 69 | base: 'src', 70 | module: 'btorfs.multiselect.templates' 71 | }, 72 | main: { 73 | src: ['src/**/*.html'], 74 | dest: 'dist/angular-bootstrap-multiselect-templates.js' 75 | } 76 | }, 77 | 78 | concat: { 79 | options: {}, 80 | files: { 81 | src: ['src/**/*.js', 'dist/angular-bootstrap-multiselect-templates.js'], 82 | dest: 'dist/angular-bootstrap-multiselect.js' 83 | } 84 | }, 85 | 86 | uglify: { 87 | files: { 88 | src: 'dist/angular-bootstrap-multiselect.js', 89 | dest: 'dist/angular-bootstrap-multiselect.min.js' 90 | } 91 | }, 92 | 93 | karma: { 94 | ci: { 95 | configFile: 'test/unit/karma.conf.js', 96 | reporters: ["dots"] 97 | }, 98 | dev: { 99 | configFile: 'test/unit/karma.conf.js' 100 | } 101 | }, 102 | 103 | watch: { 104 | karma: { 105 | files: ['src/**/*', 'test/unit/**/*'], 106 | tasks: ['build', 'karma:dev'] 107 | } 108 | }, 109 | 110 | connect: { 111 | e2e: { 112 | options: { 113 | port: 9000, 114 | base: '.' 115 | } 116 | } 117 | }, 118 | 119 | protractor: { 120 | options: { 121 | keepAlive: false, 122 | configFile: "test/e2e/protractor.conf.js", 123 | args: { 124 | baseUrl: 'http://localhost:9000' 125 | } 126 | }, 127 | run: {} 128 | }, 129 | 130 | bump: { 131 | options: { 132 | files: ['package.json'], 133 | commitFiles: ['package.json'], 134 | tagName: '%VERSION%', 135 | pushTo: 'origin' 136 | } 137 | } 138 | 139 | }); 140 | 141 | // Quality checks 142 | grunt.registerTask('check', ['bootlint', 'jshint', 'jscs']); 143 | 144 | // Build files 145 | grunt.registerTask('build', ['html2js', 'concat', 'ngAnnotate', 'uglify']); 146 | 147 | // Continuous integration task 148 | grunt.registerTask('ci', ['clean', 'check', 'build', 'karma:ci']); 149 | 150 | // Run UI tests 151 | grunt.registerTask('e2e', ['connect', 'protractor']); 152 | 153 | // Continously build and execute unit tests after every file change, during development 154 | grunt.registerTask('dev', ['build', 'watch']); 155 | 156 | // Default task: does everything including UI tests 157 | grunt.registerTask('default', ['clean', 'check', 'build', 'karma:ci', 'e2e']); 158 | }; 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ben Torfs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-bootstrap-multiselect", 3 | "main": "dist/angular-bootstrap-multiselect.js", 4 | "homepage": "http://bentorfs.github.io/angular-bootstrap-multiselect/", 5 | "ignore": [ 6 | "src", 7 | "test", 8 | "node_modules", 9 | "bower_components", 10 | "Gruntfile.js", 11 | "**/.*" 12 | ], 13 | "dependencies": { 14 | "angular": ">=1.3.0" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": ">=1.3.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /dist/angular-bootstrap-multiselect-templates.js: -------------------------------------------------------------------------------- 1 | angular.module('btorfs.multiselect.templates', ['multiselect.html']); 2 | 3 | angular.module("multiselect.html", []).run(["$templateCache", function ($templateCache) { 4 | $templateCache.put("multiselect.html", 5 | "
\n" + 6 | " \n" + 9 | " \n" + 56 | "
\n" + 57 | ""); 58 | }]); 59 | -------------------------------------------------------------------------------- /dist/angular-bootstrap-multiselect.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var multiselect = angular.module('btorfs.multiselect', ['btorfs.multiselect.templates']); 5 | 6 | multiselect.getRecursiveProperty = function (object, path) { 7 | return path.split('.').reduce(function (object, x) { 8 | if (object) { 9 | return object[x]; 10 | } else { 11 | return null; 12 | } 13 | }, object) 14 | }; 15 | 16 | multiselect.directive('multiselect', ['$filter', '$document', '$log', function ($filter, $document, $log) { 17 | return { 18 | restrict: 'AE', 19 | scope: { 20 | options: '=', 21 | displayProp: '@', 22 | idProp: '@', 23 | searchLimit: '=?', 24 | selectionLimit: '=?', 25 | showSelectAll: '=?', 26 | showUnselectAll: '=?', 27 | showSearch: '=?', 28 | searchFilter: '=?', 29 | disabled: '=?ngDisabled', 30 | labels: '=?', 31 | classesBtn: '=?', 32 | styleBtn: '=?', 33 | showTooltip: '=?', 34 | placeholder: '@?' 35 | }, 36 | require: 'ngModel', 37 | templateUrl: 'multiselect.html', 38 | controller: ['$scope', function($scope) { 39 | if (angular.isUndefined($scope.classesBtn)) { 40 | $scope.classesBtn = ['btn-block','btn-default']; 41 | } 42 | if (angular.isUndefined($scope.styleBtn)) { 43 | $scope.styleBtn = {}; 44 | } 45 | }], 46 | link: function ($scope, $element, $attrs, $ngModelCtrl) { 47 | $scope.selectionLimit = $scope.selectionLimit || 0; 48 | $scope.searchLimit = $scope.searchLimit || 25; 49 | 50 | $scope.searchFilter = ''; 51 | 52 | $scope.resolvedOptions = []; 53 | if (typeof $scope.options !== 'function') { 54 | $scope.resolvedOptions = $scope.options; 55 | } 56 | 57 | if (typeof $attrs.disabled != 'undefined') { 58 | $scope.disabled = true; 59 | } 60 | 61 | 62 | var closeHandler = function (event) { 63 | if (!$element[0].contains(event.target)) { 64 | $scope.$apply(function () { 65 | $scope.open = false; 66 | }); 67 | } 68 | }; 69 | 70 | $document.on('click', closeHandler); 71 | 72 | var updateSelectionLists = function () { 73 | if (!$ngModelCtrl.$viewValue) { 74 | if ($scope.selectedOptions) { 75 | $scope.selectedOptions = []; 76 | } 77 | $scope.unselectedOptions = $scope.resolvedOptions.slice(); // Take a copy 78 | } else { 79 | $scope.selectedOptions = $scope.resolvedOptions.filter(function (el) { 80 | var id = $scope.getId(el); 81 | for (var i = 0; i < $ngModelCtrl.$viewValue.length; i++) { 82 | var selectedId = $scope.getId($ngModelCtrl.$viewValue[i]); 83 | if (id === selectedId) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | }); 89 | $scope.unselectedOptions = $scope.resolvedOptions.filter(function (el) { 90 | return $scope.selectedOptions.indexOf(el) < 0; 91 | }); 92 | } 93 | }; 94 | 95 | $scope.toggleDropdown = function () { 96 | $scope.open = !$scope.open; 97 | $scope.resolvedOptions = $scope.options; 98 | updateSelectionLists(); 99 | }; 100 | 101 | $ngModelCtrl.$render = function () { 102 | updateSelectionLists(); 103 | }; 104 | 105 | $ngModelCtrl.$viewChangeListeners.push(function () { 106 | updateSelectionLists(); 107 | }); 108 | 109 | $ngModelCtrl.$isEmpty = function (value) { 110 | if (value) { 111 | return (value.length === 0); 112 | } else { 113 | return true; 114 | } 115 | }; 116 | 117 | var watcher = $scope.$watch('selectedOptions', function () { 118 | $ngModelCtrl.$setViewValue(angular.copy($scope.selectedOptions)); 119 | }, true); 120 | 121 | $scope.$on('$destroy', function () { 122 | $document.off('click', closeHandler); 123 | if (watcher) { 124 | watcher(); // Clean watcher 125 | } 126 | }); 127 | 128 | $scope.getButtonText = function () { 129 | if ($scope.selectedOptions && $scope.selectedOptions.length === 1) { 130 | return $scope.getDisplay($scope.selectedOptions[0]); 131 | } 132 | if ($scope.selectedOptions && $scope.selectedOptions.length > 1) { 133 | var totalSelected = angular.isDefined($scope.selectedOptions) ? $scope.selectedOptions.length : 0; 134 | if (totalSelected === 0) { 135 | return $scope.labels && $scope.labels.select ? $scope.labels.select : ($scope.placeholder || 'Select'); 136 | } else { 137 | return totalSelected + ' ' + ($scope.labels && $scope.labels.itemsSelected ? $scope.labels.itemsSelected : 'selected'); 138 | } 139 | } else { 140 | return $scope.labels && $scope.labels.select ? $scope.labels.select : ($scope.placeholder || 'Select'); 141 | } 142 | }; 143 | 144 | $scope.selectAll = function () { 145 | $scope.selectedOptions = $scope.resolvedOptions.slice(); // Take a copy; 146 | $scope.unselectedOptions = []; 147 | }; 148 | 149 | $scope.unselectAll = function () { 150 | $scope.selectedOptions = []; 151 | $scope.unselectedOptions = $scope.resolvedOptions.slice(); // Take a copy; 152 | }; 153 | 154 | $scope.toggleItem = function (item) { 155 | if (typeof $scope.selectedOptions === 'undefined') { 156 | $scope.selectedOptions = []; 157 | } 158 | var selectedIndex = $scope.selectedOptions.indexOf(item); 159 | var currentlySelected = (selectedIndex !== -1); 160 | if (currentlySelected) { 161 | $scope.unselectedOptions.push($scope.selectedOptions[selectedIndex]); 162 | $scope.selectedOptions.splice(selectedIndex, 1); 163 | } else if (!currentlySelected && ($scope.selectionLimit === 0 || $scope.selectedOptions.length < $scope.selectionLimit)) { 164 | var unselectedIndex = $scope.unselectedOptions.indexOf(item); 165 | $scope.unselectedOptions.splice(unselectedIndex, 1); 166 | $scope.selectedOptions.push(item); 167 | } 168 | }; 169 | 170 | $scope.getId = function (item) { 171 | if (angular.isString(item)) { 172 | return item; 173 | } else if (angular.isObject(item)) { 174 | if ($scope.idProp) { 175 | return multiselect.getRecursiveProperty(item, $scope.idProp); 176 | } else { 177 | $log.error('Multiselect: when using objects as model, a idProp value is mandatory.'); 178 | return ''; 179 | } 180 | } else { 181 | return item; 182 | } 183 | }; 184 | 185 | $scope.getDisplay = function (item) { 186 | if (angular.isString(item)) { 187 | return item; 188 | } else if (angular.isObject(item)) { 189 | if ($scope.displayProp) { 190 | return multiselect.getRecursiveProperty(item, $scope.displayProp); 191 | } else { 192 | $log.error('Multiselect: when using objects as model, a displayProp value is mandatory.'); 193 | return ''; 194 | } 195 | } else { 196 | return item; 197 | } 198 | }; 199 | 200 | $scope.isSelected = function (item) { 201 | if (!$scope.selectedOptions) { 202 | return false; 203 | } 204 | var itemId = $scope.getId(item); 205 | for (var i = 0; i < $scope.selectedOptions.length; i++) { 206 | var selectedElement = $scope.selectedOptions[i]; 207 | if ($scope.getId(selectedElement) === itemId) { 208 | return true; 209 | } 210 | } 211 | return false; 212 | }; 213 | 214 | $scope.updateOptions = function () { 215 | if (typeof $scope.options === 'function') { 216 | $scope.options().then(function (resolvedOptions) { 217 | $scope.resolvedOptions = resolvedOptions; 218 | updateSelectionLists(); 219 | }); 220 | } 221 | }; 222 | 223 | // This search function is optimized to take into account the search limit. 224 | // Using angular limitTo filter is not efficient for big lists, because it still runs the search for 225 | // all elements, even if the limit is reached 226 | $scope.search = function () { 227 | var counter = 0; 228 | return function (item) { 229 | if (counter > $scope.searchLimit) { 230 | return false; 231 | } 232 | var displayName = $scope.getDisplay(item); 233 | if (displayName) { 234 | var result = displayName.toLowerCase().indexOf($scope.searchFilter.toLowerCase()) > -1; 235 | if (result) { 236 | counter++; 237 | } 238 | return result; 239 | } 240 | } 241 | }; 242 | 243 | } 244 | }; 245 | }]); 246 | 247 | }()); 248 | 249 | angular.module('btorfs.multiselect.templates', ['multiselect.html']); 250 | 251 | angular.module("multiselect.html", []).run(["$templateCache", function ($templateCache) { 252 | $templateCache.put("multiselect.html", 253 | "
\n" + 254 | " \n" + 257 | " \n" + 304 | "
\n" + 305 | ""); 306 | }]); 307 | -------------------------------------------------------------------------------- /dist/angular-bootstrap-multiselect.min.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";var a=angular.module("btorfs.multiselect",["btorfs.multiselect.templates"]);a.getRecursiveProperty=function(a,b){return b.split(".").reduce(function(a,b){return a?a[b]:null},a)},a.directive("multiselect",["$filter","$document","$log",function(b,c,d){return{restrict:"AE",scope:{options:"=",displayProp:"@",idProp:"@",searchLimit:"=?",selectionLimit:"=?",showSelectAll:"=?",showUnselectAll:"=?",showSearch:"=?",searchFilter:"=?",disabled:"=?ngDisabled",labels:"=?",classesBtn:"=?",styleBtn:"=?",showTooltip:"=?",placeholder:"@?"},require:"ngModel",templateUrl:"multiselect.html",controller:["$scope",function(a){angular.isUndefined(a.classesBtn)&&(a.classesBtn=["btn-block","btn-default"]);angular.isUndefined(a.styleBtn)&&(a.styleBtn={})}],link:function(b,e,f,g){b.selectionLimit=b.selectionLimit||0,b.searchLimit=b.searchLimit||25,b.searchFilter="",b.resolvedOptions=[],"function"!=typeof b.options&&(b.resolvedOptions=b.options),"undefined"!=typeof f.disabled&&(b.disabled=!0);var h=function(a){e[0].contains(a.target)||b.$apply(function(){b.open=!1})};c.on("click",h);var i=function(){g.$viewValue?(b.selectedOptions=b.resolvedOptions.filter(function(a){for(var c=b.getId(a),d=0;d1){var a=angular.isDefined(b.selectedOptions)?b.selectedOptions.length:0;return 0===a?b.labels&&b.labels.select?b.labels.select:b.placeholder||"Select":a+" "+(b.labels&&b.labels.itemsSelected?b.labels.itemsSelected:"selected")}return b.labels&&b.labels.select?b.labels.select:b.placeholder||"Select"},b.selectAll=function(){b.selectedOptions=b.resolvedOptions.slice(),b.unselectedOptions=[]},b.unselectAll=function(){b.selectedOptions=[],b.unselectedOptions=b.resolvedOptions.slice()},b.toggleItem=function(a){"undefined"==typeof b.selectedOptions&&(b.selectedOptions=[]);var c=b.selectedOptions.indexOf(a),d=c!==-1;if(d)b.unselectedOptions.push(b.selectedOptions[c]),b.selectedOptions.splice(c,1);else if(!d&&(0===b.selectionLimit||b.selectedOptions.lengthb.searchLimit)return!1;var d=b.getDisplay(c);if(d){var e=d.toLowerCase().indexOf(b.searchFilter.toLowerCase())>-1;return e&&a++,e}}}}}}])}(),angular.module("btorfs.multiselect.templates",["multiselect.html"]),angular.module("multiselect.html",[]).run(["$templateCache",function(a){a.put("multiselect.html",'\n')}]); 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/angular-bootstrap-multiselect'); 2 | module.exports = 'btorfs.multiselect'; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-bootstrap-multiselect", 3 | "version": "1.1.11", 4 | "homepage": "http://bentorfs.github.io/angular-bootstrap-multiselect/", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/bentorfs/angular-bootstrap-multiselect.git" 9 | }, 10 | "main": "index.js", 11 | "devDependencies": { 12 | "grunt": "~1.0.1", 13 | "grunt-bootlint": "~0.10.1", 14 | "grunt-bump": "~0.8.0", 15 | "grunt-contrib-clean": "~1.0.0", 16 | "grunt-contrib-concat": "~1.0.1", 17 | "grunt-contrib-connect": "~1.0.2", 18 | "grunt-contrib-copy": "~1.0.0", 19 | "grunt-contrib-jshint": "~1.1.0", 20 | "grunt-contrib-nodeunit": "~1.0.0", 21 | "grunt-contrib-uglify": "~2.0.0", 22 | "grunt-contrib-watch": "~1.0.0", 23 | "grunt-html2js": "~0.3.6", 24 | "grunt-jscs": "~3.0.1", 25 | "grunt-karma": "~2.0.0", 26 | "grunt-maven-tasks": "~1.4.0", 27 | "grunt-ng-annotate": "~3.0.0", 28 | "grunt-protractor-runner": "~4.0.0", 29 | "karma": "~1.3.0", 30 | "karma-chrome-launcher": "~2.0.0", 31 | "karma-jasmine": "~1.1.0", 32 | "karma-phantomjs-launcher": "~1.0.2", 33 | "protractor": "~4.0.13", 34 | "protractor-screenshot-reporter": "0.0.5" 35 | }, 36 | "scripts": { 37 | "pretest": "node node_modules/protractor/bin/webdriver-manager update", 38 | "test": "grunt ci" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Angular Bootstrap Multiselect 2 | ======== 3 | 4 | ![Build status](https://travis-ci.org/bentorfs/angular-bootstrap-multiselect.svg?branch=master) 5 | 6 | Find documentation on [ the github page](http://bentorfs.github.io/angular-bootstrap-multiselect/) 7 | 8 | Contributions welcome -------------------------------------------------------------------------------- /src/multiselect.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | 52 |
53 | -------------------------------------------------------------------------------- /src/multiselect.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var multiselect = angular.module('btorfs.multiselect', ['btorfs.multiselect.templates']); 5 | 6 | multiselect.getRecursiveProperty = function (object, path) { 7 | return path.split('.').reduce(function (object, x) { 8 | if (object) { 9 | return object[x]; 10 | } else { 11 | return null; 12 | } 13 | }, object) 14 | }; 15 | 16 | multiselect.directive('multiselect', function ($filter, $document, $log) { 17 | return { 18 | restrict: 'AE', 19 | scope: { 20 | options: '=', 21 | displayProp: '@', 22 | idProp: '@', 23 | searchLimit: '=?', 24 | selectionLimit: '=?', 25 | showSelectAll: '=?', 26 | showUnselectAll: '=?', 27 | showSearch: '=?', 28 | searchFilter: '=?', 29 | disabled: '=?ngDisabled', 30 | labels: '=?', 31 | classesBtn: '=?', 32 | styleBtn: '=?', 33 | showTooltip: '=?', 34 | placeholder: '@?' 35 | }, 36 | require: 'ngModel', 37 | templateUrl: 'multiselect.html', 38 | controller: function($scope) { 39 | if (angular.isUndefined($scope.classesBtn)) { 40 | $scope.classesBtn = ['btn-block','btn-default']; 41 | } 42 | if (angular.isUndefined($scope.styleBtn)) { 43 | $scope.styleBtn = {}; 44 | } 45 | }, 46 | link: function ($scope, $element, $attrs, $ngModelCtrl) { 47 | $scope.selectionLimit = $scope.selectionLimit || 0; 48 | $scope.searchLimit = $scope.searchLimit || 25; 49 | 50 | $scope.searchFilter = ''; 51 | 52 | $scope.resolvedOptions = []; 53 | if ($scope.options && typeof $scope.options !== 'function') { 54 | $scope.resolvedOptions = $scope.options; 55 | } 56 | 57 | if (typeof $attrs.disabled != 'undefined') { 58 | $scope.disabled = true; 59 | } 60 | 61 | 62 | var closeHandler = function (event) { 63 | if (!$element[0].contains(event.target)) { 64 | $scope.$apply(function () { 65 | $scope.open = false; 66 | }); 67 | } 68 | }; 69 | 70 | $document.on('click', closeHandler); 71 | 72 | var updateSelectionLists = function () { 73 | if (!$ngModelCtrl.$viewValue) { 74 | if ($scope.selectedOptions) { 75 | $scope.selectedOptions = []; 76 | } 77 | $scope.unselectedOptions = $scope.resolvedOptions.slice(); // Take a copy 78 | } else { 79 | $scope.selectedOptions = $scope.resolvedOptions.filter(function (el) { 80 | var id = $scope.getId(el); 81 | for (var i = 0; i < $ngModelCtrl.$viewValue.length; i++) { 82 | var selectedId = $scope.getId($ngModelCtrl.$viewValue[i]); 83 | if (id === selectedId) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | }); 89 | $scope.unselectedOptions = $scope.resolvedOptions.filter(function (el) { 90 | return $scope.selectedOptions.indexOf(el) < 0; 91 | }); 92 | } 93 | }; 94 | 95 | $scope.toggleDropdown = function () { 96 | $scope.open = !$scope.open; 97 | $scope.resolvedOptions = $scope.options; 98 | updateSelectionLists(); 99 | }; 100 | 101 | $ngModelCtrl.$render = function () { 102 | updateSelectionLists(); 103 | }; 104 | 105 | $ngModelCtrl.$viewChangeListeners.push(function () { 106 | updateSelectionLists(); 107 | }); 108 | 109 | $ngModelCtrl.$isEmpty = function (value) { 110 | if (value) { 111 | return (value.length === 0); 112 | } else { 113 | return true; 114 | } 115 | }; 116 | 117 | var watcher = $scope.$watch('selectedOptions', function () { 118 | $ngModelCtrl.$setViewValue(angular.copy($scope.selectedOptions)); 119 | }, true); 120 | 121 | $scope.$on('$destroy', function () { 122 | $document.off('click', closeHandler); 123 | if (watcher) { 124 | watcher(); // Clean watcher 125 | } 126 | }); 127 | 128 | $scope.getButtonText = function () { 129 | if ($scope.selectedOptions && $scope.selectedOptions.length === 1) { 130 | return $scope.getDisplay($scope.selectedOptions[0]); 131 | } 132 | if ($scope.selectedOptions && $scope.selectedOptions.length > 1) { 133 | var totalSelected = angular.isDefined($scope.selectedOptions) ? $scope.selectedOptions.length : 0; 134 | if (totalSelected === 0) { 135 | return $scope.labels && $scope.labels.select ? $scope.labels.select : ($scope.placeholder || 'Select'); 136 | } else { 137 | return totalSelected + ' ' + ($scope.labels && $scope.labels.itemsSelected ? $scope.labels.itemsSelected : 'selected'); 138 | } 139 | } else { 140 | return $scope.labels && $scope.labels.select ? $scope.labels.select : ($scope.placeholder || 'Select'); 141 | } 142 | }; 143 | 144 | $scope.selectAll = function () { 145 | $scope.selectedOptions = $scope.resolvedOptions.slice(); // Take a copy; 146 | $scope.unselectedOptions = []; 147 | }; 148 | 149 | $scope.unselectAll = function () { 150 | $scope.selectedOptions = []; 151 | $scope.unselectedOptions = $scope.resolvedOptions.slice(); // Take a copy; 152 | }; 153 | 154 | $scope.toggleItem = function (item) { 155 | if (typeof $scope.selectedOptions === 'undefined') { 156 | $scope.selectedOptions = []; 157 | } 158 | var selectedIndex = $scope.selectedOptions.indexOf(item); 159 | var currentlySelected = (selectedIndex !== -1); 160 | if (currentlySelected) { 161 | $scope.unselectedOptions.push($scope.selectedOptions[selectedIndex]); 162 | $scope.selectedOptions.splice(selectedIndex, 1); 163 | } else if (!currentlySelected && ($scope.selectionLimit === 0 || $scope.selectedOptions.length < $scope.selectionLimit)) { 164 | var unselectedIndex = $scope.unselectedOptions.indexOf(item); 165 | $scope.unselectedOptions.splice(unselectedIndex, 1); 166 | $scope.selectedOptions.push(item); 167 | } 168 | }; 169 | 170 | $scope.getId = function (item) { 171 | if (angular.isString(item)) { 172 | return item; 173 | } else if (angular.isObject(item)) { 174 | if ($scope.idProp) { 175 | return multiselect.getRecursiveProperty(item, $scope.idProp); 176 | } else { 177 | $log.error('Multiselect: when using objects as model, a idProp value is mandatory.'); 178 | return ''; 179 | } 180 | } else { 181 | return item; 182 | } 183 | }; 184 | 185 | $scope.getDisplay = function (item) { 186 | if (angular.isString(item)) { 187 | return item; 188 | } else if (angular.isObject(item)) { 189 | if ($scope.displayProp) { 190 | return multiselect.getRecursiveProperty(item, $scope.displayProp); 191 | } else { 192 | $log.error('Multiselect: when using objects as model, a displayProp value is mandatory.'); 193 | return ''; 194 | } 195 | } else { 196 | return item; 197 | } 198 | }; 199 | 200 | $scope.isSelected = function (item) { 201 | if (!$scope.selectedOptions) { 202 | return false; 203 | } 204 | var itemId = $scope.getId(item); 205 | for (var i = 0; i < $scope.selectedOptions.length; i++) { 206 | var selectedElement = $scope.selectedOptions[i]; 207 | if ($scope.getId(selectedElement) === itemId) { 208 | return true; 209 | } 210 | } 211 | return false; 212 | }; 213 | 214 | $scope.updateOptions = function () { 215 | if (typeof $scope.options === 'function') { 216 | $scope.options().then(function (resolvedOptions) { 217 | $scope.resolvedOptions = resolvedOptions; 218 | updateSelectionLists(); 219 | }); 220 | } 221 | }; 222 | 223 | // This search function is optimized to take into account the search limit. 224 | // Using angular limitTo filter is not efficient for big lists, because it still runs the search for 225 | // all elements, even if the limit is reached 226 | $scope.search = function () { 227 | var counter = 0; 228 | return function (item) { 229 | if (counter > $scope.searchLimit) { 230 | return false; 231 | } 232 | var displayName = $scope.getDisplay(item); 233 | if (displayName) { 234 | var result = displayName.toLowerCase().indexOf($scope.searchFilter.toLowerCase()) > -1; 235 | if (result) { 236 | counter++; 237 | } 238 | return result; 239 | } 240 | } 241 | }; 242 | 243 | } 244 | }; 245 | }); 246 | 247 | }()); 248 | -------------------------------------------------------------------------------- /test/e2e/multiselect-async-datasources-e2e-test.js: -------------------------------------------------------------------------------- 1 | describe('Multiselect, using string models, ', function () { 2 | 3 | beforeEach(function () { 4 | 5 | }); 6 | 7 | it('should update the options on every key press in the filter box', function () { 8 | browser.get('test/e2e/test-async-datasources.html'); 9 | 10 | var form = element(by.name('asyncTest')); 11 | form.element(by.className('dropdown-toggle')).click(); 12 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 13 | expect(unselectedItems.count()).toBe(0); 14 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 15 | expect(selectedItems.count()).toBe(0); 16 | 17 | form.element(by.model('searchFilter')).sendKeys('FOO'); 18 | 19 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 20 | expect(unselectedItems.count()).toBe(4); 21 | expect(unselectedItems.get(0).getInnerHtml()).toContain('FOO1'); 22 | 23 | form.element(by.model('searchFilter')).sendKeys('BAR'); 24 | 25 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 26 | expect(unselectedItems.count()).toBe(4); 27 | expect(unselectedItems.get(0).getInnerHtml()).toContain('FOOBAR1'); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/e2e/multiselect-objectmodels-e2e-test.js: -------------------------------------------------------------------------------- 1 | describe('Multiselect, using object models, ', function () { 2 | 3 | beforeEach(function () { 4 | 5 | }); 6 | 7 | it('should be able to select multiple elements', function () { 8 | browser.get('test/e2e/test-objectmodels.html'); 9 | 10 | var form = element(by.name('minimalTest')); 11 | form.element(by.className('dropdown-toggle')).click(); 12 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 13 | expect(unselectedItems.count()).toBe(4); 14 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 15 | expect(selectedItems.count()).toBe(0); 16 | 17 | unselectedItems.get(0).click(); 18 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 19 | expect(unselectedItems.count()).toBe(3); 20 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 21 | expect(selectedItems.count()).toBe(1); 22 | 23 | unselectedItems.get(0).click(); 24 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 25 | expect(unselectedItems.count()).toBe(2); 26 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 27 | expect(selectedItems.count()).toBe(2); 28 | }); 29 | 30 | it('should be able to unselect elements', function () { 31 | browser.get('test/e2e/test-objectmodels.html'); 32 | 33 | var form = element(by.name('minimalTest')); 34 | form.element(by.className('dropdown-toggle')).click(); 35 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 36 | expect(unselectedItems.count()).toBe(4); 37 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 38 | expect(selectedItems.count()).toBe(0); 39 | 40 | unselectedItems.get(0).click(); 41 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 42 | expect(unselectedItems.count()).toBe(3); 43 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 44 | expect(selectedItems.count()).toBe(1); 45 | 46 | selectedItems.get(0).click(); 47 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 48 | expect(unselectedItems.count()).toBe(4); 49 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 50 | expect(selectedItems.count()).toBe(0); 51 | }); 52 | 53 | it('has a button to select and unselect everything at once', function () { 54 | browser.get('test/e2e/test-objectmodels.html'); 55 | 56 | var form = element(by.name('allOptionsTest')); 57 | form.element(by.className('dropdown-toggle')).click(); 58 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 59 | expect(unselectedItems.count()).toBe(4); 60 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 61 | expect(selectedItems.count()).toBe(0); 62 | 63 | var selectAllButton = form.element(by.partialLinkText('Select All')); 64 | expect(selectAllButton.isDisplayed()).toBe(true); 65 | selectAllButton.click(); 66 | 67 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 68 | expect(unselectedItems.count()).toBe(0); 69 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 70 | expect(selectedItems.count()).toBe(4); 71 | 72 | var unselectAllButton = form.element(by.partialLinkText('Unselect All')); 73 | expect(unselectAllButton.isDisplayed()).toBe(true); 74 | unselectAllButton.click(); 75 | 76 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 77 | expect(unselectedItems.count()).toBe(4); 78 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 79 | expect(selectedItems.count()).toBe(0); 80 | }); 81 | 82 | it('cannot select more than the selection limit', function () { 83 | browser.get('test/e2e/test-objectmodels.html'); 84 | 85 | var form = element(by.name('selectionLimitTest')); 86 | form.element(by.className('dropdown-toggle')).click(); 87 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 88 | expect(unselectedItems.count()).toBe(4); 89 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 90 | expect(selectedItems.count()).toBe(0); 91 | 92 | unselectedItems.get(0).click(); 93 | unselectedItems.get(1).click(); 94 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 95 | expect(unselectedItems.count()).toBe(2); 96 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 97 | expect(selectedItems.count()).toBe(2); 98 | 99 | // The other items should now be disabled 100 | var disabledItems = form.all(by.className('disabled')); 101 | expect(disabledItems.count()).toBe(2); 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /test/e2e/multiselect-stringmodels-e2e-test.js: -------------------------------------------------------------------------------- 1 | describe('Multiselect, using string models, ', function () { 2 | 3 | beforeEach(function () { 4 | 5 | }); 6 | 7 | it('should be able to select multiple elements', function () { 8 | browser.get('test/e2e/test-stringmodels.html'); 9 | 10 | var form = element(by.name('minimalTest')); 11 | form.element(by.className('dropdown-toggle')).click(); 12 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 13 | expect(unselectedItems.count()).toBe(4); 14 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 15 | expect(selectedItems.count()).toBe(0); 16 | 17 | unselectedItems.get(0).click(); 18 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 19 | expect(unselectedItems.count()).toBe(3); 20 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 21 | expect(selectedItems.count()).toBe(1); 22 | 23 | unselectedItems.get(0).click(); 24 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 25 | expect(unselectedItems.count()).toBe(2); 26 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 27 | expect(selectedItems.count()).toBe(2); 28 | }); 29 | 30 | it('should be able to unselect elements', function () { 31 | browser.get('test/e2e/test-stringmodels.html'); 32 | 33 | var form = element(by.name('minimalTest')); 34 | form.element(by.className('dropdown-toggle')).click(); 35 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 36 | expect(unselectedItems.count()).toBe(4); 37 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 38 | expect(selectedItems.count()).toBe(0); 39 | 40 | unselectedItems.get(0).click(); 41 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 42 | expect(unselectedItems.count()).toBe(3); 43 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 44 | expect(selectedItems.count()).toBe(1); 45 | 46 | selectedItems.get(0).click(); 47 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 48 | expect(unselectedItems.count()).toBe(4); 49 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 50 | expect(selectedItems.count()).toBe(0); 51 | }); 52 | 53 | it('has a button to select and unselect everything at once', function () { 54 | browser.get('test/e2e/test-stringmodels.html'); 55 | 56 | var form = element(by.name('allOptionsTest')); 57 | form.element(by.className('dropdown-toggle')).click(); 58 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 59 | expect(unselectedItems.count()).toBe(4); 60 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 61 | expect(selectedItems.count()).toBe(0); 62 | 63 | var selectAllButton = form.element(by.partialLinkText('Select All')); 64 | expect(selectAllButton.isDisplayed()).toBe(true); 65 | selectAllButton.click(); 66 | 67 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 68 | expect(unselectedItems.count()).toBe(0); 69 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 70 | expect(selectedItems.count()).toBe(4); 71 | 72 | var unselectAllButton = form.element(by.partialLinkText('Unselect All')); 73 | expect(unselectAllButton.isDisplayed()).toBe(true); 74 | unselectAllButton.click(); 75 | 76 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 77 | expect(unselectedItems.count()).toBe(4); 78 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 79 | expect(selectedItems.count()).toBe(0); 80 | }); 81 | 82 | it('cannot select more than the selection limit', function () { 83 | browser.get('test/e2e/test-stringmodels.html'); 84 | 85 | var form = element(by.name('selectionLimitTest')); 86 | form.element(by.className('dropdown-toggle')).click(); 87 | var unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 88 | expect(unselectedItems.count()).toBe(4); 89 | var selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 90 | expect(selectedItems.count()).toBe(0); 91 | 92 | unselectedItems.get(0).click(); 93 | unselectedItems.get(1).click(); 94 | unselectedItems = form.all(by.tagName('li')).all(by.className('item-unselected')); 95 | expect(unselectedItems.count()).toBe(2); 96 | selectedItems = form.all(by.tagName('li')).all(by.className('item-selected')); 97 | expect(selectedItems.count()).toBe(2); 98 | 99 | // The other items should now be disabled 100 | var disabledItems = form.all(by.className('disabled')); 101 | expect(disabledItems.count()).toBe(2); 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /test/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | var ScreenShotReporter = require('protractor-screenshot-reporter'); 2 | 3 | exports.config = { 4 | onPrepare: function () { 5 | 6 | // Store screenshots on test failure 7 | jasmine.getEnv().addReporter(new ScreenShotReporter({ 8 | baseDirectory: 'test/e2e/screenshots/', 9 | pathBuilder: function pathBuilder(spec, descriptions, results, capabilities) { 10 | return descriptions.join('-'); 11 | }, 12 | takeScreenShotsOnlyForFailedSpecs: true 13 | })); 14 | 15 | // Maximize the browser window 16 | browser.driver.manage().window().maximize(); 17 | 18 | // Disable all animations 19 | var disableNgAnimate = function () { 20 | angular.module('disableNgAnimate', []).run(['$animate', function ($animate) { 21 | $animate.enabled(false); 22 | }]); 23 | }; 24 | browser.addMockModule('disableNgAnimate', disableNgAnimate); 25 | }, 26 | specs: ['**/*.js'], 27 | multiCapabilities: [{ 28 | 'browserName': 'chrome', 29 | 'chromeOptions': { 30 | 'args': ['show-fps-counter=true'] 31 | } 32 | }] 33 | }; 34 | -------------------------------------------------------------------------------- /test/e2e/test-async-datasources.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Bootstrap Multiselect Test 6 | 7 | 8 | 9 | 10 |
11 | 12 |

Test Async Data source

13 | 14 |
15 |
Model value: {{selection1}}
16 | 18 |
19 | 20 |
21 | 22 | 23 | 24 | 37 | 38 | -------------------------------------------------------------------------------- /test/e2e/test-objectmodels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Bootstrap Multiselect Test 6 | 7 | 8 | 9 | 10 |
11 | 12 |

Test without options

13 | 14 |
15 |
Model value: {{selection1}}
16 | 17 |
18 | 19 |

Test with all options enabled

20 | 21 |
22 |
Model value: {{selection2}}
23 | 26 |
27 | 28 |

Test with required

29 | 30 |
31 |
Model value: {{selection3}}
32 | allOptionsTest.$valid: {{allOptionsTest.$valid}}
33 | 34 |
35 | 36 |

Test with selection limit

37 | 38 |
39 |
Model value: {{selection2}}
40 | 42 |
43 | 44 |

Test with many items

45 | 46 |
47 |
Model value: {{selection4}}
48 | 50 |
51 | 52 |
53 | 54 | 55 | 56 | 72 | 73 | -------------------------------------------------------------------------------- /test/e2e/test-stringmodels.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Bootstrap Multiselect Test 6 | 7 | 8 | 9 | 10 |
11 | 12 |

Test without options

13 | 14 |
15 |
Model value: {{selection1}}
16 | 17 |
18 | 19 |

Test with all options enabled

20 | 21 |
22 |
Model value: {{selection2}}
23 | 25 |
26 | 27 |

Test with required

28 | 29 |
30 |
Model value: {{selection3}}
31 | allOptionsTest.$valid: {{allOptionsTest.$valid}}
32 | 33 |
34 | 35 |

Test with selection limit

36 | 37 |
38 |
Model value: {{selection2}}
39 | 40 |
41 | 42 |

Test with many items

43 | 44 |
45 |
Model value: {{selection4}}
46 | 47 |
48 | 49 |

Disabled test

50 | 51 |
52 |
Model value: {{selection5}}
53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 |
61 | 62 | 63 | 64 | 75 | 76 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '../..', 4 | files: [ 5 | 'bower_components/angular/angular.js', 6 | 'bower_components/angular-mocks/angular-mocks.js', 7 | 'dist/angular-bootstrap-multiselect.min.js', 8 | 'test/unit/*-test.js' 9 | ], 10 | browsers: ['PhantomJS'], 11 | frameworks: ['jasmine'], 12 | reporters: ['dots'], 13 | port: 9877, 14 | colors: true, 15 | exclude: [], 16 | singleRun: true 17 | }); 18 | }; -------------------------------------------------------------------------------- /test/unit/multiselect-objectmodels-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe("The multiselect directive, when using object models,", function () { 4 | 5 | var $scope; 6 | var $rootScope; 7 | var $compile; 8 | 9 | beforeEach(angular.mock.module('btorfs.multiselect')); 10 | 11 | beforeEach(inject(function (_$compile_, _$rootScope_) { 12 | $scope = _$rootScope_.$new(); 13 | $compile = _$compile_; 14 | $rootScope = _$rootScope_; 15 | })); 16 | 17 | it('initializes the list lazily, when the first item is chosen', function () { 18 | $scope.options = [ 19 | { 20 | name: 'el1', 21 | id: '1' 22 | }, 23 | { 24 | name: 'el2', 25 | id: '2' 26 | }, 27 | { 28 | name: 'el3', 29 | id: '3' 30 | } 31 | ]; 32 | var element = $compile("")($scope); 33 | $scope.$digest(); 34 | expect(element.isolateScope().selectedOptions).toBeUndefined(); 35 | 36 | element.isolateScope().toggleItem($scope.options[0]); 37 | expect(element.isolateScope().selectedOptions).toBeDefined(); 38 | expect(element.isolateScope().selectedOptions.length).toBe(1); 39 | }); 40 | 41 | it('can toggle items in the selection', function () { 42 | $scope.options = [ 43 | { 44 | name: 'el1', 45 | id: '1' 46 | }, 47 | { 48 | name: 'el2', 49 | id: '2' 50 | }, 51 | { 52 | name: 'el3', 53 | id: '3' 54 | } 55 | ]; 56 | var element = $compile("")($scope); 57 | $scope.$digest(); 58 | 59 | expect(element.isolateScope().unselectedOptions.length).toBe(3); 60 | element.isolateScope().toggleItem(element.isolateScope().unselectedOptions[0]); 61 | expect(element.isolateScope().selectedOptions).toBeDefined(); 62 | expect(element.isolateScope().selectedOptions.length).toBe(1); 63 | expect(element.isolateScope().unselectedOptions.length).toBe(2); 64 | 65 | element.isolateScope().toggleItem(element.isolateScope().selectedOptions[0]); 66 | expect(element.isolateScope().selectedOptions.length).toBe(0); 67 | expect(element.isolateScope().unselectedOptions.length).toBe(3); 68 | }); 69 | 70 | it('shows a label on the button when no items have been chosen', function () { 71 | $scope.options = [ 72 | { 73 | name: 'el1', 74 | id: '1' 75 | }, 76 | { 77 | name: 'el2', 78 | id: '2' 79 | }, 80 | { 81 | name: 'el3', 82 | id: '3' 83 | } 84 | ]; 85 | var element = $compile("")($scope); 86 | $scope.$digest(); 87 | 88 | expect(element.isolateScope().getButtonText()).toBe('Select'); 89 | }); 90 | 91 | it('shows the name of the element when one item is chosen', function () { 92 | $scope.options = [ 93 | { 94 | name: 'el1', 95 | id: '1' 96 | }, 97 | { 98 | name: 'el2', 99 | id: '2' 100 | }, 101 | { 102 | name: 'el3', 103 | id: '3' 104 | } 105 | ]; 106 | $scope.selection = [{ 107 | id: '1' 108 | }]; 109 | var element = $compile("")($scope); 110 | $scope.$digest(); 111 | 112 | expect(element.isolateScope().getButtonText()).toBe('el1'); 113 | }); 114 | 115 | it('shows the number of elements when multiple items are chosen', function () { 116 | $scope.options = [ 117 | { 118 | name: 'el1', 119 | id: '1' 120 | }, 121 | { 122 | name: 'el2', 123 | id: '2' 124 | }, 125 | { 126 | name: 'el3', 127 | id: '3' 128 | } 129 | ]; 130 | $scope.selection = [{ 131 | id: '1' 132 | }, { 133 | id: '2' 134 | }]; 135 | var element = $compile("")($scope); 136 | $scope.$digest(); 137 | 138 | expect(element.isolateScope().getButtonText()).toBe('2 selected'); 139 | }); 140 | 141 | it('can select and unselect all at once', function () { 142 | $scope.options = [ 143 | { 144 | name: 'el1', 145 | id: '1' 146 | }, 147 | { 148 | name: 'el2', 149 | id: '2' 150 | }, 151 | { 152 | name: 'el3', 153 | id: '3' 154 | } 155 | ]; 156 | $scope.selection = []; 157 | var element = $compile("")($scope); 158 | $scope.$digest(); 159 | 160 | element.isolateScope().selectAll(); 161 | $scope.$digest(); 162 | expect($scope.selection.length).toBe(3); 163 | expect(element.isolateScope().selectedOptions.length).toBe(3); 164 | expect(element.isolateScope().unselectedOptions.length).toBe(0); 165 | 166 | element.isolateScope().unselectAll(); 167 | $scope.$digest(); 168 | expect($scope.selection.length).toBe(0); 169 | expect(element.isolateScope().selectedOptions.length).toBe(0); 170 | expect(element.isolateScope().unselectedOptions.length).toBe(3); 171 | }); 172 | 173 | it('knows which items are selected', function () { 174 | $scope.options = [ 175 | { 176 | name: 'el1', 177 | id: '1' 178 | }, 179 | { 180 | name: 'el2', 181 | id: '2' 182 | }, 183 | { 184 | name: 'el3', 185 | id: '3' 186 | } 187 | ]; 188 | $scope.selection = [{ 189 | id: '2' 190 | }]; 191 | var element = $compile("")($scope); 192 | $scope.$digest(); 193 | 194 | expect(element.isolateScope().isSelected($scope.options[1])).toBeTruthy(); 195 | expect(element.isolateScope().isSelected($scope.options[2])).toBeFalsy(); 196 | }); 197 | 198 | it('can search inside the options', function () { 199 | $scope.options = [ 200 | { 201 | name: 'el1', 202 | id: '1' 203 | }, 204 | { 205 | name: 'el2', 206 | id: '2' 207 | }, 208 | { 209 | name: 'el3', 210 | id: '3' 211 | } 212 | ]; 213 | $scope.selection = [{ 214 | id: '2' 215 | }]; 216 | var element = $compile("")($scope); 217 | $scope.$digest(); 218 | 219 | element.isolateScope().searchFilter = '2'; 220 | expect(element.isolateScope().search()($scope.options[0])).toBeFalsy(); 221 | expect(element.isolateScope().search()($scope.options[1])).toBeTruthy(); 222 | expect(element.isolateScope().search()($scope.options[2])).toBeFalsy(); 223 | 224 | element.isolateScope().searchFilter = '5'; 225 | expect(element.isolateScope().search()($scope.options[0])).toBeFalsy(); 226 | expect(element.isolateScope().search()($scope.options[1])).toBeFalsy(); 227 | expect(element.isolateScope().search()($scope.options[2])).toBeFalsy(); 228 | }); 229 | 230 | }); 231 | -------------------------------------------------------------------------------- /test/unit/multiselect-stringmodels-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe("The multiselect directive, when using string models,", function () { 4 | 5 | var $scope; 6 | var $rootScope; 7 | var $compile; 8 | 9 | beforeEach(angular.mock.module('btorfs.multiselect')); 10 | 11 | beforeEach(inject(function (_$compile_, _$rootScope_) { 12 | $scope = _$rootScope_.$new(); 13 | $compile = _$compile_; 14 | $rootScope = _$rootScope_; 15 | })); 16 | 17 | it('initializes the list lazily, when the first item is chosen', function () { 18 | $scope.options = ['el1', 'el2', 'el3']; 19 | var element = $compile("")($scope); 20 | $scope.$digest(); 21 | expect(element.isolateScope().selectedOptions).toBeUndefined(); 22 | 23 | element.isolateScope().toggleItem($scope.options[0]); 24 | expect(element.isolateScope().selectedOptions).toBeDefined(); 25 | expect(element.isolateScope().selectedOptions.length).toBe(1); 26 | }); 27 | 28 | it('can toggle items in the selection', function () { 29 | $scope.options = ['el1', 'el2', 'el3']; 30 | var element = $compile("")($scope); 31 | $scope.$digest(); 32 | 33 | expect(element.isolateScope().unselectedOptions.length).toBe(3); 34 | element.isolateScope().toggleItem(element.isolateScope().unselectedOptions[0]); 35 | expect(element.isolateScope().selectedOptions).toBeDefined(); 36 | expect(element.isolateScope().selectedOptions.length).toBe(1); 37 | expect(element.isolateScope().unselectedOptions.length).toBe(2); 38 | 39 | element.isolateScope().toggleItem(element.isolateScope().selectedOptions[0]); 40 | expect(element.isolateScope().selectedOptions.length).toBe(0); 41 | expect(element.isolateScope().unselectedOptions.length).toBe(3); 42 | }); 43 | 44 | it('shows a default label on the button when no items have been chosen', function () { 45 | $scope.options = ['el1', 'el2', 'el3']; 46 | var element = $compile("")($scope); 47 | $scope.$digest(); 48 | 49 | expect(element.isolateScope().getButtonText()).toBe('Select'); 50 | }); 51 | 52 | it('shows the label passed to the directive on the button when no items have been chosen', function () { 53 | $scope.options = ['el1', 'el2', 'el3']; 54 | $scope.labels = { 55 | itemsSelected: 'selecionados', 56 | search: 'Procurar...', 57 | select: 'Selecionar', 58 | selectAll: 'Selecionar Todos', 59 | unselectAll: 'Deselecionar Todos' 60 | }; 61 | 62 | var element = $compile("")($scope); 63 | $scope.$digest(); 64 | 65 | expect(element.isolateScope().labels).toBeDefined(); 66 | expect(element.isolateScope().labels.itemsSelected).toEqual('selecionados'); 67 | expect(element.isolateScope().labels.search).toEqual('Procurar...'); 68 | expect(element.isolateScope().labels.select).toEqual('Selecionar'); 69 | expect(element.isolateScope().labels.selectAll).toEqual('Selecionar Todos'); 70 | expect(element.isolateScope().labels.unselectAll).toEqual('Deselecionar Todos'); 71 | 72 | expect(element.isolateScope().getButtonText()).toBe('Selecionar'); 73 | }); 74 | 75 | it('shows the name of the element when one item is chosen', function () { 76 | $scope.options = ['el1', 'el2', 'el3']; 77 | $scope.selection = ['el1']; 78 | var element = $compile("")($scope); 79 | $scope.$digest(); 80 | 81 | expect(element.isolateScope().getButtonText()).toBe('el1'); 82 | }); 83 | 84 | it('shows the number of elements when multiple items are chosen', function () { 85 | $scope.options = ['el1', 'el2', 'el3']; 86 | $scope.selection = ['el1', 'el2']; 87 | var element = $compile("")($scope); 88 | $scope.$digest(); 89 | 90 | expect(element.isolateScope().getButtonText()).toBe('2 selected'); 91 | }); 92 | 93 | it('shows the number of elements and the label that is passed when multiple items are chosen', function () { 94 | $scope.options = ['el1', 'el2', 'el3']; 95 | $scope.selection = ['el1', 'el2']; 96 | $scope.labels = { 97 | itemsSelected: 'selecionados', 98 | search: 'Procurar...', 99 | select: 'Selecionar', 100 | selectAll: 'Selecionar Todos', 101 | unselectAll: 'Deselecionar Todos' 102 | }; 103 | var element = $compile("")($scope); 104 | $scope.$digest(); 105 | 106 | expect(element.isolateScope().getButtonText()).toBe('2 selecionados'); 107 | }); 108 | 109 | it('can select and unselect all at once', function () { 110 | $scope.options = ['el1', 'el2', 'el3']; 111 | $scope.selection = []; 112 | var element = $compile("")($scope); 113 | $scope.$digest(); 114 | 115 | element.isolateScope().selectAll(); 116 | $scope.$digest(); 117 | expect($scope.selection.length).toBe(3); 118 | expect(element.isolateScope().selectedOptions.length).toBe(3); 119 | expect(element.isolateScope().unselectedOptions.length).toBe(0); 120 | 121 | element.isolateScope().unselectAll(); 122 | $scope.$digest(); 123 | expect($scope.selection.length).toBe(0); 124 | expect(element.isolateScope().selectedOptions.length).toBe(0); 125 | expect(element.isolateScope().unselectedOptions.length).toBe(3); 126 | }); 127 | 128 | it('knows which items are selected', function () { 129 | $scope.options = ['el1', 'el2', 'el3']; 130 | $scope.selection = ['el2']; 131 | var element = $compile("")($scope); 132 | $scope.$digest(); 133 | 134 | expect(element.isolateScope().isSelected($scope.options[1])).toBeTruthy(); 135 | expect(element.isolateScope().isSelected($scope.options[2])).toBeFalsy(); 136 | }); 137 | 138 | it('can search inside the options', function () { 139 | $scope.options = ['el1', 'el2', 'el3']; 140 | $scope.selection = ['el2']; 141 | var element = $compile("")($scope); 142 | $scope.$digest(); 143 | 144 | element.isolateScope().searchFilter = '2'; 145 | expect(element.isolateScope().search()($scope.options[0])).toBeFalsy(); 146 | expect(element.isolateScope().search()($scope.options[1])).toBeTruthy(); 147 | expect(element.isolateScope().search()($scope.options[2])).toBeFalsy(); 148 | 149 | element.isolateScope().searchFilter = '5'; 150 | expect(element.isolateScope().search()($scope.options[0])).toBeFalsy(); 151 | expect(element.isolateScope().search()($scope.options[1])).toBeFalsy(); 152 | expect(element.isolateScope().search()($scope.options[2])).toBeFalsy(); 153 | }); 154 | 155 | }); 156 | --------------------------------------------------------------------------------