bootstrap-multiselect-rails 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 209905ce2f3ed58adb2a4b612d313c503c66ec42
4
+ data.tar.gz: 8471b006251914b1e32056fb5052f64bfa07a0df
5
+ SHA512:
6
+ metadata.gz: ebb318bd3d83fd0aedcdead2a0f38e57463958c7dc120286482913a4c00c68ba4ce8972398c4db442b4da77a77bd77d58f56c2b769a260d4a4a2added9bd3747
7
+ data.tar.gz: e672577800668d926e952739f5b71433fe3df2ac04f590aa9e1cfdf0c26102ebb46d772ba89748b152522532c5922435df2448debe395861f7e4948f1344b102
data/README.md CHANGED
@@ -16,11 +16,11 @@ And then execute:
16
16
 
17
17
  In `application.js`:
18
18
 
19
- //= require bootstrap-multiselect-rails
19
+ //= require bootstrap-multiselect
20
20
 
21
21
  In `application.css`:
22
22
 
23
- *= require bootstrap-multiselect-rails
23
+ *= require bootstrap-multiselect
24
24
 
25
25
  ## License
26
26
 
@@ -1,4 +1,4 @@
1
1
  module BootstrapMultiselectRails
2
- class Engine < Rails::Engine
3
- end
2
+ class Engine < Rails::Engine
3
+ end
4
4
  end
@@ -1,3 +1,3 @@
1
1
  module BootstrapMultiselectRails
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -1,296 +1,452 @@
1
1
  /**
2
- * bootstrap-multiselect.js 1.0.0
2
+ * bootstrap-multiselect.js
3
3
  * https://github.com/davidstutz/bootstrap-multiselect
4
4
  *
5
- * Copyright 2012 David Stutz
5
+ * Copyright 2012 - 2014 David Stutz
6
6
  *
7
- * Licensed under the Apache License, Version 2.0 (the "License");
8
- * you may not use this file except in compliance with the License.
9
- * You may obtain a copy of the License at
10
- *
11
- * http://www.apache.org/licenses/LICENSE-2.0
12
- *
13
- * Unless required by applicable law or agreed to in writing, software
14
- * distributed under the License is distributed on an "AS IS" BASIS,
15
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
- * See the License for the specific language governing permissions and
17
- * limitations under the License.
7
+ * Dual licensed under the BSD-3-Clause and the Apache License, Version 2.0.
18
8
  */
19
- !function($) {"use strict";// jshint ;_;
9
+ !function($) {
20
10
 
21
- if ( typeof ko != 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
11
+ "use strict";// jshint ;_;
12
+
13
+ if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
22
14
  ko.bindingHandlers.multiselect = {
23
- init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
15
+
16
+ init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
17
+
18
+ var listOfSelectedItems = allBindingsAccessor().selectedOptions,
19
+ config = ko.utils.unwrapObservable(valueAccessor());
20
+
21
+ $(element).multiselect(config);
22
+
23
+ if (isObservableArray(listOfSelectedItems)) {
24
+ // Subscribe to the selectedOptions: ko.observableArray
25
+ listOfSelectedItems.subscribe(function (changes) {
26
+ var addedArray = [], deletedArray = [];
27
+ changes.forEach(function (change) {
28
+ switch (change.status) {
29
+ case 'added':
30
+ addedArray.push(change.value);
31
+ break;
32
+ case 'deleted':
33
+ deletedArray.push(change.value);
34
+ break;
35
+ }
36
+ });
37
+ if (addedArray.length > 0) {
38
+ $(element).multiselect('select', addedArray);
39
+ };
40
+ if (deletedArray.length > 0) {
41
+ $(element).multiselect('deselect', deletedArray);
42
+ };
43
+ }, null, "arrayChange");
44
+ }
24
45
  },
25
- update : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
26
- var ms = $(element).data('multiselect');
46
+
47
+ update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
48
+
49
+ var listOfItems = allBindingsAccessor().options,
50
+ ms = $(element).data('multiselect'),
51
+ config = ko.utils.unwrapObservable(valueAccessor());
52
+
53
+ if (isObservableArray(listOfItems)) {
54
+ // Subscribe to the options: ko.observableArray incase it changes later
55
+ listOfItems.subscribe(function (theArray) {
56
+ $(element).multiselect('rebuild');
57
+ });
58
+ }
59
+
27
60
  if (!ms) {
28
- $(element).multiselect(ko.utils.unwrapObservable(valueAccessor()));
61
+ $(element).multiselect(config);
29
62
  }
30
- else
31
- if (allBindingsAccessor().options && allBindingsAccessor().options().length !== ms.originalOptions.length) {
63
+ else {
32
64
  ms.updateOriginalOptions();
33
- $(element).multiselect('rebuild');
34
65
  }
35
66
  }
36
67
  };
37
68
  }
38
69
 
70
+ function isObservableArray(obj) {
71
+ return ko.isObservable(obj) && !(obj.destroyAll === undefined);
72
+ }
73
+
74
+ /**
75
+ * Constructor to create a new multiselect using the given select.
76
+ *
77
+ * @param {jQuery} select
78
+ * @param {Object} options
79
+ * @returns {Multiselect}
80
+ */
39
81
  function Multiselect(select, options) {
40
82
 
41
- this.options = this.getOptions(options);
83
+ this.options = this.mergeOptions(options);
42
84
  this.$select = $(select);
85
+
86
+ // Initialization.
87
+ // We have to clone to create a new reference.
43
88
  this.originalOptions = this.$select.clone()[0].options;
44
- //we have to clone to create a new reference
45
89
  this.query = '';
46
90
  this.searchTimeout = null;
47
91
 
48
- this.options.multiple = this.$select.attr('multiple') == "multiple";
49
-
50
- this.$container = $(this.options.buttonContainer).append('<button type="button" class="multiselect dropdown-toggle ' + this.options.buttonClass + '" data-toggle="dropdown">' + this.options.buttonText(this.getSelected(), this.$select) + '</button>')
51
- .append('<ul class="multiselect-container dropdown-menu' + (this.options.dropRight ? ' pull-right' : '') + '"></ul>');
52
-
53
- if (this.options.buttonWidth) {
54
- $('button', this.$container).css({
55
- 'width' : this.options.buttonWidth
56
- });
57
- }
58
-
59
- // Set max height of dropdown menu to activate auto scrollbar.
60
- if (this.options.maxHeight) {
61
- // TODO: Add a class for this option to move the css declarations.
62
- $('.multiselect-container', this.$container).css({
63
- 'max-height' : this.options.maxHeight + 'px',
64
- 'overflow-y' : 'auto',
65
- 'overflow-x' : 'hidden'
66
- });
67
- }
68
-
69
- // Enable filtering.
70
- if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
71
- this.buildFilter();
72
- }
92
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
93
+ this.options.onChange = $.proxy(this.options.onChange, this);
94
+ this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
95
+ this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
73
96
 
97
+ // Build select all if enabled.
98
+ this.buildContainer();
99
+ this.buildButton();
100
+ this.buildSelectAll();
74
101
  this.buildDropdown();
102
+ this.buildDropdownOptions();
103
+ this.buildFilter();
104
+
75
105
  this.updateButtonText();
106
+ this.updateSelectAll();
76
107
 
77
108
  this.$select.hide().after(this.$container);
78
109
  };
