Formidable Forms – Form Builder for WordPress - Version 5.1

Version Description

  • Updated Bootstrap Multiselect to version 1.1.1, fixing issues with the accessibility of backend multiselect dropdowns for blind users.
  • New: Inputs with errors will now add the aria-describedby attribute during JavaScript validation for more accessible errors.
  • New: Form errors will now always include the role="alert" attribute for more accessible errors. New fields will now also include role="alert" in custom field HTML.
  • New: Added a new frm_entries_column_value filter hook.
Download this release

Release Info

Developer formidableforms
Plugin Icon 128x128 Formidable Forms – Form Builder for WordPress
Version 5.1
Comparing to
See all releases

Code changes from version 5.0.17 to 5.1

classes/controllers/FrmAppController.php CHANGED
@@ -460,7 +460,7 @@ class FrmAppController {
460
 
461
  // load multselect js
462
  $depends_on = array( 'jquery', 'bootstrap_tooltip' );
463
- wp_register_script( 'bootstrap-multiselect', FrmAppHelper::plugin_url() . '/js/bootstrap-multiselect.js', $depends_on, '0.9.8', true );
464
 
465
  $page = FrmAppHelper::simple_get( 'page', 'sanitize_title' );
466
  $post_type = FrmAppHelper::simple_get( 'post_type', 'sanitize_title' );
460
 
461
  // load multselect js
462
  $depends_on = array( 'jquery', 'bootstrap_tooltip' );
463
+ wp_register_script( 'bootstrap-multiselect', FrmAppHelper::plugin_url() . '/js/bootstrap-multiselect.js', $depends_on, '1.1.1', true );
464
 
465
  $page = FrmAppHelper::simple_get( 'page', 'sanitize_title' );
466
  $post_type = FrmAppHelper::simple_get( 'post_type', 'sanitize_title' );
classes/controllers/FrmFieldsController.php CHANGED
@@ -743,7 +743,9 @@ class FrmFieldsController {
743
  $error_body = substr( $custom_html, $start + 10, $end - $start - 10 );
744
  $default_html = array(
745
  '<div class="frm_error" id="frm_error_field_[key]">[error]</div>',
 
746
  '<div class="frm_error">[error]</div>',
 
747
  );
748
 
749
  if ( in_array( $error_body, $default_html, true ) ) {
743
  $error_body = substr( $custom_html, $start + 10, $end - $start - 10 );
744
  $default_html = array(
745
  '<div class="frm_error" id="frm_error_field_[key]">[error]</div>',
746
+ '<div class="frm_error" role="alert" id="frm_error_field_[key]">[error]</div>',
747
  '<div class="frm_error">[error]</div>',
748
+ '<div class="frm_error" role="alert">[error]</div>',
749
  );
750
 
751
  if ( in_array( $error_body, $default_html, true ) ) {
classes/controllers/FrmFormsController.php CHANGED
@@ -685,7 +685,7 @@ class FrmFormsController {
685
  /**
686
  * Check the page being loaded, determine if this is a page that should include the form popup.
687
  *
688
- * @since x.x
689
  *
690
  * @return bool
691
  */
685
  /**
686
  * Check the page being loaded, determine if this is a page that should include the form popup.
687
  *
688
+ * @since 5.0.14
689
  *
690
  * @return bool
691
  */
classes/helpers/FrmAppHelper.php CHANGED
@@ -11,7 +11,7 @@ class FrmAppHelper {
11
  /**
12
  * @since 2.0
13
  */
14
- public static $plug_version = '5.0.17';
15
 
16
  /**
17
  * @since 1.07.02
@@ -2934,9 +2934,11 @@ class FrmAppHelper {
2934
 
2935
  /**
2936
  * Output HTML containing reference text for accessibility
 
 
2937
  */
2938
  public static function multiselect_accessibility() {
2939
- include_once self::plugin_path() . '/classes/views/frm-forms/multiselect-accessibility.php';
2940
  }
2941
 
2942
  public static function get_menu_icon_class() {
11
  /**
12
  * @since 2.0
13
  */
14
+ public static $plug_version = '5.1';
15
 
16
  /**
17
  * @since 1.07.02
2934
 
2935
  /**
2936
  * Output HTML containing reference text for accessibility
2937
+ *
2938
+ * @deprecated 5.1
2939
  */
2940
  public static function multiselect_accessibility() {
2941
+ _deprecated_function( __METHOD__, '5.1' );
2942
  }
2943
 
2944
  public static function get_menu_icon_class() {
classes/helpers/FrmEntriesListHelper.php CHANGED
@@ -291,6 +291,16 @@ class FrmEntriesListHelper extends FrmListHelper {
291
  if ( $val === false ) {
292
  $this->get_column_value( $item, $val );
293
  }
 
 
 
 
 
 
 
 
 
 
294
  }
295
 
296
  return $val;
291
  if ( $val === false ) {
292
  $this->get_column_value( $item, $val );
293
  }
294
+
295
+ /**
296
+ * Allows changing entries list column value.
297
+ *
298
+ * @since 5.1
299
+ *
300
+ * @param mixed $val Column value.
301
+ * @param array $args Contains `item` and `col_name`.
302
+ */
303
+ $val = apply_filters( 'frm_entries_column_value', $val, compact( 'item', 'col_name' ) );
304
  }
305
 
306
  return $val;
classes/models/FrmAntiSpam.php CHANGED
@@ -8,7 +8,7 @@ if ( ! defined( 'ABSPATH' ) ) {
8
  *
9
  * This token class generates tokens that are used in our Anti-Spam checking.
10
  *
11
- * @since xx.xx
12
  */
13
  class FrmAntiSpam extends FrmValidate {
14
 
@@ -32,7 +32,7 @@ class FrmAntiSpam extends FrmValidate {
32
  /**
33
  * Initialise the actions for the Anti-spam.
34
  *
35
- * @since xx.xx
36
  */
37
  public function init() {
38
  add_filter( 'frm_form_attributes', array( $this, 'add_token_to_form' ), 10, 1 );
@@ -42,7 +42,7 @@ class FrmAntiSpam extends FrmValidate {
42
  /**
43
  * Return a valid token.
44
  *
45
- * @since xx.xx
46
  *
47
  * @param mixed $current True to use current time, otherwise a timestamp string.
48
  *
@@ -91,7 +91,7 @@ class FrmAntiSpam extends FrmValidate {
91
  * 'frm_form_token_check_before_today'
92
  * 'frm_form_token_check_after_today'
93
  *
94
- * @since xx.xx
95
  *
96
  * @return array Array of all valid tokens to check against.
97
  */
@@ -143,7 +143,7 @@ class FrmAntiSpam extends FrmValidate {
143
  * and frm_token_validity_in_days to extend the validation period).
144
  * By default tokens are valid for day.
145
  *
146
- * @since xx.xx
147
  *
148
  * @param string $token Token to validate.
149
  *
@@ -157,7 +157,7 @@ class FrmAntiSpam extends FrmValidate {
157
  /**
158
  * Add the token field to the form.
159
  *
160
- * @since xx.xx
161
  *
162
  * @param string $attributes
163
  *
@@ -188,7 +188,7 @@ class FrmAntiSpam extends FrmValidate {
188
  /**
189
  * Validate Anti-spam if enabled.
190
  *
191
- * @since xx.xx
192
  *
193
  * @return bool|string True or a string with the error.
194
  */
@@ -235,7 +235,7 @@ class FrmAntiSpam extends FrmValidate {
235
  /**
236
  * Helper to run our filter on all the responses for the antispam checks.
237
  *
238
- * @since xx.xx
239
  *
240
  * @param bool|string $is_valid Is valid entry or not.
241
  *
@@ -248,7 +248,7 @@ class FrmAntiSpam extends FrmValidate {
248
  /**
249
  * Helper to get the missing token message.
250
  *
251
- * @since xx.xx
252
  *
253
  * @return string missing token message.
254
  */
@@ -259,7 +259,7 @@ class FrmAntiSpam extends FrmValidate {
259
  /**
260
  * Helper to get the invalid token message.
261
  *
262
- * @since xx.xx
263
  *
264
  * @return string Invalid token message.
265
  */
@@ -270,7 +270,7 @@ class FrmAntiSpam extends FrmValidate {
270
  /**
271
  * If a user is a super admin, add a support link to the message.
272
  *
273
- * @since xx.xx
274
  *
275
  * @return string Support text if super admin, empty string if not.
276
  */
8
  *
9
  * This token class generates tokens that are used in our Anti-Spam checking.
10
  *
11
+ * @since 4.11
12
  */
13
  class FrmAntiSpam extends FrmValidate {
14
 
32
  /**
33
  * Initialise the actions for the Anti-spam.
34
  *
35
+ * @since 4.11
36
  */
37
  public function init() {
38
  add_filter( 'frm_form_attributes', array( $this, 'add_token_to_form' ), 10, 1 );
42
  /**
43
  * Return a valid token.
44
  *
45
+ * @since 4.11
46
  *
47
  * @param mixed $current True to use current time, otherwise a timestamp string.
48
  *
91
  * 'frm_form_token_check_before_today'
92
  * 'frm_form_token_check_after_today'
93
  *
94
+ * @since 4.11
95
  *
96
  * @return array Array of all valid tokens to check against.
97
  */
143
  * and frm_token_validity_in_days to extend the validation period).
144
  * By default tokens are valid for day.
145
  *
146
+ * @since 4.11
147
  *
148
  * @param string $token Token to validate.
149
  *
157
  /**
158
  * Add the token field to the form.
159
  *
160
+ * @since 4.11
161
  *
162
  * @param string $attributes
163
  *
188
  /**
189
  * Validate Anti-spam if enabled.
190
  *
191
+ * @since 4.11
192
  *
193
  * @return bool|string True or a string with the error.
194
  */
235
  /**
236
  * Helper to run our filter on all the responses for the antispam checks.
237
  *
238
+ * @since 4.11
239
  *
240
  * @param bool|string $is_valid Is valid entry or not.
241
  *
248
  /**
249
  * Helper to get the missing token message.
250
  *
251
+ * @since 4.11
252
  *
253
  * @return string missing token message.
254
  */
259
  /**
260
  * Helper to get the invalid token message.
261
  *
262
+ * @since 4.11
263
  *
264
  * @return string Invalid token message.
265
  */
270
  /**
271
  * If a user is a super admin, add a support link to the message.
272
  *
273
+ * @since 4.11
274
  *
275
  * @return string Support text if super admin, empty string if not.
276
  */
classes/models/FrmFieldFormHtml.php CHANGED
@@ -230,9 +230,39 @@ class FrmFieldFormHtml {
230
  private function replace_error_shortcode() {
231
  $this->maybe_add_error_id();
232
  $error = isset( $this->pass_args['errors'][ 'field' . $this->field_id ] ) ? $this->pass_args['errors'][ 'field' . $this->field_id ] : false;
 
 
 
 
 
 
 
 
 
233
  FrmShortcodeHelper::remove_inline_conditions( ! empty( $error ), 'error', $error, $this->html );
234
  }
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  /**
237
  * Add an ID to the error message for aria-describedby.
238
  * This ID was added to the HTML in v3.06.02.
230
  private function replace_error_shortcode() {
231
  $this->maybe_add_error_id();
232
  $error = isset( $this->pass_args['errors'][ 'field' . $this->field_id ] ) ? $this->pass_args['errors'][ 'field' . $this->field_id ] : false;
233
+
234
+ if ( ! empty( $error ) && false === strpos( $this->html, 'role="alert"' ) ) {
235
+ $error_body = self::get_error_body( $this->html );
236
+ if ( is_string( $error_body ) && false === strpos( $error_body, 'role=' ) ) {
237
+ $new_error_body = preg_replace( '/class="frm_error/', 'role="alert" class="frm_error', $error_body, 1 );
238
+ $this->html = str_replace( '[if error]' . $error_body . '[/if error]', '[if error]' . $new_error_body . '[/if error]', $this->html );
239
+ }
240
+ }
241
+
242
  FrmShortcodeHelper::remove_inline_conditions( ! empty( $error ), 'error', $error, $this->html );
243
  }
244
 
245
+ /**
246
+ * Pull the HTML between [if error] and [/if error] shortcodes.
247
+ *
248
+ * @param string $html
249
+ * @return string|false
250
+ */
251
+ private static function get_error_body( $html ) {
252
+ $start = strpos( $html, '[if error]' );
253
+ if ( false === $start ) {
254
+ return false;
255
+ }
256
+
257
+ $end = strpos( $html, '[/if error]', $start );
258
+ if ( false === $end ) {
259
+ return false;
260
+ }
261
+
262
+ $error_body = substr( $html, $start + 10, $end - $start - 10 );
263
+ return $error_body;
264
+ }
265
+
266
  /**
267
  * Add an ID to the error message for aria-describedby.
268
  * This ID was added to the HTML in v3.06.02.
classes/models/fields/FrmFieldType.php CHANGED
@@ -177,7 +177,7 @@ abstract class FrmFieldType {
177
  </$label>
178
  $input
179
  [if description]<div class="frm_description" id="frm_desc_field_[key]">[description]</div>[/if description]
180
- [if error]<div class="frm_error" id="frm_error_field_[key]">[error]</div>[/if error]
181
  </div>
182
  DEFAULT_HTML;
183
 
177
  </$label>
178
  $input
179
  [if description]<div class="frm_description" id="frm_desc_field_[key]">[description]</div>[/if description]
180
+ [if error]<div class="frm_error" role="alert" id="frm_error_field_[key]">[error]</div>[/if error]
181
  </div>
182
  DEFAULT_HTML;
183
 
classes/views/frm-fields/front-end/combo-field/combo-field.php CHANGED
@@ -80,7 +80,7 @@ $inputs_attrs = $this->get_inputs_container_attrs();
80
  // Don't show individual field errors when there is a combo field error.
81
  if ( ! empty( $errors ) && isset( $errors[ 'field' . $field_id . '-' . $name ] ) && ! isset( $errors[ 'field' . $field_id ] ) ) {
82
  ?>
83
- <div class="frm_error"><?php echo esc_html( $errors[ 'field' . $field_id . '-' . $name ] ); ?></div>
84
  <?php } ?>
85
  </div>
86
  <?php } ?>
80
  // Don't show individual field errors when there is a combo field error.
81
  if ( ! empty( $errors ) && isset( $errors[ 'field' . $field_id . '-' . $name ] ) && ! isset( $errors[ 'field' . $field_id ] ) ) {
82
  ?>
83
+ <div class="frm_error" role="alert"><?php echo esc_html( $errors[ 'field' . $field_id . '-' . $name ] ); ?></div>
84
  <?php } ?>
85
  </div>
86
  <?php } ?>
classes/views/frm-forms/form.php CHANGED
@@ -64,4 +64,3 @@ if ( ! defined( 'ABSPATH' ) ) {
64
  </div>
65
  <?php
66
  FrmFieldsHelper::bulk_options_overlay();
67
- FrmAppHelper::multiselect_accessibility();
64
  </div>
65
  <?php
66
  FrmFieldsHelper::bulk_options_overlay();
 
classes/views/frm-forms/multiselect-accessibility.php CHANGED
@@ -2,9 +2,4 @@
2
  if ( ! defined( 'ABSPATH' ) ) {
3
  die( 'You are not allowed to call this page directly.' );
4
  }
5
- ?>
6
- <div class="hidden">
7
- <div id="frm_press_space_checked"><?php esc_html_e( 'Checked. To uncheck this option, press Space or Enter', 'formidable' ); ?></div>
8
- <div id="frm_press_space_unchecked"><?php esc_html_e( 'Unchecked. To check this option, press Space or Enter', 'formidable' ); ?></div>
9
- <div id="frm_multiselect_button"><?php esc_html_e( 'You are on a Custom List of Checkboxes. To open, press Enter. Use Up and Down arrow keys to switch between options', 'formidable' ); ?></div>
10
- </div>
2
  if ( ! defined( 'ABSPATH' ) ) {
3
  die( 'You are not allowed to call this page directly.' );
4
  }
5
+ _deprecated_file( esc_html( basename( __FILE__ ) ), 'x.x' );
 
 
 
 
 
classes/views/frm-forms/settings.php CHANGED
@@ -64,6 +64,3 @@ if ( ! defined( 'ABSPATH' ) ) {
64
  </div>
65
  </form>
66
  </div>
67
-
68
- <?php
69
- FrmAppHelper::multiselect_accessibility();
64
  </div>
65
  </form>
66
  </div>
 
 
 
classes/views/frm-settings/permissions.php CHANGED
@@ -14,12 +14,9 @@ if ( ! defined( 'ABSPATH' ) ) {
14
  ?>
15
  <tr>
16
  <td class="frm_left_label">
17
- <label id="for_<?php echo esc_attr( str_replace( '[]', '', $role_field_name ) ); ?>"><?php echo esc_html( $frm_role_description ); ?></label>
18
  </td>
19
  <td><?php FrmAppHelper::wp_roles_dropdown( $role_field_name, $frm_settings->$frm_role, 'multiple' ); ?></td>
20
  </tr>
21
  <?php } ?>
22
  </table>
23
-
24
- <?php
25
- FrmAppHelper::multiselect_accessibility();
14
  ?>
15
  <tr>
16
  <td class="frm_left_label">
17
+ <label id="for_<?php echo esc_attr( str_replace( '[]', '', $role_field_name ) ); ?>" for="<?php echo esc_attr( $role_field_name ); ?>"><?php echo esc_html( $frm_role_description ); ?></label>
18
  </td>
19
  <td><?php FrmAppHelper::wp_roles_dropdown( $role_field_name, $frm_settings->$frm_role, 'multiple' ); ?></td>
20
  </tr>
21
  <?php } ?>
22
  </table>
 
 
 
css/frm_admin.css CHANGED
@@ -2883,51 +2883,29 @@ a.frm_option_icon:hover::before {
2883
  font-family: "s11-fp" !important;
2884
  }
2885
 
2886
- .multiselect-container.frm-dropdown-menu input[type=radio] {
2887
- display: none;
2888
- }
2889
-
2890
  .multiselect-container {
2891
  position: absolute;
2892
  list-style-type: none;
2893
  margin: 0;
2894
- padding: 0
2895
- }
2896
-
2897
- .multiselect-container .input-group {
2898
- margin: 5px
2899
- }
2900
-
2901
- .multiselect-container > li {
2902
  padding: 0;
2903
- margin: 0;
2904
- }
2905
-
2906
- .multiselect-container > li > a.multiselect-all label {
2907
- font-weight: 700
2908
- }
2909
-
2910
- .multiselect-container > li > label.multiselect-group {
2911
- margin: 0;
2912
- padding: 3px 20px;
2913
- height: 100%;
2914
- font-weight: 700
2915
  }
2916
 
2917
- .frm-dropdown-menu.multiselect-container > li > a {
2918
- padding: 0
 
 
 
 
 
2919
  }
2920
 
2921
- .multiselect-container > li > a > label {
2922
- margin: 0;
2923
- padding: 3px 25px;
2924
- height: 100%;
2925
- cursor: pointer;
2926
- font-weight: 400;
2927
- display: block;
2928
  }
2929
 
2930
- .accordion-container .multiselect-container > li > a > label {
2931
  padding: 3px 19px 3px 7px;
2932
  }
2933
 
@@ -2936,17 +2914,6 @@ a.frm_option_icon:hover::before {
2936
  border-bottom-left-radius: 4px
2937
  }
2938
 
2939
- .form-inline .multiselect-container label.checkbox,
2940
- .form-inline .multiselect-container label.radio {
2941
- padding: 3px 20px;
2942
- }
2943
-
2944
- .form-inline .multiselect-container li a label.checkbox input[type=checkbox],
2945
- .form-inline .multiselect-container li a label.radio input[type=radio] {
2946
- margin-left: -20px;
2947
- margin-right: 0;
2948
- }
2949
-
2950
  .frm-btn-group.btn-group, .frm-btn-group.btn-group-vertical {
2951
  display: block;
2952
  vertical-align: middle;
2883
  font-family: "s11-fp" !important;
2884
  }
2885
 
 
 
 
 
2886
  .multiselect-container {
2887
  position: absolute;
2888
  list-style-type: none;
2889
  margin: 0;
 
 
 
 
 
 
 
 
2890
  padding: 0;
2891
+ width: 100%;
2892
+ max-width: 250px;
 
 
 
 
 
 
 
 
 
 
2893
  }
2894
 
2895
+ .multiselect-container button.multiselect-option {
2896
+ display: block;
2897
+ width: 100%;
2898
+ text-align: left;
2899
+ background: none;
2900
+ border: none;
2901
+ padding: 5px 0 5px 15px;
2902
  }
2903
 
2904
+ .multiselect-container button.multiselect-option label {
2905
+ margin-left: 5px;
 
 
 
 
 
2906
  }
2907
 
2908
+ .accordion-container .multiselect-container label {
2909
  padding: 3px 19px 3px 7px;
2910
  }
2911
 
2914
  border-bottom-left-radius: 4px
2915
  }
2916
 
 
 
 
 
 
 
 
 
 
 
 
2917
  .frm-btn-group.btn-group, .frm-btn-group.btn-group-vertical {
2918
  display: block;
2919
  vertical-align: middle;
formidable.php CHANGED
@@ -2,7 +2,7 @@
2
  /*
3
  Plugin Name: Formidable Forms
4
  Description: Quickly and easily create drag-and-drop forms
5
- Version: 5.0.17
6
  Plugin URI: https://formidableforms.com/
7
  Author URI: https://formidableforms.com/
8
  Author: Strategy11
2
  /*
3
  Plugin Name: Formidable Forms
4
  Description: Quickly and easily create drag-and-drop forms
5
+ Version: 5.1
6
  Plugin URI: https://formidableforms.com/
7
  Author URI: https://formidableforms.com/
8
  Author: Strategy11
js/bootstrap-multiselect.js CHANGED
@@ -2,7 +2,7 @@
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
@@ -15,7 +15,7 @@
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
@@ -41,14 +41,25 @@
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
 
@@ -58,9 +69,9 @@
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.
@@ -79,9 +90,9 @@
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
  },
@@ -96,9 +107,9 @@
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
  },
@@ -144,12 +155,12 @@
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
 
@@ -208,6 +219,7 @@
208
  this.buildSelectAll();
209
  this.buildDropdownOptions();
210
  this.buildFilter();
 
211
 
212
  this.updateButtonText();
213
  this.updateSelectAll(true);
@@ -217,11 +229,17 @@
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
 
@@ -237,35 +255,33 @@
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
  });
@@ -280,7 +296,7 @@
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
  }
@@ -295,7 +311,7 @@
295
  return selected.substr(0, selected.length - this.delimiterText.length);
296
  }
297
  },
298
- checkboxName: function(option) {
299
  return false; // no checkbox name
300
  },
301
  /**
@@ -304,7 +320,7 @@
304
  * @param {jQuery} element
305
  * @returns {String}
306
  */
307
- optionLabel: function(element){
308
  return $(element).attr('label') || $(element).text();
309
  },
310
  /**
@@ -313,7 +329,7 @@
313
  * @param {jQuery} element
314
  * @returns {String}
315
  */
316
- optionClass: function(element) {
317
  return $(element).attr('class') || '';
318
  },
319
  /**
@@ -324,7 +340,7 @@
324
  * @param {jQuery} option
325
  * @param {Boolean} checked
326
  */
327
- onChange : function(option, checked) {
328
 
329
  },
330
  /**
@@ -332,7 +348,7 @@
332
  *
333
  * @param {jQuery} event
334
  */
335
- onDropdownShow: function(event) {
336
 
337
  },
338
  /**
@@ -340,7 +356,7 @@
340
  *
341
  * @param {jQuery} event
342
  */
343
- onDropdownHide: function(event) {
344
 
345
  },
346
  /**
@@ -348,7 +364,7 @@
348
  *
349
  * @param {jQuery} event
350
  */
351
- onDropdownShown: function(event) {
352
 
353
  },
354
  /**
@@ -356,19 +372,19 @@
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
  /**
@@ -377,7 +393,7 @@
377
  * @param {jQuery} $select
378
  * @param {jQuery} $container
379
  */
380
- onInitialized: function($select, $container) {
381
 
382
  },
383
  /**
@@ -385,11 +401,11 @@
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" />',
@@ -420,6 +436,7 @@
420
  nonSelectedText: 'None selected',
421
  nSelectedText: 'selected',
422
  allSelectedText: 'All selected',
 
423
  numberDisplayed: 3,
424
  disableIfEmpty: false,
425
  disabledText: '',
@@ -427,15 +444,21 @@
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
 
@@ -444,9 +467,18 @@
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);
@@ -455,7 +487,7 @@
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'));
@@ -471,15 +503,27 @@
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) {
@@ -490,41 +534,59 @@
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
  /**
@@ -532,9 +594,9 @@
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.
@@ -554,7 +616,7 @@
554
  this.createDivider();
555
  }
556
  else {
557
- this.createOptionValue(element);
558
  }
559
 
560
  }
@@ -563,8 +625,8 @@
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;
@@ -573,11 +635,11 @@
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
  }
@@ -609,7 +671,7 @@
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);
@@ -620,7 +682,7 @@
620
  }
621
 
622
  if (this.options.selectedClass === "active") {
623
- $optionsNotThis.closest("a").css("outline", "");
624
  }
625
  }
626
  else {
@@ -642,34 +704,36 @@
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;
@@ -681,12 +745,12 @@
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
 
@@ -702,100 +766,124 @@
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);
@@ -815,13 +903,14 @@
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
 
@@ -834,11 +923,45 @@
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
  /**
@@ -846,7 +969,7 @@
846
  *
847
  * @param {jQuery} element
848
  */
849
- createOptionValue: function(element) {
850
  var $element = $(element);
851
  if ($element.is(':selected')) {
852
  $element.prop('selected', true);
@@ -857,61 +980,46 @@
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
  },
@@ -921,9 +1029,9 @@
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
  /**
@@ -931,66 +1039,74 @@
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
 
@@ -999,7 +1115,7 @@
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
  }
@@ -1007,38 +1123,21 @@
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
  }
@@ -1047,7 +1146,7 @@
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) {
@@ -1058,15 +1157,25 @@
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
 
@@ -1075,31 +1184,39 @@
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')) {
@@ -1134,13 +1251,13 @@
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.
@@ -1172,6 +1289,8 @@
1172
  this.updateOptGroups();
1173
  }
1174
 
 
 
1175
  this.options.onFiltering(event.target);
1176
 
1177
  }, this), 300, this);
@@ -1180,11 +1299,67 @@
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
@@ -1198,8 +1373,8 @@
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) {
@@ -1210,7 +1385,7 @@
1210
  $input.prop('checked', true);
1211
 
1212
  if (this.options.selectedClass) {
1213
- $input.closest('li')
1214
  .addClass(this.options.selectedClass);
1215
  }
1216
  }
@@ -1218,7 +1393,7 @@
1218
  $input.prop('checked', false);
1219
 
1220
  if (this.options.selectedClass) {
1221
- $input.closest('li')
1222
  .removeClass(this.options.selectedClass);
1223
  }
1224
  }
@@ -1226,12 +1401,12 @@
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));
@@ -1253,8 +1428,8 @@
1253
  * @param {Array} selectValues
1254
  * @param {Boolean} triggerOnChange
1255
  */
1256
- select: function(selectValues, triggerOnChange) {
1257
- if(!$.isArray(selectValues)) {
1258
  selectValues = [selectValues];
1259
  }
1260
 
@@ -1268,22 +1443,27 @@
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
  }
@@ -1319,8 +1499,13 @@
1319
  * @param {Array} deselectValues
1320
  * @param {Boolean} triggerOnChange
1321
  */
1322
- deselect: function(deselectValues, triggerOnChange) {
1323
- if(!$.isArray(deselectValues)) {
 
 
 
 
 
1324
  deselectValues = [deselectValues];
1325
  }
1326
 
@@ -1334,12 +1519,12 @@
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
 
@@ -1368,40 +1553,55 @@
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
 
@@ -1413,40 +1613,55 @@
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
 
@@ -1455,8 +1670,8 @@
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";
@@ -1464,6 +1679,7 @@
1464
  this.buildSelectAll();
1465
  this.buildDropdownOptions();
1466
  this.buildFilter();
 
1467
 
1468
  this.updateButtonText();
1469
  this.updateSelectAll(true);
@@ -1472,22 +1688,33 @@
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();
@@ -1504,7 +1731,7 @@
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,
@@ -1514,10 +1741,10 @@
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
  }
@@ -1533,7 +1760,7 @@
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);
@@ -1550,19 +1777,30 @@
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
  /**
@@ -1570,7 +1808,7 @@
1570
  *
1571
  * @param {Array} options
1572
  */
1573
- setOptions: function(options) {
1574
  this.options = this.mergeOptions(options);
1575
  },
1576
 
@@ -1580,7 +1818,7 @@
1580
  * @param {Array} options
1581
  * @returns {Array}
1582
  */
1583
- mergeOptions: function(options) {
1584
  return $.extend(true, {}, this.defaults, this.options, options);
1585
  },
1586
 
@@ -1589,24 +1827,24 @@
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')) {
@@ -1630,21 +1868,21 @@
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
  },
@@ -1652,7 +1890,7 @@
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.
@@ -1665,6 +1903,7 @@
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
  /**
@@ -1672,7 +1911,7 @@
1672
  *
1673
  * @returns {jQUery}
1674
  */
1675
- getSelected: function() {
1676
  return $('option', this.$select).filter(":selected");
1677
  },
1678
 
@@ -1703,7 +1942,7 @@
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) {
@@ -1717,25 +1956,36 @@
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
 
@@ -1758,8 +2008,8 @@
1758
 
1759
  $.fn.multiselect.Constructor = Multiselect;
1760
 
1761
- $(function() {
1762
  $("select[data-role=multiselect]").multiselect();
1763
  });
1764
 
1765
- }(window.jQuery);
2
  * Bootstrap Multiselect (http://davidstutz.de/bootstrap-multiselect/)
3
  *
4
  * Apache License, Version 2.0:
5
+ * Copyright (c) 2012 - 2021 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
15
  * under the License.
16
  *
17
  * BSD 3-Clause License:
18
+ * Copyright (c) 2012 - 2021 David Stutz
19
  * All rights reserved.
20
  *
21
  * Redistribution and use in source and binary forms, with or without
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 (root, factory) {
45
+ // check to see if 'knockout' AMD module is specified if using requirejs
46
+ if (typeof define === 'function' && define.amd &&
47
+ typeof require === 'function' && typeof require.specified === 'function' && require.specified('knockout')) {
48
+
49
+ // AMD. Register as an anonymous module.
50
+ define(['jquery', 'knockout'], factory);
51
+ } else {
52
+ // Browser globals
53
+ factory(root.jQuery, root.ko);
54
+ }
55
+ })(this, function ($, ko) {
56
  "use strict";// jshint ;_;
57
 
58
  if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
59
  ko.bindingHandlers.multiselect = {
60
  after: ['options', 'value', 'selectedOptions', 'enable', 'disable'],
61
 
62
+ init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
63
  var $element = $(element);
64
  var config = ko.toJS(valueAccessor());
65
 
69
  var options = allBindings.get('options');
70
  if (ko.isObservable(options)) {
71
  ko.computed({
72
+ read: function () {
73
  options();
74
+ setTimeout(function () {
75
  var ms = $element.data('multiselect');
76
  if (ms)
77
  ms.updateOriginalOptions();//Not sure how beneficial this is.
90
  var value = allBindings.get('value');
91
  if (ko.isObservable(value)) {
92
  ko.computed({
93
+ read: function () {
94
  value();
95
+ setTimeout(function () {
96
  $element.multiselect('refresh');
97
  }, 1);
98
  },
107
  var selectedOptions = allBindings.get('selectedOptions');
108
  if (ko.isObservable(selectedOptions)) {
109
  ko.computed({
110
+ read: function () {
111
  selectedOptions();
112
+ setTimeout(function () {
113
  $element.multiselect('refresh');
114
  }, 1);
115
  },
155
  }
156
  }
157
 
158
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
159
  $element.multiselect('destroy');
160
  });
161
  },
162
 
163
+ update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
164
  var $element = $(element);
165
  var config = ko.toJS(valueAccessor());
166
 
219
  this.buildSelectAll();
220
  this.buildDropdownOptions();
221
  this.buildFilter();
222
+ this.buildButtons();
223
 
224
  this.updateButtonText();
225
  this.updateSelectAll(true);
229
  }
230
 
231
  this.options.wasDisabled = this.$select.prop('disabled');
232
+ if (this.options.disableIfEmpty && $('option', this.$select).length <= 0 && !this.options.wasDisabled) {
233
+ this.disable(true);
234
  }
235
 
236
  this.$select.wrap('<span class="multiselect-native-select" />').after(this.$container);
237
+ this.$select.prop('tabindex', '-1');
238
+
239
+ if (this.options.widthSynchronizationMode !== 'never') {
240
+ this.synchronizeButtonAndPopupWidth();
241
+ }
242
+
243
  this.options.onInitialized(this.$select, this.$container);
244
  }
245
 
255
  * @param {jQuery} select
256
  * @returns {String}
257
  */
258
+ buttonText: function (selectedOptions, select) {
259
+ if (this.disabledText.length > 0 && select.prop('disabled')) {
 
 
260
  return this.disabledText;
261
  }
262
+ else if (selectedOptions.length === 0) {
263
  return this.nonSelectedText;
264
  }
265
  else if (this.allSelectedText
266
+ && selectedOptions.length === $('option', $(select)).length
267
+ && $('option', $(select)).length !== 1
268
+ && this.multiple) {
269
 
270
  if (this.selectAllNumber) {
271
+ return this.allSelectedText + ' (' + selectedOptions.length + ')';
272
  }
273
  else {
274
  return this.allSelectedText;
275
  }
276
  }
277
+ else if (this.numberDisplayed != 0 && selectedOptions.length > this.numberDisplayed) {
278
+ return selectedOptions.length + ' ' + this.nSelectedText;
279
  }
280
  else {
281
  var selected = '';
282
  var delimiter = this.delimiterText;
283
 
284
+ selectedOptions.each(function () {
285
  var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
286
  selected += label + delimiter;
287
  });
296
  * @param {jQuery} select
297
  * @returns {@exp;selected@call;substr}
298
  */
299
+ buttonTitle: function (options, select) {
300
  if (options.length === 0) {
301
  return this.nonSelectedText;
302
  }
311
  return selected.substr(0, selected.length - this.delimiterText.length);
312
  }
313
  },
314
+ checkboxName: function (option) {
315
  return false; // no checkbox name
316
  },
317
  /**
320
  * @param {jQuery} element
321
  * @returns {String}
322
  */
323
+ optionLabel: function (element) {
324
  return $(element).attr('label') || $(element).text();
325
  },
326
  /**
329
  * @param {jQuery} element
330
  * @returns {String}
331
  */
332
+ optionClass: function (element) {
333
  return $(element).attr('class') || '';
334
  },
335
  /**
340
  * @param {jQuery} option
341
  * @param {Boolean} checked
342
  */
343
+ onChange: function (option, checked) {
344
 
345
  },
346
  /**
348
  *
349
  * @param {jQuery} event
350
  */
351
+ onDropdownShow: function (event) {
352
 
353
  },
354
  /**
356
  *
357
  * @param {jQuery} event
358
  */
359
+ onDropdownHide: function (event) {
360
 
361
  },
362
  /**
364
  *
365
  * @param {jQuery} event
366
  */
367
+ onDropdownShown: function (event) {
368
 
369
  },
370
  /**
372
  *
373
  * @param {jQuery} event
374
  */
375
+ onDropdownHidden: function (event) {
376
 
377
  },
378
  /**
379
  * Triggered on select all.
380
  */
381
+ onSelectAll: function () {
382
 
383
  },
384
  /**
385
  * Triggered on deselect all.
386
  */
387
+ onDeselectAll: function () {
388
 
389
  },
390
  /**
393
  * @param {jQuery} $select
394
  * @param {jQuery} $container
395
  */
396
+ onInitialized: function ($select, $container) {
397
 
398
  },
399
  /**
401
  *
402
  * @param {jQuery} $filter
403
  */
404
+ onFiltering: function ($filter) {
405
 
406
  },
407
  enableHTML: false,
408
+ buttonClass: 'custom-select',
409
  inheritClass: false,
410
  buttonWidth: 'auto',
411
  buttonContainer: '<div class="btn-group" />',
436
  nonSelectedText: 'None selected',
437
  nSelectedText: 'selected',
438
  allSelectedText: 'All selected',
439
+ resetButtonText: 'Reset',
440
  numberDisplayed: 3,
441
  disableIfEmpty: false,
442
  disabledText: '',
444
  includeResetOption: false,
445
  includeResetDivider: false,
446
  resetText: 'Reset',
447
+ indentGroupOptions: true,
448
+ // possible options: 'never', 'always', 'ifPopupIsSmaller', 'ifPopupIsWider'
449
+ widthSynchronizationMode: 'never',
450
+ buttonTextAlignment: 'center',
451
+ enableResetButton: false,
452
  templates: {
453
+ button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span></button>',
454
+ popupContainer: '<div class="multiselect-container dropdown-menu"></div>',
455
+ filter: '<div class="multiselect-filter d-flex align-items-center"><i class="fas fa-sm fa-search text-muted"></i><input type="search" class="multiselect-search form-control" /></div>',
456
+ buttonGroup: '<div class="multiselect-buttons btn-group" style="display:flex;"></div>',
457
+ buttonGroupReset: '<button type="button" class="multiselect-reset btn btn-secondary btn-block"></button>',
458
+ option: '<button type="button" class="multiselect-option dropdown-item"></button>',
459
+ divider: '<div class="dropdown-divider"></div>',
460
+ optionGroup: '<button type="button" class="multiselect-group dropdown-item"></button>',
461
+ resetButton: '<div class="multiselect-reset text-center p-2"><button type="button" class="btn btn-sm btn-block btn-outline-secondary"></button></div>'
462
  }
463
  },
464
 
467
  /**
468
  * Builds the container of the multiselect.
469
  */
470
+ buildContainer: function () {
471
  this.$container = $(this.options.buttonContainer);
472
+ if (this.options.widthSynchronizationMode !== 'never') {
473
+ this.$container.on('show.bs.dropdown', $.proxy(function () {
474
+ // the width needs to be synchronized again in case the width of the button changed in between
475
+ this.synchronizeButtonAndPopupWidth();
476
+ this.options.onDropdownShow();
477
+ }, this));
478
+ }
479
+ else {
480
+ this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
481
+ }
482
  this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
483
  this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
484
  this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
487
  /**
488
  * Builds the button of the multiselect.
489
  */
490
+ buildButton: function () {
491
  this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
492
  if (this.$select.attr('class') && this.options.inheritClass) {
493
  this.$button.addClass(this.$select.attr('class'));
503
  // Manually add button width if set.
504
  if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
505
  this.$button.css({
506
+ 'width': '100%' //this.options.buttonWidth,
 
 
507
  });
508
  this.$container.css({
509
  'width': this.options.buttonWidth
510
  });
511
  }
512
 
513
+ if (this.options.buttonTextAlignment) {
514
+ switch (this.options.buttonTextAlignment) {
515
+ case 'left':
516
+ this.$button.addClass('text-left');
517
+ break;
518
+ case 'center':
519
+ this.$button.addClass('text-center');
520
+ break;
521
+ case 'right':
522
+ this.$button.addClass('text-right');
523
+ break;
524
+ }
525
+ }
526
+
527
  // Keep the tab index from the select.
528
  var tabindex = this.$select.attr('tabindex');
529
  if (tabindex) {
534
  },
535
 
536
  /**
537
+ * Builds the popup container representing the dropdown menu.
538
  */
539
+ buildDropdown: function () {
540
 
541
+ // Build popup container.
542
+ this.$popupContainer = $(this.options.templates.popupContainer);
543
 
544
  if (this.options.dropRight) {
545
+ this.$container.addClass('dropright');
546
+ }
547
+ else if (this.options.dropUp) {
548
+ this.$container.addClass("dropup");
549
  }
550
 
551
  // Set max height of dropdown menu to activate auto scrollbar.
552
  if (this.options.maxHeight) {
553
  // TODO: Add a class for this option to move the css declarations.
554
+ this.$popupContainer.css({
555
  'max-height': this.options.maxHeight + 'px',
556
  'overflow-y': 'auto',
557
  'overflow-x': 'hidden'
558
  });
559
  }
560
 
561
+ if (this.options.widthSynchronizationMode !== 'never') {
562
+ this.$popupContainer.css('overflow-x', 'hidden');
563
+ }
564
 
565
+ this.$popupContainer.on("touchstart click", function (e) {
566
+ e.stopPropagation();
567
+ });
568
 
569
+ this.$container.append(this.$popupContainer);
570
+ },
571
+
572
+ synchronizeButtonAndPopupWidth: function () {
573
+ if (!this.$popupContainer || this.options.widthSynchronizationMode === 'never') {
574
+ return;
575
  }
576
 
577
+ var buttonWidth = this.$button.outerWidth();
578
+ switch (this.options.widthSynchronizationMode) {
579
+ case 'always':
580
+ this.$popupContainer.css('min-width', buttonWidth);
581
+ this.$popupContainer.css('max-width', buttonWidth);
582
+ break;
583
+ case 'ifPopupIsSmaller':
584
+ this.$popupContainer.css('min-width', buttonWidth);
585
+ break;
586
+ case 'ifPopupIsWider':
587
+ this.$popupContainer.css('max-width', buttonWidth);
588
+ break;
589
+ }
590
  },
591
 
592
  /**
594
  *
595
  * Uses createDivider and createOptionValue to create the necessary options.
596
  */
597
+ buildDropdownOptions: function () {
598
 
599
+ this.$select.children().each($.proxy(function (index, element) {
600
 
601
  var $element = $(element);
602
  // Support optgroups and options without a group simultaneously.
616
  this.createDivider();
617
  }
618
  else {
619
+ this.createOptionValue(element, false);
620
  }
621
 
622
  }
625
  }, this));
626
 
627
  // Bind the change event on the dropdown elements.
628
+ $(this.$popupContainer).off('change', '> *:not(.multiselect-group) input[type="checkbox"], > *:not(.multiselect-group) input[type="radio"]');
629
+ $(this.$popupContainer).on('change', '> *:not(.multiselect-group) input[type="checkbox"], > *:not(.multiselect-group) input[type="radio"]', $.proxy(function (event) {
630
  var $target = $(event.target);
631
 
632
  var checked = $target.prop('checked') || false;
635
  // Apply or unapply the configured selected class.
636
  if (this.options.selectedClass) {
637
  if (checked) {
638
+ $target.closest('.multiselect-option')
639
  .addClass(this.options.selectedClass);
640
  }
641
  else {
642
+ $target.closest('.multiselect-option')
643
  .removeClass(this.options.selectedClass);
644
  }
645
  }
671
  else {
672
  // Unselect all other options and corresponding checkboxes.
673
  if (this.options.selectedClass) {
674
+ $($checkboxesNotThis).closest('.dropdown-item').removeClass(this.options.selectedClass);
675
  }
676
 
677
  $($checkboxesNotThis).prop('checked', false);
682
  }
683
 
684
  if (this.options.selectedClass === "active") {
685
+ $optionsNotThis.closest(".dropdown-item").css("outline", "");
686
  }
687
  }
688
  else {
704
  this.$select.change();
705
  this.updateButtonText();
706
 
707
+ if (this.options.preventInputChangeEvent) {
708
  return false;
709
  }
710
  }, this));
711
 
712
+ $('.multiselect-option', this.$popupContainer).off('mousedown');
713
+ $('.multiselect-option', this.$popupContainer).on('mousedown', function (e) {
714
  if (e.shiftKey) {
715
  // Prevent selecting text by Shift+click
716
  return false;
717
  }
718
  });
719
 
720
+ $(this.$popupContainer).off('touchstart click', '.multiselect-option, .multiselect-all, .multiselect-group');
721
+ $(this.$popupContainer).on('touchstart click', '.multiselect-option, .multiselect-all, .multiselect-group', $.proxy(function (event) {
722
  event.stopPropagation();
723
 
724
  var $target = $(event.target);
725
 
726
  if (event.shiftKey && this.options.multiple) {
727
+ if (!$target.is("input")) { // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
728
  event.preventDefault();
729
+ $target = $target.closest(".multiselect-option").find("input");
730
  $target.prop("checked", !$target.prop("checked"));
731
  }
732
  var checked = $target.prop('checked') || false;
733
 
734
  if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
735
+ var from = this.$popupContainer.find(".multiselect-option:visible").index($target.closest(".multiselect-option"));
736
+ var to = this.$popupContainer.find(".multiselect-option:visible").index(this.lastToggledInput.closest(".multiselect-option"));
737
 
738
  if (from > to) { // Swap the indices
739
  var tmp = to;
745
  ++to;
746
 
747
  // Change the checkboxes and underlying options
748
+ var range = this.$popupContainer.find(".multiselect-option:not(.multiselect-filter-hidden)").slice(from, to).find("input");
749
 
750
  range.prop('checked', checked);
751
 
752
  if (this.options.selectedClass) {
753
+ range.closest('.multiselect-option')
754
  .toggleClass(this.options.selectedClass, checked);
755
  }
756
 
766
  // Trigger the select "change" event
767
  $target.trigger("change");
768
  }
769
+ else if (!$target.is('input')) {
770
+ var $checkbox = $target.closest('.multiselect-option, .multiselect-all').find('.form-check-input');
771
+ if ($checkbox.length > 0) {
772
+ if (this.options.multiple || !$checkbox.prop('checked')) {
773
+ $checkbox.prop('checked', !$checkbox.prop('checked'));
774
+ $checkbox.change();
775
+ }
776
+ }
777
+ else if (this.options.enableClickableOptGroups && this.options.multiple && !$target.hasClass("caret-container")) {
778
+ var groupItem = $target;
779
+ if (!groupItem.hasClass("multiselect-group")) {
780
+ groupItem = $target.closest('.multiselect-group');
781
+ }
782
+ $checkbox = groupItem.find(".form-check-input");
783
+ if ($checkbox.length > 0) {
784
+ $checkbox.prop('checked', !$checkbox.prop('checked'));
785
+ $checkbox.change();
786
+ }
787
+ }
788
+
789
+ event.preventDefault();
790
+ }
791
 
792
  // Remembers last clicked option
793
+ var $input = $target.closest(".multiselect-option").find("input[type='checkbox'], input[type='radio']");
794
+ if ($input.length > 0) {
795
  this.lastToggledInput = $target;
796
  }
797
+ else {
798
+ this.lastToggledInput = null;
799
+ }
800
 
801
+ $target.blur();
802
  }, this));
803
 
804
+ //Keyboard support.
805
+ this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function (event) {
806
+ var $items = $(this.$container).find(".multiselect-option:not(.disabled), .multiselect-group:not(.disabled), .multiselect-all").filter(":visible");
807
+ var index = $items.index($items.filter(':focus'));
808
+ var $search = $('.multiselect-search', this.$container);
809
 
810
+ // keyCode 9 == Tab
811
+ if (event.keyCode === 9 && this.$container.hasClass('show')) {
812
  this.$button.click();
813
  }
814
+ // keyCode 13 = Enter
815
+ else if (event.keyCode == 13) {
816
+ var $current = $items.eq(index);
817
+ setTimeout(function () {
818
+ $current.focus();
819
+ }, 1);
820
+ }
821
+ // keyCode 38 = Arrow Up
822
+ else if (event.keyCode == 38) {
823
+ if (index == 0 && !$search.is(':focus')) {
824
+ setTimeout(function () {
825
+ $search.focus();
826
+ }, 1);
827
  }
828
+ }
829
+ // keyCode 40 = Arrow Down
830
+ else if (event.keyCode == 40) {
831
+ if ($search.is(':focus')) {
832
+ var $first = $items.eq(0);
833
+ setTimeout(function () {
834
+ $search.blur();
835
+ $first.focus();
836
+ }, 1);
837
  }
838
+ else if (index == -1) {
839
+ setTimeout(function () {
840
+ $search.focus();
841
+ }, 1);
842
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
843
  }
844
  }, this));
845
 
846
  if (this.options.enableClickableOptGroups && this.options.multiple) {
847
+ $(".multiselect-group input", this.$popupContainer).off("change");
848
+ $(".multiselect-group input", this.$popupContainer).on("change", $.proxy(function (event) {
849
  event.stopPropagation();
850
 
851
  var $target = $(event.target);
852
  var checked = $target.prop('checked') || false;
853
 
854
+ var $item = $(event.target).closest('.dropdown-item');
855
+ var $group = $item.nextUntil(".multiselect-group")
856
  .not('.multiselect-filter-hidden')
857
  .not('.disabled');
858
 
859
  var $inputs = $group.find("input");
860
 
 
861
  var $options = [];
862
 
863
  if (this.options.selectedClass) {
864
  if (checked) {
865
+ $item.addClass(this.options.selectedClass);
866
  }
867
  else {
868
+ $item.removeClass(this.options.selectedClass);
869
  }
870
  }
871
 
872
+ $.each($inputs, $.proxy(function (index, input) {
873
+ var $input = $(input);
874
+ var value = $input.val();
875
  var $option = this.getOptionByValue(value);
876
 
877
  if (checked) {
878
+ $input.prop('checked', true);
879
+ $input.closest('.dropdown-item')
880
  .addClass(this.options.selectedClass);
881
 
882
  $option.prop('selected', true);
883
  }
884
  else {
885
+ $input.prop('checked', false);
886
+ $input.closest('.dropdown-item')
887
  .removeClass(this.options.selectedClass);
888
 
889
  $option.prop('selected', false);
903
  }
904
 
905
  if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
906
+ $(".multiselect-group .caret-container", this.$popupContainer).off("click");
907
+ $(".multiselect-group .caret-container", this.$popupContainer).on("click", $.proxy(function (event) {
908
+ var $group = $(event.target).closest('.multiselect-group');
909
+ var $inputs = $group.nextUntil(".multiselect-group")
910
+ .not('.multiselect-filter-hidden');
911
 
912
  var visible = true;
913
+ $inputs.each(function () {
914
  visible = visible && !$(this).hasClass('multiselect-collapsible-hidden');
915
  });
916
 
923
  .removeClass('multiselect-collapsible-hidden');
924
  }
925
  }, this));
926
+ }
927
+ },
928
+
929
+ /**
930
+ * Create a checkbox container with input and label based on given values
931
+ * @param {JQuery} $item
932
+ * @param {String} label
933
+ * @param {String} name
934
+ * @param {String} value
935
+ * @param {String} inputType
936
+ * @returns {JQuery}
937
+ */
938
+ createCheckbox: function ($item, labelContent, name, value, title, inputType) {
939
+ var $wrapper = $('<span />');
940
+ $wrapper.addClass("form-check");
941
+
942
+ if (this.options.enableHTML && $(labelContent).length > 0) {
943
+ var $checkboxLabel = $('<label class="form-check-label" />');
944
+ $checkboxLabel.html(labelContent);
945
+ $wrapper.append($checkboxLabel);
946
+ }
947
+ else {
948
+ var $checkboxLabel = $('<label class="form-check-label" />');
949
+ $checkboxLabel.text(labelContent);
950
+ $wrapper.append($checkboxLabel);
951
+ }
952
 
953
+ var $checkbox = $('<input class="form-check-input"/>').attr('type', inputType);
954
+ $checkbox.val(value);
955
+ $wrapper.prepend($checkbox);
956
+
957
+ if (name) {
958
+ $checkbox.attr('name', name);
959
  }
960
+
961
+ $item.prepend($wrapper);
962
+ $item.attr("title", title || labelContent);
963
+
964
+ return $checkbox;
965
  },
966
 
967
  /**
969
  *
970
  * @param {jQuery} element
971
  */
972
+ createOptionValue: function (element, isGroupOption) {
973
  var $element = $(element);
974
  if ($element.is(':selected')) {
975
  $element.prop('selected', true);
980
  var classes = this.options.optionClass(element);
981
  var value = $element.val();
982
  var inputType = this.options.multiple ? "checkbox" : "radio";
983
+ var title = $element.attr('title');
984
 
985
+ var $option = $(this.options.templates.option);
986
+ $option.addClass(classes);
 
 
 
987
 
988
+ if (isGroupOption && this.options.indentGroupOptions) {
989
+ $option.addClass("multiselect-group-option-indented")
 
 
990
  }
991
 
992
+ // Hide all children items when collapseOptGroupsByDefault is true
993
+ if (this.options.collapseOptGroupsByDefault && $(element).parent().prop("tagName").toLowerCase() === "optgroup") {
994
+ $option.addClass("multiselect-collapsible-hidden");
995
+ $option.hide();
 
996
  }
997
 
 
 
998
  var name = this.options.checkboxName($element);
999
+ var $checkbox = this.createCheckbox($option, label, name, value, title, inputType);
 
 
 
 
1000
 
1001
  var selected = $element.prop('selected') || false;
 
1002