├── .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 \n \n
\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 | 
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 |
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 |
18 |
19 |
Test with all options enabled
20 |
21 |
27 |
28 |
Test with required
29 |
30 |
35 |
36 |
Test with selection limit
37 |
38 |
43 |
44 |
Test with many items
45 |
46 |
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 |
18 |
19 |
Test with all options enabled
20 |
21 |
26 |
27 |
Test with required
28 |
29 |
34 |
35 |
Test with selection limit
36 |
37 |
41 |
42 |
Test with many items
43 |
44 |
48 |
49 |
Disabled test
50 |
51 |
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 |
--------------------------------------------------------------------------------