bootstrap-multiselect-rails 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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
  };