├── .bowerrc
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .jshintrc
├── .travis.yml
├── CHANGELOG.md
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── common
└── module.js
├── dist
├── angular-bootstrap-duallistbox.js
└── angular-bootstrap-duallistbox.min.js
├── example
├── index.html
├── scripts
│ ├── app.js
│ └── controllers
│ │ └── main.js
└── styles
│ └── main.css
├── karma.conf.js
├── package.json
├── src
└── directives
│ └── bsDuallistbox.js
└── test
├── .jshintrc
├── runner.html
└── spec
└── directives
└── bsDuallistboxSpec.js
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 |
10 | # Change these settings to your own preference
11 | indent_style = space
12 | indent_size = 2
13 |
14 | # We recommend you to keep these unchanged
15 | end_of_line = lf
16 | charset = utf-8
17 | trim_trailing_whitespace = true
18 | insert_final_newline = true
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bower_components
2 | node_modules
3 | .tmp
4 | .sass-cache
5 | .idea/
6 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "quotmark": "single",
15 | "regexp": true,
16 | "undef": true,
17 | "unused": true,
18 | "strict": true,
19 | "trailing": true,
20 | "smarttabs": true,
21 | "globals": {
22 | "angular": false,
23 | "$": false,
24 | "jQuery": false
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - '0.10'
5 |
6 | before_script:
7 | - export DISPLAY=:99.0
8 | - export PHANTOMJS_BIN=/usr/local/phantomjs/bin/phantomjs
9 | - sh -e /etc/init.d/xvfb start
10 | - sleep 3 # give xvfb some time to start
11 | - 'npm install -g bower grunt-cli'
12 | - 'npm install'
13 | - 'bower install'
14 |
15 | script:
16 | - grunt
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | CHANGELOG
2 | =========
3 |
4 | # 0.1.0 (2015-06-13)
5 |
6 | - Update to angular-1.4.0
7 |
8 | # 0.0.3 (2014-11-20)
9 |
10 | - Update to angular-1.3.3
11 |
12 | # 0.0.2 (2014-03-27)
13 |
14 | - Update to bootstrap-duallistbox-3.0.1
15 |
16 | # 0.0.1 (2014-02-04)
17 |
18 | - First release
19 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (grunt) {
4 | // load all grunt tasks
5 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks);
6 |
7 | // configurable paths
8 | var yeomanConfig = {
9 | src: 'src',
10 | dist: 'dist',
11 | test: 'test',
12 | temp: '.temp'
13 | };
14 |
15 | try {
16 | yeomanConfig.src = require('./bower.json').appPath || yeomanConfig.src;
17 | } catch (e) {}
18 |
19 | grunt.initConfig({
20 | yeoman: yeomanConfig,
21 | pkg: grunt.file.readJSON('bower.json'),
22 | meta: {
23 | banner:
24 | '/**\n' +
25 | ' * <%= pkg.name %>\n' +
26 | ' * @version v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' +
27 | ' * @author <%= pkg.author.name %> (<%= pkg.author.email %>)\n' +
28 | ' * @link <%= pkg.homepage %>\n' +
29 | ' * @license <%= _.pluck(pkg.licenses, "type").join(", ") %>\n' +
30 | '**/\n\n'
31 | },
32 | jshint: {
33 | options: {
34 | jshintrc: '.jshintrc'
35 | },
36 | all: [
37 | 'Gruntfile.js',
38 | '<%= yeoman.src %>/**/*.js'
39 | ],
40 | test: {
41 | src: ['<%= yeoman.test %>/spec/**/*.js'],
42 | options: {
43 | jshintrc: '<%= yeoman.test %>/.jshintrc'
44 | }
45 | }
46 | },
47 | karma: {
48 | options: {
49 | configFile: 'karma.conf.js'
50 | },
51 | unit: {
52 | options: {
53 | singleRun: false
54 | }
55 | },
56 | phantom: {
57 | browsers: ['PhantomJS']
58 | }
59 | },
60 | clean: {
61 | dist: {
62 | files: [{
63 | dot: true,
64 | src: [
65 | '<%= yeoman.dist %>/*',
66 | '!<%= yeoman.dist %>/.git*'
67 | ]
68 | }]
69 | },
70 | temp: {
71 | src: ['<%= yeoman.dist %>/<%= yeoman.temp %>']
72 | }
73 | },
74 | ngmin: {
75 | dist: {
76 | expand: true,
77 | cwd: '<%= yeoman.src %>',
78 | src: ['**/*.js'],
79 | dest: '<%= yeoman.dist %>/<%= yeoman.temp %>'
80 | }
81 | },
82 | concat: {
83 | options: {
84 | banner: '<%= meta.banner %>\'use strict\';\n',
85 | process: function(src, filepath) {
86 | return '// Source: ' + filepath + '\n' +
87 | src.replace(/(^|\n)[ \t]*('use strict'|"use strict");?\s*/g, '$1');
88 | }
89 | },
90 | dist: {
91 | src: ['common/*.js', '<%= yeoman.dist %>/<%= yeoman.temp %>/**/*.js'],
92 | dest: '<%= yeoman.dist %>/<%= pkg.name %>.js'
93 | }
94 | },
95 | uglify: {
96 | options: {
97 | banner: '<%= meta.banner %>'
98 | },
99 | min: {
100 | files: {
101 | '<%= yeoman.dist %>/<%= pkg.name %>.min.js': '<%= concat.dist.dest %>'
102 | }
103 | }
104 | }
105 | });
106 |
107 | // Test the directive
108 | grunt.registerTask('test', ['jshint', 'karma:unit']);
109 | grunt.registerTask('test-travis', ['jshint', 'karma:phantom']);
110 |
111 | // Build the directive
112 | // - clean, cleans the output directory
113 | // - ngmin, prepares the angular files
114 | // - concat, concatenates and adds a banner to the debug file
115 | // - uglify, minifies and adds a banner to the minified file
116 | // - clean:temp, cleans the ngmin-ified directory
117 | grunt.registerTask('build', ['clean', 'ngmin', 'concat', 'uglify', 'clean:temp']);
118 |
119 | // Default task, do everything
120 | grunt.registerTask('default', ['test-travis', 'build']);
121 | };
122 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | angular-bootstrap-duallistbox [](https://travis-ci.org/frapontillo/angular-bootstrap-duallistbox)
2 | ========================
3 |
4 | Angular directive to include [Bootstrap Dual Listbox](https://github.com/istvan-ujjmeszaros/bootstrap-duallistbox) in your apps.
5 |
6 | ##Usage
7 |
8 | ###Installation
9 | ```shell
10 | $ bower install angular-bootstrap-duallistbox
11 | ```
12 |
13 | This will install AngularJS, jQuery, and the original bootstrap-duallistbox.
14 |
15 | ###Directive
16 | The directive has to be applied to an already existing `select` element with the `multiple` attribute:
17 |
18 | ```html
19 |
25 | ```
26 |
27 | Set the `select` attributes as you would normally do for a regular multiple list, and add `bs-duallistbox` to enable the plugin.
28 |
29 | ####Options
30 |
31 | The available options for the dual listbox are:
32 |
33 | - `bootstrap2`, if `true` enables compatibility with Bootstrap 2, otherwise it defaults to Bootstrap 3.
34 | - `postfix`, the added selects will have the same name as the original one, concatenated with this string and `1`
35 | (for the non selected list, e.g. `element_helper1`) or `2` (for the selected list, e.g. `element_helper2`).
36 | - `select-min-height`, the minimum height for the dual listbox.
37 | - `filter`, if `true` it enables filtering the dual listbox, otherwise it defaults to `false`.
38 | - `filter-clear`, the text of the "Clear filter" `button`s.
39 | - `filter-placeholder`, the placeholder text for the filter `input`s.
40 | - `filter-values`, if `true` enables filtering on values too.
41 | - `filter-non-selected`, the variable that will be bound to the filter `input` of the non-selected options.
42 | - `filter-selected`, the variable that will be bound to the filter `input` of the selected options.
43 | - `move-on-select`, defaults to `true`, determines whether to move options upon selection.
44 | - `preserve-selection`, can be
45 | - `'all'`, for selecting both moved elements and the already selected ones in the target list
46 | - `'moved'`, for selecting moved elements only
47 | - `false`, the default not to preserve anything
48 | - `move-selected-label`, is the label for the "Move Selected" button, defaults to `'Move selected'`.
49 | - `move-all-label`, is the label for the "Move All" button, defaults to `'Move all'`.
50 | - `remove-selected-label`, is the label for the "Remove Selected" button, defaults to `'Remove selected'`.
51 | - `remove-all-label`, is the label for the "Remove All" button, defaults to `'Remove all'`.
52 | - `selected-list-label`, can be a `string` specifying the name of the selected list., defaults to `false`.
53 | - `non-selected-list-label`, can be a `string` specifying the name of the non selected list., defaults to `false`.
54 | - `info-all`, defaults to `'Showing all {0}'`, determines which `string` format to use when all options are visible.
55 | Set this to `false` to hide this information. Remember to insert the `{0}` placeholder.
56 | - `info-filtered`, defaults to `'Filtered {0} from {1}'`, determines which
57 | element format to use when some element is filtered. Remember to insert the `{0}` and `{1} `placeholders.
58 | - `info-empty`, defaults to `'Empty list'`, determines the `string` to use when there are no options in the list.
59 |
60 | ###Example
61 | The `example` folder shows a simple working demo of the dual list box.
62 |
63 | ##Development
64 |
65 | ###Test and build
66 |
67 | To build the directive yourself you need to have NodeJS. Then do the following:
68 |
69 | ```shell
70 | $ npm install -g grunt-cli bower karma
71 | $ npm install
72 | $ bower install
73 | $ grunt
74 | ```
75 |
76 | ###Contribute
77 |
78 | To contribute, please follow the generic [AngularJS Contributing Guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md), with the only exception to send the PR to the `develop` branch instead of `master`.
79 |
80 | ##Author
81 |
82 | Francesco Pontillo ()
83 |
84 | ##License
85 |
86 | ```
87 | Copyright 2014 Francesco Pontillo
88 |
89 | Licensed under the Apache License, Version 2.0 (the "License");
90 | you may not use this file except in compliance with the License.
91 | You may obtain a copy of the License at
92 |
93 | http://www.apache.org/licenses/LICENSE-2.0
94 |
95 | Unless required by applicable law or agreed to in writing, software
96 | distributed under the License is distributed on an "AS IS" BASIS,
97 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
98 | See the License for the specific language governing permissions and
99 | limitations under the License.
100 |
101 | ```
102 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-bootstrap-duallistbox",
3 | "version": "0.1.0",
4 | "author": {
5 | "name": "Francesco Pontillo",
6 | "email": "francescopontillo@gmail.com",
7 | "url": "https://github.com/frapontillo"
8 | },
9 | "homepage": "https://github.com/frapontillo/angular-bootstrap-duallistbox",
10 | "repository": {
11 | "type": "git",
12 | "url": "git@github.com:frapontillo/angular-bootstrap-duallistbox.git"
13 | },
14 | "licenses": [
15 | {
16 | "type": "Apache License 2.0",
17 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
18 | }
19 | ],
20 | "main": "./dist/angular-bootstrap-duallistbox.js",
21 | "dependencies": {
22 | "angular": "~1.4.0",
23 | "jquery": "~2.0.3",
24 | "bootstrap": ">=2.3.2",
25 | "bootstrap-duallistbox": "3.0.2"
26 | },
27 | "devDependencies": {
28 | "angular-mocks": "~1.4.0",
29 | "angular-scenario": "~1.4.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/common/module.js:
--------------------------------------------------------------------------------
1 | angular.module('frapontillo.bootstrap-duallistbox', []);
--------------------------------------------------------------------------------
/dist/angular-bootstrap-duallistbox.js:
--------------------------------------------------------------------------------
1 | /**
2 | * angular-bootstrap-duallistbox
3 | * @version v0.1.0 - 2015-06-13
4 | * @author Francesco Pontillo (francescopontillo@gmail.com)
5 | * @link https://github.com/frapontillo/angular-bootstrap-duallistbox
6 | * @license Apache License 2.0
7 | **/
8 |
9 | 'use strict';
10 | // Source: common/module.js
11 | angular.module('frapontillo.bootstrap-duallistbox', []);
12 | // Source: dist/.temp/directives/bsDuallistbox.js
13 | angular.module('frapontillo.bootstrap-duallistbox').directive('bsDuallistbox', [
14 | '$compile',
15 | '$timeout',
16 | function ($compile, $timeout) {
17 | return {
18 | restrict: 'A',
19 | require: 'ngModel',
20 | link: function link(scope, element, attrs) {
21 | //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
22 | var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
23 | // The select collection
24 | var collection = attrs.ngOptions.match(NG_OPTIONS_REGEXP)[7];
25 | var getBooleanValue = function (attributeValue) {
26 | return attributeValue === true || attributeValue === 'true';
27 | };
28 | // The attribute names to $observe with related functions to call
29 | var attributes = {
30 | 'bootstrap2': {
31 | changeFn: 'setBootstrap2Compatible',
32 | transformFn: getBooleanValue
33 | },
34 | 'postfix': 'setHelperSelectNamePostfix',
35 | 'selectMinHeight': {
36 | changeFn: 'setSelectOrMinimalHeight',
37 | defaultValue: 100
38 | },
39 | 'filter': {
40 | changeFn: 'setShowFilterInputs',
41 | defaultValue: true,
42 | transformFn: getBooleanValue
43 | },
44 | 'filterClear': {
45 | changeFn: 'setFilterTextClear',
46 | defaultValue: 'show all'
47 | },
48 | 'filterPlaceholder': 'setFilterPlaceHolder',
49 | 'filterValues': {
50 | changeFn: 'setFilterOnValues',
51 | transformFn: getBooleanValue
52 | },
53 | 'moveOnSelect': {
54 | changeFn: 'setMoveOnSelect',
55 | defaultValue: true,
56 | transformFn: getBooleanValue
57 | },
58 | 'preserveSelection': 'setPreserveSelectionOnMove',
59 | 'moveSelectedLabel': 'setMoveSelectedLabel',
60 | 'moveAllLabel': 'setMoveAllLabel',
61 | 'removeSelectedLabel': 'setRemoveSelectedLabel',
62 | 'removeAllLabel': 'setRemoveAllLabel',
63 | 'selectedListLabel': 'setSelectedListLabel',
64 | 'nonSelectedListLabel': 'setNonSelectedListLabel',
65 | 'infoAll': {
66 | changeFn: 'setInfoText',
67 | defaultValue: 'Showing all {0}'
68 | },
69 | 'infoFiltered': {
70 | changeFn: 'setInfoTextFiltered',
71 | defaultValue: 'Filtered {0} from {1}'
72 | },
73 | 'infoEmpty': {
74 | changeFn: 'setInfoTextEmpty',
75 | defaultValue: 'Empty list'
76 | }
77 | };
78 | // The duallistbox element
79 | var dualListBox;
80 | /**
81 | * Calculates the proper attribute value, given its name.
82 | * The attribute value depends on:
83 | * - the current value
84 | * - the default value, if it exists
85 | * - the transformFn, if it exists
86 | *
87 | * If the current value is undefined, it gets the default value.
88 | * The calculated value is then applied to the transformFn, if it is defined; the result is then returned.
89 | *
90 | * @param attributeName The {String} name of the attribute.
91 | * @returns {Object} representing the value of the attribute.
92 | */
93 | var getAttributeValueOrDefault = function (attributeName) {
94 | // get the attribute function/object for default and transformation
95 | var attributeFunction = attributes[attributeName];
96 | // get the current attribute value
97 | var attributeValue = attrs[attributeName];
98 | // By default, the default value and the transform function are not defined
99 | var defaultValue;
100 | var transformFn;
101 | // If the attributeFunction is an object
102 | if (angular.isObject(attributeFunction)) {
103 | // extract the default value and the transform function
104 | defaultValue = attributeFunction.defaultValue;
105 | transformFn = attributeFunction.transformFn;
106 | }
107 | // If the upcoming value is falsy, get the default
108 | if (!attributeValue) {
109 | attributeValue = defaultValue;
110 | }
111 | // If a transform function is defined, use it to change the value
112 | if (angular.isFunction(transformFn)) {
113 | attributeValue = transformFn(attributeValue);
114 | }
115 | return attributeValue;
116 | };
117 | /**
118 | * Gets the name of the function of `bootstrap-duallistbox` to be called to effectively
119 | * change the attribute in input.
120 | *
121 | * @param attributeName The name of the attribute to change.
122 | * @returns {String}, name of the `bootstrap-dual-listbox` to be called.
123 | */
124 | var getAttributeChangeFunction = function (attributeName) {
125 | // get the attribute function/object for default and transformation
126 | var attributeFunction = attributes[attributeName];
127 | // By default, attributeFunction is a function
128 | var actualFunction = attributeFunction;
129 | // If the attributeFunction is an object
130 | if (angular.isObject(attributeFunction)) {
131 | // extract the actual function name
132 | actualFunction = attributeFunction.changeFn;
133 | }
134 | return actualFunction;
135 | };
136 | /**
137 | * Listen to model changes.
138 | */
139 | var listenToModel = function () {
140 | // When ngModel changes, refresh the list
141 | // controller.$formatters.push(refresh);
142 | scope.$watch(attrs.ngModel, function () {
143 | initMaybe();
144 | refresh();
145 | });
146 | // When ngOptions changes, refresh the list
147 | scope.$watch(collection, refresh, true);
148 | // Watch for changes to the filter scope variables
149 | scope.$watch(attrs.filterNonSelected, function () {
150 | refresh();
151 | });
152 | scope.$watch(attrs.filterSelected, function () {
153 | refresh();
154 | });
155 | // $observe every attribute change
156 | angular.forEach(attributes, function (attributeFunction, attributeName) {
157 | attrs.$observe(attributeName, function () {
158 | var actualFunction = getAttributeChangeFunction(attributeName);
159 | var actualValue = getAttributeValueOrDefault(attributeName);
160 | // Depending on the attribute, call the right function (and always refresh)
161 | element.bootstrapDualListbox(actualFunction, actualValue, true);
162 | });
163 | });
164 | };
165 | /**
166 | * Refresh the Dual List Box using its own API.
167 | */
168 | var refresh = function () {
169 | // TODO: consider removing $timeout calls
170 | $timeout(function () {
171 | element.bootstrapDualListbox('refresh');
172 | });
173 | };
174 | /**
175 | * If the directive has not been initialized yet, do so.
176 | */
177 | var initMaybe = function () {
178 | // if it's the first initialization
179 | if (!dualListBox) {
180 | init();
181 | }
182 | };
183 | // Delay listbox init
184 | var init = function () {
185 | var defaults = {};
186 | // for every attribute the directive handles
187 | angular.forEach(attributes, function (attributeFunction, attributeName) {
188 | var actualValue = getAttributeValueOrDefault(attributeName);
189 | defaults[attributeName] = actualValue;
190 | });
191 | // Init the plugin
192 | dualListBox = element.bootstrapDualListbox({
193 | bootstrap2Compatible: defaults.bootstrap2,
194 | filterTextClear: defaults.filterClear,
195 | filterPlaceHolder: defaults.filterPlaceholder,
196 | moveSelectedLabel: defaults.moveSelectedLabel,
197 | moveAllLabel: defaults.moveAllLabel,
198 | removeSelectedLabel: defaults.removeSelectedLabel,
199 | removeAllLabel: defaults.removeAllLabel,
200 | moveOnSelect: defaults.moveOnSelect,
201 | preserveSelectionOnMove: defaults.preserveSelection,
202 | selectedListLabel: defaults.selectedListLabel,
203 | nonSelectedListLabel: defaults.nonSelectedListLabel,
204 | helperSelectNamePostfix: defaults.postfix,
205 | selectOrMinimalHeight: defaults.selectMinHeight,
206 | showFilterInputs: defaults.filter,
207 | nonSelectedFilter: '',
208 | selectedFilter: '',
209 | infoText: defaults.infoAll,
210 | infoTextFiltered: defaults.infoFiltered,
211 | infoTextEmpty: defaults.infoEmpty,
212 | filterOnValues: defaults.filterValues
213 | });
214 | // Inject the ng-model into the filters and re-compile them
215 | var container = element.bootstrapDualListbox('getContainer');
216 | var filterNonSelectedInput = container.find('.box1 .filter');
217 | filterNonSelectedInput.attr('ng-model', attrs.filterNonSelected);
218 | $compile(filterNonSelectedInput)(scope);
219 | var filterSelectedInput = container.find('.box2 .filter');
220 | filterSelectedInput.attr('ng-model', attrs.filterSelected);
221 | $compile(filterSelectedInput)(scope);
222 | };
223 | // Listen and respond to model changes
224 | listenToModel();
225 | // On destroy, collect ya garbage
226 | scope.$on('$destroy', function () {
227 | element.bootstrapDualListbox('destroy');
228 | });
229 | }
230 | };
231 | }
232 | ]);
--------------------------------------------------------------------------------
/dist/angular-bootstrap-duallistbox.min.js:
--------------------------------------------------------------------------------
1 | /**
2 | * angular-bootstrap-duallistbox
3 | * @version v0.1.0 - 2015-06-13
4 | * @author Francesco Pontillo (francescopontillo@gmail.com)
5 | * @link https://github.com/frapontillo/angular-bootstrap-duallistbox
6 | * @license Apache License 2.0
7 | **/
8 |
9 | "use strict";angular.module("frapontillo.bootstrap-duallistbox",[]),angular.module("frapontillo.bootstrap-duallistbox").directive("bsDuallistbox",["$compile","$timeout",function(a,b){return{restrict:"A",require:"ngModel",link:function(c,d,e){var f,g=/^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,h=e.ngOptions.match(g)[7],i=function(a){return a===!0||"true"===a},j={bootstrap2:{changeFn:"setBootstrap2Compatible",transformFn:i},postfix:"setHelperSelectNamePostfix",selectMinHeight:{changeFn:"setSelectOrMinimalHeight",defaultValue:100},filter:{changeFn:"setShowFilterInputs",defaultValue:!0,transformFn:i},filterClear:{changeFn:"setFilterTextClear",defaultValue:"show all"},filterPlaceholder:"setFilterPlaceHolder",filterValues:{changeFn:"setFilterOnValues",transformFn:i},moveOnSelect:{changeFn:"setMoveOnSelect",defaultValue:!0,transformFn:i},preserveSelection:"setPreserveSelectionOnMove",moveSelectedLabel:"setMoveSelectedLabel",moveAllLabel:"setMoveAllLabel",removeSelectedLabel:"setRemoveSelectedLabel",removeAllLabel:"setRemoveAllLabel",selectedListLabel:"setSelectedListLabel",nonSelectedListLabel:"setNonSelectedListLabel",infoAll:{changeFn:"setInfoText",defaultValue:"Showing all {0}"},infoFiltered:{changeFn:"setInfoTextFiltered",defaultValue:'Filtered {0} from {1}'},infoEmpty:{changeFn:"setInfoTextEmpty",defaultValue:"Empty list"}},k=function(a){var b,c,d=j[a],f=e[a];return angular.isObject(d)&&(b=d.defaultValue,c=d.transformFn),f||(f=b),angular.isFunction(c)&&(f=c(f)),f},l=function(a){var b=j[a],c=b;return angular.isObject(b)&&(c=b.changeFn),c},m=function(){c.$watch(e.ngModel,function(){o(),n()}),c.$watch(h,n,!0),c.$watch(e.filterNonSelected,function(){n()}),c.$watch(e.filterSelected,function(){n()}),angular.forEach(j,function(a,b){e.$observe(b,function(){var a=l(b),c=k(b);d.bootstrapDualListbox(a,c,!0)})})},n=function(){b(function(){d.bootstrapDualListbox("refresh")})},o=function(){f||p()},p=function(){var b={};angular.forEach(j,function(a,c){var d=k(c);b[c]=d}),f=d.bootstrapDualListbox({bootstrap2Compatible:b.bootstrap2,filterTextClear:b.filterClear,filterPlaceHolder:b.filterPlaceholder,moveSelectedLabel:b.moveSelectedLabel,moveAllLabel:b.moveAllLabel,removeSelectedLabel:b.removeSelectedLabel,removeAllLabel:b.removeAllLabel,moveOnSelect:b.moveOnSelect,preserveSelectionOnMove:b.preserveSelection,selectedListLabel:b.selectedListLabel,nonSelectedListLabel:b.nonSelectedListLabel,helperSelectNamePostfix:b.postfix,selectOrMinimalHeight:b.selectMinHeight,showFilterInputs:b.filter,nonSelectedFilter:"",selectedFilter:"",infoText:b.infoAll,infoTextFiltered:b.infoFiltered,infoTextEmpty:b.infoEmpty,filterOnValues:b.filterValues});var g=d.bootstrapDualListbox("getContainer"),h=g.find(".box1 .filter");h.attr("ng-model",e.filterNonSelected),a(h)(c);var i=g.find(".box2 .filter");i.attr("ng-model",e.filterSelected),a(i)(c)};m(),c.$on("$destroy",function(){d.bootstrapDualListbox("destroy")})}}}]);
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | AngularJS Bootstrap Duallistbox example
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/example/scripts/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('bsDuallistboxApp', ['frapontillo.bootstrap-duallistbox']);
4 |
--------------------------------------------------------------------------------
/example/scripts/controllers/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('bsDuallistboxApp')
4 | .controller('MainCtrl', function ($scope) {
5 | var lastIndex = 3;
6 | $scope.list = [];
7 |
8 | var updateList = function() {
9 | $scope.list.push({
10 | 'id': '_1',
11 | 'text': 'one'
12 | },{
13 | 'id': '_2',
14 | 'text': 'two'
15 | },{
16 | 'id': '_3',
17 | 'text': 'three'
18 | },{
19 | 'id': '_4',
20 | 'text': 'four'
21 | });
22 | };
23 |
24 | $scope.reset = function() {
25 | $scope.model = [];
26 | };
27 |
28 | $scope.add = function() {
29 | lastIndex++;
30 | updateList();
31 | };
32 |
33 | $scope.settings = {
34 | bootstrap2: false,
35 | filterClear: 'Show all!',
36 | filterPlaceHolder: 'Filter!',
37 | moveSelectedLabel: 'Move selected only',
38 | moveAllLabel: 'Move all!',
39 | removeSelectedLabel: 'Remove selected only',
40 | removeAllLabel: 'Remove all!',
41 | moveOnSelect: true,
42 | preserveSelection: 'moved',
43 | selectedListLabel: 'The selected',
44 | nonSelectedListLabel: 'The unselected',
45 | postfix: '_helperz',
46 | selectMinHeight: 130,
47 | filter: true,
48 | filterNonSelected: '1',
49 | filterSelected: '4',
50 | infoAll: 'Showing all {0}!',
51 | infoFiltered: 'Filtered {0} from {1}!',
52 | infoEmpty: 'Empty list!',
53 | filterValues: true
54 | };
55 |
56 | updateList();
57 |
58 | });
59 |
--------------------------------------------------------------------------------
/example/styles/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #fafafa;
3 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
4 | color: #333;
5 | }
6 |
7 | .hero-unit {
8 | margin: 50px auto 0 auto;
9 | width: 300px;
10 | font-size: 18px;
11 | font-weight: 200;
12 | line-height: 30px;
13 | background-color: #eee;
14 | border-radius: 6px;
15 | padding: 60px;
16 | }
17 |
18 | .hero-unit h1 {
19 | font-size: 60px;
20 | line-height: 1;
21 | letter-spacing: -1px;
22 | }
23 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration
2 | // Generated on Tue Mar 11 2014 19:55:38 GMT+0100 (ora solare Europa occidentale)
3 |
4 | module.exports = function(config) {
5 | config.set({
6 |
7 | // base path, that will be used to resolve files and exclude
8 | basePath: '',
9 |
10 |
11 | // frameworks to use
12 | frameworks: ['jasmine'],
13 |
14 |
15 | // list of files / patterns to load in the browser
16 | files: [
17 | 'bower_components/jquery/jquery.js',
18 | 'bower_components/angular/angular.js',
19 | 'bower_components/angular-mocks/angular-mocks.js',
20 | 'bower_components/bootstrap-duallistbox/src/jquery.bootstrap-duallistbox.js',
21 | 'common/module.js',
22 | 'src/**/*.js',
23 | 'test/**/*.js'
24 | ],
25 |
26 |
27 | // list of files to exclude
28 | exclude: [
29 |
30 | ],
31 |
32 |
33 | // test results reporter to use
34 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
35 | reporters: ['progress'],
36 |
37 |
38 | // web server port
39 | port: 9876,
40 |
41 |
42 | // enable / disable colors in the output (reporters and logs)
43 | colors: true,
44 |
45 |
46 | // level of logging
47 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
48 | logLevel: config.LOG_INFO,
49 |
50 |
51 | // enable / disable watching file and executing tests whenever any file changes
52 | autoWatch: true,
53 |
54 |
55 | // Start these browsers, currently available:
56 | // - Chrome
57 | // - ChromeCanary
58 | // - Firefox
59 | // - Opera (has to be installed with `npm install karma-opera-launcher`)
60 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
61 | // - PhantomJS
62 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
63 | browsers: ['PhantomJS'],
64 |
65 |
66 | // If browser does not capture in given timeout [ms], kill it
67 | captureTimeout: 60000,
68 |
69 |
70 | // Continuous Integration mode
71 | // if true, it capture browsers, run tests and exit
72 | singleRun: true
73 | });
74 | };
75 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-bootstrap-duallistbox",
3 | "version": "0.1.0",
4 | "author": {
5 | "name": "Francesco Pontillo",
6 | "email": "francescopontillo@gmail.com",
7 | "url": "https://github.com/frapontillo"
8 | },
9 | "homepage": "https://github.com/frapontillo/angular-bootstrap-duallistbox",
10 | "repository": {
11 | "type": "git",
12 | "url": "git@github.com:frapontillo/angular-bootstrap-duallistbox.git"
13 | },
14 | "licenses": [
15 | {
16 | "type": "Apache License 2.0",
17 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
18 | }
19 | ],
20 | "dependencies": {},
21 | "devDependencies": {
22 | "grunt": "~0.4.3",
23 | "grunt-contrib-concat": "~0.3.0",
24 | "grunt-contrib-uglify": "~0.4.0",
25 | "grunt-contrib-jshint": "~0.8.0",
26 | "grunt-contrib-clean": "~0.5.0",
27 | "grunt-karma": "~0.8.0",
28 | "matchdep": "~0.3.0",
29 | "grunt-ngmin": "0.0.3",
30 | "karma": "~0.12.0",
31 | "karma-jasmine": "~0.2.0",
32 | "karma-chrome-launcher": "^0.1.2",
33 | "karma-script-launcher": "^0.1.0",
34 | "karma-firefox-launcher": "^0.1.3",
35 | "karma-html2js-preprocessor": "^0.1.0",
36 | "requirejs": "^2.1.11",
37 | "karma-requirejs": "^0.2.1",
38 | "karma-phantomjs-launcher": "^0.1.2"
39 | },
40 | "engines": {
41 | "node": ">=0.10.0"
42 | },
43 | "scripts": {
44 | "test": "grunt test"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/directives/bsDuallistbox.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('frapontillo.bootstrap-duallistbox')
4 | .directive('bsDuallistbox', function ($compile, $timeout) {
5 | return {
6 | restrict: 'A',
7 | require: 'ngModel',
8 | link: function link(scope, element, attrs) {
9 | //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
10 | var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
11 |
12 | // The select collection
13 | var collection = attrs.ngOptions.match(NG_OPTIONS_REGEXP)[7];
14 |
15 | var getBooleanValue = function(attributeValue) {
16 | return (attributeValue === true || attributeValue === 'true');
17 | };
18 |
19 | // The attribute names to $observe with related functions to call
20 | var attributes = {
21 | 'bootstrap2': {changeFn: 'setBootstrap2Compatible', transformFn: getBooleanValue},
22 | 'postfix': 'setHelperSelectNamePostfix',
23 | 'selectMinHeight': {changeFn: 'setSelectOrMinimalHeight', defaultValue: 100},
24 |
25 | 'filter': {changeFn: 'setShowFilterInputs', defaultValue: true, transformFn: getBooleanValue},
26 | 'filterClear': {changeFn: 'setFilterTextClear', defaultValue: 'show all'},
27 | 'filterPlaceholder': 'setFilterPlaceHolder',
28 | 'filterValues': {changeFn: 'setFilterOnValues', transformFn: getBooleanValue},
29 |
30 | 'moveOnSelect': {changeFn: 'setMoveOnSelect', defaultValue: true, transformFn: getBooleanValue},
31 | 'preserveSelection': 'setPreserveSelectionOnMove',
32 |
33 | 'moveSelectedLabel': 'setMoveSelectedLabel',
34 | 'moveAllLabel': 'setMoveAllLabel',
35 | 'removeSelectedLabel': 'setRemoveSelectedLabel',
36 | 'removeAllLabel': 'setRemoveAllLabel',
37 | 'selectedListLabel' : 'setSelectedListLabel',
38 | 'nonSelectedListLabel' : 'setNonSelectedListLabel',
39 |
40 | 'infoAll': {changeFn: 'setInfoText', defaultValue: 'Showing all {0}'},
41 | 'infoFiltered': {changeFn: 'setInfoTextFiltered', defaultValue: 'Filtered {0} from {1}'},
42 | 'infoEmpty': {changeFn: 'setInfoTextEmpty', defaultValue: 'Empty list'}
43 | };
44 |
45 | // The duallistbox element
46 | var dualListBox;
47 |
48 | /**
49 | * Calculates the proper attribute value, given its name.
50 | * The attribute value depends on:
51 | * - the current value
52 | * - the default value, if it exists
53 | * - the transformFn, if it exists
54 | *
55 | * If the current value is undefined, it gets the default value.
56 | * The calculated value is then applied to the transformFn, if it is defined; the result is then returned.
57 | *
58 | * @param attributeName The {String} name of the attribute.
59 | * @returns {Object} representing the value of the attribute.
60 | */
61 | var getAttributeValueOrDefault = function(attributeName) {
62 | // get the attribute function/object for default and transformation
63 | var attributeFunction = attributes[attributeName];
64 | // get the current attribute value
65 | var attributeValue = attrs[attributeName];
66 |
67 | // By default, the default value and the transform function are not defined
68 | var defaultValue;
69 | var transformFn;
70 | // If the attributeFunction is an object
71 | if (angular.isObject(attributeFunction)) {
72 | // extract the default value and the transform function
73 | defaultValue = attributeFunction.defaultValue;
74 | transformFn = attributeFunction.transformFn;
75 | }
76 | // If the upcoming value is falsy, get the default
77 | if (!attributeValue) {
78 | attributeValue = defaultValue;
79 | }
80 | // If a transform function is defined, use it to change the value
81 | if (angular.isFunction(transformFn)) {
82 | attributeValue = transformFn(attributeValue);
83 | }
84 |
85 | return attributeValue;
86 | };
87 |
88 | /**
89 | * Gets the name of the function of `bootstrap-duallistbox` to be called to effectively
90 | * change the attribute in input.
91 | *
92 | * @param attributeName The name of the attribute to change.
93 | * @returns {String}, name of the `bootstrap-dual-listbox` to be called.
94 | */
95 | var getAttributeChangeFunction = function(attributeName) {
96 | // get the attribute function/object for default and transformation
97 | var attributeFunction = attributes[attributeName];
98 |
99 | // By default, attributeFunction is a function
100 | var actualFunction = attributeFunction;
101 | // If the attributeFunction is an object
102 | if (angular.isObject(attributeFunction)) {
103 | // extract the actual function name
104 | actualFunction = attributeFunction.changeFn;
105 | }
106 |
107 | return actualFunction;
108 | };
109 |
110 | /**
111 | * Listen to model changes.
112 | */
113 | var listenToModel = function () {
114 | // When ngModel changes, refresh the list
115 | // controller.$formatters.push(refresh);
116 | scope.$watch(attrs.ngModel, function() {
117 | initMaybe();
118 | refresh();
119 | });
120 |
121 | // When ngOptions changes, refresh the list
122 | scope.$watch(collection, refresh, true);
123 |
124 | // Watch for changes to the filter scope variables
125 | scope.$watch(attrs.filterNonSelected, function() {
126 | refresh();
127 | });
128 | scope.$watch(attrs.filterSelected, function() {
129 | refresh();
130 | });
131 |
132 | // $observe every attribute change
133 | angular.forEach(attributes, function(attributeFunction, attributeName) {
134 | attrs.$observe(attributeName, function() {
135 | var actualFunction = getAttributeChangeFunction(attributeName);
136 | var actualValue = getAttributeValueOrDefault(attributeName);
137 | // Depending on the attribute, call the right function (and always refresh)
138 | element.bootstrapDualListbox(actualFunction, actualValue, true);
139 | });
140 | });
141 | };
142 |
143 | /**
144 | * Refresh the Dual List Box using its own API.
145 | */
146 | var refresh = function() {
147 | // TODO: consider removing $timeout calls
148 | $timeout(function () {
149 | element.bootstrapDualListbox('refresh');
150 | });
151 | };
152 |
153 | /**
154 | * If the directive has not been initialized yet, do so.
155 | */
156 | var initMaybe = function() {
157 | // if it's the first initialization
158 | if (!dualListBox) {
159 | init();
160 | }
161 | };
162 |
163 | // Delay listbox init
164 | var init = function() {
165 | var defaults = {};
166 | // for every attribute the directive handles
167 | angular.forEach(attributes, function(attributeFunction, attributeName) {
168 | var actualValue = getAttributeValueOrDefault(attributeName);
169 | defaults[attributeName] = actualValue;
170 | });
171 |
172 | // Init the plugin
173 | dualListBox = element.bootstrapDualListbox({
174 | bootstrap2Compatible: defaults.bootstrap2,
175 | filterTextClear: defaults.filterClear,
176 | filterPlaceHolder: defaults.filterPlaceholder,
177 | moveSelectedLabel: defaults.moveSelectedLabel,
178 | moveAllLabel: defaults.moveAllLabel,
179 | removeSelectedLabel: defaults.removeSelectedLabel,
180 | removeAllLabel: defaults.removeAllLabel,
181 | moveOnSelect: defaults.moveOnSelect,
182 | preserveSelectionOnMove: defaults.preserveSelection,
183 | selectedListLabel: defaults.selectedListLabel,
184 | nonSelectedListLabel: defaults.nonSelectedListLabel,
185 | helperSelectNamePostfix: defaults.postfix,
186 | selectOrMinimalHeight: defaults.selectMinHeight,
187 | showFilterInputs: defaults.filter,
188 | nonSelectedFilter: '',
189 | selectedFilter: '',
190 | infoText: defaults.infoAll,
191 | infoTextFiltered: defaults.infoFiltered,
192 | infoTextEmpty: defaults.infoEmpty,
193 | filterOnValues: defaults.filterValues // TODO Test
194 | });
195 |
196 | // Inject the ng-model into the filters and re-compile them
197 | var container = element.bootstrapDualListbox('getContainer');
198 | var filterNonSelectedInput = container.find('.box1 .filter');
199 | filterNonSelectedInput.attr('ng-model', attrs.filterNonSelected);
200 | $compile(filterNonSelectedInput)(scope);
201 | var filterSelectedInput = container.find('.box2 .filter');
202 | filterSelectedInput.attr('ng-model', attrs.filterSelected);
203 | $compile(filterSelectedInput)(scope);
204 | };
205 |
206 | // Listen and respond to model changes
207 | listenToModel();
208 |
209 | // On destroy, collect ya garbage
210 | scope.$on('$destroy', function () {
211 | element.bootstrapDualListbox('destroy');
212 | });
213 | }
214 | };
215 | });
216 |
--------------------------------------------------------------------------------
/test/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "node": true,
3 | "browser": true,
4 | "esnext": true,
5 | "bitwise": true,
6 | "camelcase": true,
7 | "curly": true,
8 | "eqeqeq": true,
9 | "immed": true,
10 | "indent": 2,
11 | "latedef": true,
12 | "newcap": true,
13 | "noarg": true,
14 | "quotmark": "single",
15 | "regexp": true,
16 | "undef": true,
17 | "unused": true,
18 | "strict": true,
19 | "trailing": true,
20 | "smarttabs": true,
21 | "globals": {
22 | "after": false,
23 | "afterEach": false,
24 | "angular": false,
25 | "before": false,
26 | "beforeEach": false,
27 | "browser": false,
28 | "describe": false,
29 | "expect": false,
30 | "inject": false,
31 | "it": false,
32 | "spyOn": false
33 | }
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/test/runner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | End2end Test Runner
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test/spec/directives/bsDuallistboxSpec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | describe('Directive: bsDuallistbox', function () {
4 | var scope, $sandbox, $compile, $timeout;
5 |
6 | beforeEach(module('frapontillo.bootstrap-duallistbox'));
7 |
8 | /* jshint camelcase: false */
9 | beforeEach(inject(function ($injector, $rootScope, _$compile_, _$timeout_) {
10 | scope = $rootScope;
11 | $compile = _$compile_;
12 | $timeout = _$timeout_;
13 | $sandbox = angular.element('').appendTo(angular.element.find('body'));
14 | }));
15 | /* jshint camelcase: true */
16 |
17 | afterEach(function () {
18 | $sandbox.remove();
19 | scope.$destroy();
20 | });
21 |
22 | var templates = {
23 | 'default': {
24 | scope: {
25 | list: ['one', 'two', 'three', 'four', 'five', 'forty-two'],
26 | model: []
27 | },
28 | element: 'ng-options="element for element in list" '
29 | },
30 | 'bootstrap2': {
31 | scope: {
32 | bootstrap2: false
33 | },
34 | element: 'bootstrap2="{{ bootstrap2 }}" ' +
35 | 'ng-options="element for element in list" '
36 | },
37 | 'filter': {
38 | scope: {
39 | filter: { },
40 | info: { },
41 | list: [{
42 | 'id': '_1',
43 | 'text': 'one'
44 | },{
45 | 'id': '_2',
46 | 'text': 'two'
47 | },{
48 | 'id': '_3',
49 | 'text': 'three'
50 | },{
51 | 'id': '_4',
52 | 'text': 'four'
53 | }],
54 | model: [{
55 | 'id': '_2',
56 | 'text': 'two'
57 | }, {
58 | 'id': '_4',
59 | 'text': 'four'
60 | }]
61 | },
62 | element: '' +
63 | 'ng-options="element as element.text for element in list track by element.id" ' +
64 | 'filter-placeholder="{{ filter.filterPlaceholder }}" ' +
65 | 'show-filter-inputs="{{ filter.showFilterInputs }}" ' +
66 | 'filter-non-selected="filter.filterNonSelected" ' +
67 | 'filter-selected="filter.filterSelected" ' +
68 | 'filter-clear="{{ filter.filterClear }}" ' +
69 | 'filter-values="{{ filter.filterValues }}" ' +
70 | 'info-all="{{ info.text }}" ' +
71 | 'info-filtered="{{ info.textFiltered }}" '
72 | },
73 | 'empty': {
74 | element: '' +
75 | 'ng-options="element for element in list" ' +
76 | 'info-empty="{{ info.textEmpty }}" '
77 | },
78 | 'buttons': {
79 | scope: {
80 | labels: {
81 | }
82 | },
83 | element: '' +
84 | 'ng-options="element for element in list" ' +
85 | 'move-selected-label="{{ labels.moveSelected }}" ' +
86 | 'move-all-label="{{ labels.moveAll }}" ' +
87 | 'remove-selected-label="{{ labels.removeSelected }}" ' +
88 | 'remove-all-label="{{ labels.removeAll }}" '
89 | },
90 | 'lists': {
91 | scope: {
92 | labels: {
93 | }
94 | },
95 | element: '' +
96 | 'ng-options="element for element in list" ' +
97 | 'non-selected-list-label="{{ labels.nonSelected }}" ' +
98 | 'selected-list-label="{{ labels.selected }}" ' +
99 | 'postfix="{{ postfix }}" '
100 | },
101 | 'move': {
102 | element: '' +
103 | 'ng-options="element for element in list" ' +
104 | 'move-on-select="{{ moveOnSelect }}" '
105 | },
106 | 'preserve': {
107 | element: '' +
108 | 'ng-options="element for element in list" ' +
109 | 'move-on-select="{{ moveOnSelect }}" ' +
110 | 'preserve-selection="{{ preserveSelection }}" ',
111 | scope: {
112 | model: ['one','two','three'],
113 | moveOnSelect: false,
114 | preserveSelection: false
115 | }
116 | },
117 | 'height': {
118 | element: '' +
119 | 'ng-options="element for element in list" ' +
120 | 'select-min-height="{{ minimalHeight }}" '
121 | }
122 | };
123 |
124 | var basicElement = [
125 | ''
129 | ];
130 |
131 | /**
132 | * Build an element string.
133 | * @param template The template element to be used
134 | * @returns {string} The HTML element as a string
135 | */
136 | function buildElement(template) {
137 | var elementContent = template.element;
138 | var realElement;
139 | if (template.override) {
140 | realElement = elementContent;
141 | } else {
142 | realElement = basicElement[0] + elementContent + basicElement[1];
143 | }
144 | return realElement;
145 | }
146 |
147 | /**
148 | * Compile a given template object as a `select`.
149 | * @param template The template object
150 | * @returns {*} compiled angular element
151 | */
152 | function compileDirective(template) {
153 | // If the template is undefined, default to 'default'
154 | var compileTemplate = template || 'default';
155 | template = templates[compileTemplate];
156 | // Extend the current scope with the default one and then the selected one
157 | angular.extend(scope, angular.copy(templates['default'].scope), angular.copy(template.scope));
158 | var content = buildElement(template);
159 | var $element = angular.element(content).appendTo($sandbox);
160 | $compile($element)(scope);
161 | scope.$apply();
162 | $timeout.flush();
163 | $element = $sandbox.find('*:first-child');
164 | return $element;
165 | }
166 |
167 | /**
168 | * Gets the text of an {HTMLOption} element.
169 | * @param option The {HTMLOption} element.
170 | * @returns {String} content of the element.
171 | */
172 | function getText(option) {
173 | return option.text;
174 | }
175 |
176 | describe('default behaviour', function() {
177 | var element;
178 | beforeEach(function() {
179 | element = compileDirective('default');
180 | });
181 |
182 | // Test the duallistbox creation and defaults
183 | it('should create a duallistbox', inject(function () {
184 | expect(element.hasClass('bootstrap-duallistbox-container')).toBe(true);
185 | expect(element.find('.box1')).toBeDefined();
186 | expect(element.find('.box2')).toBeDefined();
187 | expect($sandbox.find('> select').css('display')).toBe('none');
188 | }));
189 |
190 | // Test the unselected and selected list after bootstrap
191 | it('should properly initialize both list', inject(function () {
192 | expect(element.find('#bootstrap-duallistbox-nonselected-list_mySelect').length).not.toBe(0);
193 | expect(element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option').length).toBe(6);
194 | expect(element.find('#bootstrap-duallistbox-selected-list_mySelect').length).not.toBe(0);
195 | expect(element.find('#bootstrap-duallistbox-selected-list_mySelect > option').length).toBe(0);
196 | }));
197 |
198 | // A model change should update both lists accordingly
199 | it('should update lists according to the model change', inject(function () {
200 | scope.model = ['one', 'two'];
201 | scope.$apply();
202 | $timeout.flush();
203 |
204 | var nonSelectedOpts = element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option');
205 | expect(nonSelectedOpts.length).toBe(4);
206 | var nonSelectedTexts = Array.prototype.map.call(nonSelectedOpts, getText);
207 | expect(nonSelectedTexts).toContain('three');
208 | expect(nonSelectedTexts).toContain('four');
209 | expect(nonSelectedTexts).toContain('five');
210 | expect(nonSelectedTexts).toContain('forty-two');
211 |
212 | var selectedOpts = element.find('#bootstrap-duallistbox-selected-list_mySelect > option');
213 | expect(selectedOpts.length).toBe(2);
214 | var selectedTexts = Array.prototype.map.call(selectedOpts, getText);
215 | expect(selectedTexts).toContain('one');
216 | expect(selectedTexts).toContain('two');
217 | }));
218 | });
219 |
220 | describe('appearance', function() {
221 | // Test the bootstrap2 compatibility
222 | it('should be compatible with bootstrap version 2', inject(function () {
223 | var element = compileDirective('bootstrap2');
224 | expect(element.hasClass('row')).toBeTruthy();
225 | expect(element.hasClass('row-fluid bs2compatible')).toBeFalsy();
226 | scope.bootstrap2 = true;
227 | scope.$apply();
228 | expect(element.hasClass('row')).toBeFalsy();
229 | expect(element.hasClass('row-fluid bs2compatible')).toBeTruthy();
230 | }));
231 |
232 | // Test the height of the selects
233 | it('should be more than about 100px of height', inject(function () {
234 | var element = compileDirective('height');
235 | var h1 = element.find('.box1 select').height();
236 | var h2 = element.find('.box2 select').height();
237 | expect(h1).toBeGreaterThan(95);
238 | expect(h2).toBeGreaterThan(95);
239 | }));
240 |
241 | // Test the height of the selects
242 | it('should be more than about 200px of height', inject(function () {
243 | var element = compileDirective('height');
244 | scope.minimalHeight = 200;
245 | scope.$apply();
246 | var h1 = element.find('.box1 select').height();
247 | var h2 = element.find('.box2 select').height();
248 | expect(h1).toBeGreaterThan(195);
249 | expect(h2).toBeGreaterThan(195);
250 | }));
251 | });
252 |
253 | describe('filter', function() {
254 | var element;
255 | beforeEach(function() {
256 | element = compileDirective('filter');
257 | });
258 |
259 | // Test the filter defaults
260 | it('should setup the default filter parameters (placeholder, visibility)', inject(function () {
261 | element.find('.filter').each(function (position, filterInput) {
262 | var $input = angular.element(filterInput);
263 | expect($input.attr('placeholder')).toBe('Filter');
264 | expect($input.is(':visible')).toBeTruthy();
265 | // filter inputs should be empty
266 | expect($input.val()).toBe('');
267 | });
268 | // both selects have elements
269 | expect(element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option').length).toBe(2);
270 | expect(element.find('#bootstrap-duallistbox-selected-list_mySelect > option').length).toBe(2);
271 | }));
272 |
273 | // Test the filtering system
274 | it('should filter the selects when the model changes', inject(function () {
275 | // filter both lists
276 | scope.filter.filterNonSelected = 't';
277 | scope.filter.filterSelected = 'f';
278 | scope.$apply();
279 | $timeout.flush();
280 | // the non selected list must contain only 'three' and 'forty-two'
281 | var nonSelectedOpts = element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option');
282 | expect(nonSelectedOpts.length).toBe(1);
283 | expect(Array.prototype.map.call(nonSelectedOpts, getText)).toContain('three');
284 | // the selected list must contain only 'four'
285 | var selectedOpts = element.find('#bootstrap-duallistbox-selected-list_mySelect > option');
286 | expect(selectedOpts.length).toBe(1);
287 | expect(Array.prototype.map.call(selectedOpts, getText)).toContain('four');
288 | }));
289 |
290 | // Test the values filtering system
291 | it('should filter the selects with values when the model changes', inject(function () {
292 | // filter both lists
293 | scope.filter.filterValues = true;
294 | scope.filter.filterNonSelected = '1';
295 | scope.filter.filterSelected = '4';
296 | scope.$apply();
297 | $timeout.flush();
298 | // the non selected list must contain only 'one' and 'four'
299 | var nonSelectedOpts = element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option');
300 | expect(nonSelectedOpts.length).toBe(1);
301 | expect(Array.prototype.map.call(nonSelectedOpts, getText)).toContain('one');
302 | // the selected list must contain only 'four'
303 | var selectedOpts = element.find('#bootstrap-duallistbox-selected-list_mySelect > option');
304 | expect(selectedOpts.length).toBe(1);
305 | expect(Array.prototype.map.call(selectedOpts, getText)).toContain('four');
306 | }));
307 |
308 | // Test the filtering system by view
309 | it('should filter the selects when the view changes', inject(function () {
310 | // filter both lists
311 | element.find('.box1 .filter').val('t').change();
312 | element.find('.box2 .filter').val('f').change();
313 | // the non selected list must contain only 'three' and 'forty-two'
314 | var nonSelectedOpts = element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option');
315 | expect(nonSelectedOpts.length).toBe(1);
316 | expect(Array.prototype.map.call(nonSelectedOpts, getText)).toContain('three');
317 | // the selected list must contain only 'four'
318 | var selectedOpts = element.find('#bootstrap-duallistbox-selected-list_mySelect > option');
319 | expect(selectedOpts.length).toBe(1);
320 | expect(Array.prototype.map.call(selectedOpts, getText)).toContain('four');
321 | }));
322 |
323 | // Test the values filtering system by view
324 | it('should filter the selects with values when the view changes', inject(function () {
325 | // filter both lists
326 | scope.filter.filterValues = true;
327 | element.find('.box1 .filter').val('1').change();
328 | element.find('.box2 .filter').val('4').change();
329 | scope.$apply();
330 | // the non selected list must contain only 'one' and 'four'
331 | var nonSelectedOpts = element.find('#bootstrap-duallistbox-nonselected-list_mySelect > option');
332 | expect(nonSelectedOpts.length).toBe(1);
333 | expect(Array.prototype.map.call(nonSelectedOpts, getText)).toContain('one');
334 | // the selected list must contain only 'four'
335 | var selectedOpts = element.find('#bootstrap-duallistbox-selected-list_mySelect > option');
336 | expect(selectedOpts.length).toBe(1);
337 | expect(Array.prototype.map.call(selectedOpts, getText)).toContain('four');
338 | }));
339 | });
340 |
341 | describe('info text', function() {
342 | // Test the info text for unfiltered lists
343 | it('should change the info text for unfiltered lists', inject(function () {
344 | var element = compileDirective('filter');
345 | // filter inputs should be empty
346 | expect(element.find('.box1 .info').html()).toBe('Showing all 2');
347 | expect(element.find('.box2 .info').html()).toBe('Showing all 2');
348 | scope.info.text = 'All {0}';
349 | scope.$apply();
350 | expect(element.find('.box1 .info').html()).toBe('All 2');
351 | expect(element.find('.box2 .info').html()).toBe('All 2');
352 | }));
353 |
354 | // Test the info text for filtered lists
355 | it('should change the info text for filtered lists', inject(function () {
356 | var element = compileDirective('filter');
357 | scope.filter.filterNonSelected = 't';
358 | scope.filter.filterSelected = 'f';
359 | scope.$apply();
360 | $timeout.flush();
361 | // filter inputs should be empty
362 | expect(element.find('.box1 .info').html()).toBe('Filtered 1 from 2');
363 | expect(element.find('.box2 .info').html()).toBe('Filtered 1 from 2');
364 | scope.info.textFiltered = '{0}/{1}';
365 | scope.$apply();
366 | expect(element.find('.box1 .info').html()).toBe('1/2');
367 | expect(element.find('.box2 .info').html()).toBe('1/2');
368 | }));
369 |
370 | // Test the info text for empty lists
371 | it('should change the info text for empty lists', inject(function () {
372 | var element = compileDirective('empty');
373 |
374 | // test default behavior
375 | expect(element.find('.box2 .info').html()).toBe('Empty list');
376 | // move all elements
377 | scope.model = angular.copy(scope.list);
378 | scope.$apply();
379 | $timeout.flush();
380 | expect(element.find('.box1 .info').html()).toBe('Empty list');
381 |
382 | // change the empty text
383 | scope.info = { textEmpty: 'No luck, mate'};
384 | scope.$apply();
385 | expect(element.find('.box1 .info').html()).toBe('No luck, mate');
386 | scope.model = [];
387 | scope.$apply();
388 | $timeout.flush();
389 | expect(element.find('.box2 .info').html()).toBe('No luck, mate');
390 | }));
391 | });
392 |
393 | describe('clear button', function() {
394 | // Test the clear text button
395 | it('should change the text for the clear button', inject(function () {
396 | var element = compileDirective('filter');
397 | // filter both lists
398 | scope.filter.filterNonSelected = 't';
399 | scope.filter.filterSelected = 'f';
400 | scope.$apply();
401 | $timeout.flush();
402 | expect(element.find('.box1 .clear1').html()).toBe('show all');
403 | expect(element.find('.box2 .clear2').html()).toBe('show all');
404 |
405 | // change the button text
406 | scope.filter.filterClear = 'Clear filter';
407 | scope.$apply();
408 | expect(element.find('.box1 .clear1').html()).toBe('Clear filter');
409 | expect(element.find('.box2 .clear2').html()).toBe('Clear filter');
410 | }));
411 | });
412 |
413 | describe('button title', function() {
414 | var element;
415 | beforeEach(function() {
416 | element = compileDirective('buttons');
417 | });
418 |
419 | // Test the default button titles
420 | it('should set the default button titles', inject(function () {
421 | expect(element.find('.box1 .move').attr('title')).toBe('Move selected');
422 | expect(element.find('.box1 .moveall').attr('title')).toBe('Move all');
423 | expect(element.find('.box2 .remove').attr('title')).toBe('Remove selected');
424 | expect(element.find('.box2 .removeall').attr('title')).toBe('Remove all');
425 | }));
426 |
427 | // Test the change of button titles
428 | it('should change the button titles', inject(function () {
429 | scope.labels.moveSelected = 'Move this';
430 | scope.labels.moveAll = 'Move them all';
431 | scope.labels.removeSelected = 'Remove this';
432 | scope.labels.removeAll = 'Remove them all';
433 | scope.$apply();
434 | expect(element.find('.box1 .move').attr('title')).toBe(scope.labels.moveSelected);
435 | expect(element.find('.box1 .moveall').attr('title')).toBe(scope.labels.moveAll);
436 | expect(element.find('.box2 .remove').attr('title')).toBe(scope.labels.removeSelected);
437 | expect(element.find('.box2 .removeall').attr('title')).toBe(scope.labels.removeAll);
438 | }));
439 | });
440 |
441 | describe('list label', function() {
442 | var element;
443 | beforeEach(function() {
444 | element = compileDirective('lists');
445 | });
446 |
447 | // Test the default list labels
448 | it('should set the default list labels', inject(function () {
449 | expect(element.find('.box1 > label').is(':visible')).toBeFalsy();
450 | expect(element.find('.box1 > label').html()).toBe('');
451 | expect(element.find('.box2 > label').is(':visible')).toBeFalsy();
452 | expect(element.find('.box2 > label').html()).toBe('');
453 | }));
454 |
455 | // Test the change of list labels
456 | it('should change the list labels', inject(function () {
457 | scope.labels.nonSelected = 'The unselected';
458 | scope.labels.selected = 'The selected';
459 | scope.$apply();
460 | expect(element.find('.box1 > label').is(':visible')).toBeTruthy();
461 | expect(element.find('.box1 > label').html()).toBe(scope.labels.nonSelected);
462 | expect(element.find('.box2 > label').is(':visible')).toBeTruthy();
463 | expect(element.find('.box2 > label').html()).toBe(scope.labels.selected);
464 | }));
465 | });
466 |
467 | describe('move and remove', function() {
468 | var element;
469 | beforeEach(function() {
470 | element = compileDirective('move');
471 | });
472 |
473 | // Test the selects after the click on options
474 | it('should move and remove elements when they are selected (default)', inject(function () {
475 | // select 'one','two','three'
476 | element.find('.box1 select option').slice(0, 3).prop('selected', true).change();
477 | expect(scope.model.length).toBe(3);
478 | expect(scope.model).toContain('one');
479 | expect(scope.model).toContain('two');
480 | expect(scope.model).toContain('three');
481 | element.find('.box2 select option').slice(0, 3).prop('selected', true).change();
482 | expect(scope.model.length).toBe(0);
483 | }));
484 |
485 | // Test the selects after the click on options
486 | it('should not move elements when they are selected', inject(function () {
487 | scope.moveOnSelect = false;
488 | scope.$apply();
489 | // select 'one','two','three'
490 | element.find('.box1 select option').slice(0, 3).prop('selected', true).change();
491 | expect(scope.model.length).toBe(0);
492 | }));
493 |
494 | // Test the left select after options are selected and moved
495 | it('should move elements when "move" button is clicked', inject(function () {
496 | scope.moveOnSelect = false;
497 | scope.$apply();
498 | // select 'one','two','three'
499 | element.find('.box1 select option').slice(0, 3).prop('selected', true).change();
500 | element.find('.box1 .move').click();
501 | expect(scope.model.length).toBe(3);
502 | expect(scope.model).toContain('one');
503 | expect(scope.model).toContain('two');
504 | expect(scope.model).toContain('three');
505 | }));
506 |
507 | // Test the left select after all options are moved
508 | it('should move all elements when "move all" button is clicked', inject(function () {
509 | scope.moveOnSelect = false;
510 | scope.$apply();
511 | // move all
512 | element.find('.box1 .moveall').click();
513 | expect(scope.model.length).toBe(6);
514 | }));
515 |
516 | // Test the right select after options are selected and removed
517 | it('should remove elements when "remove" button is clicked', inject(function () {
518 | scope.moveOnSelect = false;
519 | // select 'one','two','three'
520 | scope.model = ['one','two','three'];
521 | scope.$apply();
522 | expect(scope.model.length).toBe(3);
523 | // move two
524 | element.find('.box2 select option').slice(0, 2).prop('selected', true).change();
525 | element.find('.box2 .remove').click();
526 | expect(scope.model.length).toBe(1);
527 | }));
528 |
529 | // Test the right select after all options are removed
530 | it('should remove all elements when "remove all" button is clicked', inject(function () {
531 | scope.moveOnSelect = false;
532 | // select 'one','two','three'
533 | scope.model = ['one','two','three'];
534 | scope.$apply();
535 | expect(scope.model.length).toBe(3);
536 | // move all
537 | element.find('.box2 .removeall').click();
538 | expect(scope.model.length).toBe(0);
539 | }));
540 | });
541 |
542 | describe('preserve', function() {
543 | var element;
544 | beforeEach(function() {
545 | element = compileDirective('preserve');
546 | });
547 |
548 | // Test both selects after options are selected, then moved or removed
549 | it('should not preserve any selection', inject(function () {
550 | // select 'four' and move it
551 | element.find('.box1 select option').slice(0, 1).prop('selected', true).change();
552 | element.find('.box1 .move').click();
553 | // no option should be selected after the move
554 | expect(element.find('select option:selected').length).toBe(0);
555 | }));
556 |
557 | // Test both selects after options are selected, then moved or removed
558 | it('should preserve moved selections only', inject(function () {
559 | scope.preserveSelection = 'moved';
560 | scope.$apply();
561 | // select 'four' among the unselected list
562 | element.find('.box1 select option').slice(0, 1).prop('selected', true).change();
563 | // select 'one' among the selected list
564 | element.find('.box2 select option').slice(0, 1).prop('selected', true).change();
565 | // move 'four' to the selected list
566 | element.find('.box1 .move').click();
567 | // only 'four' should be selected
568 | expect(element.find('select option:selected').length).toBe(1);
569 | expect(element.find('select option:selected').text()).toBe('four');
570 | }));
571 |
572 | // Test both selects after options are selected, then moved or removed
573 | it('should preserve all selections', inject(function () {
574 | scope.preserveSelection = 'all';
575 | scope.$apply();
576 | // select 'four' among the unselected list
577 | element.find('.box1 select option').slice(0, 1).prop('selected', true).change();
578 | // select 'one' among the selected list
579 | element.find('.box2 select option').slice(0, 1).prop('selected', true).change();
580 | // move 'four' to the selected list
581 | element.find('.box1 .move').click();
582 | // both 'one' and 'four' should be selected
583 | expect(element.find('select option:selected').length).toBe(2);
584 | expect(Array.prototype.map.call(element.find('select option:selected'), getText)).toContain('one');
585 | expect(Array.prototype.map.call(element.find('select option:selected'), getText)).toContain('four');
586 | }));
587 | });
588 |
589 | describe('postfix and name', function() {
590 | var element;
591 | beforeEach(function() {
592 | element = compileDirective('lists');
593 | });
594 |
595 | var testNamesIds = function(postfix) {
596 | var thePostfix = postfix || '_helper';
597 | expect(element.find('.box1 select').attr('name')).toBe('mySelect' + thePostfix + '1');
598 | expect(element.find('.box2 select').attr('name')).toBe('mySelect' + thePostfix + '2');
599 | };
600 |
601 | it('should generate the correct ids', function() {
602 | expect(element.find('.box1 > label').attr('for')).toBe('bootstrap-duallistbox-nonselected-list_mySelect');
603 | expect(element.find('.box1 select').attr('id')).toBe('bootstrap-duallistbox-nonselected-list_mySelect');
604 | expect(element.find('.box2 > label').attr('for')).toBe('bootstrap-duallistbox-selected-list_mySelect');
605 | expect(element.find('.box2 select').attr('id')).toBe('bootstrap-duallistbox-selected-list_mySelect');
606 | });
607 |
608 | // Test the default names and ids
609 | it('should generate the default names and ids', inject(function () {
610 | testNamesIds(scope.postfix);
611 | }));
612 |
613 | // Test the default names and ids
614 | it('should generate custom names and ids', inject(function () {
615 | scope.postfix = '_my-postfix_';
616 | scope.$apply();
617 | testNamesIds(scope.postfix);
618 | }));
619 | });
620 |
621 | });
622 |
--------------------------------------------------------------------------------