PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 4.05.02
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v4.05.02
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.05.02, at js/bootstrap-multiselect.js

1,381 lines 51.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Bootstrap Multiselect v0.9.13 (https://github.com/davidstutz/bootstrap-multiselect)
3 *
4 * Copyright 2012 - 2015 David Stutz
5 *
6 * Dual licensed under the BSD-3-Clause and the Apache License, Version 2.0.
7 */
8 !function ($) {
9 "use strict";// jshint ;_;
10
11 if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
12 ko.bindingHandlers.multiselect = {
13 after: ['options', 'value', 'selectedOptions'],
14
15 init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
16 var $element = $(element);
17 var config = ko.toJS(valueAccessor());
18
19 $element.multiselect(config);
20
21 if (allBindings.has('options')) {
22 var options = allBindings.get('options');
23 if (ko.isObservable(options)) {
24 ko.computed({
25 read: function() {
26 options();
27 setTimeout(function() {
28 var ms = $element.data('multiselect');
29 if (ms)
30 ms.updateOriginalOptions();//Not sure how beneficial this is.
31 $element.multiselect('rebuild');
32 }, 1);
33 },
34 disposeWhenNodeIsRemoved: element
35 });
36 }
37 }
38
39 //value and selectedOptions are two-way, so these will be triggered even by our own actions.
40 //It needs some way to tell if they are triggered because of us or because of outside change.
41 //It doesn't loop but it's a waste of processing.
42 if (allBindings.has('value')) {
43 var value = allBindings.get('value');
44 if (ko.isObservable(value)) {
45 ko.computed({
46 read: function() {
47 value();
48 setTimeout(function() {
49 $element.multiselect('refresh');
50 }, 1);
51 },
52 disposeWhenNodeIsRemoved: element
53 }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
54 }
55 }
56
57 //Switched from arrayChange subscription to general subscription using 'refresh'.
58 //Not sure performance is any better using 'select' and 'deselect'.
59 if (allBindings.has('selectedOptions')) {
60 var selectedOptions = allBindings.get('selectedOptions');
61 if (ko.isObservable(selectedOptions)) {
62 ko.computed({
63 read: function() {
64 selectedOptions();
65 setTimeout(function() {
66 $element.multiselect('refresh');
67 }, 1);
68 },
69 disposeWhenNodeIsRemoved: element
70 }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
71 }
72 }
73
74 ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
75 $element.multiselect('destroy');
76 });
77 },
78
79 update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
80 var $element = $(element);
81 var config = ko.toJS(valueAccessor());
82
83 $element.multiselect('setOptions', config);
84 $element.multiselect('rebuild');
85 }
86 };
87 }
88
89 function forEach(array, callback) {
90 for (var index = 0; index < array.length; ++index) {
91 callback(array[index], index);
92 }
93 }
94
95 /**
96 * Constructor to create a new multiselect using the given select.
97 *
98 * @param {jQuery} select
99 * @param {Object} options
100 * @returns {Multiselect}
101 */
102 function Multiselect(select, options) {
103
104 this.$select = $(select);
105
106 // Placeholder via data attributes
107 if (this.$select.attr("data-placeholder")) {
108 options.nonSelectedText = this.$select.data("placeholder");
109 }
110
111 this.options = this.mergeOptions($.extend({}, options, this.$select.data()));
112
113 // Initialization.
114 // We have to clone to create a new reference.
115 this.originalOptions = this.$select.clone()[0].options;
116 this.query = '';
117 this.searchTimeout = null;
118 this.lastToggledInput = null;
119
120 this.options.multiple = this.$select.attr('multiple') === "multiple";
121 this.options.onChange = $.proxy(this.options.onChange, this);
122 this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
123 this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
124 this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this);
125 this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this);
126
127 // Build select all if enabled.
128 this.buildContainer();
129 this.buildButton();
130 this.buildDropdown();
131 this.buildSelectAll();
132 this.buildDropdownOptions();
133 this.buildFilter();
134
135 this.updateButtonText();
136 this.updateSelectAll();
137
138 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
139 this.disable();
140 }
141
142 this.$select.hide().after(this.$container);
143 }
144
145 Multiselect.prototype = {
146
147 defaults: {
148 /**
149 * Default text function will either print 'None selected' in case no
150 * option is selected or a list of the selected options up to a length
151 * of 3 selected options.
152 *
153 * @param {jQuery} options
154 * @param {jQuery} select
155 * @returns {String}
156 */
157 buttonText: function(options, select) {
158 if (options.length === 0) {
159 return this.nonSelectedText;
160 }
161 else if (this.allSelectedText
162 && options.length === $('option', $(select)).length
163 && $('option', $(select)).length !== 1
164 && this.multiple) {
165
166 if (this.selectAllNumber) {
167 return this.allSelectedText + ' (' + options.length + ')';
168 }
169 else {
170 return this.allSelectedText;
171 }
172 }
173 else if (options.length > this.numberDisplayed) {
174 return options.length + ' ' + this.nSelectedText;
175 }
176 else {
177 var selected = '';
178 var delimiter = this.delimiterText;
179
180 options.each(function() {
181 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
182 selected += label + delimiter;
183 });
184
185 return selected.substr(0, selected.length - 2);
186 }
187 },
188 /**
189 * Updates the title of the button similar to the buttonText function.
190 *
191 * @param {jQuery} options
192 * @param {jQuery} select
193 * @returns {@exp;selected@call;substr}
194 */
195 buttonTitle: function(options, select) {
196 if (options.length === 0) {
197 return this.nonSelectedText;
198 }
199 else {
200 var selected = '';
201 var delimiter = this.delimiterText;
202
203 options.each(function () {
204 var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
205 selected += label + delimiter;
206 });
207 return selected.substr(0, selected.length - 2);
208 }
209 },
210 /**
211 * Create a label.
212 *
213 * @param {jQuery} element
214 * @returns {String}
215 */
216 optionLabel: function(element){
217 return $(element).attr('label') || $(element).text();
218 },
219 /**
220 * Triggered on change of the multiselect.
221 *
222 * Not triggered when selecting/deselecting options manually.
223 *
224 * @param {jQuery} option
225 * @param {Boolean} checked
226 */
227 onChange : function(option, checked) {
228
229 },
230 /**
231 * Triggered when the dropdown is shown.
232 *
233 * @param {jQuery} event
234 */
235 onDropdownShow: function(event) {
236
237 },
238 /**
239 * Triggered when the dropdown is hidden.
240 *
241 * @param {jQuery} event
242 */
243 onDropdownHide: function(event) {
244
245 },
246 /**
247 * Triggered after the dropdown is shown.
248 *
249 * @param {jQuery} event
250 */
251 onDropdownShown: function(event) {
252
253 },
254 /**
255 * Triggered after the dropdown is hidden.
256 *
257 * @param {jQuery} event
258 */
259 onDropdownHidden: function(event) {
260
261 },
262 /**
263 * Triggered on select all.
264 */
265 onSelectAll: function() {
266
267 },
268 enableHTML: false,
269 buttonClass: 'btn btn-default',
270 inheritClass: false,
271 buttonWidth: 'auto',
272 buttonContainer: '<div class="btn-group" />',
273 dropRight: false,
274 selectedClass: 'active',
275 // Maximum height of the dropdown menu.
276 // If maximum height is exceeded a scrollbar will be displayed.
277 maxHeight: false,
278 checkboxName: false,
279 includeSelectAllOption: false,
280 includeSelectAllIfMoreThan: 0,
281 selectAllText: ' Select all',
282 selectAllValue: 'multiselect-all',
283 selectAllName: false,
284 selectAllNumber: true,
285 enableFiltering: false,
286 enableCaseInsensitiveFiltering: false,
287 enableClickableOptGroups: false,
288 filterPlaceholder: 'Search',
289 // possible options: 'text', 'value', 'both'
290 filterBehavior: 'text',
291 includeFilterClearBtn: true,
292 preventInputChangeEvent: false,
293 nonSelectedText: 'None selected',
294 nSelectedText: 'selected',
295 allSelectedText: 'All selected',
296 numberDisplayed: 3,
297 disableIfEmpty: false,
298 delimiterText: ', ',
299 templates: {
300 button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span> <b class="caret"></b></button>',
301 ul: '<ul class="multiselect-container dropdown-menu"></ul>',
302 filter: '<li class="multiselect-item 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>',
303 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>',
304 li: '<li><a tabindex="0"><label></label></a></li>',
305 divider: '<li class="multiselect-item divider"></li>',
306 liGroup: '<li class="multiselect-item multiselect-group"><label></label></li>'
307 }
308 },
309
310 constructor: Multiselect,
311
312 /**
313 * Builds the container of the multiselect.
314 */
315 buildContainer: function() {
316 this.$container = $(this.options.buttonContainer);
317 this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
318 this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
319 this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
320 this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
321 },
322
323 /**
324 * Builds the button of the multiselect.
325 */
326 buildButton: function() {
327 this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
328 if (this.$select.attr('class') && this.options.inheritClass) {
329 this.$button.addClass(this.$select.attr('class'));
330 }
331 // Adopt active state.
332 if (this.$select.prop('disabled')) {
333 this.disable();
334 }
335 else {
336 this.enable();
337 }
338
339 // Manually add button width if set.
340 if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
341 this.$button.css({
342 'width' : this.options.buttonWidth,
343 'overflow' : 'hidden',
344 'text-overflow' : 'ellipsis'
345 });
346 this.$container.css({
347 'width': this.options.buttonWidth
348 });
349 }
350
351 // Keep the tab index from the select.
352 var tabindex = this.$select.attr('tabindex');
353 if (tabindex) {
354 this.$button.attr('tabindex', tabindex);
355 }
356
357 this.$container.prepend(this.$button);
358 },
359
360 /**
361 * Builds the ul representing the dropdown menu.
362 */
363 buildDropdown: function() {
364
365 // Build ul.
366 this.$ul = $(this.options.templates.ul);
367
368 if (this.options.dropRight) {
369 this.$ul.addClass('pull-right');
370 }
371
372 // Set max height of dropdown menu to activate auto scrollbar.
373 if (this.options.maxHeight) {
374 // TODO: Add a class for this option to move the css declarations.
375 this.$ul.css({
376 'max-height': this.options.maxHeight + 'px',
377 'overflow-y': 'auto',
378 'overflow-x': 'hidden'
379 });
380 }
381
382 this.$container.append(this.$ul);
383 },
384
385 /**
386 * Build the dropdown options and binds all nessecary events.
387 *
388 * Uses createDivider and createOptionValue to create the necessary options.
389 */
390 buildDropdownOptions: function() {
391
392 this.$select.children().each($.proxy(function(index, element) {
393
394 var $element = $(element);
395 // Support optgroups and options without a group simultaneously.
396 var tag = $element.prop('tagName')
397 .toLowerCase();
398
399 if ($element.prop('value') === this.options.selectAllValue) {
400 return;
401 }
402
403 if (tag === 'optgroup') {
404 this.createOptgroup(element);
405 }
406 else if (tag === 'option') {
407
408 if ($element.data('role') === 'divider') {
409 this.createDivider();
410 }
411 else {
412 this.createOptionValue(element);
413 }
414
415 }
416
417 // Other illegal tags will be ignored.
418 }, this));
419
420 // Bind the change event on the dropdown elements.
421 $('li input', this.$ul).on('change', $.proxy(function(event) {
422 var $target = $(event.target);
423
424 var checked = $target.prop('checked') || false;
425 var isSelectAllOption = $target.val() === this.options.selectAllValue;
426
427 // Apply or unapply the configured selected class.
428 if (this.options.selectedClass) {
429 if (checked) {
430 $target.closest('li')
431 .addClass(this.options.selectedClass);
432 }
433 else {
434 $target.closest('li')
435 .removeClass(this.options.selectedClass);
436 }
437 }
438
439 // Get the corresponding option.
440 var value = $target.val();
441 var $option = this.getOptionByValue(value);
442
443 var $optionsNotThis = $('option', this.$select).not($option);
444 var $checkboxesNotThis = $('input', this.$container).not($target);
445
446 if (isSelectAllOption) {
447 if (checked) {
448 this.selectAll();
449 }
450 else {
451 this.deselectAll();
452 }
453 }
454
455 if(!isSelectAllOption){
456 if (checked) {
457 $option.prop('selected', true);
458
459 if (this.options.multiple) {
460 // Simply select additional option.
461 $option.prop('selected', true);
462 }
463 else {
464 // Unselect all other options and corresponding checkboxes.
465 if (this.options.selectedClass) {
466 $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass);
467 }
468
469 $($checkboxesNotThis).prop('checked', false);
470 $optionsNotThis.prop('selected', false);
471
472 // It's a single selection, so close.
473 this.$button.click();
474 }
475
476 if (this.options.selectedClass === "active") {
477 $optionsNotThis.closest("a").css("outline", "");
478 }
479 }
480 else {
481 // Unselect option.
482 $option.prop('selected', false);
483 }
484 }
485
486 this.$select.change();
487
488 this.updateButtonText();
489 this.updateSelectAll();
490
491 this.options.onChange($option, checked);
492
493 if(this.options.preventInputChangeEvent) {
494 return false;
495 }
496 }, this));
497
498 $('li a', this.$ul).on('mousedown', function(e) {
499 if (e.shiftKey) {
500 // Prevent selecting text by Shift+click
501 return false;
502 }
503 });
504
505 $('li a', this.$ul).on('touchstart click', $.proxy(function(event) {
506 event.stopPropagation();
507
508 var $target = $(event.target);
509
510 if (event.shiftKey && this.options.multiple) {
511 if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
512 event.preventDefault();
513 $target = $target.find("input");
514 $target.prop("checked", !$target.prop("checked"));
515 }
516 var checked = $target.prop('checked') || false;
517
518 if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
519 var from = $target.closest("li").index();
520 var to = this.lastToggledInput.closest("li").index();
521
522 if (from > to) { // Swap the indices
523 var tmp = to;
524 to = from;
525 from = tmp;
526 }
527
528 // Make sure we grab all elements since slice excludes the last index
529 ++to;
530
531 // Change the checkboxes and underlying options
532 var range = this.$ul.find("li").slice(from, to).find("input");
533
534 range.prop('checked', checked);
535
536 if (this.options.selectedClass) {
537 range.closest('li')
538 .toggleClass(this.options.selectedClass, checked);
539 }
540
541 for (var i = 0, j = range.length; i < j; i++) {
542 var $checkbox = $(range[i]);
543
544 var $option = this.getOptionByValue($checkbox.val());
545
546 $option.prop('selected', checked);
547 }
548 }
549
550 // Trigger the select "change" event
551 $target.trigger("change");
552 }
553
554 // Remembers last clicked option
555 if($target.is("input") && !$target.closest("li").is(".multiselect-item")){
556 this.lastToggledInput = $target;
557 }
558
559 $target.blur();
560 }, this));
561
562 // Keyboard support.
563 this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) {
564 if ($('input[type="text"]', this.$container).is(':focus')) {
565 return;
566 }
567
568 if (event.keyCode === 9 && this.$container.hasClass('open')) {
569 this.$button.click();
570 }
571 else {
572 var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible");
573
574 if (!$items.length) {
575 return;
576 }
577
578 var index = $items.index($items.filter(':focus'));
579
580 // Navigation up.
581 if (event.keyCode === 38 && index > 0) {
582 index--;
583 }
584 // Navigate down.
585 else if (event.keyCode === 40 && index < $items.length - 1) {
586 index++;
587 }
588 else if (!~index) {
589 index = 0;
590 }
591
592 var $current = $items.eq(index);
593 $current.focus();
594
595 if (event.keyCode === 32 || event.keyCode === 13) {
596 var $checkbox = $current.find('input');
597
598 $checkbox.prop("checked", !$checkbox.prop("checked"));
599 $checkbox.change();
600 }
601
602 event.stopPropagation();
603 event.preventDefault();
604 }
605 }, this));
606
607 if(this.options.enableClickableOptGroups && this.options.multiple) {
608 $('li.multiselect-group', this.$ul).on('click', $.proxy(function(event) {
609 event.stopPropagation();
610
611 var group = $(event.target).parent();
612
613 // Search all option in optgroup
614 var $options = group.nextUntil('li.multiselect-group');
615 var $visibleOptions = $options.filter(":visible:not(.disabled)");
616
617 // check or uncheck items
618 var allChecked = true;
619 var optionInputs = $visibleOptions.find('input');
620 optionInputs.each(function() {
621 allChecked = allChecked && $(this).prop('checked');
622 });
623
624 optionInputs.prop('checked', !allChecked).trigger('change');
625 }, this));
626 }
627 },
628
629 /**
630 * Create an option using the given select option.
631 *
632 * @param {jQuery} element
633 */
634 createOptionValue: function(element) {
635 var $element = $(element);
636 if ($element.is(':selected')) {
637 $element.prop('selected', true);
638 }
639
640 // Support the label attribute on options.
641 var label = this.options.optionLabel(element);
642 var value = $element.val();
643 var inputType = this.options.multiple ? "checkbox" : "radio";
644
645 var $li = $(this.options.templates.li);
646 var $label = $('label', $li);
647 $label.addClass(inputType);
648
649 if (this.options.enableHTML) {
650 $label.html(" " + label);
651 }
652 else {
653 $label.text(" " + label);
654 }
655
656 var $checkbox = $('<input/>').attr('type', inputType);
657
658 if (this.options.checkboxName) {
659 $checkbox.attr('name', this.options.checkboxName);
660 }
661 $label.prepend($checkbox);
662
663 var selected = $element.prop('selected') || false;
664 $checkbox.val(value);
665
666 if (value === this.options.selectAllValue) {
667 $li.addClass("multiselect-item multiselect-all");
668 $checkbox.parent().parent()
669 .addClass('multiselect-all');
670 }
671
672 $label.attr('title', $element.attr('title'));
673
674 this.$ul.append($li);
675
676 if ($element.is(':disabled')) {
677 $checkbox.attr('disabled', 'disabled')
678 .prop('disabled', true)
679 .closest('a')
680 .attr("tabindex", "-1")
681 .closest('li')
682 .addClass('disabled');
683 }
684
685 $checkbox.prop('checked', selected);
686
687 if (selected && this.options.selectedClass) {
688 $checkbox.closest('li')
689 .addClass(this.options.selectedClass);
690 }
691 },
692
693 /**
694 * Creates a divider using the given select option.
695 *
696 * @param {jQuery} element
697 */
698 createDivider: function(element) {
699 var $divider = $(this.options.templates.divider);
700 this.$ul.append($divider);
701 },
702
703 /**
704 * Creates an optgroup.
705 *
706 * @param {jQuery} group
707 */
708 createOptgroup: function(group) {
709 var groupName = $(group).prop('label');
710
711 // Add a header for the group.
712 var $li = $(this.options.templates.liGroup);
713
714 if (this.options.enableHTML) {
715 $('label', $li).html(groupName);
716 }
717 else {
718 $('label', $li).text(groupName);
719 }
720
721 if (this.options.enableClickableOptGroups) {
722 $li.addClass('multiselect-group-clickable');
723 }
724
725 this.$ul.append($li);
726
727 if ($(group).is(':disabled')) {
728 $li.addClass('disabled');
729 }
730
731 // Add the options of the group.
732 $('option', group).each($.proxy(function(index, element) {
733 this.createOptionValue(element);
734 }, this));
735 },
736
737 /**
738 * Build the selct all.
739 *
740 * Checks if a select all has already been created.
741 */
742 buildSelectAll: function() {
743 if (typeof this.options.selectAllValue === 'number') {
744 this.options.selectAllValue = this.options.selectAllValue.toString();
745 }
746
747 var alreadyHasSelectAll = this.hasSelectAll();
748
749 if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
750 && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) {
751
752 // Check whether to add a divider after the select all.
753 if (this.options.includeSelectAllDivider) {
754 this.$ul.prepend($(this.options.templates.divider));
755 }
756
757 var $li = $(this.options.templates.li);
758 $('label', $li).addClass("checkbox");
759
760 if (this.options.enableHTML) {
761 $('label', $li).html(" " + this.options.selectAllText);
762 }
763 else {
764 $('label', $li).text(" " + this.options.selectAllText);
765 }
766
767 if (this.options.selectAllName) {
768 $('label', $li).prepend('<input type="checkbox" name="' + this.options.selectAllName + '" />');
769 }
770 else {
771 $('label', $li).prepend('<input type="checkbox" />');
772 }
773
774 var $checkbox = $('input', $li);
775 $checkbox.val(this.options.selectAllValue);
776
777 $li.addClass("multiselect-item multiselect-all");
778 $checkbox.parent().parent()
779 .addClass('multiselect-all');
780
781 this.$ul.prepend($li);
782
783 $checkbox.prop('checked', false);
784 }
785 },
786
787 /**
788 * Builds the filter.
789 */
790 buildFilter: function() {
791
792 // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
793 if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
794 var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
795
796 if (this.$select.find('option').length >= enableFilterLength) {
797
798 this.$filter = $(this.options.templates.filter);
799 $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
800
801 // Adds optional filter clear button
802 if(this.options.includeFilterClearBtn){
803 var clearBtn = $(this.options.templates.filterClearBtn);
804 clearBtn.on('click', $.proxy(function(event){
805 clearTimeout(this.searchTimeout);
806 this.$filter.find('.multiselect-search').val('');
807 $('li', this.$ul).show().removeClass("filter-hidden");
808 this.updateSelectAll();
809 }, this));
810 this.$filter.find('.input-group').append(clearBtn);
811 }
812
813 this.$ul.prepend(this.$filter);
814
815 this.$filter.val(this.query).on('click', function(event) {
816 event.stopPropagation();
817 }).on('input keydown', $.proxy(function(event) {
818 // Cancel enter key default behaviour
819 if (event.which === 13) {
820 event.preventDefault();
821 }
822
823 // This is useful to catch "keydown" events after the browser has updated the control.
824 clearTimeout(this.searchTimeout);
825
826 this.searchTimeout = this.asyncFunction($.proxy(function() {
827
828 if (this.query !== event.target.value) {
829 this.query = event.target.value;
830
831 var currentGroup, currentGroupVisible;
832 $.each($('li', this.$ul), $.proxy(function(index, element) {
833 var value = $('input', element).length > 0 ? $('input', element).val() : "";
834 var text = $('label', element).text();
835
836 var filterCandidate = '';
837 if ((this.options.filterBehavior === 'text')) {
838 filterCandidate = text;
839 }
840 else if ((this.options.filterBehavior === 'value')) {
841 filterCandidate = value;
842 }
843 else if (this.options.filterBehavior === 'both') {
844 filterCandidate = text + '\n' + value;
845 }
846
847 if (value !== this.options.selectAllValue && text) {
848 // By default lets assume that element is not
849 // interesting for this search.
850 var showElement = false;
851
852 if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
853 showElement = true;
854 }
855 else if (filterCandidate.indexOf(this.query) > -1) {
856 showElement = true;
857 }
858
859 // Toggle current element (group or group item) according to showElement boolean.
860 $(element).toggle(showElement).toggleClass('filter-hidden', !showElement);
861
862 // Differentiate groups and group items.
863 if ($(element).hasClass('multiselect-group')) {
864 // Remember group status.
865 currentGroup = element;
866 currentGroupVisible = showElement;
867 }
868 else {
869 // Show group name when at least one of its items is visible.
870 if (showElement) {
871 $(currentGroup).show().removeClass('filter-hidden');
872 }
873
874 // Show all group items when group name satisfies filter.
875 if (!showElement && currentGroupVisible) {
876 $(element).show().removeClass('filter-hidden');
877 }
878 }
879 }
880 }, this));
881 }
882
883 this.updateSelectAll();
884 }, this), 300, this);
885 }, this));
886 }
887 }
888 },
889
890 /**
891 * Unbinds the whole plugin.
892 */
893 destroy: function() {
894 this.$container.remove();
895 this.$select.show();
896 this.$select.data('multiselect', null);
897 },
898
899 /**
900 * Refreshs the multiselect based on the selected options of the select.
901 */
902 refresh: function() {
903 $('option', this.$select).each($.proxy(function(index, element) {
904 var $input = $('li input', this.$ul).filter(function() {
905 return $(this).val() === $(element).val();
906 });
907
908 if ($(element).is(':selected')) {
909 $input.prop('checked', true);
910
911 if (this.options.selectedClass) {
912 $input.closest('li')
913 .addClass(this.options.selectedClass);
914 }
915 }
916 else {
917 $input.prop('checked', false);
918
919 if (this.options.selectedClass) {
920 $input.closest('li')
921 .removeClass(this.options.selectedClass);
922 }
923 }
924
925 if ($(element).is(":disabled")) {
926 $input.attr('disabled', 'disabled')
927 .prop('disabled', true)
928 .closest('li')
929 .addClass('disabled');
930 }
931 else {
932 $input.prop('disabled', false)
933 .closest('li')
934 .removeClass('disabled');
935 }
936 }, this));
937
938 this.updateButtonText();
939 this.updateSelectAll();
940 },
941
942 /**
943 * Select all options of the given values.
944 *
945 * If triggerOnChange is set to true, the on change event is triggered if
946 * and only if one value is passed.
947 *
948 * @param {Array} selectValues
949 * @param {Boolean} triggerOnChange
950 */
951 select: function(selectValues, triggerOnChange) {
952 if(!$.isArray(selectValues)) {
953 selectValues = [selectValues];
954 }
955
956 for (var i = 0; i < selectValues.length; i++) {
957 var value = selectValues[i];
958
959 if (value === null || value === undefined) {
960 continue;
961 }
962
963 var $option = this.getOptionByValue(value);
964 var $checkbox = this.getInputByValue(value);
965
966 if($option === undefined || $checkbox === undefined) {
967 continue;
968 }
969
970 if (!this.options.multiple) {
971 this.deselectAll(false);
972 }
973
974 if (this.options.selectedClass) {
975 $checkbox.closest('li')
976 .addClass(this.options.selectedClass);
977 }
978
979 $checkbox.prop('checked', true);
980 $option.prop('selected', true);
981
982 if (triggerOnChange) {
983 this.options.onChange($option, true);
984 }
985 }
986
987 this.updateButtonText();
988 this.updateSelectAll();
989 },
990
991 /**
992 * Clears all selected items.
993 */
994 clearSelection: function () {
995 this.deselectAll(false);
996 this.updateButtonText();
997 this.updateSelectAll();
998 },
999
1000 /**
1001 * Deselects all options of the given values.
1002 *
1003 * If triggerOnChange is set to true, the on change event is triggered, if
1004 * and only if one value is passed.
1005 *
1006 * @param {Array} deselectValues
1007 * @param {Boolean} triggerOnChange
1008 */
1009 deselect: function(deselectValues, triggerOnChange) {
1010 if(!$.isArray(deselectValues)) {
1011 deselectValues = [deselectValues];
1012 }
1013
1014 for (var i = 0; i < deselectValues.length; i++) {
1015 var value = deselectValues[i];
1016
1017 if (value === null || value === undefined) {
1018 continue;
1019 }
1020
1021 var $option = this.getOptionByValue(value);
1022 var $checkbox = this.getInputByValue(value);
1023
1024 if($option === undefined || $checkbox === undefined) {
1025 continue;
1026 }
1027
1028 if (this.options.selectedClass) {
1029 $checkbox.closest('li')
1030 .removeClass(this.options.selectedClass);
1031 }
1032
1033 $checkbox.prop('checked', false);
1034 $option.prop('selected', false);
1035
1036 if (triggerOnChange) {
1037 this.options.onChange($option, false);
1038 }
1039 }
1040
1041 this.updateButtonText();
1042 this.updateSelectAll();
1043 },
1044
1045 /**
1046 * Selects all enabled & visible options.
1047 *
1048 * If justVisible is true or not specified, only visible options are selected.
1049 *
1050 * @param {Boolean} justVisible
1051 * @param {Boolean} triggerOnSelectAll
1052 */
1053 selectAll: function (justVisible, triggerOnSelectAll) {
1054 var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1055 var allCheckboxes = $("li input[type='checkbox']:enabled", this.$ul);
1056 var visibleCheckboxes = allCheckboxes.filter(":visible");
1057 var allCheckboxesCount = allCheckboxes.length;
1058 var visibleCheckboxesCount = visibleCheckboxes.length;
1059
1060 if(justVisible) {
1061 visibleCheckboxes.prop('checked', true);
1062 $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").addClass(this.options.selectedClass);
1063 }
1064 else {
1065 allCheckboxes.prop('checked', true);
1066 $("li:not(.divider):not(.disabled)", this.$ul).addClass(this.options.selectedClass);
1067 }
1068
1069 if (allCheckboxesCount === visibleCheckboxesCount || justVisible === false) {
1070 $("option:enabled", this.$select).prop('selected', true);
1071 }
1072 else {
1073 var values = visibleCheckboxes.map(function() {
1074 return $(this).val();
1075 }).get();
1076
1077 $("option:enabled", this.$select).filter(function(index) {
1078 return $.inArray($(this).val(), values) !== -1;
1079 }).prop('selected', true);
1080 }
1081
1082 if (triggerOnSelectAll) {
1083 this.options.onSelectAll();
1084 }
1085 },
1086
1087 /**
1088 * Deselects all options.
1089 *
1090 * If justVisible is true or not specified, only visible options are deselected.
1091 *
1092 * @param {Boolean} justVisible
1093 */
1094 deselectAll: function (justVisible) {
1095 var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1096
1097 if(justVisible) {
1098 var visibleCheckboxes = $("li input[type='checkbox']:not(:disabled)", this.$ul).filter(":visible");
1099 visibleCheckboxes.prop('checked', false);
1100
1101 var values = visibleCheckboxes.map(function() {
1102 return $(this).val();
1103 }).get();
1104
1105 $("option:enabled", this.$select).filter(function(index) {
1106 return $.inArray($(this).val(), values) !== -1;
1107 }).prop('selected', false);
1108
1109 if (this.options.selectedClass) {
1110 $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").removeClass(this.options.selectedClass);
1111 }
1112 }
1113 else {
1114 $("li input[type='checkbox']:enabled", this.$ul).prop('checked', false);
1115 $("option:enabled", this.$select).prop('selected', false);
1116
1117 if (this.options.selectedClass) {
1118 $("li:not(.divider):not(.disabled)", this.$ul).removeClass(this.options.selectedClass);
1119 }
1120 }
1121 },
1122
1123 /**
1124 * Rebuild the plugin.
1125 *
1126 * Rebuilds the dropdown, the filter and the select all option.
1127 */
1128 rebuild: function() {
1129 this.$ul.html('');
1130
1131 // Important to distinguish between radios and checkboxes.
1132 this.options.multiple = this.$select.attr('multiple') === "multiple";
1133
1134 this.buildSelectAll();
1135 this.buildDropdownOptions();
1136 this.buildFilter();
1137
1138 this.updateButtonText();
1139 this.updateSelectAll();
1140
1141 if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
1142 this.disable();
1143 }
1144 else {
1145 this.enable();
1146 }
1147
1148 if (this.options.dropRight) {
1149 this.$ul.addClass('pull-right');
1150 }
1151 },
1152
1153 /**
1154 * The provided data will be used to build the dropdown.
1155 */
1156 dataprovider: function(dataprovider) {
1157
1158 var groupCounter = 0;
1159 var $select = this.$select.empty();
1160
1161 $.each(dataprovider, function (index, option) {
1162 var $tag;
1163
1164 if ($.isArray(option.children)) { // create optiongroup tag
1165 groupCounter++;
1166
1167 $tag = $('<optgroup/>').attr({
1168 label: option.label || 'Group ' + groupCounter,
1169 disabled: !!option.disabled
1170 });
1171
1172 forEach(option.children, function(subOption) { // add children option tags
1173 $tag.append($('<option/>').attr({
1174 value: subOption.value,
1175 label: subOption.label || subOption.value,
1176 title: subOption.title,
1177 selected: !!subOption.selected,
1178 disabled: !!subOption.disabled
1179 }));
1180 });
1181 }
1182 else {
1183 $tag = $('<option/>').attr({
1184 value: option.value,
1185 label: option.label || option.value,
1186 title: option.title,
1187 selected: !!option.selected,
1188 disabled: !!option.disabled
1189 });
1190 }
1191
1192 $select.append($tag);
1193 });
1194
1195 this.rebuild();
1196 },
1197
1198 /**
1199 * Enable the multiselect.
1200 */
1201 enable: function() {
1202 this.$select.prop('disabled', false);
1203 this.$button.prop('disabled', false)
1204 .removeClass('disabled');
1205 },
1206
1207 /**
1208 * Disable the multiselect.
1209 */
1210 disable: function() {
1211 this.$select.prop('disabled', true);
1212 this.$button.prop('disabled', true)
1213 .addClass('disabled');
1214 },
1215
1216 /**
1217 * Set the options.
1218 *
1219 * @param {Array} options
1220 */
1221 setOptions: function(options) {
1222 this.options = this.mergeOptions(options);
1223 },
1224
1225 /**
1226 * Merges the given options with the default options.
1227 *
1228 * @param {Array} options
1229 * @returns {Array}
1230 */
1231 mergeOptions: function(options) {
1232 return $.extend(true, {}, this.defaults, this.options, options);
1233 },
1234
1235 /**
1236 * Checks whether a select all checkbox is present.
1237 *
1238 * @returns {Boolean}
1239 */
1240 hasSelectAll: function() {
1241 return $('li.multiselect-all', this.$ul).length > 0;
1242 },
1243
1244 /**
1245 * Updates the select all checkbox based on the currently displayed and selected checkboxes.
1246 */
1247 updateSelectAll: function() {
1248 if (this.hasSelectAll()) {
1249 var allBoxes = $("li:not(.multiselect-item):not(.filter-hidden) input:enabled", this.$ul);
1250 var allBoxesLength = allBoxes.length;
1251 var checkedBoxesLength = allBoxes.filter(":checked").length;
1252 var selectAllLi = $("li.multiselect-all", this.$ul);
1253 var selectAllInput = selectAllLi.find("input");
1254
1255 if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) {
1256 selectAllInput.prop("checked", true);
1257 selectAllLi.addClass(this.options.selectedClass);
1258 this.options.onSelectAll();
1259 }
1260 else {
1261 selectAllInput.prop("checked", false);
1262 selectAllLi.removeClass(this.options.selectedClass);
1263 }
1264 }
1265 },
1266
1267 /**
1268 * Update the button text and its title based on the currently selected options.
1269 */
1270 updateButtonText: function() {
1271 var options = this.getSelected();
1272
1273 // First update the displayed button text.
1274 if (this.options.enableHTML) {
1275 $('.multiselect .multiselect-selected-text', this.$container).html(this.options.buttonText(options, this.$select));
1276 }
1277 else {
1278 $('.multiselect .multiselect-selected-text', this.$container).text(this.options.buttonText(options, this.$select));
1279 }
1280
1281 // Now update the title attribute of the button.
1282 $('.multiselect', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
1283 },
1284
1285 /**
1286 * Get all selected options.
1287 *
1288 * @returns {jQUery}
1289 */
1290 getSelected: function() {
1291 return $('option', this.$select).filter(":selected");
1292 },
1293
1294 /**
1295 * Gets a select option by its value.
1296 *
1297 * @param {String} value
1298 * @returns {jQuery}
1299 */
1300 getOptionByValue: function (value) {
1301
1302 var options = $('option', this.$select);
1303 var valueToCompare = value.toString();
1304
1305 for (var i = 0; i < options.length; i = i + 1) {
1306 var option = options[i];
1307 if (option.value === valueToCompare) {
1308 return $(option);
1309 }
1310 }
1311 },
1312
1313 /**
1314 * Get the input (radio/checkbox) by its value.
1315 *
1316 * @param {String} value
1317 * @returns {jQuery}
1318 */
1319 getInputByValue: function (value) {
1320
1321 var checkboxes = $('li input', this.$ul);
1322 var valueToCompare = value.toString();
1323
1324 for (var i = 0; i < checkboxes.length; i = i + 1) {
1325 var checkbox = checkboxes[i];
1326 if (checkbox.value === valueToCompare) {
1327 return $(checkbox);
1328 }
1329 }
1330 },
1331
1332 /**
1333 * Used for knockout integration.
1334 */
1335 updateOriginalOptions: function() {
1336 this.originalOptions = this.$select.clone()[0].options;
1337 },
1338
1339 asyncFunction: function(callback, timeout, self) {
1340 var args = Array.prototype.slice.call(arguments, 3);
1341 return setTimeout(function() {
1342 callback.apply(self || window, args);
1343 }, timeout);
1344 },
1345
1346 setAllSelectedText: function(allSelectedText) {
1347 this.options.allSelectedText = allSelectedText;
1348 this.updateButtonText();
1349 }
1350 };
1351
1352 $.fn.multiselect = function(option, parameter, extraOptions) {
1353 return this.each(function() {
1354 var data = $(this).data('multiselect');
1355 var options = typeof option === 'object' && option;
1356
1357 // Initialize the multiselect.
1358 if (!data) {
1359 data = new Multiselect(this, options);
1360 $(this).data('multiselect', data);
1361 }
1362
1363 // Call multiselect method.
1364 if (typeof option === 'string') {
1365 data[option](parameter, extraOptions);
1366
1367 if (option === 'destroy') {
1368 $(this).data('multiselect', false);
1369 }
1370 }
1371 });
1372 };
1373
1374 $.fn.multiselect.Constructor = Multiselect;
1375
1376 $(function() {
1377 $("select[data-role=multiselect]").multiselect();
1378 });
1379
1380 }(window.jQuery);
1381