79
110
 
80
111
  Multiselect.prototype = {
81
112
 
82
- defaults : {
83
- // Default text function will either print 'None selected' in case no
84
- // option is selected, or a list of the selected options up to a length of 3 selected options.
85
- // If more than 3 options are selected, the number of selected options is printed.
86
- buttonText : function(options, select) {
87
- if (options.length == 0) {
88
- return this.nonSelectedText + '<b class="caret"></b>';
113
+ defaults: {
114
+ /**
115
+ * Default text function will either print 'None selected' in case no
116
+ * option is selected or a list of the selected options up to a length of 3 selected options.
117
+ *
118
+ * @param {jQuery} options
119
+ * @param {jQuery} select
120
+ * @returns {String}
121
+ */
122
+ buttonText: function(options, select) {
123
+ if (options.length === 0) {
124
+ return this.nonSelectedText + ' <b class="caret"></b>';
89
125
  }
90
- else
91
- if (options.length > 3) {
92
- return options.length + ' ' + this.nSelectedText + ' <b class="caret"></b>';
126
+ else {
127
+ if (options.length > this.numberDisplayed) {
128
+ return options.length + ' ' + this.nSelectedText + ' <b class="caret"></b>';
129
+ }
130
+ else {
131
+ var selected = '';
132
+ options.each(function() {
133
+ var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).html();
134
+
135
+ selected += label + ', ';
136
+ });
137
+ return selected.substr(0, selected.length - 2) + ' <b class="caret"></b>';
138
+ }
139
+ }
140
+ },
141
+ /**
142
+ * Updates the title of the button similar to the buttonText function.
143
+ * @param {jQuery} options
144
+ * @param {jQuery} select
145
+ * @returns {@exp;selected@call;substr}
146
+ */
147
+ buttonTitle: function(options, select) {
148
+ if (options.length === 0) {
149
+ return this.nonSelectedText;
93
150
  }
94
151
  else {
95
152
  var selected = '';
96
- options.each(function() {
97
- var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).html();
98
-
99
- selected += label + ', ';
153
+ options.each(function () {
154
+ selected += $(this).text() + ', ';
100
155
  });
101
- return selected.substr(0, selected.length - 2) + ' <b class="caret"></b>';
156
+ return selected.substr(0, selected.length - 2);
102
157
  }
103
158
  },
104
- // Is triggered on change of the selected options.
159
+ /**
160
+ * Create a label.
161
+ *
162
+ * @param {jQuery} element
163
+ * @returns {String}
164
+ */
165
+ label: function(element){
166
+ return $(element).attr('label') || $(element).html();
167
+ },
168
+ /**
169
+ * Triggered on change of the multiselect.
170
+ * Not triggered when selecting/deselecting options manually.
171
+ *
172
+ * @param {jQuery} option
173
+ * @param {Boolean} checked
174
+ */
105
175
  onChange : function(option, checked) {
106
176
 
107
177
  },
108
- buttonClass : 'btn',
109
- dropRight : false,
110
- selectedClass : 'active',
111
- buttonWidth : 'auto',
112
- buttonContainer : '<div class="btn-group" />',
178
+ /**
179
+ * Triggered when the dropdown is shown.
180
+ *
181
+ * @param {jQuery} event
182
+ */
183
+ onDropdownShow: function(event) {
184
+
185
+ },
186
+ /**
187
+ * Triggered when the dropdown is hidden.
188
+ *
189
+ * @param {jQuery} event
190
+ */
191
+ onDropdownHide: function(event) {
192
+
193
+ },
194
+ buttonClass: 'btn btn-default',
195
+ dropRight: false,
196
+ selectedClass: 'active',
197
+ buttonWidth: 'auto',
198
+ buttonContainer: '<div class="btn-group" />',
113
199
  // Maximum height of the dropdown menu.
114
200
  // If maximum height is exceeded a scrollbar will be displayed.
115
- maxHeight : false,
116
- includeSelectAllOption : false,
117
- selectAllText : ' Select all',
118
- selectAllValue : 'multiselect-all',
119
- enableFiltering : false,
120
- enableCaseInsensitiveFiltering : false,
121
- filterPlaceholder : 'Search',
201
+ maxHeight: false,
202
+ includeSelectAllOption: false,
203
+ selectAllText: ' Select all',
204
+ selectAllValue: 'multiselect-all',
205
+ enableFiltering: false,
206
+ enableCaseInsensitiveFiltering: false,
207
+ filterPlaceholder: 'Search',
122
208
  // possible options: 'text', 'value', 'both'
123
- filterBehavior : 'text',
209
+ filterBehavior: 'text',
124
210
  preventInputChangeEvent: false,
125
211
  nonSelectedText: 'None selected',
126
- nSelectedText: 'selected'
212
+ nSelectedText: 'selected',
213
+ numberDisplayed: 3
127
214
  },
128
215
 
129
- constructor : Multiselect,
216
+ templates: {
217
+ button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"></button>',
218
+ ul: '<ul class="multiselect-container dropdown-menu"></ul>',
219
+ filter: '<div class="input-group"><span class="input-group-addon"><i class="glyphicon glyphicon-search"></i></span><input class="form-control multiselect-search" type="text"></div>',
220
+ li: '<li><a href="javascript:void(0);"><label></label></a></li>',
221
+ divider: '<li class="divider"></li>',
222
+ liGroup: '<li><label class="multiselect-group"></label></li>'
223
+ },
130
224
 
131
- // Will build an dropdown element for the given option.
132
- createOptionValue : function(element) {
133
- if ($(element).is(':selected')) {
134
- $(element).attr('selected', 'selected').prop('selected', true);
135
- }
225
+ constructor: Multiselect,
136
226
 
137
- // Support the label attribute on options.
138
- var label = $(element).attr('label') || $(element).html();
139
- var value = $(element).val();
140
- var inputType = this.options.multiple ? "checkbox" : "radio";
227
+ /**
228
+ * Builds the container of the multiselect.
229
+ */
230
+ buildContainer: function() {
231
+ this.$container = $(this.options.buttonContainer);
232
+ this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
233
+ this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
234
+ },
141
235
 
142
- var $li = $('<li><a href="javascript:void(0);"><label class="' + inputType + '"><input type="' + inputType + '" /></label></a></li>');
236
+ /**
237
+ * Builds the button of the multiselect.
238
+ */
239
+ buildButton: function() {
240
+ this.$button = $(this.templates.button).addClass(this.options.buttonClass);
143
241
 
144
- var selected = $(element).prop('selected') || false;
145
- var $checkbox = $('input', $li);
146
- $checkbox.val(value);
242
+ // Adopt active state.
243
+ if (this.$select.prop('disabled')) {
244
+ this.disable();
245
+ }
246
+ else {
247
+ this.enable();
248
+ }
147
249
 
148
- if (value == this.options.selectAllValue) {
149
- $checkbox.parent().parent().addClass('multiselect-all');
250
+ // Manually add button width if set.
251
+ if (this.options.buttonWidth && this.options.buttonWidth != 'auto') {
252
+ this.$button.css({
253
+ 'width' : this.options.buttonWidth
254
+ });
150
255
  }
151
256
 
152
- $('label', $li).append(" " + label);
257
+ // Keep the tab index from the select.
258
+ var tabindex = this.$select.attr('tabindex');
259
+ if (tabindex) {
260
+ this.$button.attr('tabindex', tabindex);
261
+ }
153
262
 
154
- $('.multiselect-container', this.$container).append($li);
263
+ this.$container.prepend(this.$button);
264
+ },
155
265
 
156
- if ($(element).is(':disabled')) {
157
- $checkbox.attr('disabled', 'disabled').prop('disabled', true).parents('li').addClass('disabled');
158
- }
266
+ /**
267
+ * Builds the ul representing the dropdown menu.
268
+ */
269
+ buildDropdown: function() {
159
270
 
160
- $checkbox.prop('checked', selected);
271
+ // Build ul.
272
+ this.$ul = $(this.templates.ul);
161
273
 
162
- if (selected && this.options.selectedClass) {
163
- $checkbox.parents('li').addClass(this.options.selectedClass);
274
+ if (this.options.dropRight) {
275
+ this.$ul.addClass('pull-right');
164
276
  }
165
- },
166
277
 
167
- toggleActiveState : function(shouldBeActive) {
168
- if (this.$select.attr('disabled') == undefined) {
169
- $('button.multiselect.dropdown-toggle', this.$container).removeClass('disabled');
170
- }
171
- else {
172
- $('button.multiselect.dropdown-toggle', this.$container).addClass('disabled');
278
+ // Set max height of dropdown menu to activate auto scrollbar.
279
+ if (this.options.maxHeight) {
280
+ // TODO: Add a class for this option to move the css declarations.
281
+ this.$ul.css({
282
+ 'max-height': this.options.maxHeight + 'px',
283
+ 'overflow-y': 'auto',
284
+ 'overflow-x': 'hidden'
285
+ });
173
286
  }
174
- },
175
-
176
- // Build the dropdown and bind event handling.
177
- buildDropdown : function() {
178
- var alreadyHasSelectAll = this.$select[0][0] ? this.$select[0][0].value == this.options.selectAllValue : false;
179
287
 
180
- // If options.includeSelectAllOption === true, add the include all
181
- // checkbox.
182
- if (this.options.includeSelectAllOption && this.options.multiple && !alreadyHasSelectAll) {
183
- this.$select.prepend('<option value="' + this.options.selectAllValue + '">' + this.options.selectAllText + '</option>');
184
- }
288
+ this.$container.append(this.$ul);
289
+ },
185
290
 
186
- this.toggleActiveState();
291
+ /**
292
+ * Build the dropdown options and binds all nessecary events.
293
+ * Uses createDivider and createOptionValue to create the necessary options.
294
+ */
295
+ buildDropdownOptions: function() {
187
296
 
188
297
  this.$select.children().each($.proxy(function(index, element) {
298
+
189
299
  // Support optgroups and options without a group simultaneously.
190
- var tag = $(element).prop('tagName').toLowerCase();
191
- if (tag == 'optgroup') {
192
- var group = element;
193
- var groupName = $(group).prop('label');
194
-
195
- // Add a header for the group.
196
- var $li = $('<li><label class="multiselect-group"></label></li>');
197
- $('label', $li).text(groupName);
198
- $('.multiselect-container', this.$container).append($li);
199
-
200
- // Add the options of the group.
201
- $('option', group).each($.proxy(function(index, element) {
202
- this.createOptionValue(element);
203
- }, this));
204
- }
205
- else
206
- if (tag == 'option') {
207
- this.createOptionValue(element);
300
+ var tag = $(element).prop('tagName')
301
+ .toLowerCase();
302
+
303
+ if (tag === 'optgroup') {
304
+ this.createOptgroup(element);
208
305
  }
209
- else {
210
- // Ignore illegal tags.
306
+ else if (tag === 'option') {
307
+
308
+ if ($(element).data('role') === 'divider') {
309
+ this.createDivider();
310
+ }
311
+ else {
312
+ this.createOptionValue(element);
313
+ }
314
+
211
315
  }
316
+
317
+ // Other illegal tags will be ignored.
212
318
  }, this));
213
319
 
214
320
  // Bind the change event on the dropdown elements.
215
- $('.multiselect-container li input', this.$container).on('change', $.proxy(function(event) {
321
+ $('li input', this.$ul).on('change', $.proxy(function(event) {
216
322
  var checked = $(event.target).prop('checked') || false;
217
- var isSelectAllOption = $(event.target).val() == this.options.selectAllValue;
323
+ var isSelectAllOption = $(event.target).val() === this.options.selectAllValue;
218
324
 
219
325
  // Apply or unapply the configured selected class.
220
326
  if (this.options.selectedClass) {
221
327
  if (checked) {
222
- $(event.target).parents('li').addClass(this.options.selectedClass);
328
+ $(event.target).parents('li')
329
+ .addClass(this.options.selectedClass);
223
330
  }
224
331
  else {
225
- $(event.target).parents('li').removeClass(this.options.selectedClass);
332
+ $(event.target).parents('li')
333
+ .removeClass(this.options.selectedClass);
226
334
  }
227
335
  }
228
336
 
229
- var $option = $('option', this.$select).filter(function() {
230
- return $(this).val() == $(event.target).val();
231
- });
337
+ // Get the corresponding option.
338
+ var value = $(event.target).val();
339
+ var $option = this.getOptionByValue(value);
232
340
 
233
341
  var $optionsNotThis = $('option', this.$select).not($option);
234
342
  var $checkboxesNotThis = $('input', this.$container).not($(event.target));
235
343
 
236
- // Toggle all options if the select all option was changed.
237
344
  if (isSelectAllOption) {
238
- $checkboxesNotThis.filter(function() {
239
- return $(this).is(':checked') != checked;
240
- }).trigger('click');
345
+ if (this.$select[0][0].value === this.options.selectAllValue) {
346
+ var values = [];
347
+ var options = $('option[value!="' + this.options.selectAllValue + '"]', this.$select);
348
+ for (var i = 0; i < options.length; i++) {
349
+ // Additionally check whether the option is visible within the dropcown.
350
+ if (options[i].value !== this.options.selectAllValue && this.getInputByValue(options[i].value).is(':visible')) {
351
+ values.push(options[i].value);
352
+ }
353
+ }
354
+
355
+ if (checked) {
356
+ this.select(values);
357
+ }
358
+ else {
359
+ this.deselect(values);
360
+ }
361
+ }
241
362
  }
242
363
 
243
364
  if (checked) {
244
365
  $option.prop('selected', true);
245
366
 
246
367
  if (this.options.multiple) {
247
- $option.attr('selected', 'selected');
368
+ // Simply select additional option.
369
+ $option.prop('selected', true);
248
370
  }
249
371
  else {
372
+ // Unselect all other options and corresponding checkboxes.
250
373
  if (this.options.selectedClass) {
251
374
  $($checkboxesNotThis).parents('li').removeClass(this.options.selectedClass);
252
375
  }
253
376
 
254
377
  $($checkboxesNotThis).prop('checked', false);
255
-
256
- $optionsNotThis.removeAttr('selected').prop('selected', false);
378
+ $optionsNotThis.prop('selected', false);
257
379
 
258
380
  // It's a single selection, so close.
259
- $(this.$container).find(".multiselect.dropdown-toggle").click();
381
+ this.$button.click();
260
382
  }
261
383
 
262
- if (this.options.selectedClass == "active") {
384
+ if (this.options.selectedClass === "active") {
263
385
  $optionsNotThis.parents("a").css("outline", "");
264
386
  }
265
-
266
387
  }
267
388
  else {
268
- $option.removeAttr('selected').prop('selected', false);
389
+ // Unselect option.
390
+ $option.prop('selected', false);
269
391
  }
270
392
 
271
- this.updateButtonText();
272
-
393
+ this.$select.change();
273
394
  this.options.onChange($option, checked);
274
395
 
275
- this.$select.change();
396
+ this.updateButtonText();
397
+ this.updateSelectAll();
276
398
 
277
399
  if(this.options.preventInputChangeEvent) {
278
400
  return false;
279
401
  }
280
402
  }, this));
281
403
 
282
- $('.multiselect-container li a', this.$container).on('touchstart click', function(event) {
404
+ $('li a', this.$ul).on('touchstart click', function(event) {
283
405
  event.stopPropagation();
406
+
407
+ if (event.shiftKey) {
408
+ var checked = $(event.target).prop('checked') || false;
409
+
410
+ if (checked) {
411
+ var prev = $(event.target).parents('li:last')
412
+ .siblings('li[class="active"]:first');
413
+
414
+ var currentIdx = $(event.target).parents('li')
415
+ .index();
416
+ var prevIdx = prev.index();
417
+
418
+ if (currentIdx > prevIdx) {
419
+ $(event.target).parents("li:last").prevUntil(prev).each(
420
+ function() {
421
+ $(this).find("input:first").prop("checked", true)
422
+ .trigger("change");
423
+ }
424
+ );
425
+ }
426
+ else {
427
+ $(event.target).parents("li:last").nextUntil(prev).each(
428
+ function() {
429
+ $(this).find("input:first").prop("checked", true)
430
+ .trigger("change");
431
+ }
432
+ );
433
+ }
434
+ }
435
+ }
436
+
284
437
  $(event.target).blur();
285
438
  });
286
439
 
287
440
  // Keyboard support.
288
441
  this.$container.on('keydown', $.proxy(function(event) {
289
- if ($('input[type="text"]', this.$container).is(':focus'))
442
+ if ($('input[type="text"]', this.$container).is(':focus')) {
290
443
  return;
291
- if ((event.keyCode == 9 || event.keyCode == 27) && this.$container.hasClass('open')) {
444
+ }
445
+ if ((event.keyCode === 9 || event.keyCode === 27)
446
+ && this.$container.hasClass('open')) {
447
+
292
448
  // Close on tab or escape.
293
- $(this.$container).find(".multiselect.dropdown-toggle").click();
449
+ this.$button.click();
294
450
  }
295
451
  else {
296
452
  var $items = $(this.$container).find("li:not(.divider):visible a");
@@ -302,30 +458,21 @@
302
458
  var index = $items.index($items.filter(':focus'));
303
459
 
304
460
  // Navigation up.
305
- if (event.keyCode == 38 && index > 0) {
461
+ if (event.keyCode === 38 && index > 0) {
306
462
  index--;
307
463
  }
308
464
  // Navigate down.
309
- else
310
- if (event.keyCode == 40 && index < $items.length - 1) {
465
+ else if (event.keyCode === 40 && index < $items.length - 1) {
311
466
  index++;
312
467
  }
313
- else
314
- if (!~index) {
468
+ else if (!~index) {
315
469
  index = 0;
316
470
  }
317
471
 
318
472
  var $current = $items.eq(index);
319
473
  $current.focus();
320
474
 
321
- // Override style for items in li:active.
322
- if (this.options.selectedClass == "active") {
323
- $current.css("outline", "thin dotted #333").css("outline", "5px auto -webkit-focus-ring-color");
324
-
325
- $items.not($current).css("outline", "");
326
- }
327
-
328
- if (event.keyCode == 32 || event.keyCode == 13) {
475
+ if (event.keyCode === 32 || event.keyCode === 13) {
329
476
  var $checkbox = $current.find('input');
330
477
 
331
478
  $checkbox.prop("checked", !$checkbox.prop("checked"));
@@ -338,174 +485,468 @@
338
485
  }, this));
339
486
  },
340
487
 
341
- // Build and bind filter.
342
- buildFilter: function() {
343
- $('.multiselect-container', this.$container).prepend('<div class="input-prepend"><span class="add-on"><i class="icon-search"></i></span><input class="multiselect-search" type="text" placeholder="' + this.options.filterPlaceholder + '"></div>');
488
+ /**
489
+ * Create an option using the given select option.
490
+ *
491
+ * @param {jQuery} element
492
+ */
493
+ createOptionValue: function(element) {
494
+ if ($(element).is(':selected')) {
495
+ $(element).prop('selected', true);
496
+ }
344
497
 
345
- $('.multiselect-search', this.$container).val(this.query).on('click', function(event) {
346
- event.stopPropagation();
347
- }).on('keydown', $.proxy(function(event) {
348
- // This is useful to catch "keydown" events after the browser has
349
- // updated the control.
350
- clearTimeout(this.searchTimeout);
351
-
352
- this.searchTimeout = this.asyncFunction($.proxy(function() {
353
-
354
- if (this.query != event.target.value) {
355
- this.query = event.target.value;
356
-
357
- $.each($('.multiselect-container li', this.$container), $.proxy(function(index, element) {
358
- var value = $('input', element).val();
359
- if (value != this.options.selectAllValue) {
360
- var text = $('label', element).text();
361
- var value = $('input', element).val();
362
- if (value && text && value != this.options.selectAllValue) {
363
- // by default lets assume that element is not
364
- // interesting for this search
365
- var showElement = false;
366
-
367
- var filterCandidate = '';
368
- if ((this.options.filterBehavior == 'text' || this.options.filterBehavior == 'both')) {
369
- filterCandidate = text;
370
- }
371
- if ((this.options.filterBehavior == 'value' || this.options.filterBehavior == 'both')) {
372
- filterCandidate = value;
373
- }
498
+ // Support the label attribute on options.
499
+ var label = this.options.label(element);
500
+ var value = $(element).val();
501
+ var inputType = this.options.multiple ? "checkbox" : "radio";
374
502
 
375
- if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
376
- showElement = true;
377
- }
378
- else if (filterCandidate.indexOf(this.query) > -1) {
379
- showElement = true;
380
- }
503
+ var $li = $(this.templates.li);
504
+ $('label', $li).addClass(inputType);
505
+ $('label', $li).append('<input type="' + inputType + '" />');
381
506
 
382
- if (showElement) {
383
- $(element).show();
384
- }
385
- else {
386
- $(element).hide();
507
+ var selected = $(element).prop('selected') || false;
508
+ var $checkbox = $('input', $li);
509
+ $checkbox.val(value);
510
+
511
+ if (value === this.options.selectAllValue) {
512
+ $checkbox.parent().parent()
513
+ .addClass('multiselect-all');
514
+ }
515
+
516
+ $('label', $li).append(" " + label);
517
+
518
+ this.$ul.append($li);
519
+
520
+ if ($(element).is(':disabled')) {
521
+ $checkbox.attr('disabled', 'disabled')
522
+ .prop('disabled', true)
523
+ .parents('li')
524
+ .addClass('disabled');
525
+ }
526
+
527
+ $checkbox.prop('checked', selected);
528
+
529
+ if (selected && this.options.selectedClass) {
530
+ $checkbox.parents('li')
531
+ .addClass(this.options.selectedClass);
532
+ }
533
+ },
534
+
535
+ /**
536
+ * Creates a divider using the given select option.
537
+ *
538
+ * @param {jQuery} element
539
+ */
540
+ createDivider: function(element) {
541
+ var $divider = $(this.templates.divider);
542
+ this.$ul.append($divider);
543
+ },
544
+
545
+ /**
546
+ * Creates an optgroup.
547
+ *
548
+ * @param {jQuery} group
549
+ */
550
+ createOptgroup: function(group) {
551
+ var groupName = $(group).prop('label');
552
+
553
+ // Add a header for the group.
554
+ var $li = $(this.templates.liGroup);
555
+ $('label', $li).text(groupName);
556
+
557
+ this.$ul.append($li);
558
+
559
+ if ($(group).is(':disabled')) {
560
+ $li.addClass('disabled');
561
+ }
562
+
563
+ // Add the options of the group.
564
+ $('option', group).each($.proxy(function(index, element) {
565
+ this.createOptionValue(element);
566
+ }, this));
567
+ },
568
+
569
+ /**
570
+ * Build the selct all.
571
+ * Checks if a select all ahs already been created.
572
+ */
573
+ buildSelectAll: function() {
574
+ var alreadyHasSelectAll = this.hasSelectAll();
575
+
576
+ // If options.includeSelectAllOption === true, add the include all checkbox.
577
+ if (this.options.includeSelectAllOption && this.options.multiple && !alreadyHasSelectAll) {
578
+ if (this.options.includeSelectAllDivider) {
579
+ this.$select.prepend('<option value="" disabled="disabled" data-role="divider">');
580
+ }
581
+ this.$select.prepend('<option value="' + this.options.selectAllValue + '">' + this.options.selectAllText + '</option>');
582
+ }
583
+ },
584
+
585
+ /**
586
+ * Builds the filter.
587
+ */
588
+ buildFilter: function() {
589
+
590
+ // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
591
+ if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
592
+ var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
593
+
594
+ if (this.$select.find('option').length >= enableFilterLength) {
595
+
596
+ this.$filter = $(this.templates.filter);
597
+ $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
598
+ this.$ul.prepend(this.$filter);
599
+
600
+ this.$filter.val(this.query).on('click', function(event) {
601
+ event.stopPropagation();
602
+ }).on('input keydown', $.proxy(function(event) {
603
+ // This is useful to catch "keydown" events after the browser has updated the control.
604
+ clearTimeout(this.searchTimeout);
605
+
606
+ this.searchTimeout = this.asyncFunction($.proxy(function() {
607
+
608
+ if (this.query !== event.target.value) {
609
+ this.query = event.target.value;
610
+
611
+ $.each($('li', this.$ul), $.proxy(function(index, element) {
612
+ var value = $('input', element).val();
613
+ var text = $('label', element).text();
614
+
615
+ if (value !== this.options.selectAllValue && text) {
616
+ // by default lets assume that element is not
617
+ // interesting for this search
618
+ var showElement = false;
619
+
620
+ var filterCandidate = '';
621
+ if ((this.options.filterBehavior === 'text' || this.options.filterBehavior === 'both')) {
622
+ filterCandidate = text;
623
+ }
624
+ if ((this.options.filterBehavior === 'value' || this.options.filterBehavior === 'both')) {
625
+ filterCandidate = value;
626
+ }
627
+
628
+ if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
629
+ showElement = true;
630
+ }
631
+ else if (filterCandidate.indexOf(this.query) > -1) {
632
+ showElement = true;
633
+ }
634
+
635
+ if (showElement) {
636
+ $(element).show();
637
+ }
638
+ else {
639
+ $(element).hide();
640
+ }
387
641
  }
388
- }
642
+ }, this));
389
643
  }
390
- }, this));
391
- }
392
- }, this), 300, this);
393
- }, this));
644
+
645
+ // TODO: check whether select all option needs to be updated.
646
+ }, this), 300, this);
647
+ }, this));
648
+ }
649
+ }
394
650
  },
395
651
 
396
- // Destroy - unbind - the plugin.
397
- destroy : function() {
652
+ /**
653
+ * Unbinds the whole plugin.
654
+ */
655
+ destroy: function() {
398
656
  this.$container.remove();
399
657
  this.$select.show();
400
658
  },
401
659
 
402
- // Refreshs the checked options based on the current state of the select.
403
- refresh : function() {
660
+ /**
661
+ * Refreshs the multiselect based on the selected options of the select.
662
+ */
663
+ refresh: function() {
404
664
  $('option', this.$select).each($.proxy(function(index, element) {
405
- var $input = $('.multiselect-container li input', this.$container).filter(function() {
406
- return $(this).val() == $(element).val();
665
+ var $input = $('li input', this.$ul).filter(function() {
666
+ return $(this).val() === $(element).val();
407
667
  });
408
668
 
409
669
  if ($(element).is(':selected')) {
410
670
  $input.prop('checked', true);
411
671
 
412
672
  if (this.options.selectedClass) {
413
- $input.parents('li').addClass(this.options.selectedClass);
673
+ $input.parents('li')
674
+ .addClass(this.options.selectedClass);
414
675
  }
415
676
  }
416
677
  else {
417
678
  $input.prop('checked', false);
418
679
 
419
680
  if (this.options.selectedClass) {
420
- $input.parents('li').removeClass(this.options.selectedClass);
681
+ $input.parents('li')
682
+ .removeClass(this.options.selectedClass);
421
683
  }
422
684
  }
423
685
 
424
686
  if ($(element).is(":disabled")) {
425
- $input.attr('disabled', 'disabled').prop('disabled', true).parents('li').addClass('disabled');
687
+ $input.attr('disabled', 'disabled')
688
+ .prop('disabled', true)
689
+ .parents('li')
690
+ .addClass('disabled');
426
691
  }
427
692
  else {
428
- $input.removeAttr('disabled').prop('disabled', false).parents('li').removeClass('disabled');
693
+ $input.prop('disabled', false)
694
+ .parents('li')
695
+ .removeClass('disabled');
429
696
  }
430
697
  }, this));
431
698
 
432
699
  this.updateButtonText();
700
+ this.updateSelectAll();
433
701
  },
434
702
 
435
- // Select an option by its value.
436
- select : function(value) {
437
- var $option = $('option', this.$select).filter(function() {
438
- return $(this).val() == value;
439
- });
440
- var $checkbox = $('.multiselect-container li input', this.$container).filter(function() {
441
- return $(this).val() == value;
442
- });
443
-
444
- if (this.options.selectedClass) {
445
- $checkbox.parents('li').addClass(this.options.selectedClass);
703
+ /**
704
+ * Select all options of the given values.
705
+ *
706
+ * @param {Array} selectValues
707
+ */
708
+ select: function(selectValues) {
709
+ if(selectValues && !$.isArray(selectValues)) {
710
+ selectValues = [selectValues];
446
711
  }
447
712
 
448
- $checkbox.prop('checked', true);
713
+ for (var i = 0; i < selectValues.length; i++) {
714
+ var value = selectValues[i];
449
715
 
450
- $option.attr('selected', 'selected').prop('selected', true);
716
+ var $option = this.getOptionByValue(value);
717
+ var $checkbox = this.getInputByValue(value);
718
+
719
+ if (this.options.selectedClass) {
720
+ $checkbox.parents('li')
721
+ .addClass(this.options.selectedClass);
722
+ }
723
+
724
+ $checkbox.prop('checked', true);
725
+ $option.prop('selected', true);
726
+ }
451
727
 
452
728
  this.updateButtonText();
453
- this.options.onChange($option, true);
454
729
  },
455
730
 
456
- // Deselect an option by its value.
457
- deselect : function(value) {
458
- var $option = $('option', this.$select).filter(function() {
459
- return $(this).val() == value;
460
- });
461
- var $checkbox = $('.multiselect-container li input', this.$container).filter(function() {
462
- return $(this).val() == value;
463
- });
731
+ /**
732
+ * Clears all selected items
733
+ *
734
+ */
735
+ clearSelection: function () {
464
736
 
465
- if (this.options.selectedClass) {
466
- $checkbox.parents('li').removeClass(this.options.selectedClass);
737
+ var selected = this.getSelected();
738
+
739
+ if (selected.length) {
740
+
741
+ var arry = [];
742
+
743
+ for (var i = 0; i < selected.length; i = i + 1) {
744
+ arry.push(selected[i].value);
745
+ }
746
+
747
+ this.deselect(arry);
748
+ this.$select.change();
467
749
  }
750
+ },
468
751
 
469
- $checkbox.prop('checked', false);
752
+ /**
753
+ * Deselects all options of the given values.
754
+ *
755
+ * @param {Array} deselectValues
756
+ */
757
+ deselect: function(deselectValues) {
758
+ if(deselectValues && !$.isArray(deselectValues)) {
759
+ deselectValues = [deselectValues];
760
+ }
470
761
 
471
- $option.removeAttr('selected').prop('selected', false);
762
+ for (var i = 0; i < deselectValues.length; i++) {
763
+
764
+ var value = deselectValues[i];
765
+
766
+ var $option = this.getOptionByValue(value);
767
+ var $checkbox = this.getInputByValue(value);
768
+
769
+ if (this.options.selectedClass) {
770
+ $checkbox.parents('li')
771
+ .removeClass(this.options.selectedClass);
772
+ }
773
+
774
+ $checkbox.prop('checked', false);
775
+ $option.prop('selected', false);
776
+ }
472
777
 
473
778
  this.updateButtonText();
474
- this.options.onChange($option, false);
475
779
  },
476
780
 
477
- // Rebuild the whole dropdown menu.
478
- rebuild : function() {
479
- $('.multiselect-container', this.$container).html('');
480
- this.buildDropdown(this.$select, this.options);
781
+ /**
782
+ * Rebuild the plugin.
783
+ * Rebuilds the dropdown, the filter and the select all option.
784
+ */
785
+ rebuild: function() {
786
+ this.$ul.html('');
787
+
788
+ // Remove select all option in select.
789
+ $('option[value="' + this.options.selectAllValue + '"]', this.$select).remove();
790
+
791
+ // Important to distinguish between radios and checkboxes.
792
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
793
+
794
+ this.buildSelectAll();
795
+ this.buildDropdownOptions();
796
+ this.buildFilter();
797
+
481
798
  this.updateButtonText();
799
+ this.updateSelectAll();
800
+ },
482
801
 
483
- // Enable filtering.
484
- if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
485
- this.buildFilter();
486
- }
802
+ /**
803
+ * The provided data will be used to build the dropdown.
804
+ *
805
+ * @param {Array} dataprovider
806
+ */
807
+ dataprovider: function(dataprovider) {
808
+ var optionDOM = "";
809
+ dataprovider.forEach(function (option) {
810
+ optionDOM += '<option value="' + option.value + '">' + option.label + '</option>';
811
+ });
812
+
813
+ this.$select.html(optionDOM);
814
+ this.rebuild();
487
815
  },
488
816
 
489
- // Get options by merging defaults and given options.
490
- getOptions : function(options) {
491
- return $.extend({}, this.defaults, options);
817
+ /**
818
+ * Enable the multiselect.
819
+ */
820
+ enable: function() {
821
+ this.$select.prop('disabled', false);
822
+ this.$button.prop('disabled', false)
823
+ .removeClass('disabled');
492
824
  },
493
825
 
494
- updateButtonText : function() {
826
+ /**
827
+ * Disable the multiselect.
828
+ */
829
+ disable: function() {
830
+ this.$select.prop('disabled', true);
831
+ this.$button.prop('disabled', true)
832
+ .addClass('disabled');
833
+ },
834
+
835
+ /**
836
+ * Set the options.
837
+ *
838
+ * @param {Array} options
839
+ */
840
+ setOptions: function(options) {
841
+ this.options = this.mergeOptions(options);
842
+ },
843
+
844
+ /**
845
+ * Merges the given options with the default options.
846
+ *
847
+ * @param {Array} options
848
+ * @returns {Array}
849
+ */
850
+ mergeOptions: function(options) {
851
+ return $.extend({}, this.defaults, this.options, options);
852
+ },
853
+
854
+ /**
855
+ * Checks whether a select all option is present.
856
+ *
857
+ * @returns {Boolean}
858
+ */
859
+ hasSelectAll: function() {
860
+ return this.$select[0][0] ? this.$select[0][0].value === this.options.selectAllValue : false;
861
+ },
862
+
863
+ /**
864
+ * Updates the select all option based on the currently selected options.
865
+ */
866
+ updateSelectAll: function() {
867
+ if (this.hasSelectAll()) {
868
+ var selected = this.getSelected();
869
+
870
+ if (selected.length === $('option:not([data-role=divider])', this.$select).length - 1) {
871
+ this.select(this.options.selectAllValue);
872
+ }
873
+ else {
874
+ this.deselect(this.options.selectAllValue);
875
+ }
876
+ }
877
+ },
878
+
879
+ /**
880
+ * Update the button text and its title based on the currently selected options.
881
+ */
882
+ updateButtonText: function() {
495
883
  var options = this.getSelected();
884
+
885
+ // First update the displayed button text.
496
886
  $('button', this.$container).html(this.options.buttonText(options, this.$select));
887
+
888
+ // Now update the title attribute of the button.
889
+ $('button', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
890
+
891
+ },
892
+
893
+ /**
894
+ * Get all selected options.
895
+ *
896
+ * @returns {jQUery}
897
+ */
898
+ getSelected: function() {
899
+ return $('option[value!="' + this.options.selectAllValue + '"]:selected', this.$select).filter(function() {
900
+ return $(this).prop('selected');
901
+ });
902
+ },
903
+
904
+ /**
905
+ * Gets a select option by its value.
906
+ *
907
+ * @param {String} value
908
+ * @returns {jQuery}
909
+ */
910
+ getOptionByValue: function (value) {
911
+
912
+ var options = $('option', this.$select);
913
+ var valueToCompare = value.toString();
914
+
915
+ for (var i = 0; i < options.length; i = i + 1) {
916
+ var option = options[i];
917
+ if (option.value == valueToCompare) {
918
+ return $(option);
919
+ }
920
+ }
497
921
  },
498
922
 
499
- // Get all selected options.
500
- getSelected : function() {
501
- return $('option:selected[value!="' + this.options.selectAllValue + '"]', this.$select);
923
+ /**
924
+ * Get the input (radio/checkbox) by its value.
925
+ *
926
+ * @param {String} value
927
+ * @returns {jQuery}
928
+ */
929
+ getInputByValue: function (value) {
930
+
931
+ var checkboxes = $('li input', this.$ul);
932
+ var valueToCompare = value.toString();
933
+
934
+ for (var i = 0; i < checkboxes.length; i = i + 1) {
935
+ var checkbox = checkboxes[i];
936
+ if (checkbox.value == valueToCompare) {
937
+ return $(checkbox);
938
+ }
939
+ }
502
940
  },
503
941
 
504
- updateOriginalOptions : function() {
942
+ /**
943
+ * Used for knockout integration.
944
+ */
945
+ updateOriginalOptions: function() {
505
946
  this.originalOptions = this.$select.clone()[0].options;
506
947
  },
507
948
 
508
- asyncFunction : function(callback, timeout, self) {
949
+ asyncFunction: function(callback, timeout, self) {
509
950
  var args = Array.prototype.slice.call(arguments, 3);
510
951
  return setTimeout(function() {
511
952
  callback.apply(self || window, args);
@@ -515,16 +956,22 @@
515
956
 
516
957
  $.fn.multiselect = function(option, parameter) {
517
958
  return this.each(function() {
518
- var data = $(this).data('multiselect'), options = typeof option == 'object' && option;
959
+ var data = $(this).data('multiselect');
960
+ var options = typeof option === 'object' && option;
519
961
 
520
962
  // Initialize the multiselect.
521
963
  if (!data) {
522
- $(this).data('multiselect', ( data = new Multiselect(this, options)));
964
+ data = new Multiselect(this, options);
965
+ $(this).data('multiselect', data);
523
966
  }
524
967
 
525
968
  // Call multiselect method.
526
- if ( typeof option == 'string') {
969
+ if (typeof option === 'string') {
527
970
  data[option](parameter);
971
+
972
+ if (option === 'destroy') {
973
+ $(this).data('multiselect', false);
974
+ }
528
975
  }
529
976
  });
530
977
  };