bootstrap-multiselect_rails 0.9.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.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Trevor Strieber
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # bootstrap-multiselect_rails
2
+
3
+ This gem packages the [bootstrap-multiselect](https://github.com/davidstutz/bootstrap-multiselect) library for the Rails 3.1+ asset pipeline.
4
+
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+ ```
10
+ gem 'bootstrap-multiselect_rails', '~> 0.9.4'
11
+ ```
12
+
13
+ And then execute:
14
+ ```bash
15
+ $ bundle
16
+ ```
17
+
18
+ Or install it yourself:
19
+ ```bash
20
+ $ gem install bootstrap-multiselect_rails
21
+ ```
22
+
23
+ To start using the bootstrap-multiselect library in your rails application, enable it via the asset pipeline.
24
+
25
+ Add the following to your Javascript manifest file (`application.js`):
26
+ ```js
27
+ //= require bootstrap-multiselect
28
+ ```
29
+
30
+ Add the following to your stylesheet file:
31
+
32
+ If you are using SCSS, modify your `application.css.scss`
33
+ ```scss
34
+ @import 'bootstrap-multiselect';
35
+ ```
36
+
37
+ If you're using just plain CSS, modify your `application.css`
38
+ ```css
39
+ *= require bootstrap-multiselect
40
+ ```
41
+ ## Usage
42
+
43
+ Check out the documentation at: http://davidstutz.github.io/bootstrap-multiselect/
44
+
45
+ ## Contributing
46
+
47
+ 1. Fork it
48
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
49
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
50
+ 4. Push to the branch (`git push origin my-new-feature`)
51
+ 5. Create new Pull Request
@@ -0,0 +1,8 @@
1
+ require 'bootstrap-multiselect_rails/version'
2
+
3
+ module BootstrapMultiselect
4
+ module Rails
5
+ class Engine < ::Rails::Engine
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module BootstrapMultiselect
2
+ module Rails
3
+ VERSION = '0.9.4'
4
+ end
5
+ end
@@ -0,0 +1,994 @@
1
+ /**
2
+ * bootstrap-multiselect.js
3
+ * https://github.com/davidstutz/bootstrap-multiselect
4
+ *
5
+ * Copyright 2012 - 2014 David Stutz
6
+ *
7
+ * Dual licensed under the BSD-3-Clause and the Apache License, Version 2.0.
8
+ */
9
+ !function($) {
10
+
11
+ "use strict";// jshint ;_;
12
+
13
+ if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
14
+ ko.bindingHandlers.multiselect = {
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
+ }
45
+ },
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
+
60
+ if (!ms) {
61
+ $(element).multiselect(config);
62
+ }
63
+ else {
64
+ ms.updateOriginalOptions();
65
+ }
66
+ }
67
+ };
68
+ }
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
+ */
81
+ function Multiselect(select, options) {
82
+
83
+ this.options = this.mergeOptions(options);
84
+ this.$select = $(select);
85
+
86
+ // Initialization.
87
+ // We have to clone to create a new reference.
88
+ this.originalOptions = this.$select.clone()[0].options;
89
+ this.query = '';
90
+ this.searchTimeout = null;
91
+
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);
96
+
97
+ // Build select all if enabled.
98
+ this.buildContainer();
99
+ this.buildButton();
100
+ this.buildSelectAll();
101
+ this.buildDropdown();
102
+ this.buildDropdownOptions();
103
+ this.buildFilter();
104
+
105
+ this.updateButtonText();
106
+ this.updateSelectAll();
107
+
108
+ this.$select.hide().after(this.$container);
109
+ };
110
+
111
+ Multiselect.prototype = {
112
+
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>';
125
+ }
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;
150
+ }
151
+ else {
152
+ var selected = '';
153
+ options.each(function () {
154
+ selected += $(this).text() + ', ';
155
+ });
156
+ return selected.substr(0, selected.length - 2);
157
+ }
158
+ },
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
+ */
175
+ onChange : function(option, checked) {
176
+
177
+ },
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" />',
199
+ // Maximum height of the dropdown menu.
200
+ // If maximum height is exceeded a scrollbar will be displayed.
201
+ maxHeight: false,
202
+ checkboxName: 'multiselect',
203
+ includeSelectAllOption: false,
204
+ includeSelectAllIfMoreThan: 0,
205
+ selectAllText: ' Select all',
206
+ selectAllValue: 'multiselect-all',
207
+ enableFiltering: false,
208
+ enableCaseInsensitiveFiltering: false,
209
+ filterPlaceholder: 'Search',
210
+ // possible options: 'text', 'value', 'both'
211
+ filterBehavior: 'text',
212
+ preventInputChangeEvent: false,
213
+ nonSelectedText: 'None selected',
214
+ nSelectedText: 'selected',
215
+ numberDisplayed: 3,
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
+ }
224
+ },
225
+
226
+ constructor: Multiselect,
227
+
228
+ /**
229
+ * Builds the container of the multiselect.
230
+ */
231
+ buildContainer: function() {
232
+ this.$container = $(this.options.buttonContainer);
233
+ this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
234
+ this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
235
+ },
236
+
237
+ /**
238
+ * Builds the button of the multiselect.
239
+ */
240
+ buildButton: function() {
241
+ this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
242
+
243
+ // Adopt active state.
244
+ if (this.$select.prop('disabled')) {
245
+ this.disable();
246
+ }
247
+ else {
248
+ this.enable();
249
+ }
250
+
251
+ // Manually add button width if set.
252
+ if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
253
+ this.$button.css({
254
+ 'width' : this.options.buttonWidth
255
+ });
256
+ }
257
+
258
+ // Keep the tab index from the select.
259
+ var tabindex = this.$select.attr('tabindex');
260
+ if (tabindex) {
261
+ this.$button.attr('tabindex', tabindex);
262
+ }
263
+
264
+ this.$container.prepend(this.$button);
265
+ },
266
+
267
+ /**
268
+ * Builds the ul representing the dropdown menu.
269
+ */
270
+ buildDropdown: function() {
271
+
272
+ // Build ul.
273
+ this.$ul = $(this.options.templates.ul);
274
+
275
+ if (this.options.dropRight) {
276
+ this.$ul.addClass('pull-right');
277
+ }
278
+
279
+ // Set max height of dropdown menu to activate auto scrollbar.
280
+ if (this.options.maxHeight) {
281
+ // TODO: Add a class for this option to move the css declarations.
282
+ this.$ul.css({
283
+ 'max-height': this.options.maxHeight + 'px',
284
+ 'overflow-y': 'auto',
285
+ 'overflow-x': 'hidden'
286
+ });
287
+ }
288
+
289
+ this.$container.append(this.$ul);
290
+ },
291
+
292
+ /**
293
+ * Build the dropdown options and binds all nessecary events.
294
+ * Uses createDivider and createOptionValue to create the necessary options.
295
+ */
296
+ buildDropdownOptions: function() {
297
+
298
+ this.$select.children().each($.proxy(function(index, element) {
299
+
300
+ // Support optgroups and options without a group simultaneously.
301
+ var tag = $(element).prop('tagName')
302
+ .toLowerCase();
303
+
304
+ if (tag === 'optgroup') {
305
+ this.createOptgroup(element);
306
+ }
307
+ else if (tag === 'option') {
308
+
309
+ if ($(element).data('role') === 'divider') {
310
+ this.createDivider();
311
+ }
312
+ else {
313
+ this.createOptionValue(element);
314
+ }
315
+
316
+ }
317
+
318
+ // Other illegal tags will be ignored.
319
+ }, this));
320
+
321
+ // Bind the change event on the dropdown elements.
322
+ $('li input', this.$ul).on('change', $.proxy(function(event) {
323
+ var $target = $(event.target);
324
+
325
+ var checked = $target.prop('checked') || false;
326
+ var isSelectAllOption = $target.val() === this.options.selectAllValue;
327
+
328
+ // Apply or unapply the configured selected class.
329
+ if (this.options.selectedClass) {
330
+ if (checked) {
331
+ $target.parents('li')
332
+ .addClass(this.options.selectedClass);
333
+ }
334
+ else {
335
+ $target.parents('li')
336
+ .removeClass(this.options.selectedClass);
337
+ }
338
+ }
339
+
340
+ // Get the corresponding option.
341
+ var value = $target.val();
342
+ var $option = this.getOptionByValue(value);
343
+
344
+ var $optionsNotThis = $('option', this.$select).not($option);
345
+ var $checkboxesNotThis = $('input', this.$container).not($target);
346
+
347
+ if (isSelectAllOption) {
348
+ var values = [];
349
+
350
+ // Select the visible checkboxes except the "select-all" and possible divider.
351
+ var availableInputs = $('li input[value!="' + this.options.selectAllValue + '"][data-role!="divider"]', this.$ul).filter(':visible');
352
+ for (var i = 0, j = availableInputs.length; i < j; i++) {
353
+ values.push(availableInputs[i].value);
354
+ }
355
+
356
+ if (checked) {
357
+ this.select(values);
358
+ }
359
+ else {
360
+ this.deselect(values);
361
+ }
362
+ }
363
+
364
+ if (checked) {
365
+ $option.prop('selected', true);
366
+
367
+ if (this.options.multiple) {
368
+ // Simply select additional option.
369
+ $option.prop('selected', true);
370
+ }
371
+ else {
372
+ // Unselect all other options and corresponding checkboxes.
373
+ if (this.options.selectedClass) {
374
+ $($checkboxesNotThis).parents('li').removeClass(this.options.selectedClass);
375
+ }
376
+
377
+ $($checkboxesNotThis).prop('checked', false);
378
+ $optionsNotThis.prop('selected', false);
379
+
380
+ // It's a single selection, so close.
381
+ this.$button.click();
382
+ }
383
+
384
+ if (this.options.selectedClass === "active") {
385
+ $optionsNotThis.parents("a").css("outline", "");
386
+ }
387
+ }
388
+ else {
389
+ // Unselect option.
390
+ $option.prop('selected', false);
391
+ }
392
+
393
+ this.$select.change();
394
+ this.options.onChange($option, checked);
395
+
396
+ this.updateButtonText();
397
+ this.updateSelectAll();
398
+
399
+ if(this.options.preventInputChangeEvent) {
400
+ return false;
401
+ }
402
+ }, this));
403
+
404
+ $('li a', this.$ul).on('touchstart click', function(event) {
405
+ event.stopPropagation();
406
+
407
+ var $target = $(event.target);
408
+
409
+ if (event.shiftKey) {
410
+ var checked = $target.prop('checked') || false;
411
+
412
+ if (checked) {
413
+ var prev = $target.parents('li:last')
414
+ .siblings('li[class="active"]:first');
415
+
416
+ var currentIdx = $target.parents('li')
417
+ .index();
418
+ var prevIdx = prev.index();
419
+
420
+ if (currentIdx > prevIdx) {
421
+ $target.parents("li:last").prevUntil(prev).each(
422
+ function() {
423
+ $(this).find("input:first").prop("checked", true)
424
+ .trigger("change");
425
+ }
426
+ );
427
+ }
428
+ else {
429
+ $target.parents("li:last").nextUntil(prev).each(
430
+ function() {
431
+ $(this).find("input:first").prop("checked", true)
432
+ .trigger("change");
433
+ }
434
+ );
435
+ }
436
+ }
437
+ }
438
+
439
+ $target.blur();
440
+ });
441
+
442
+ // Keyboard support.
443
+ this.$container.on('keydown', $.proxy(function(event) {
444
+ if ($('input[type="text"]', this.$container).is(':focus')) {
445
+ return;
446
+ }
447
+ if ((event.keyCode === 9 || event.keyCode === 27)
448
+ && this.$container.hasClass('open')) {
449
+
450
+ // Close on tab or escape.
451
+ this.$button.click();
452
+ }
453
+ else {
454
+ var $items = $(this.$container).find("li:not(.divider):visible a");
455
+
456
+ if (!$items.length) {
457
+ return;
458
+ }
459
+
460
+ var index = $items.index($items.filter(':focus'));
461
+
462
+ // Navigation up.
463
+ if (event.keyCode === 38 && index > 0) {
464
+ index--;
465
+ }
466
+ // Navigate down.
467
+ else if (event.keyCode === 40 && index < $items.length - 1) {
468
+ index++;
469
+ }
470
+ else if (!~index) {
471
+ index = 0;
472
+ }
473
+
474
+ var $current = $items.eq(index);
475
+ $current.focus();
476
+
477
+ if (event.keyCode === 32 || event.keyCode === 13) {
478
+ var $checkbox = $current.find('input');
479
+
480
+ $checkbox.prop("checked", !$checkbox.prop("checked"));
481
+ $checkbox.change();
482
+ }
483
+
484
+ event.stopPropagation();
485
+ event.preventDefault();
486
+ }
487
+ }, this));
488
+ },
489
+
490
+ /**
491
+ * Create an option using the given select option.
492
+ *
493
+ * @param {jQuery} element
494
+ */
495
+ createOptionValue: function(element) {
496
+ if ($(element).is(':selected')) {
497
+ $(element).prop('selected', true);
498
+ }
499
+
500
+ // Support the label attribute on options.
501
+ var label = this.options.label(element);
502
+ var value = $(element).val();
503
+ var inputType = this.options.multiple ? "checkbox" : "radio";
504
+
505
+ var $li = $(this.options.templates.li);
506
+ $('label', $li).addClass(inputType);
507
+ $('label', $li).append('<input type="' + inputType + '" name="' + this.options.checkboxName + '" />');
508
+
509
+ var selected = $(element).prop('selected') || false;
510
+ var $checkbox = $('input', $li);
511
+ $checkbox.val(value);
512
+
513
+ if (value === this.options.selectAllValue) {
514
+ $checkbox.parent().parent()
515
+ .addClass('multiselect-all');
516
+ }
517
+
518
+ $('label', $li).append(" " + label);
519
+
520
+ this.$ul.append($li);
521
+
522
+ if ($(element).is(':disabled')) {
523
+ $checkbox.attr('disabled', 'disabled')
524
+ .prop('disabled', true)
525
+ .parents('li')
526
+ .addClass('disabled');
527
+ }
528
+
529
+ $checkbox.prop('checked', selected);
530
+
531
+ if (selected && this.options.selectedClass) {
532
+ $checkbox.parents('li')
533
+ .addClass(this.options.selectedClass);
534
+ }
535
+ },
536
+
537
+ /**
538
+ * Creates a divider using the given select option.
539
+ *
540
+ * @param {jQuery} element
541
+ */
542
+ createDivider: function(element) {
543
+ var $divider = $(this.options.templates.divider);
544
+ this.$ul.append($divider);
545
+ },
546
+
547
+ /**
548
+ * Creates an optgroup.
549
+ *
550
+ * @param {jQuery} group
551
+ */
552
+ createOptgroup: function(group) {
553
+ var groupName = $(group).prop('label');
554
+
555
+ // Add a header for the group.
556
+ var $li = $(this.options.templates.liGroup);
557
+ $('label', $li).text(groupName);
558
+
559
+ this.$ul.append($li);
560
+
561
+ if ($(group).is(':disabled')) {
562
+ $li.addClass('disabled');
563
+ }
564
+
565
+ // Add the options of the group.
566
+ $('option', group).each($.proxy(function(index, element) {
567
+ this.createOptionValue(element);
568
+ }, this));
569
+ },
570
+
571
+ /**
572
+ * Build the selct all.
573
+ * Checks if a select all ahs already been created.
574
+ */
575
+ buildSelectAll: function() {
576
+ var alreadyHasSelectAll = this.hasSelectAll();
577
+
578
+ if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
579
+ && $('option[data-role!="divider"]', this.$select).length > this.options.includeSelectAllIfMoreThan) {
580
+
581
+ // Check whether to add a divider after the select all.
582
+ if (this.options.includeSelectAllDivider) {
583
+ this.$select.prepend('<option value="" disabled="disabled" data-role="divider">');
584
+ }
585
+
586
+ this.$select.prepend('<option value="' + this.options.selectAllValue + '">' + this.options.selectAllText + '</option>');
587
+ }
588
+ },
589
+
590
+ /**
591
+ * Builds the filter.
592
+ */
593
+ buildFilter: function() {
594
+
595
+ // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
596
+ if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
597
+ var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
598
+
599
+ if (this.$select.find('option').length >= enableFilterLength) {
600
+
601
+ this.$filter = $(this.options.templates.filter);
602
+ $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
603
+ this.$ul.prepend(this.$filter);
604
+
605
+ this.$filter.val(this.query).on('click', function(event) {
606
+ event.stopPropagation();
607
+ }).on('input keydown', $.proxy(function(event) {
608
+ // This is useful to catch "keydown" events after the browser has updated the control.
609
+ clearTimeout(this.searchTimeout);
610
+
611
+ this.searchTimeout = this.asyncFunction($.proxy(function() {
612
+
613
+ if (this.query !== event.target.value) {
614
+ this.query = event.target.value;
615
+
616
+ $.each($('li', this.$ul), $.proxy(function(index, element) {
617
+ var value = $('input', element).val();
618
+ var text = $('label', element).text();
619
+
620
+ var filterCandidate = '';
621
+ if ((this.options.filterBehavior === 'text')) {
622
+ filterCandidate = text;
623
+ }
624
+ else if ((this.options.filterBehavior === 'value')) {
625
+ filterCandidate = value;
626
+ }
627
+ else if (this.options.filterBehavior === 'both') {
628
+ filterCandidate = text + '\n' + value;
629
+ }
630
+
631
+ if (value !== this.options.selectAllValue && text) {
632
+ // by default lets assume that element is not
633
+ // interesting for this search
634
+ var showElement = false;
635
+
636
+ if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
637
+ showElement = true;
638
+ }
639
+ else if (filterCandidate.indexOf(this.query) > -1) {
640
+ showElement = true;
641
+ }
642
+
643
+ if (showElement) {
644
+ $(element).show();
645
+ }
646
+ else {
647
+ $(element).hide();
648
+ }
649
+ }
650
+ }, this));
651
+ }
652
+
653
+ // TODO: check whether select all option needs to be updated.
654
+ }, this), 300, this);
655
+ }, this));
656
+ }
657
+ }
658
+ },
659
+
660
+ /**
661
+ * Unbinds the whole plugin.
662
+ */
663
+ destroy: function() {
664
+ this.$container.remove();
665
+ this.$select.show();
666
+ this.$select.data('multiselect', null);
667
+ },
668
+
669
+ /**
670
+ * Refreshs the multiselect based on the selected options of the select.
671
+ */
672
+ refresh: function() {
673
+ $('option', this.$select).each($.proxy(function(index, element) {
674
+ var $input = $('li input', this.$ul).filter(function() {
675
+ return $(this).val() === $(element).val();
676
+ });
677
+
678
+ if ($(element).is(':selected')) {
679
+ $input.prop('checked', true);
680
+
681
+ if (this.options.selectedClass) {
682
+ $input.parents('li')
683
+ .addClass(this.options.selectedClass);
684
+ }
685
+ }
686
+ else {
687
+ $input.prop('checked', false);
688
+
689
+ if (this.options.selectedClass) {
690
+ $input.parents('li')
691
+ .removeClass(this.options.selectedClass);
692
+ }
693
+ }
694
+
695
+ if ($(element).is(":disabled")) {
696
+ $input.attr('disabled', 'disabled')
697
+ .prop('disabled', true)
698
+ .parents('li')
699
+ .addClass('disabled');
700
+ }
701
+ else {
702
+ $input.prop('disabled', false)
703
+ .parents('li')
704
+ .removeClass('disabled');
705
+ }
706
+ }, this));
707
+
708
+ this.updateButtonText();
709
+ this.updateSelectAll();
710
+ },
711
+
712
+ /**
713
+ * Select all options of the given values.
714
+ *
715
+ * @param {Array} selectValues
716
+ */
717
+ select: function(selectValues) {
718
+ if(!$.isArray(selectValues)) {
719
+ selectValues = [selectValues];
720
+ }
721
+
722
+ for (var i = 0; i < selectValues.length; i++) {
723
+ var value = selectValues[i];
724
+
725
+ var $option = this.getOptionByValue(value);
726
+ var $checkbox = this.getInputByValue(value);
727
+
728
+ if (this.options.selectedClass) {
729
+ $checkbox.parents('li')
730
+ .addClass(this.options.selectedClass);
731
+ }
732
+
733
+ $checkbox.prop('checked', true);
734
+ $option.prop('selected', true);
735
+ }
736
+
737
+ this.updateButtonText();
738
+ },
739
+
740
+ /**
741
+ * Clears all selected items
742
+ *
743
+ */
744
+ clearSelection: function () {
745
+
746
+ var selected = this.getSelected();
747
+
748
+ if (selected.length) {
749
+
750
+ var arry = [];
751
+
752
+ for (var i = 0; i < selected.length; i = i + 1) {
753
+ arry.push(selected[i].value);
754
+ }
755
+
756
+ this.deselect(arry);
757
+ this.$select.change();
758
+ }
759
+ },
760
+
761
+ /**
762
+ * Deselects all options of the given values.
763
+ *
764
+ * @param {Array} deselectValues
765
+ */
766
+ deselect: function(deselectValues) {
767
+ if(!$.isArray(deselectValues)) {
768
+ deselectValues = [deselectValues];
769
+ }
770
+
771
+ for (var i = 0; i < deselectValues.length; i++) {
772
+
773
+ var value = deselectValues[i];
774
+
775
+ var $option = this.getOptionByValue(value);
776
+ var $checkbox = this.getInputByValue(value);
777
+
778
+ if (this.options.selectedClass) {
779
+ $checkbox.parents('li')
780
+ .removeClass(this.options.selectedClass);
781
+ }
782
+
783
+ $checkbox.prop('checked', false);
784
+ $option.prop('selected', false);
785
+ }
786
+
787
+ this.updateButtonText();
788
+ },
789
+
790
+ /**
791
+ * Rebuild the plugin.
792
+ * Rebuilds the dropdown, the filter and the select all option.
793
+ */
794
+ rebuild: function() {
795
+ this.$ul.html('');
796
+
797
+ // Remove select all option in select.
798
+ $('option[value="' + this.options.selectAllValue + '"]', this.$select).remove();
799
+
800
+ // Important to distinguish between radios and checkboxes.
801
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
802
+
803
+ this.buildSelectAll();
804
+ this.buildDropdownOptions();
805
+ this.buildFilter();
806
+
807
+ this.updateButtonText();
808
+ this.updateSelectAll();
809
+ },
810
+
811
+ /**
812
+ * The provided data will be used to build the dropdown.
813
+ *
814
+ * @param {Array} dataprovider
815
+ */
816
+ dataprovider: function(dataprovider) {
817
+ var optionDOM = "";
818
+ dataprovider.forEach(function (option) {
819
+ optionDOM += '<option value="' + option.value + '">' + option.label + '</option>';
820
+ });
821
+
822
+ this.$select.html(optionDOM);
823
+ this.rebuild();
824
+ },
825
+
826
+ /**
827
+ * Enable the multiselect.
828
+ */
829
+ enable: function() {
830
+ this.$select.prop('disabled', false);
831
+ this.$button.prop('disabled', false)
832
+ .removeClass('disabled');
833
+ },
834
+
835
+ /**
836
+ * Disable the multiselect.
837
+ */
838
+ disable: function() {
839
+ this.$select.prop('disabled', true);
840
+ this.$button.prop('disabled', true)
841
+ .addClass('disabled');
842
+ },
843
+
844
+ /**
845
+ * Set the options.
846
+ *
847
+ * @param {Array} options
848
+ */
849
+ setOptions: function(options) {
850
+ this.options = this.mergeOptions(options);
851
+ },
852
+
853
+ /**
854
+ * Merges the given options with the default options.
855
+ *
856
+ * @param {Array} options
857
+ * @returns {Array}
858
+ */
859
+ mergeOptions: function(options) {
860
+ return $.extend(true, {}, this.defaults, options);
861
+ },
862
+
863
+ /**
864
+ * Checks whether a select all option is present.
865
+ *
866
+ * @returns {Boolean}
867
+ */
868
+ hasSelectAll: function() {
869
+ return $('option[value="' + this.options.selectAllValue + '"]', this.$select).length > 0;
870
+ },
871
+
872
+ /**
873
+ * Updates the select all option based on the currently selected options.
874
+ */
875
+ updateSelectAll: function() {
876
+ if (this.hasSelectAll()) {
877
+ var selected = this.getSelected();
878
+
879
+ if (selected.length === $('option:not([data-role=divider])', this.$select).length - 1) {
880
+ this.select(this.options.selectAllValue);
881
+ }
882
+ else {
883
+ this.deselect(this.options.selectAllValue);
884
+ }
885
+ }
886
+ },
887
+
888
+ /**
889
+ * Update the button text and its title based on the currently selected options.
890
+ */
891
+ updateButtonText: function() {
892
+ var options = this.getSelected();
893
+
894
+ // First update the displayed button text.
895
+ $('button', this.$container).html(this.options.buttonText(options, this.$select));
896
+
897
+ // Now update the title attribute of the button.
898
+ $('button', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
899
+
900
+ },
901
+
902
+ /**
903
+ * Get all selected options.
904
+ *
905
+ * @returns {jQUery}
906
+ */
907
+ getSelected: function() {
908
+ return $('option[value!="' + this.options.selectAllValue + '"]:selected', this.$select).filter(function() {
909
+ return $(this).prop('selected');
910
+ });
911
+ },
912
+
913
+ /**
914
+ * Gets a select option by its value.
915
+ *
916
+ * @param {String} value
917
+ * @returns {jQuery}
918
+ */
919
+ getOptionByValue: function (value) {
920
+
921
+ var options = $('option', this.$select);
922
+ var valueToCompare = value.toString();
923
+
924
+ for (var i = 0; i < options.length; i = i + 1) {
925
+ var option = options[i];
926
+ if (option.value === valueToCompare) {
927
+ return $(option);
928
+ }
929
+ }
930
+ },
931
+
932
+ /**
933
+ * Get the input (radio/checkbox) by its value.
934
+ *
935
+ * @param {String} value
936
+ * @returns {jQuery}
937
+ */
938
+ getInputByValue: function (value) {
939
+
940
+ var checkboxes = $('li input', this.$ul);
941
+ var valueToCompare = value.toString();
942
+
943
+ for (var i = 0; i < checkboxes.length; i = i + 1) {
944
+ var checkbox = checkboxes[i];
945
+ if (checkbox.value === valueToCompare) {
946
+ return $(checkbox);
947
+ }
948
+ }
949
+ },
950
+
951
+ /**
952
+ * Used for knockout integration.
953
+ */
954
+ updateOriginalOptions: function() {
955
+ this.originalOptions = this.$select.clone()[0].options;
956
+ },
957
+
958
+ asyncFunction: function(callback, timeout, self) {
959
+ var args = Array.prototype.slice.call(arguments, 3);
960
+ return setTimeout(function() {
961
+ callback.apply(self || window, args);
962
+ }, timeout);
963
+ }
964
+ };
965
+
966
+ $.fn.multiselect = function(option, parameter) {
967
+ return this.each(function() {
968
+ var data = $(this).data('multiselect');
969
+ var options = typeof option === 'object' && option;
970
+
971
+ // Initialize the multiselect.
972
+ if (!data) {
973
+ data = new Multiselect(this, options);
974
+ $(this).data('multiselect', data);
975
+ }
976
+
977
+ // Call multiselect method.
978
+ if (typeof option === 'string') {
979
+ data[option](parameter);
980
+
981
+ if (option === 'destroy') {
982
+ $(this).data('multiselect', false);
983
+ }
984
+ }
985
+ });
986
+ };
987
+
988
+ $.fn.multiselect.Constructor = Multiselect;
989
+
990
+ $(function() {
991
+ $("select[data-role=multiselect]").multiselect();
992
+ });
993
+
994
+ }(window.jQuery);
@@ -0,0 +1 @@
1
+ .multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li>label.multiselect-group{margin:0;padding:3px 20px;height:100%;font-weight:700}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bootstrap-multiselect_rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Trevor Strieber
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: This gem packages the Bootstrap multiselect (JS & CSS) for Rails 3.1+
47
+ asset pipeline.
48
+ email:
49
+ - trevor@strieber.org
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/bootstrap-multiselect_rails.rb
55
+ - lib/bootstrap-multiselect_rails/version.rb
56
+ - vendor/assets/stylesheets/bootstrap-multiselect.css
57
+ - vendor/assets/javascripts/bootstrap-multiselect.js
58
+ - LICENSE.txt
59
+ - README.md
60
+ homepage: http://github.com/TrevorS/bootstrap-multiselect_rails
61
+ licenses:
62
+ - MIT
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.23
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Bootstrap 2/3 multiselect's JS & CSS for Rails 3.1+ asset pipeline.
85
+ test_files: []
86
+ has_rdoc: