PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 4.10.03
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v4.10.03
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / js / bootstrap-multiselect.js

bootstrap-multiselect.js in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 4.10.03, at js/bootstrap-multiselect.js

1,766 lines 67.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Bootstrap Multiselect (http://davidstutz.de/bootstrap-multiselect/)
3 *
4 * Apache License, Version 2.0:
5 * Copyright (c) 2012 - 2018 David Stutz
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
8 * use this file except in compliance with the License. You may obtain a
9 * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 * License for the specific language governing permissions and limitations
15 * under the License.
16 *
17 * BSD 3-Clause License:
18 * Copyright (c) 2012 - 2018 David Stutz
19 * All rights reserved.
20 *
21 * Redistribution and use in source and binary forms, with or without
22 * modification, are permitted provided that the following conditions are met:
23 * - Redistributions of source code must retain the above copyright notice,
24 * this list of conditions and the following disclaimer.
25 * - Redistributions in binary form must reproduce the above copyright notice,
26 * this list of conditions and the following disclaimer in the documentation
27 * and/or other materials provided with the distribution.
28 * - Neither the name of David Stutz nor the names of its contributors may be
29 * used to endorse or promote products derived from this software without
30 * specific prior written permission.
31 *
32 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
33 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
34 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
35 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
36 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
37 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
39 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
40 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
41 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43 */
44 !function ($) {
45 "use strict";// jshint ;_;
46
47 if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
48 ko.bindingHandlers.multiselect = {
49 after: ['options', 'value', 'selectedOptions', 'enable', 'disable'],
50
51 init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
52 var $element = $(element);
53 var config = ko.toJS(valueAccessor());
54
55 $element.multiselect(config);
56
57 if (allBindings.has('options')) {
58 var options = allBindings.get('options');
59 if (ko.isObservable(options)) {
60 ko.computed({
61 read: function() {
62 options();
63 setTimeout(function() {
64 var ms = $element.data('multiselect');
65 if (ms)
66 ms.updateOriginalOptions();//Not sure how beneficial this is.
67 $element.multiselect('rebuild');
68 }, 1);
69 },
70 disposeWhenNodeIsRemoved: element
71 });
72 }
73 }
74
75 //value and selectedOptions are two-way, so these will be triggered even by our own actions.
76 //It needs some way to tell if they are triggered because of us or because of outside change.
77 //It doesn't loop but it's a waste of processing.
78 if (allBindings.has('value')) {
79 var value = allBindings.get('value');
80 if (ko.isObservable(value)) {
81 ko.computed({
82 read: function() {
83 value();
84 setTimeout(function() {
85 $element.multiselect('refresh');
86 }, 1);
87 },
88 disposeWhenNodeIsRemoved: element
89 }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
90 }
91 }
92
93 //Switched from arrayChange subscription to general subscription using 'refresh'.
94 //Not sure performance is any better using 'select' and 'deselect'.
95 if (allBindings.has('selectedOptions')) {
96 var selectedOptions = allBindings.get('selectedOptions');
97 if (ko.isObservable(selectedOptions)) {
98 ko.computed({
99 read: function() {
100 selectedOptions();
101 setTimeout(function() {
102 $element.multiselect('refresh');
103 }, 1);
104 },
105 disposeWhenNodeIsRemoved: element
106 }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
107 }
108 }
109
110 var setEnabled = function (enable) {
111 setTimeout(function () {
112 if (enable)
113 $element.multiselect('enable');
114 else
115 $element.multiselect('disable');
116 });
117 };
118
119 if (allBindings.has('enable')) {
120 var enable = allBindings.get('enable');
121 if (ko.isObservable(enable)) {
122 ko.computed({
123 read: function () {
124 setEnabled(enable());
125 },
126 disposeWhenNodeIsRemoved: element
127 }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
128 } else {
129 setEnabled(enable);
130 }
131 }
132
133 if (allBindings.has('disable')) {
134 var disable = allBindings.get('disable');
135 if (ko.isObservable(disable)) {
136 ko.computed({
137 read: function () {
138 setEnabled(!disable());
139 },
140 disposeWhenNodeIsRemoved: element
141 }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
142 } else {
143 setEnabled(!disable);
144 }
145 }
146
147 ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
148 $element.multiselect('destroy');
149 });
150 },
151
152 update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
153 var $element = $(element);
154 var config = ko.toJS(valueAccessor());
155
156 $element.multiselect('setOptions', config);
157 $element.multiselect('rebuild');
158 }
159 };
160 }
161
162 function forEach(array, callback) {
163 for (var index = 0; index < array.length; ++index) {
164 callback(array[index], index);
165 }
166 }
167
168 /**
169 * Constructor to create a new multiselect using the given select.
170 *
171 * @param {jQuery} select
172 * @param {Object} options
173 * @returns {Multiselect}
174 */
175 function Multiselect(select, options) {
176
177 this.$select = $(select);
178 this.options = this.mergeOptions($.extend({}, options, this.$select.data()));
179
180 // Placeholder via data attributes
181 if (this.$select.attr("data-placeholder")) {
182 this.options.nonSelectedText = this.$select.data("placeholder");
183 }
184
185 // Initialization.
186 // We have to clone to create a new reference.
187 this.originalOptions = this.$select.clone()[0].options;
188 this.query = '';
189 this.searchTimeout = null;
190 this.lastToggledInput = null;
191
192 this.options.multiple = this.$select.attr('multiple') === "multiple";
193 this.options.onChange = $.proxy(this.options.onChange, this);
194 this.options.onSelectAll = $.proxy(this.options.onSelectAll, this);
195 this.options.onDeselectAll = $.proxy(this.options.onDeselectAll, this);
196 this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
197 this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
198 this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this);
199 this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this);
200 this.options.onInitialized = $.proxy(this.options.onInitialized, this);
201 this.options.onFiltering = $.proxy(this.options.onFiltering, this);
202
203 // Build select all if enabled.
204 this.buildContainer();
205 this.buildButton();
206 this.buildDropdown();
207 this.buildReset();
208 this.buildSelectAll();
209 this.buildDropdownOptions();
210 this.buildFilter();
211
212 this.updateButtonText();
213 this.updateSelectAll(true);
214
215 if (this.options.enableClickableOptGroups && this.options.multiple) {
216 this.updateOptGroups();
217 }
218
219 this.options.wasDisabled = this.$select.prop('disabled');
220 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
221 this.disable();
222 }
223
224 this.$select.wrap('<span class="multiselect-native-select" />').after(this.$container);
225 this.options.onInitialized(this.$select, this.$container);
226 }
227
228 Multiselect.prototype = {
229
230 defaults: {
231 /**
232 * Default text function will either print 'None selected' in case no
233 * option is selected or a list of the selected options up to a length
234 * of 3 selected options.
235 *
236 * @param {jQuery} options
237 * @param {jQuery} select
238 * @returns {String}
239 */
240 buttonText: function(options, select) {
241 if (this.disabledText.length > 0
242 && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) {
243
244 return this.disabledText;
245 }
246 else if (options.length === 0) {
247 return this.nonSelectedText;
248 }
249 else if (this.allSelectedText
250 && options.length === $('option', $(select)).length
251 && $('option', $(select)).length !== 1
252 && this.multiple) {
253
254 if (this.selectAllNumber) {
255 return this.allSelectedText + ' (' + options.length + ')';
256 }
257 else {
258 return this.allSelectedText;
259 }
260 }
261 else if (this.numberDisplayed != 0 && options.length > this.numberDisplayed) {
262 return options.length + ' ' + this.nSelectedText;
263 }
264 else {
265 var selected = '';
266 var delimiter = this.delimiterText;
267
268 options.each(function() {
269 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
270 selected += label + delimiter;
271 });
272
273 return selected.substr(0, selected.length - this.delimiterText.length);
274 }
275 },
276 /**
277 * Updates the title of the button similar to the buttonText function.
278 *
279 * @param {jQuery} options
280 * @param {jQuery} select
281 * @returns {@exp;selected@call;substr}
282 */
283 buttonTitle: function(options, select) {
284 if (options.length === 0) {
285 return this.nonSelectedText;
286 }
287 else {
288 var selected = '';
289 var delimiter = this.delimiterText;
290
291 options.each(function () {
292 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
293 selected += label + delimiter;
294 });
295 return selected.substr(0, selected.length - this.delimiterText.length);
296 }
297 },
298 checkboxName: function(option) {
299 return false; // no checkbox name
300 },
301 /**
302 * Create a label.
303 *
304 * @param {jQuery} element
305 * @returns {String}
306 */
307 optionLabel: function(element){
308 return $(element).attr('label') || $(element).text();
309 },
310 /**
311 * Create a class.
312 *
313 * @param {jQuery} element
314 * @returns {String}
315 */
316 optionClass: function(element) {
317 return $(element).attr('class') || '';
318 },
319 /**
320 * Triggered on change of the multiselect.
321 *
322 * Not triggered when selecting/deselecting options manually.
323 *
324 * @param {jQuery} option
325 * @param {Boolean} checked
326 */
327 onChange : function(option, checked) {
328
329 },
330 /**
331 * Triggered when the dropdown is shown.
332 *
333 * @param {jQuery} event
334 */
335 onDropdownShow: function(event) {
336
337 },
338 /**
339 * Triggered when the dropdown is hidden.
340 *
341 * @param {jQuery} event
342 */
343 onDropdownHide: function(event) {
344
345 },
346 /**
347 * Triggered after the dropdown is shown.
348 *
349 * @param {jQuery} event
350 */
351 onDropdownShown: function(event) {
352
353 },
354 /**
355 * Triggered after the dropdown is hidden.
356 *
357 * @param {jQuery} event
358 */
359 onDropdownHidden: function(event) {
360
361 },
362 /**
363 * Triggered on select all.
364 */
365 onSelectAll: function() {
366
367 },
368 /**
369 * Triggered on deselect all.
370 */
371 onDeselectAll: function() {
372
373 },
374 /**
375 * Triggered after initializing.
376 *
377 * @param {jQuery} $select
378 * @param {jQuery} $container
379 */
380 onInitialized: function($select, $container) {
381
382 },
383 /**
384 * Triggered on filtering.
385 *
386 * @param {jQuery} $filter
387 */
388 onFiltering: function($filter) {
389
390 },
391 enableHTML: false,
392 buttonClass: 'btn btn-default',
393 inheritClass: false,
394 buttonWidth: 'auto',
395 buttonContainer: '<div class="btn-group" />',
396 dropRight: false,
397 dropUp: false,
398 selectedClass: 'active',
399 // Maximum height of the dropdown menu.
400 // If maximum height is exceeded a scrollbar will be displayed.
401 maxHeight: false,
402 includeSelectAllOption: false,
403 includeSelectAllIfMoreThan: 0,
404 selectAllText: ' Select all',
405 selectAllValue: 'multiselect-all',
406 selectAllName: false,
407 selectAllNumber: true,
408 selectAllJustVisible: true,
409 enableFiltering: false,
410 enableCaseInsensitiveFiltering: false,
411 enableFullValueFiltering: false,
412 enableClickableOptGroups: false,
413 enableCollapsibleOptGroups: false,
414 collapseOptGroupsByDefault: false,
415 filterPlaceholder: 'Search',
416 // possible options: 'text', 'value', 'both'
417 filterBehavior: 'text',
418 includeFilterClearBtn: true,
419 preventInputChangeEvent: false,
420 nonSelectedText: 'None selected',
421 nSelectedText: 'selected',
422 allSelectedText: 'All selected',
423 numberDisplayed: 3,
424 disableIfEmpty: false,
425 disabledText: '',
426 delimiterText: ', ',
427 includeResetOption: false,
428 includeResetDivider: false,
429 resetText: 'Reset',
430 templates: {
431 button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span> <b class="caret"></b></button>',
432 ul: '<ul class="multiselect-container dropdown-menu"></ul>',
433 filter: '<li class="multiselect-item multiselect-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></li>',
434 filterClearBtn: '<span class="input-group-btn"><button class="btn btn-default multiselect-clear-filter" type="button"><i class="glyphicon glyphicon-remove-circle"></i></button></span>',
435 li: '<li><a tabindex="0"><label></label></a></li>',
436 divider: '<li class="multiselect-item divider"></li>',
437 liGroup: '<li class="multiselect-item multiselect-group"><label></label></li>',
438 resetButton: '<li class="multiselect-reset text-center"><div class="input-group"><a class="btn btn-default btn-block"></a></div></li>'
439 }
440 },
441
442 constructor: Multiselect,
443
444 /**
445 * Builds the container of the multiselect.
446 */
447 buildContainer: function() {
448 this.$container = $(this.options.buttonContainer);
449 this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
450 this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
451 this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
452 this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
453 },
454
455 /**
456 * Builds the button of the multiselect.
457 */
458 buildButton: function() {
459 this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
460 if (this.$select.attr('class') && this.options.inheritClass) {
461 this.$button.addClass(this.$select.attr('class'));
462 }
463 // Adopt active state.
464 if (this.$select.prop('disabled')) {
465 this.disable();
466 }
467 else {
468 this.enable();
469 }
470
471 // Manually add button width if set.
472 if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
473 this.$button.css({
474 'width' : '100%', //this.options.buttonWidth,
475 'overflow' : 'hidden',
476 'text-overflow' : 'ellipsis'
477 });
478 this.$container.css({
479 'width': this.options.buttonWidth
480 });
481 }
482
483 // Keep the tab index from the select.
484 var tabindex = this.$select.attr('tabindex');
485 if (tabindex) {
486 this.$button.attr('tabindex', tabindex);
487 }
488
489 this.$container.prepend(this.$button);
490 },
491
492 /**
493 * Builds the ul representing the dropdown menu.
494 */
495 buildDropdown: function() {
496
497 // Build ul.
498 this.$ul = $(this.options.templates.ul);
499
500 if (this.options.dropRight) {
501 this.$ul.addClass('pull-right');
502 }
503
504 // Set max height of dropdown menu to activate auto scrollbar.
505 if (this.options.maxHeight) {
506 // TODO: Add a class for this option to move the css declarations.
507 this.$ul.css({
508 'max-height': this.options.maxHeight + 'px',
509 'overflow-y': 'auto',
510 'overflow-x': 'hidden'
511 });
512 }
513
514 if (this.options.dropUp) {
515
516 var height = Math.min(this.options.maxHeight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeSelectAllOption ? 26 : 0) + (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering ? 44 : 0));
517 var moveCalc = height + 34;
518
519 this.$ul.css({
520 'max-height': height + 'px',
521 'overflow-y': 'auto',
522 'overflow-x': 'hidden',
523 'margin-top': "-" + moveCalc + 'px'
524 });
525 }
526
527 this.$container.append(this.$ul);
528 },
529
530 /**
531 * Build the dropdown options and binds all necessary events.
532 *
533 * Uses createDivider and createOptionValue to create the necessary options.
534 */
535 buildDropdownOptions: function() {
536
537 this.$select.children().each($.proxy(function(index, element) {
538
539 var $element = $(element);
540 // Support optgroups and options without a group simultaneously.
541 var tag = $element.prop('tagName')
542 .toLowerCase();
543
544 if ($element.prop('value') === this.options.selectAllValue) {
545 return;
546 }
547
548 if (tag === 'optgroup') {
549 this.createOptgroup(element);
550 }
551 else if (tag === 'option') {
552
553 if ($element.data('role') === 'divider') {
554 this.createDivider();
555 }
556 else {
557 this.createOptionValue(element);
558 }
559
560 }
561
562 // Other illegal tags will be ignored.
563 }, this));
564
565 // Bind the change event on the dropdown elements.
566 $(this.$ul).off('change', 'li:not(.multiselect-group) input[type="checkbox"], li:not(.multiselect-group) input[type="radio"]');
567 $(this.$ul).on('change', 'li:not(.multiselect-group) input[type="checkbox"], li:not(.multiselect-group) input[type="radio"]', $.proxy(function(event) {
568 var $target = $(event.target);
569
570 var checked = $target.prop('checked') || false;
571 var isSelectAllOption = $target.val() === this.options.selectAllValue;
572
573 // Apply or unapply the configured selected class.
574 if (this.options.selectedClass) {
575 if (checked) {
576 $target.closest('li')
577 .addClass(this.options.selectedClass);
578 }
579 else {
580 $target.closest('li')
581 .removeClass(this.options.selectedClass);
582 }
583 }
584
585 // Get the corresponding option.
586 var value = $target.val();
587 var $option = this.getOptionByValue(value);
588
589 var $optionsNotThis = $('option', this.$select).not($option);
590 var $checkboxesNotThis = $('input', this.$container).not($target);
591
592 if (isSelectAllOption) {
593
594 if (checked) {
595 this.selectAll(this.options.selectAllJustVisible, true);
596 }
597 else {
598 this.deselectAll(this.options.selectAllJustVisible, true);
599 }
600 }
601 else {
602 if (checked) {
603 $option.prop('selected', true);
604
605 if (this.options.multiple) {
606 // Simply select additional option.
607 $option.prop('selected', true);
608 }
609 else {
610 // Unselect all other options and corresponding checkboxes.
611 if (this.options.selectedClass) {
612 $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass);
613 }
614
615 $($checkboxesNotThis).prop('checked', false);
616 $optionsNotThis.prop('selected', false);
617
618 // It's a single selection, so close.
619 this.$button.click();
620 }
621
622 if (this.options.selectedClass === "active") {
623 $optionsNotThis.closest("a").css("outline", "");
624 }
625 }
626 else {
627 // Unselect option.
628 $option.prop('selected', false);
629 }
630
631 // To prevent select all from firing onChange: #575
632 this.options.onChange($option, checked);
633
634 // Do not update select all or optgroups on select all change!
635 this.updateSelectAll();
636
637 if (this.options.enableClickableOptGroups && this.options.multiple) {
638 this.updateOptGroups();
639 }
640 }
641
642 this.$select.change();
643 this.updateButtonText();
644
645 if(this.options.preventInputChangeEvent) {
646 return false;
647 }
648 }, this));
649
650 $('li a', this.$ul).on('mousedown', function(e) {
651 if (e.shiftKey) {
652 // Prevent selecting text by Shift+click
653 return false;
654 }
655 });
656
657 $(this.$ul).on('touchstart click', 'li a', $.proxy(function(event) {
658 event.stopPropagation();
659
660 var $target = $(event.target);
661
662 if (event.shiftKey && this.options.multiple) {
663 if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
664 event.preventDefault();
665 $target = $target.find("input");
666 $target.prop("checked", !$target.prop("checked"));
667 }
668 var checked = $target.prop('checked') || false;
669
670 if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
671 var from = this.$ul.find("li:visible").index($target.parents("li"));
672 var to = this.$ul.find("li:visible").index(this.lastToggledInput.parents("li"));
673
674 if (from > to) { // Swap the indices
675 var tmp = to;
676 to = from;
677 from = tmp;
678 }
679
680 // Make sure we grab all elements since slice excludes the last index
681 ++to;
682
683 // Change the checkboxes and underlying options
684 var range = this.$ul.find("li").not(".multiselect-filter-hidden").slice(from, to).find("input");
685
686 range.prop('checked', checked);
687
688 if (this.options.selectedClass) {
689 range.closest('li')
690 .toggleClass(this.options.selectedClass, checked);
691 }
692
693 for (var i = 0, j = range.length; i < j; i++) {
694 var $checkbox = $(range[i]);
695
696 var $option = this.getOptionByValue($checkbox.val());
697
698 $option.prop('selected', checked);
699 }
700 }
701
702 // Trigger the select "change" event
703 $target.trigger("change");
704 }
705
706 // Remembers last clicked option
707 if($target.is("input") && !$target.closest("li").is(".multiselect-item")){
708 this.lastToggledInput = $target;
709 }
710
711 $target.trigger( 'blur' );
712 }, this));
713
714 // Keyboard support.
715 this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) {
716 if ($('input[type="text"]', this.$container).is(':focus')) {
717 return;
718 }
719
720 if (event.keyCode === 9 && this.$container.hasClass('open')) {
721 this.$button.click();
722 }
723 else {
724 var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible");
725
726 if (!$items.length) {
727 return;
728 }
729
730 var index = $items.index($items.filter(':focus'));
731
732 // Navigation up.
733 if (event.keyCode === 38 && index > 0) {
734 index--;
735 }
736 // Navigate down.
737 else if (event.keyCode === 40 && index < $items.length - 1) {
738 index++;
739 }
740 else if (!~index) {
741 index = 0;
742 }
743
744 var $current = $items.eq(index);
745 $current.trigger( 'focus' );
746
747 if (event.keyCode === 32 || event.keyCode === 13) {
748 var $checkbox = $current.find('input');
749
750 $checkbox.prop("checked", !$checkbox.prop("checked"));
751 $checkbox.change();
752 }
753
754 event.stopPropagation();
755 event.preventDefault();
756 }
757 }, this));
758
759 if (this.options.enableClickableOptGroups && this.options.multiple) {
760 $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) {
761 event.stopPropagation();
762
763 var $target = $(event.target);
764 var checked = $target.prop('checked') || false;
765
766 var $li = $(event.target).closest('li');
767 var $group = $li.nextUntil("li.multiselect-group")
768 .not('.multiselect-filter-hidden')
769 .not('.disabled');
770
771 var $inputs = $group.find("input");
772
773 var values = [];
774 var $options = [];
775
776 if (this.options.selectedClass) {
777 if (checked) {
778 $li.addClass(this.options.selectedClass);
779 }
780 else {
781 $li.removeClass(this.options.selectedClass);
782 }
783 }
784
785 $.each($inputs, $.proxy(function(index, input) {
786 var value = $(input).val();
787 var $option = this.getOptionByValue(value);
788
789 if (checked) {
790 $(input).prop('checked', true);
791 $(input).closest('li')
792 .addClass(this.options.selectedClass);
793
794 $option.prop('selected', true);
795 }
796 else {
797 $(input).prop('checked', false);
798 $(input).closest('li')
799 .removeClass(this.options.selectedClass);
800
801 $option.prop('selected', false);
802 }
803
804 $options.push(this.getOptionByValue(value));
805 }, this))
806
807 // Cannot use select or deselect here because it would call updateOptGroups again.
808
809 this.options.onChange($options, checked);
810
811 this.$select.change();
812 this.updateButtonText();
813 this.updateSelectAll();
814 }, this));
815 }
816
817 if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
818 $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) {
819 var $li = $(event.target).closest('li');
820 var $inputs = $li.nextUntil("li.multiselect-group")
821 .not('.multiselect-filter-hidden');
822
823 var visible = true;
824 $inputs.each(function() {
825 visible = visible && !$(this).hasClass('multiselect-collapsible-hidden');
826 });
827
828 if (visible) {
829 $inputs.hide()
830 .addClass('multiselect-collapsible-hidden');
831 }
832 else {
833 $inputs.show()
834 .removeClass('multiselect-collapsible-hidden');
835 }
836 }, this));
837
838 $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea');
839 $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px');
840 $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px');
841 }
842 },
843
844 /**
845 * Create an option using the given select option.
846 *
847 * @param {jQuery} element
848 */
849 createOptionValue: function(element) {
850 var $element = $(element);
851 if ($element.is(':selected')) {
852 $element.prop('selected', true);
853 }
854
855 // Support the label attribute on options.
856 var label = this.options.optionLabel(element);
857 var classes = this.options.optionClass(element);
858 var value = $element.val();
859 var inputType = this.options.multiple ? "checkbox" : "radio";
860
861 var $li = $(this.options.templates.li);
862 var $label = $('label', $li);
863 $label.addClass(inputType);
864 $label.attr("title", label);
865 $li.addClass(classes);
866
867 // Hide all children items when collapseOptGroupsByDefault is true
868 if (this.options.collapseOptGroupsByDefault && $(element).parent().prop("tagName").toLowerCase() === "optgroup") {
869 $li.addClass("multiselect-collapsible-hidden");
870 $li.hide();
871 }
872
873 if (this.options.enableHTML) {
874 $label.html(" " + label);
875 }
876 else {
877 $label.text(" " + label);
878 }
879
880 var $checkbox = $('<input/>').attr('type', inputType);
881
882 var name = this.options.checkboxName($element);
883 if (name) {
884 $checkbox.attr('name', name);
885 }
886
887 $label.prepend($checkbox);
888
889 var selected = $element.prop('selected') || false;
890 $checkbox.val(value);
891
892 if (value === this.options.selectAllValue) {
893 $li.addClass("multiselect-item multiselect-all");
894 $checkbox.parent().parent()
895 .addClass('multiselect-all');
896 }
897
898 $label.attr('title', $element.attr('title'));
899
900 this.$ul.append($li);
901
902 if ($element.is(':disabled')) {
903 $checkbox.attr('disabled', 'disabled')
904 .prop('disabled', true)
905 .closest('a')
906 .attr("tabindex", "-1")
907 .closest('li')
908 .addClass('disabled');
909 }
910
911 $checkbox.prop('checked', selected);
912
913 if (selected && this.options.selectedClass) {
914 $checkbox.closest('li')
915 .addClass(this.options.selectedClass);
916 }
917 },
918
919 /**
920 * Creates a divider using the given select option.
921 *
922 * @param {jQuery} element
923 */
924 createDivider: function(element) {
925 var $divider = $(this.options.templates.divider);
926 this.$ul.append($divider);
927 },
928
929 /**
930 * Creates an optgroup.
931 *
932 * @param {jQuery} group
933 */
934 createOptgroup: function(group) {
935 var label = $(group).attr("label");
936 var value = $(group).attr("value");
937 var $li = $('<li class="multiselect-item multiselect-group"><a href="javascript:void(0);"><label><b></b></label></a></li>');
938
939 var classes = this.options.optionClass(group);
940 $li.addClass(classes);
941
942 if (this.options.enableHTML) {
943 $('label b', $li).html(" " + label);
944 }
945 else {
946 $('label b', $li).text(" " + label);
947 }
948
949 if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
950 $('a', $li).append('<span class="caret-container"><b class="caret"></b></span>');
951 }
952
953 if (this.options.enableClickableOptGroups && this.options.multiple) {
954 $('a label', $li).prepend('<input type="checkbox" value="' + value + '"/>');
955 }
956
957 if ($(group).is(':disabled')) {
958 $li.addClass('disabled');
959 }
960
961 this.$ul.append($li);
962
963 $("option", group).each($.proxy(function($, group) {
964 this.createOptionValue(group);
965 }, this))
966 },
967
968 /**
969 * Build the reset.
970 *
971 */
972 buildReset: function() {
973 if (this.options.includeResetOption) {
974
975 // Check whether to add a divider after the reset.
976 if (this.options.includeResetDivider) {
977 this.$ul.prepend($(this.options.templates.divider));
978 }
979
980 var $resetButton = $(this.options.templates.resetButton);
981
982 if (this.options.enableHTML) {
983 $('a', $resetButton).html(this.options.resetText);
984 }
985 else {
986 $('a', $resetButton).text(this.options.resetText);
987 }
988
989 $('a', $resetButton).click($.proxy(function(){
990 this.clearSelection();
991 }, this));
992
993 this.$ul.prepend($resetButton);
994 }
995 },
996
997 /**
998 * Build the select all.
999 *
1000 * Checks if a select all has already been created.
1001 */
1002 buildSelectAll: function() {
1003 if (typeof this.options.selectAllValue === 'number') {
1004 this.options.selectAllValue = this.options.selectAllValue.toString();
1005 }
1006
1007 var alreadyHasSelectAll = this.hasSelectAll();
1008
1009 if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
1010 && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) {
1011
1012 // Check whether to add a divider after the select all.
1013 if (this.options.includeSelectAllDivider) {
1014 this.$ul.prepend($(this.options.templates.divider));
1015 }
1016
1017 var $li = $(this.options.templates.li);
1018 $('label', $li).addClass("checkbox");
1019
1020 if (this.options.enableHTML) {
1021 $('label', $li).html(" " + this.options.selectAllText);
1022 }
1023 else {
1024 $('label', $li).text(" " + this.options.selectAllText);
1025 }
1026
1027 if (this.options.selectAllName) {
1028 $('label', $li).prepend('<input type="checkbox" name="' + this.options.selectAllName + '" />');
1029 }
1030 else {
1031 $('label', $li).prepend('<input type="checkbox" />');
1032 }
1033
1034 var $checkbox = $('input', $li);
1035 $checkbox.val(this.options.selectAllValue);
1036
1037 $li.addClass("multiselect-item multiselect-all");
1038 $checkbox.parent().parent()
1039 .addClass('multiselect-all');
1040
1041 this.$ul.prepend($li);
1042
1043 $checkbox.prop('checked', false);
1044 }
1045 },
1046
1047 /**
1048 * Builds the filter.
1049 */
1050 buildFilter: function() {
1051
1052 // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
1053 if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
1054 var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
1055
1056 if (this.$select.find('option').length >= enableFilterLength) {
1057
1058 this.$filter = $(this.options.templates.filter);
1059 $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
1060
1061 // Adds optional filter clear button
1062 if(this.options.includeFilterClearBtn) {
1063 var clearBtn = $(this.options.templates.filterClearBtn);
1064 clearBtn.on('click', $.proxy(function(event){
1065 clearTimeout(this.searchTimeout);
1066
1067 this.query = '';
1068 this.$filter.find('.multiselect-search').val('');
1069 $('li', this.$ul).show().removeClass('multiselect-filter-hidden');
1070
1071 this.updateSelectAll();
1072
1073 if (this.options.enableClickableOptGroups && this.options.multiple) {
1074 this.updateOptGroups();
1075 }
1076
1077 }, this));
1078 this.$filter.find('.input-group').append(clearBtn);
1079 }
1080
1081 this.$ul.prepend(this.$filter);
1082
1083 this.$filter.val(this.query).on('click', function(event) {
1084 event.stopPropagation();
1085 }).on('input keydown', $.proxy(function(event) {
1086 // Cancel enter key default behaviour
1087 if (event.which === 13) {
1088 event.preventDefault();
1089 }
1090
1091 // This is useful to catch "keydown" events after the browser has updated the control.
1092 clearTimeout(this.searchTimeout);
1093
1094 this.searchTimeout = this.asyncFunction($.proxy(function() {
1095
1096 if (this.query !== event.target.value) {
1097 this.query = event.target.value;
1098
1099 var currentGroup, currentGroupVisible;
1100 $.each($('li', this.$ul), $.proxy(function(index, element) {
1101 var value = $('input', element).length > 0 ? $('input', element).val() : "";
1102 var text = $('label', element).text();
1103
1104 var filterCandidate = '';
1105 if ((this.options.filterBehavior === 'text')) {
1106 filterCandidate = text;
1107 }
1108 else if ((this.options.filterBehavior === 'value')) {
1109 filterCandidate = value;
1110 }
1111 else if (this.options.filterBehavior === 'both') {
1112 filterCandidate = text + '\n' + value;
1113 }
1114
1115 if (value !== this.options.selectAllValue && text) {
1116
1117 // By default lets assume that element is not
1118 // interesting for this search.
1119 var showElement = false;
1120
1121 if (this.options.enableCaseInsensitiveFiltering) {
1122 filterCandidate = filterCandidate.toLowerCase();
1123 this.query = this.query.toLowerCase();
1124 }
1125
1126 if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') {
1127 var valueToMatch = filterCandidate.trim().substring(0, this.query.length);
1128 if (this.query.indexOf(valueToMatch) > -1) {
1129 showElement = true;
1130 }
1131 }
1132 else if (filterCandidate.indexOf(this.query) > -1) {
1133 showElement = true;
1134 }
1135
1136 // Toggle current element (group or group item) according to showElement boolean.
1137 if(!showElement){
1138 $(element).css('display', 'none');
1139 $(element).addClass('multiselect-filter-hidden');
1140 }
1141 if(showElement){
1142 $(element).css('display', 'block');
1143 $(element).removeClass('multiselect-filter-hidden');
1144 }
1145
1146 // Differentiate groups and group items.
1147 if ($(element).hasClass('multiselect-group')) {
1148 // Remember group status.
1149 currentGroup = element;
1150 currentGroupVisible = showElement;
1151 }
1152 else {
1153 // Show group name when at least one of its items is visible.
1154 if (showElement) {
1155 $(currentGroup).show()
1156 .removeClass('multiselect-filter-hidden');
1157 }
1158
1159 // Show all group items when group name satisfies filter.
1160 if (!showElement && currentGroupVisible) {
1161 $(element).show()
1162 .removeClass('multiselect-filter-hidden');
1163 }
1164 }
1165 }
1166 }, this));
1167 }
1168
1169 this.updateSelectAll();
1170
1171 if (this.options.enableClickableOptGroups && this.options.multiple) {
1172 this.updateOptGroups();
1173 }
1174
1175 this.options.onFiltering(event.target);
1176
1177 }, this), 300, this);
1178 }, this));
1179 }
1180 }
1181 },
1182
1183 /**
1184 * Unbinds the whole plugin.
1185 */
1186 destroy: function() {
1187 this.$container.remove();
1188 this.$select.show();
1189
1190 // reset original state
1191 this.$select.prop('disabled', this.options.wasDisabled);
1192
1193 this.$select.data('multiselect', null);
1194 },
1195
1196 /**
1197 * Refreshs the multiselect based on the selected options of the select.
1198 */
1199 refresh: function () {
1200 var inputs = {};
1201 $('li input', this.$ul).each(function() {
1202 inputs[$(this).val()] = $(this);
1203 });
1204
1205 $('option', this.$select).each($.proxy(function (index, element) {
1206 var $elem = $(element);
1207 var $input = inputs[$(element).val()];
1208
1209 if ($elem.is(':selected')) {
1210 $input.prop('checked', true);
1211
1212 if (this.options.selectedClass) {
1213 $input.closest('li')
1214 .addClass(this.options.selectedClass);
1215 }
1216 }
1217 else {
1218 $input.prop('checked', false);
1219
1220 if (this.options.selectedClass) {
1221 $input.closest('li')
1222 .removeClass(this.options.selectedClass);
1223 }
1224 }
1225
1226 if ($elem.is(":disabled")) {
1227 $input.attr('disabled', 'disabled')
1228 .prop('disabled', true)
1229 .closest('li')
1230 .addClass('disabled');
1231 }
1232 else {
1233 $input.prop('disabled', false)
1234 .closest('li')
1235 .removeClass('disabled');
1236 }
1237 }, this));
1238
1239 this.updateButtonText();
1240 this.updateSelectAll();
1241
1242 if (this.options.enableClickableOptGroups && this.options.multiple) {
1243 this.updateOptGroups();
1244 }
1245 },
1246
1247 /**
1248 * Select all options of the given values.
1249 *
1250 * If triggerOnChange is set to true, the on change event is triggered if
1251 * and only if one value is passed.
1252 *
1253 * @param {Array} selectValues
1254 * @param {Boolean} triggerOnChange
1255 */
1256 select: function(selectValues, triggerOnChange) {
1257 if(!$.isArray(selectValues)) {
1258 selectValues = [selectValues];
1259 }
1260
1261 for (var i = 0; i < selectValues.length; i++) {
1262 var value = selectValues[i];
1263
1264 if (value === null || value === undefined) {
1265 continue;
1266 }
1267
1268 var $option = this.getOptionByValue(value);
1269 var $checkbox = this.getInputByValue(value);
1270
1271 if($option === undefined || $checkbox === undefined) {
1272 continue;
1273 }
1274
1275 if (!this.options.multiple) {
1276 this.deselectAll(false);
1277 }
1278
1279 if (this.options.selectedClass) {
1280 $checkbox.closest('li')
1281 .addClass(this.options.selectedClass);
1282 }
1283
1284 $checkbox.prop('checked', true);
1285 $option.prop('selected', true);
1286
1287 if (triggerOnChange) {
1288 this.options.onChange($option, true);
1289 }
1290 }
1291
1292 this.updateButtonText();
1293 this.updateSelectAll();
1294
1295 if (this.options.enableClickableOptGroups && this.options.multiple) {
1296 this.updateOptGroups();
1297 }
1298 },
1299
1300 /**
1301 * Clears all selected items.
1302 */
1303 clearSelection: function () {
1304 this.deselectAll(false);
1305 this.updateButtonText();
1306 this.updateSelectAll();
1307
1308 if (this.options.enableClickableOptGroups && this.options.multiple) {
1309 this.updateOptGroups();
1310 }
1311 },
1312
1313 /**
1314 * Deselects all options of the given values.
1315 *
1316 * If triggerOnChange is set to true, the on change event is triggered, if
1317 * and only if one value is passed.
1318 *
1319 * @param {Array} deselectValues
1320 * @param {Boolean} triggerOnChange
1321 */
1322 deselect: function(deselectValues, triggerOnChange) {
1323 if(!$.isArray(deselectValues)) {
1324 deselectValues = [deselectValues];
1325 }
1326
1327 for (var i = 0; i < deselectValues.length; i++) {
1328 var value = deselectValues[i];
1329
1330 if (value === null || value === undefined) {
1331 continue;
1332 }
1333
1334 var $option = this.getOptionByValue(value);
1335 var $checkbox = this.getInputByValue(value);
1336
1337 if($option === undefined || $checkbox === undefined) {
1338 continue;
1339 }
1340
1341 if (this.options.selectedClass) {
1342 $checkbox.closest('li')
1343 .removeClass(this.options.selectedClass);
1344 }
1345
1346 $checkbox.prop('checked', false);
1347 $option.prop('selected', false);
1348
1349 if (triggerOnChange) {
1350 this.options.onChange($option, false);
1351 }
1352 }
1353
1354 this.updateButtonText();
1355 this.updateSelectAll();
1356
1357 if (this.options.enableClickableOptGroups && this.options.multiple) {
1358 this.updateOptGroups();
1359 }
1360 },
1361
1362 /**
1363 * Selects all enabled & visible options.
1364 *
1365 * If justVisible is true or not specified, only visible options are selected.
1366 *
1367 * @param {Boolean} justVisible
1368 * @param {Boolean} triggerOnSelectAll
1369 */
1370 selectAll: function (justVisible, triggerOnSelectAll) {
1371
1372 var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1373 var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul);
1374 var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible');
1375
1376 if(justVisible) {
1377 $('input:enabled' , visibleLis).prop('checked', true);
1378 visibleLis.addClass(this.options.selectedClass);
1379
1380 $('input:enabled' , visibleLis).each($.proxy(function(index, element) {
1381 var value = $(element).val();
1382 var option = this.getOptionByValue(value);
1383 $(option).prop('selected', true);
1384 }, this));
1385 }
1386 else {
1387 $('input:enabled' , allLis).prop('checked', true);
1388 allLis.addClass(this.options.selectedClass);
1389
1390 $('input:enabled' , allLis).each($.proxy(function(index, element) {
1391 var value = $(element).val();
1392 var option = this.getOptionByValue(value);
1393 $(option).prop('selected', true);
1394 }, this));
1395 }
1396
1397 $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true);
1398
1399 if (this.options.enableClickableOptGroups && this.options.multiple) {
1400 this.updateOptGroups();
1401 }
1402
1403 if (triggerOnSelectAll) {
1404 this.options.onSelectAll();
1405 }
1406 },
1407
1408 /**
1409 * Deselects all options.
1410 *
1411 * If justVisible is true or not specified, only visible options are deselected.
1412 *
1413 * @param {Boolean} justVisible
1414 */
1415 deselectAll: function (justVisible, triggerOnDeselectAll) {
1416
1417 var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1418 var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul);
1419 var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible');
1420
1421 if(justVisible) {
1422 $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false);
1423 visibleLis.removeClass(this.options.selectedClass);
1424
1425 $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) {
1426 var value = $(element).val();
1427 var option = this.getOptionByValue(value);
1428 $(option).prop('selected', false);
1429 }, this));
1430 }
1431 else {
1432 $('input[type="checkbox"]:enabled' , allLis).prop('checked', false);
1433 allLis.removeClass(this.options.selectedClass);
1434
1435 $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) {
1436 var value = $(element).val();
1437 var option = this.getOptionByValue(value);
1438 $(option).prop('selected', false);
1439 }, this));
1440 }
1441
1442 $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false);
1443
1444 if (this.options.enableClickableOptGroups && this.options.multiple) {
1445 this.updateOptGroups();
1446 }
1447
1448 if (triggerOnDeselectAll) {
1449 this.options.onDeselectAll();
1450 }
1451 },
1452
1453 /**
1454 * Rebuild the plugin.
1455 *
1456 * Rebuilds the dropdown, the filter and the select all option.
1457 */
1458 rebuild: function() {
1459 this.$ul.html('');
1460
1461 // Important to distinguish between radios and checkboxes.
1462 this.options.multiple = this.$select.attr('multiple') === "multiple";
1463
1464 this.buildSelectAll();
1465 this.buildDropdownOptions();
1466 this.buildFilter();
1467
1468 this.updateButtonText();
1469 this.updateSelectAll(true);
1470
1471 if (this.options.enableClickableOptGroups && this.options.multiple) {
1472 this.updateOptGroups();
1473 }
1474
1475 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
1476 this.disable();
1477 }
1478 else {
1479 this.enable();
1480 }
1481
1482 if (this.options.dropRight) {
1483 this.$ul.addClass('pull-right');
1484 }
1485 },
1486
1487 /**
1488 * The provided data will be used to build the dropdown.
1489 */
1490 dataprovider: function(dataprovider) {
1491
1492 var groupCounter = 0;
1493 var $select = this.$select.empty();
1494
1495 $.each(dataprovider, function (index, option) {
1496 var $tag;
1497
1498 if ($.isArray(option.children)) { // create optiongroup tag
1499 groupCounter++;
1500
1501 $tag = $('<optgroup/>').attr({
1502 label: option.label || 'Group ' + groupCounter,
1503 disabled: !!option.disabled,
1504 value: option.value
1505 });
1506
1507 forEach(option.children, function(subOption) { // add children option tags
1508 var attributes = {
1509 value: subOption.value,
1510 label: subOption.label || subOption.value,
1511 title: subOption.title,
1512 selected: !!subOption.selected,
1513 disabled: !!subOption.disabled
1514 };
1515
1516 //Loop through attributes object and add key-value for each attribute
1517 for (var key in subOption.attributes) {
1518 attributes['data-' + key] = subOption.attributes[key];
1519 }
1520 //Append original attributes + new data attributes to option
1521 $tag.append($('<option/>').attr(attributes));
1522 });
1523 }
1524 else {
1525
1526 var attributes = {
1527 'value': option.value,
1528 'label': option.label || option.value,
1529 'title': option.title,
1530 'class': option['class'],
1531 'selected': !!option['selected'],
1532 'disabled': !!option['disabled']
1533 };
1534 //Loop through attributes object and add key-value for each attribute
1535 for (var key in option.attributes) {
1536 attributes['data-' + key] = option.attributes[key];
1537 }
1538 //Append original attributes + new data attributes to option
1539 $tag = $('<option/>').attr(attributes);
1540
1541 $tag.text(option.label || option.value);
1542 }
1543
1544 $select.append($tag);
1545 });
1546
1547 this.rebuild();
1548 },
1549
1550 /**
1551 * Enable the multiselect.
1552 */
1553 enable: function() {
1554 this.$select.prop('disabled', false);
1555 this.$button.prop('disabled', false)
1556 .removeClass('disabled');
1557 },
1558
1559 /**
1560 * Disable the multiselect.
1561 */
1562 disable: function() {
1563 this.$select.prop('disabled', true);
1564 this.$button.prop('disabled', true)
1565 .addClass('disabled');
1566 },
1567
1568 /**
1569 * Set the options.
1570 *
1571 * @param {Array} options
1572 */
1573 setOptions: function(options) {
1574 this.options = this.mergeOptions(options);
1575 },
1576
1577 /**
1578 * Merges the given options with the default options.
1579 *
1580 * @param {Array} options
1581 * @returns {Array}
1582 */
1583 mergeOptions: function(options) {
1584 return $.extend(true, {}, this.defaults, this.options, options);
1585 },
1586
1587 /**
1588 * Checks whether a select all checkbox is present.
1589 *
1590 * @returns {Boolean}
1591 */
1592 hasSelectAll: function() {
1593 return $('li.multiselect-all', this.$ul).length > 0;
1594 },
1595
1596 /**
1597 * Update opt groups.
1598 */
1599 updateOptGroups: function() {
1600 var $groups = $('li.multiselect-group', this.$ul)
1601 var selectedClass = this.options.selectedClass;
1602
1603 $groups.each(function() {
1604 var $options = $(this).nextUntil('li.multiselect-group')
1605 .not('.multiselect-filter-hidden')
1606 .not('.disabled');
1607
1608 var checked = true;
1609 $options.each(function() {
1610 var $input = $('input', this);
1611
1612 if (!$input.prop('checked')) {
1613 checked = false;
1614 }
1615 });
1616
1617 if (selectedClass) {
1618 if (checked) {
1619 $(this).addClass(selectedClass);
1620 }
1621 else {
1622 $(this).removeClass(selectedClass);
1623 }
1624 }
1625
1626 $('input', this).prop('checked', checked);
1627 });
1628 },
1629
1630 /**
1631 * Updates the select all checkbox based on the currently displayed and selected checkboxes.
1632 */
1633 updateSelectAll: function(notTriggerOnSelectAll) {
1634 if (this.hasSelectAll()) {
1635 var allBoxes = $("li:not(.multiselect-item):not(.multiselect-filter-hidden):not(.multiselect-group):not(.disabled) input:enabled", this.$ul);
1636 var allBoxesLength = allBoxes.length;
1637 var checkedBoxesLength = allBoxes.filter(":checked").length;
1638 var selectAllLi = $("li.multiselect-all", this.$ul);
1639 var selectAllInput = selectAllLi.find("input");
1640
1641 if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) {
1642 selectAllInput.prop("checked", true);
1643 selectAllLi.addClass(this.options.selectedClass);
1644 }
1645 else {
1646 selectAllInput.prop("checked", false);
1647 selectAllLi.removeClass(this.options.selectedClass);
1648 }
1649 }
1650 },
1651
1652 /**
1653 * Update the button text and its title based on the currently selected options.
1654 */
1655 updateButtonText: function() {
1656 var options = this.getSelected();
1657
1658 // First update the displayed button text.
1659 if (this.options.enableHTML) {
1660 $('.multiselect .multiselect-selected-text', this.$container).html(this.options.buttonText(options, this.$select));
1661 }
1662 else {
1663 $('.multiselect .multiselect-selected-text', this.$container).text(this.options.buttonText(options, this.$select));
1664 }
1665
1666 // Now update the title attribute of the button.
1667 $('.multiselect', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
1668 },
1669
1670 /**
1671 * Get all selected options.
1672 *
1673 * @returns {jQUery}
1674 */
1675 getSelected: function() {
1676 return $('option', this.$select).filter(":selected");
1677 },
1678
1679 /**
1680 * Gets a select option by its value.
1681 *
1682 * @param {String} value
1683 * @returns {jQuery}
1684 */
1685 getOptionByValue: function (value) {
1686
1687 var options = $('option', this.$select);
1688 var valueToCompare = value.toString();
1689
1690 for (var i = 0; i < options.length; i = i + 1) {
1691 var option = options[i];
1692 if (option.value === valueToCompare) {
1693 return $(option);
1694 }
1695 }
1696 },
1697
1698 /**
1699 * Get the input (radio/checkbox) by its value.
1700 *
1701 * @param {String} value
1702 * @returns {jQuery}
1703 */
1704 getInputByValue: function (value) {
1705
1706 var checkboxes = $('li input:not(.multiselect-search)', this.$ul);
1707 var valueToCompare = value.toString();
1708
1709 for (var i = 0; i < checkboxes.length; i = i + 1) {
1710 var checkbox = checkboxes[i];
1711 if (checkbox.value === valueToCompare) {
1712 return $(checkbox);
1713 }
1714 }
1715 },
1716
1717 /**
1718 * Used for knockout integration.
1719 */
1720 updateOriginalOptions: function() {
1721 this.originalOptions = this.$select.clone()[0].options;
1722 },
1723
1724 asyncFunction: function(callback, timeout, self) {
1725 var args = Array.prototype.slice.call(arguments, 3);
1726 return setTimeout(function() {
1727 callback.apply(self || window, args);
1728 }, timeout);
1729 },
1730
1731 setAllSelectedText: function(allSelectedText) {
1732 this.options.allSelectedText = allSelectedText;
1733 this.updateButtonText();
1734 }
1735 };
1736
1737 $.fn.multiselect = function(option, parameter, extraOptions) {
1738 return this.each(function() {
1739 var data = $(this).data('multiselect');
1740 var options = typeof option === 'object' && option;
1741
1742 // Initialize the multiselect.
1743 if (!data) {
1744 data = new Multiselect(this, options);
1745 $(this).data('multiselect', data);
1746 }
1747
1748 // Call multiselect method.
1749 if (typeof option === 'string') {
1750 data[option](parameter, extraOptions);
1751
1752 if (option === 'destroy') {
1753 $(this).data('multiselect', false);
1754 }
1755 }
1756 });
1757 };
1758
1759 $.fn.multiselect.Constructor = Multiselect;
1760
1761 $(function() {
1762 $("select[data-role=multiselect]").multiselect();
1763 });
1764
1765 }(window.jQuery);
1766