WP Meta SEO - Version 4.0.0

Version Description

  • Add : New admin UX and design
  • Add : Settings UX with AJAX search engine
  • Add : Implement new plugin dashboard with new criteria checks
  • Add : Implement bulk actions on meta bulk editor
  • Add : Implement bulk actions on image information bulk editor
  • Add : Implement bulk actions on the link title manager
  • Add : Check color for meta lenght: check is meta is too short or too long
  • Add : Plugin installer with quick configuration
  • Add : Environment checker on install (PHP Version, PHP Extensions, Apache Modules)
  • Add : System Check menu to notify of server configuration problems after install
  • Fix : Sitemap display on frontend
Download this release

Release Info

Developer JoomUnited
Plugin Icon 128x128 WP Meta SEO
Version 4.0.0
Comparing to
See all releases

Code changes from version 3.7.7 to 4.0.0

Files changed (229) hide show
  1. assets/css/better_seo.css +135 -0
  2. assets/css/bootstrap/bootstrap-multiselect.css +1 -0
  3. assets/css/bootstrap/bootstrap-multiselect.js +1630 -0
  4. assets/css/bootstrap/bootstrap.min.css +7052 -0
  5. assets/css/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  6. assets/css/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  7. assets/css/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  8. assets/css/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  9. assets/css/broken_link.css +133 -0
  10. {css → assets/css}/category_field.css +0 -0
  11. assets/css/dashboard.css +327 -0
  12. {css → assets/css}/dashboard_widgets.css +19 -4
  13. {css → assets/css}/google-analytics/admin-widgets.css +2 -5
  14. {css → assets/css}/google-analytics/jquery.ui.tooltip.html.css +0 -0
  15. {css → assets/css}/google-analytics/nprogress.css +0 -0
  16. {css → assets/css}/google-analytics/wpms-tracking-code.css +4 -5
  17. {css → assets/css}/html_sitemap.css +23 -6
  18. {css → assets/css}/jPages.css +1 -2
  19. assets/css/jquery.qtip.css +1 -0
  20. assets/css/magnific-popup.css +447 -0
  21. assets/css/main.css +714 -0
  22. {css → assets/css}/materialize/materialize.css +7 -8
  23. {css → assets/css}/materialize/materialize_frontend_accordions_theme.css +0 -0
  24. {css → assets/css}/materialize/materialize_frontend_tab_theme.css +0 -0
  25. {css → assets/css}/metabox-tabs.css +227 -99
  26. {css → assets/css}/metaseo-quirk.css +0 -0
  27. {css → assets/css}/metaseo_admin.css +404 -187
  28. assets/css/metaseo_sitemap.css +224 -0
  29. assets/css/my_qtip.css +16 -0
  30. {css → assets/css}/notification.css +5 -3
  31. assets/css/settings.css +184 -0
  32. assets/css/style.css +354 -0
  33. {css → assets/css}/tooltip-metaimage.css +0 -0
  34. {css → assets/css}/tooltip.css +0 -0
  35. {img → assets/images}/ajax-loader.gif +0 -0
  36. assets/images/ajax-loader1.gif +0 -0
  37. {img → assets/images}/arrow-down.png +0 -0
  38. {img → assets/images}/arrow-up.png +0 -0
  39. {img → assets/images}/asc.gif +0 -0
  40. {img → assets/images}/bubble-top-v2.png +0 -0
  41. assets/images/checklist/checklist.png +0 -0
  42. assets/images/checklist/checklist@2x.png +0 -0
  43. assets/images/checklist/checklist@3x.png +0 -0
  44. assets/images/dashboard/404/404.png +0 -0
  45. assets/images/dashboard/404/404@2x.png +0 -0
  46. assets/images/dashboard/404/404@3x.png +0 -0
  47. assets/images/dashboard/business/business.png +0 -0
  48. assets/images/dashboard/business/business@2x.png +0 -0
  49. assets/images/dashboard/business/business@3x.png +0 -0
  50. assets/images/dashboard/google-search-console/google-search-console.png +0 -0
  51. assets/images/dashboard/google-search-console/google-search-console@2x.png +0 -0
  52. assets/images/dashboard/google-search-console/google-search-console@3x.png +0 -0
  53. assets/images/dashboard/group/group.png +0 -0
  54. assets/images/dashboard/group/group@2x.png +0 -0
  55. assets/images/dashboard/group/group@3x.png +0 -0
  56. assets/images/dashboard/icon-cache-activation/icon-cache-activation.png +0 -0
  57. assets/images/dashboard/icon-cache-activation/icon-cache-activation@2x.png +0 -0
  58. assets/images/dashboard/icon-cache-activation/icon-cache-activation@3x.png +0 -0
  59. assets/images/dashboard/icon-cache-clean-up-copy/icon-cache-clean-up-copy.png +0 -0
  60. assets/images/dashboard/icon-cache-clean-up-copy/icon-cache-clean-up-copy@2x.png +0 -0
  61. assets/images/dashboard/icon-cache-clean-up-copy/icon-cache-clean-up-copy@3x.png +0 -0
  62. assets/images/dashboard/icon-cache-clean-up/icon-cache-clean-up.png +0 -0
  63. assets/images/dashboard/icon-cache-clean-up/icon-cache-clean-up@2x.png +0 -0
  64. assets/images/dashboard/icon-cache-clean-up/icon-cache-clean-up@3x.png +0 -0
  65. assets/images/dashboard/icon-expire-headers/icon-expire-headers.png +0 -0
  66. assets/images/dashboard/icon-expire-headers/icon-expire-headers@2x.png +0 -0
  67. assets/images/dashboard/icon-expire-headers/icon-expire-headers@3x.png +0 -0
  68. assets/images/dashboard/icon-php-version/icon-php-version.png +0 -0
  69. assets/images/dashboard/icon-php-version/icon-php-version@2x.png +0 -0
  70. assets/images/dashboard/icon-php-version/icon-php-version@3x.png +0 -0
  71. assets/images/dashboard/polylang/polylang.png +0 -0
  72. assets/images/dashboard/polylang/polylang@2x.png +0 -0
  73. assets/images/dashboard/polylang/polylang@3x.png +0 -0
  74. assets/images/dashboard/woocommerce/woocommerce.png +0 -0
  75. assets/images/dashboard/woocommerce/woocommerce@2x.png +0 -0
  76. assets/images/dashboard/woocommerce/woocommerce@3x.png +0 -0
  77. {img → assets/images}/desc.gif +0 -0
  78. assets/images/facebook/facebook.png +0 -0
  79. assets/images/facebook/facebook@2x.png +0 -0
  80. assets/images/facebook/facebook@3x.png +0 -0
  81. {img → assets/images}/globe-sm.jpg +0 -0
  82. {img → assets/images}/gplus-loader.gif +0 -0
  83. {img → assets/images}/hz-loading.gif +0 -0
  84. assets/images/icon-info/icon-info.png +0 -0
  85. assets/images/icon-info/icon-info@2x.png +0 -0
  86. assets/images/icon-info/icon-info@3x.png +0 -0
  87. assets/images/icon-information/icon-information.png +0 -0
  88. assets/images/icon-information/icon-information@2x.png +0 -0
  89. assets/images/icon-information/icon-information@3x.png +0 -0
  90. assets/images/icon-notification.png +0 -0
  91. assets/images/icon-warning/icon-warning.png +0 -0
  92. assets/images/icon-warning/icon-warning@2x.png +0 -0
  93. assets/images/icon-warning/icon-warning@3x.png +0 -0
  94. {img → assets/images}/icon.png +0 -0
  95. {img → assets/images}/icon_tip.png +0 -0
  96. {img → assets/images}/img-arrow.png +0 -0
  97. {img → assets/images}/info.png +0 -0
  98. {img → assets/images}/loop.png +0 -0
  99. assets/images/metablock.png +0 -0
  100. {img → assets/images}/metaseo_sms.png +0 -0
  101. {img → assets/images}/no-data-alert.png +0 -0
  102. assets/images/page_loader.gif +0 -0
  103. assets/images/process-bar-loading.gif +0 -0
  104. {img → assets/images}/question-mark.png +0 -0
  105. assets/images/question/question.png +0 -0
  106. assets/images/question/question@2x.png +0 -0
  107. assets/images/question/question@3x.png +0 -0
  108. assets/images/twitter/twitter.png +0 -0
  109. assets/images/twitter/twitter@2x.png +0 -0
  110. assets/images/twitter/twitter@3x.png +0 -0
  111. {img → assets/images}/update_loader.gif +0 -0
  112. {img → assets/images}/update_loading.gif +0 -0
  113. {img → assets/images}/view.gif +0 -0
  114. {img → assets/images}/view.png +0 -0
  115. {img → assets/images}/warnig-red.png +0 -0
  116. {img → assets/images}/warning-20x20.png +0 -0
  117. {img → assets/images}/warning-25x25.png +0 -0
  118. {img → assets/images}/warning.png +0 -0
  119. assets/images/white-loader.gif +0 -0
  120. {js → assets/js}/category_field.js +0 -0
  121. assets/js/circle-progress.js +552 -0
  122. {js → assets/js}/cliffpyles.js +16 -64
  123. assets/js/dashboard.js +269 -0
  124. js/dashboard.js → assets/js/dashboard_widgets.js +58 -30
  125. {js → assets/js}/google-analytics/google_analytics.js +6 -6
  126. {js → assets/js}/google-analytics/nprogress.js +0 -0
  127. {js → assets/js}/jPages.js +1 -3
  128. assets/js/jquery.magnific-popup.min.js +4 -0
  129. assets/js/jquery.qtip.min.js +5 -0
  130. {js → assets/js}/materialize/materialize.min.js +0 -0
  131. {js → assets/js}/metaseo_admin.js +69 -45
  132. {js → assets/js}/metaseo_sitemap.js +55 -11
  133. assets/js/my-qtips.js +43 -0
  134. {js → assets/js}/notification.js +0 -0
  135. assets/js/settings.js +42 -0
  136. {js → assets/js}/site-jPages.js +0 -0
  137. {js → assets/js}/wp-metaseo-admin-media.js +0 -0
  138. {js → assets/js}/wp-metaseo-metabox.js +18 -36
  139. {js → assets/js}/wpms-broken-link.js +48 -8
  140. assets/js/wpms-bulk-action.js +102 -0
  141. {js → assets/js}/wpms-link-title-field.js +0 -0
  142. assets/wordpress-css-framework/css/style.css +641 -0
  143. assets/wordpress-css-framework/css/style.css.map +7 -0
  144. assets/wordpress-css-framework/css/style.min.css +1 -0
  145. assets/wordpress-css-framework/css/style.scss +657 -0
  146. assets/wordpress-css-framework/images/logo-joomUnited-white.png +0 -0
  147. assets/wordpress-css-framework/js/script.js +121 -0
  148. assets/wordpress-css-framework/js/tabs.js +130 -0
  149. assets/wordpress-css-framework/js/velocity.min.js +5 -0
  150. assets/wordpress-css-framework/js/waves.js +341 -0
  151. css/broken_link.css +0 -58
  152. css/chart.css +0 -56
  153. css/dashboard.css +0 -163
  154. css/font-awesome.css +0 -2337
  155. css/jquery.qtip.css +0 -564
  156. css/metaseo_sitemap.css +0 -68
  157. css/my_qtip.css +0 -13
  158. css/style.css +0 -298
  159. fonts/FontAwesome.otf +0 -0
  160. fonts/fontawesome-webfont.eot +0 -0
  161. fonts/fontawesome-webfont.svg +0 -2671
  162. fonts/fontawesome-webfont.ttf +0 -0
  163. fonts/fontawesome-webfont.woff +0 -0
  164. fonts/fontawesome-webfont.woff2 +0 -0
  165. inc/class.image-helper.php +1 -2
  166. inc/class.metaseo-admin.php +522 -850
  167. inc/class.metaseo-broken-link-table.php +370 -265
  168. inc/class.metaseo-content-list-table.php +225 -57
  169. inc/class.metaseo-dashboard.php +161 -157
  170. inc/class.metaseo-image-list-table.php +232 -118
  171. inc/class.metaseo-link-list-table.php +181 -58
  172. inc/class.metaseo-meta.php +76 -57
  173. inc/class.metaseo-metabox.php +172 -82
  174. inc/class.metaseo-sitemap.php +384 -356
  175. inc/class.metaseo-snippet-preview.php +2 -2
  176. inc/install-wizard/content/done/illustration-done.png +0 -0
  177. inc/install-wizard/content/done/illustration-done@2x.png +0 -0
  178. inc/install-wizard/content/done/illustration-done@3x.png +0 -0
  179. inc/install-wizard/content/viewDone.php +27 -0
  180. inc/install-wizard/content/viewEvironment.php +93 -0
  181. inc/install-wizard/content/viewGoogleAnalytics.php +79 -0
  182. inc/install-wizard/content/viewMetaInfos.php +108 -0
  183. inc/install-wizard/content/viewSocial.php +75 -0
  184. inc/install-wizard/content/viewWizard.php +33 -0
  185. inc/install-wizard/content/welcome-illustration/welcome-illustration.png +0 -0
  186. inc/install-wizard/content/welcome-illustration/welcome-illustration@2x.png +0 -0
  187. inc/install-wizard/content/welcome-illustration/welcome-illustration@3x.png +0 -0
  188. inc/install-wizard/handler-wizard.php +282 -0
  189. inc/install-wizard/install-wizard.css +705 -0
  190. inc/install-wizard/install-wizard.php +237 -0
  191. inc/pages/better_seo.php +118 -0
  192. inc/pages/content-meta.php +45 -3
  193. inc/pages/dashboard.php +345 -381
  194. inc/pages/dashboard_widgets.php +11 -55
  195. inc/pages/google-analytics/ga-trackcode.php +201 -223
  196. inc/pages/google-analytics/google-analytics.php +15 -15
  197. inc/pages/google-analytics/menu.php +22 -6
  198. inc/pages/image-meta.php +54 -6
  199. inc/pages/link-meta.php +53 -6
  200. inc/pages/metaseo-broken-link.php +9 -8
  201. inc/pages/metaseo-image-compression.php +0 -218
  202. inc/pages/notification.php +2 -0
  203. inc/pages/settings.php +207 -250
  204. inc/pages/settings/breadcrumb.php +140 -0
  205. inc/pages/settings/general.php +89 -0
  206. inc/pages/settings/image_compression.php +309 -0
  207. inc/pages/settings/jutranslation.php +8 -0
  208. inc/pages/settings/local_business.php +7 -0
  209. inc/pages/settings/redirections_404.php +84 -0
  210. inc/pages/settings/send_email.php +7 -0
  211. inc/pages/settings/social.php +116 -0
  212. inc/pages/settings/system_check.php +72 -0
  213. inc/pages/sitemaps/general.php +229 -0
  214. inc/pages/sitemaps/metaseo-google-sitemap.php +63 -12
  215. inc/pages/sitemaps/metaseo-source_menu.php +45 -29
  216. inc/pages/sitemaps/metaseo-source_pages.php +89 -54
  217. inc/pages/sitemaps/metaseo-source_posts.php +141 -74
  218. inc/pages/sitemaps/sitemap_menus.php +5 -2
  219. js/Chart.js +0 -3379
  220. js/autosize.js +0 -245
  221. js/dashboard_widgets.js +0 -213
  222. js/jquery.knob.js +0 -802
  223. js/jquery.qtip.min.js +0 -4
  224. js/wpms-bulk-action.js +0 -133
  225. jutranslation/assets/css/jutranslation.css +2 -2
  226. jutranslation/jutranslation.php +4 -2
  227. languages/wp-meta-seo-en_US.mo +0 -0
  228. readme.txt +14 -1
  229. wp-meta-seo.php +126 -14
assets/css/better_seo.css ADDED
@@ -0,0 +1,135 @@
1
+ #wpcontent {
2
+ padding-left: 0;
3
+ }
4
+
5
+ .dashboard {
6
+ width: 100%;
7
+ padding: 0 10%;
8
+ font-family: Roboto, sans-serif;
9
+ }
10
+
11
+ .better-top {
12
+ text-align: center;
13
+ width: 60%;
14
+ margin: 20px auto;
15
+ }
16
+
17
+ .right-checkbox {
18
+ float: right;
19
+ line-height: 50px;
20
+ padding-right: 30px;
21
+ }
22
+
23
+ .ju-setting-label {
24
+ padding-left: 20px;
25
+ }
26
+
27
+ .addon-feature .title-top {
28
+ font-size: 40px;
29
+ font-weight: 600;
30
+ font-style: normal;
31
+ font-stretch: normal;
32
+ line-height: normal;
33
+ letter-spacing: 2px;
34
+ color: #394857;
35
+ padding: 0;
36
+ margin: 10px 0;
37
+ }
38
+
39
+ .addon-feature .description-top {
40
+ font-size: 14px;
41
+ font-weight: 300;
42
+ font-style: normal;
43
+ font-stretch: normal;
44
+ line-height: 2.14;
45
+ letter-spacing: 0.5px;
46
+ color: #4c6482;
47
+ padding: 0 0 20px 0;
48
+ }
49
+
50
+ .addon-feature .addon-img {
51
+ width: 100%;
52
+ height: 160px;
53
+ background: #e9f1f8;
54
+ text-align: center;
55
+ }
56
+
57
+ .addon-feature .panel-addon {
58
+ width: 170px;
59
+ height: 32px;
60
+ line-height: 32px;
61
+ text-align: center;
62
+ text-transform: uppercase;
63
+ border: 1px solid #F7F0EB;
64
+ border-radius: 5px;
65
+ color: #ffa45a;
66
+ background-color: #F7F0EB;
67
+ margin: 9px 0;
68
+ font-weight: 500;
69
+ }
70
+
71
+ .better-layout {
72
+ width: 47%;
73
+ }
74
+
75
+ .addon-feature .ju-settings-option {
76
+ width: 100%;
77
+ }
78
+
79
+ .addon-feature .better-layout .ju-settings-option:nth-child(even) {
80
+ background-color: #f7fafc;
81
+ }
82
+
83
+ .addon-feature .ju-settings-option .ju-setting-label {
84
+ font-weight: 700;
85
+ font-style: normal;
86
+ font-stretch: normal;
87
+ text-transform: capitalize;
88
+ font-size: 17px;
89
+ color: #404852;
90
+ width: 49%;
91
+ }
92
+
93
+ .addon-feature .ju-settings-option .description {
94
+ font-size: 14px;
95
+ font-weight: 300;
96
+ font-style: normal;
97
+ font-stretch: normal;
98
+ line-height: 2.14;
99
+ letter-spacing: 0.5px;
100
+ color: #9FADBD !important;
101
+ height: 100px;
102
+ overflow: hidden;
103
+ text-overflow: ellipsis;
104
+ padding: 0 20px;
105
+ }
106
+
107
+ .dashboard h1 {
108
+ padding: 10px 0 10px 20px
109
+ }
110
+
111
+ .dashboard .top_h1 {
112
+ font-size: 40px;
113
+ font-weight: 600;
114
+ font-style: normal;
115
+ font-stretch: normal;
116
+ line-height: normal;
117
+ letter-spacing: 1.5px;
118
+ color: #404852;
119
+ padding: 0;
120
+ margin: 50px 0 10px 0;
121
+ }
122
+
123
+ @media screen and (max-width: 1369px) {
124
+ .addon-feature .ju-settings-option {
125
+ width: 48%;
126
+ margin: 30px 2% 30px 0 !important;
127
+ }
128
+ }
129
+
130
+ @media screen and (max-width: 768px) {
131
+ .addon-feature .ju-settings-option {
132
+ width: 100%;
133
+ margin: 30px 0 30px 0 !important;
134
+ }
135
+ }
assets/css/bootstrap/bootstrap-multiselect.css ADDED
@@ -0,0 +1 @@
1
+ .multiselect-container{position:absolute;list-style-type:none;margin:0;padding:0}.multiselect-container .input-group{margin:5px}.multiselect-container>li{padding:0}.multiselect-container>li>a.multiselect-all label{font-weight:700}.multiselect-container>li.multiselect-group label{margin:0;padding:3px 20px 3px 20px;height:100%;font-weight:700}.multiselect-container>li.multiselect-group-clickable label{cursor:pointer}.multiselect-container>li>a{padding:0}.multiselect-container>li>a>label{margin:0;height:100%;cursor:pointer;font-weight:400;padding:3px 20px 3px 40px}.multiselect-container>li>a>label.radio,.multiselect-container>li>a>label.checkbox{margin:0}.multiselect-container>li>a>label>input[type=checkbox]{margin-bottom:5px}.btn-group>.btn-group:nth-child(2)>.multiselect.btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.form-inline .multiselect-container label.checkbox,.form-inline .multiselect-container label.radio{padding:3px 20px 3px 40px}.form-inline .multiselect-container li a label.checkbox input[type=checkbox],.form-inline .multiselect-container li a label.radio input[type=radio]{margin-left:-20px;margin-right:0}
assets/css/bootstrap/bootstrap-multiselect.js ADDED
@@ -0,0 +1,1630 @@
1
+ /**
2
+ * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect)
3
+ *
4
+ * Apache License, Version 2.0:
5
+ * Copyright (c) 2012 - 2015 David Stutz
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
8
+ * use this file except in compliance with the License. You may obtain a
9
+ * copy of the License at http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+ * License for the specific language governing permissions and limitations
15
+ * under the License.
16
+ *
17
+ * BSD 3-Clause License:
18
+ * Copyright (c) 2012 - 2015 David Stutz
19
+ * All rights reserved.
20
+ *
21
+ * Redistribution and use in source and binary forms, with or without
22
+ * modification, are permitted provided that the following conditions are met:
23
+ * - Redistributions of source code must retain the above copyright notice,
24
+ * this list of conditions and the following disclaimer.
25
+ * - Redistributions in binary form must reproduce the above copyright notice,
26
+ * this list of conditions and the following disclaimer in the documentation
27
+ * and/or other materials provided with the distribution.
28
+ * - Neither the name of David Stutz nor the names of its contributors may be
29
+ * used to endorse or promote products derived from this software without
30
+ * specific prior written permission.
31
+ *
32
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
33
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
34
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
35
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
36
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
37
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
38
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
39
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
40
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
41
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43
+ */
44
+ !function ($) {
45
+ "use strict";// jshint ;_;
46
+
47
+ if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
48
+ ko.bindingHandlers.multiselect = {
49
+ after: ['options', 'value', 'selectedOptions', 'enable', 'disable'],
50
+
51
+ init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
52
+ var $element = $(element);
53
+ var config = ko.toJS(valueAccessor());
54
+
55
+ $element.multiselect(config);
56
+
57
+ if (allBindings.has('options')) {
58
+ var options = allBindings.get('options');
59
+ if (ko.isObservable(options)) {
60
+ ko.computed({
61
+ read: function() {
62
+ options();
63
+ setTimeout(function() {
64
+ var ms = $element.data('multiselect');
65
+ if (ms)
66
+ ms.updateOriginalOptions();//Not sure how beneficial this is.
67
+ $element.multiselect('rebuild');
68
+ }, 1);
69
+ },
70
+ disposeWhenNodeIsRemoved: element
71
+ });
72
+ }
73
+ }
74
+
75
+ //value and selectedOptions are two-way, so these will be triggered even by our own actions.
76
+ //It needs some way to tell if they are triggered because of us or because of outside change.
77
+ //It doesn't loop but it's a waste of processing.
78
+ if (allBindings.has('value')) {
79
+ var value = allBindings.get('value');
80
+ if (ko.isObservable(value)) {
81
+ ko.computed({
82
+ read: function() {
83
+ value();
84
+ setTimeout(function() {
85
+ $element.multiselect('refresh');
86
+ }, 1);
87
+ },
88
+ disposeWhenNodeIsRemoved: element
89
+ }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
90
+ }
91
+ }
92
+
93
+ //Switched from arrayChange subscription to general subscription using 'refresh'.
94
+ //Not sure performance is any better using 'select' and 'deselect'.
95
+ if (allBindings.has('selectedOptions')) {
96
+ var selectedOptions = allBindings.get('selectedOptions');
97
+ if (ko.isObservable(selectedOptions)) {
98
+ ko.computed({
99
+ read: function() {
100
+ selectedOptions();
101
+ setTimeout(function() {
102
+ $element.multiselect('refresh');
103
+ }, 1);
104
+ },
105
+ disposeWhenNodeIsRemoved: element
106
+ }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
107
+ }
108
+ }
109
+
110
+ var setEnabled = function (enable) {
111
+ setTimeout(function () {
112
+ if (enable)
113
+ $element.multiselect('enable');
114
+ else
115
+ $element.multiselect('disable');
116
+ });
117
+ };
118
+
119
+ if (allBindings.has('enable')) {
120
+ var enable = allBindings.get('enable');
121
+ if (ko.isObservable(enable)) {
122
+ ko.computed({
123
+ read: function () {
124
+ setEnabled(enable());
125
+ },
126
+ disposeWhenNodeIsRemoved: element
127
+ }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
128
+ } else {
129
+ setEnabled(enable);
130
+ }
131
+ }
132
+
133
+ if (allBindings.has('disable')) {
134
+ var disable = allBindings.get('disable');
135
+ if (ko.isObservable(disable)) {
136
+ ko.computed({
137
+ read: function () {
138
+ setEnabled(!disable());
139
+ },
140
+ disposeWhenNodeIsRemoved: element
141
+ }).extend({ rateLimit: 100, notifyWhenChangesStop: true });
142
+ } else {
143
+ setEnabled(!disable);
144
+ }
145
+ }
146
+
147
+ ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
148
+ $element.multiselect('destroy');
149
+ });
150
+ },
151
+
152
+ update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
153
+ var $element = $(element);
154
+ var config = ko.toJS(valueAccessor());
155
+
156
+ $element.multiselect('setOptions', config);
157
+ $element.multiselect('rebuild');
158
+ }
159
+ };
160
+ }
161
+
162
+ function forEach(array, callback) {
163
+ for (var index = 0; index < array.length; ++index) {
164
+ callback(array[index], index);
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Constructor to create a new multiselect using the given select.
170
+ *
171
+ * @param {jQuery} select
172
+ * @param {Object} options
173
+ * @returns {Multiselect}
174
+ */
175
+ function Multiselect(select, options) {
176
+
177
+ this.$select = $(select);
178
+
179
+ // Placeholder via data attributes
180
+ if (this.$select.attr("data-placeholder")) {
181
+ options.nonSelectedText = this.$select.data("placeholder");
182
+ }
183
+
184
+ this.options = this.mergeOptions($.extend({}, options, this.$select.data()));
185
+
186
+ // Initialization.
187
+ // We have to clone to create a new reference.
188
+ this.originalOptions = this.$select.clone()[0].options;
189
+ this.query = '';
190
+ this.searchTimeout = null;
191
+ this.lastToggledInput = null;
192
+
193
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
194
+ this.options.onChange = $.proxy(this.options.onChange, this);
195
+ this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this);
196
+ this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this);
197
+ this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this);
198
+ this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this);
199
+ this.options.onInitialized = $.proxy(this.options.onInitialized, this);
200
+
201
+ // Build select all if enabled.
202
+ this.buildContainer();
203
+ this.buildButton();
204
+ this.buildDropdown();
205
+ this.buildSelectAll();
206
+ this.buildDropdownOptions();
207
+ this.buildFilter();
208
+
209
+ this.updateButtonText();
210
+ this.updateSelectAll(true);
211
+
212
+ if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
213
+ this.disable();
214
+ }
215
+
216
+ this.$select.hide().after(this.$container);
217
+ this.options.onInitialized(this.$select, this.$container);
218
+ }
219
+
220
+ Multiselect.prototype = {
221
+
222
+ defaults: {
223
+ /**
224
+ * Default text function will either print 'None selected' in case no
225
+ * option is selected or a list of the selected options up to a length
226
+ * of 3 selected options.
227
+ *
228
+ * @param {jQuery} options
229
+ * @param {jQuery} select
230
+ * @returns {String}
231
+ */
232
+ buttonText: function(options, select) {
233
+ if (this.disabledText.length > 0
234
+ && (this.disableIfEmpty || select.prop('disabled'))
235
+ && options.length == 0) {
236
+
237
+ return this.disabledText;
238
+ }
239
+ else if (options.length === 0) {
240
+ return this.nonSelectedText;
241
+ }
242
+ else if (this.allSelectedText
243
+ && options.length === $('option', $(select)).length
244
+ && $('option', $(select)).length !== 1
245
+ && this.multiple) {
246
+
247
+ if (this.selectAllNumber) {
248
+ return this.allSelectedText + ' (' + options.length + ')';
249
+ }
250
+ else {
251
+ return this.allSelectedText;
252
+ }
253
+ }
254
+ else if (options.length > this.numberDisplayed) {
255
+ return options.length + ' ' + this.nSelectedText;
256
+ }
257
+ else {
258
+ var selected = '';
259
+ var delimiter = this.delimiterText;
260
+
261
+ options.each(function() {
262
+ var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
263
+ selected += label + delimiter;
264
+ });
265
+
266
+ return selected.substr(0, selected.length - 2);
267
+ }
268
+ },
269
+ /**
270
+ * Updates the title of the button similar to the buttonText function.
271
+ *
272
+ * @param {jQuery} options
273
+ * @param {jQuery} select
274
+ * @returns {@exp;selected@call;substr}
275
+ */
276
+ buttonTitle: function(options, select) {
277
+ if (options.length === 0) {
278
+ return this.nonSelectedText;
279
+ }
280
+ else {
281
+ var selected = '';
282
+ var delimiter = this.delimiterText;
283
+
284
+ options.each(function () {
285
+ var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text();
286
+ selected += label + delimiter;
287
+ });
288
+ return selected.substr(0, selected.length - 2);
289
+ }
290
+ },
291
+ /**
292
+ * Create a label.
293
+ *
294
+ * @param {jQuery} element
295
+ * @returns {String}
296
+ */
297
+ optionLabel: function(element){
298
+ return $(element).attr('label') || $(element).text();
299
+ },
300
+ /**
301
+ * Create a class.
302
+ *
303
+ * @param {jQuery} element
304
+ * @returns {String}
305
+ */
306
+ optionClass: function(element) {
307
+ return $(element).attr('class') || '';
308
+ },
309
+ /**
310
+ * Triggered on change of the multiselect.
311
+ *
312
+ * Not triggered when selecting/deselecting options manually.
313
+ *
314
+ * @param {jQuery} option
315
+ * @param {Boolean} checked
316
+ */
317
+ onChange : function(option, checked) {
318
+
319
+ },
320
+ /**
321
+ * Triggered when the dropdown is shown.
322
+ *
323
+ * @param {jQuery} event
324
+ */
325
+ onDropdownShow: function(event) {
326
+
327
+ },
328
+ /**
329
+ * Triggered when the dropdown is hidden.
330
+ *
331
+ * @param {jQuery} event
332
+ */
333
+ onDropdownHide: function(event) {
334
+
335
+ },
336
+ /**
337
+ * Triggered after the dropdown is shown.
338
+ *
339
+ * @param {jQuery} event
340
+ */
341
+ onDropdownShown: function(event) {
342
+
343
+ },
344
+ /**
345
+ * Triggered after the dropdown is hidden.
346
+ *
347
+ * @param {jQuery} event
348
+ */
349
+ onDropdownHidden: function(event) {
350
+
351
+ },
352
+ /**
353
+ * Triggered on select all.
354
+ */
355
+ onSelectAll: function(checked) {
356
+
357
+ },
358
+ /**
359
+ * Triggered after initializing.
360
+ *
361
+ * @param {jQuery} $select
362
+ * @param {jQuery} $container
363
+ */
364
+ onInitialized: function($select, $container) {
365
+
366
+ },
367
+ enableHTML: false,
368
+ buttonClass: 'btn btn-default',
369
+ inheritClass: false,
370
+ buttonWidth: 'auto',
371
+ buttonContainer: '<div class="btn-group" />',
372
+ dropRight: false,
373
+ dropUp: false,
374
+ selectedClass: 'active',
375
+ // Maximum height of the dropdown menu.
376
+ // If maximum height is exceeded a scrollbar will be displayed.
377
+ maxHeight: false,
378
+ checkboxName: false,
379
+ includeSelectAllOption: false,
380
+ includeSelectAllIfMoreThan: 0,
381
+ selectAllText: ' Select all',
382
+ selectAllValue: 'multiselect-all',
383
+ selectAllName: false,
384
+ selectAllNumber: true,
385
+ selectAllJustVisible: true,
386
+ enableFiltering: false,
387
+ enableCaseInsensitiveFiltering: false,
388
+ enableFullValueFiltering: false,
389
+ enableClickableOptGroups: false,
390
+ enableCollapsibelOptGroups: false,
391
+ filterPlaceholder: 'Search',
392
+ // possible options: 'text', 'value', 'both'
393
+ filterBehavior: 'text',
394
+ includeFilterClearBtn: true,
395
+ preventInputChangeEvent: false,
396
+ nonSelectedText: 'None selected',
397
+ nSelectedText: 'selected',
398
+ allSelectedText: 'All selected',
399
+ numberDisplayed: 3,
400
+ disableIfEmpty: false,
401
+ disabledText: '',
402
+ delimiterText: ', ',
403
+ templates: {
404
+ button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"><span class="multiselect-selected-text"></span> <b class="caret"></b></button>',
405
+ ul: '<ul class="multiselect-container dropdown-menu"></ul>',
406
+ 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>',
407
+ 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>',
408
+ li: '<li><a tabindex="0"><label></label></a></li>',
409
+ divider: '<li class="multiselect-item divider"></li>',
410
+ liGroup: '<li class="multiselect-item multiselect-group"><label></label></li>'
411
+ }
412
+ },
413
+
414
+ constructor: Multiselect,
415
+
416
+ /**
417
+ * Builds the container of the multiselect.
418
+ */
419
+ buildContainer: function() {
420
+ this.$container = $(this.options.buttonContainer);
421
+ this.$container.on('show.bs.dropdown', this.options.onDropdownShow);
422
+ this.$container.on('hide.bs.dropdown', this.options.onDropdownHide);
423
+ this.$container.on('shown.bs.dropdown', this.options.onDropdownShown);
424
+ this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden);
425
+ },
426
+
427
+ /**
428
+ * Builds the button of the multiselect.
429
+ */
430
+ buildButton: function() {
431
+ this.$button = $(this.options.templates.button).addClass(this.options.buttonClass);
432
+ if (this.$select.attr('class') && this.options.inheritClass) {
433
+ this.$button.addClass(this.$select.attr('class'));
434
+ }
435
+ // Adopt active state.
436
+ if (this.$select.prop('disabled')) {
437
+ this.disable();
438
+ }
439
+ else {
440
+ this.enable();
441
+ }
442
+
443
+ // Manually add button width if set.
444
+ if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') {
445
+ this.$button.css({
446
+ 'width' : this.options.buttonWidth,
447
+ 'overflow' : 'hidden',
448
+ 'text-overflow' : 'ellipsis'
449
+ });
450
+ this.$container.css({
451
+ 'width': this.options.buttonWidth
452
+ });
453
+ }
454
+
455
+ // Keep the tab index from the select.
456
+ var tabindex = this.$select.attr('tabindex');
457
+ if (tabindex) {
458
+ this.$button.attr('tabindex', tabindex);
459
+ }
460
+
461
+ this.$container.prepend(this.$button);
462
+ },
463
+
464
+ /**
465
+ * Builds the ul representing the dropdown menu.
466
+ */
467
+ buildDropdown: function() {
468
+
469
+ // Build ul.
470
+ this.$ul = $(this.options.templates.ul);
471
+
472
+ if (this.options.dropRight) {
473
+ this.$ul.addClass('pull-right');
474
+ }
475
+
476
+ // Set max height of dropdown menu to activate auto scrollbar.
477
+ if (this.options.maxHeight) {
478
+ // TODO: Add a class for this option to move the css declarations.
479
+ this.$ul.css({
480
+ 'max-height': this.options.maxHeight + 'px',
481
+ 'overflow-y': 'auto',
482
+ 'overflow-x': 'hidden'
483
+ });
484
+ }
485
+
486
+ if (this.options.dropUp) {
487
+
488
+ 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));
489
+ var moveCalc = height + 34;
490
+
491
+ this.$ul.css({
492
+ 'max-height': height + 'px',
493
+ 'overflow-y': 'auto',
494
+ 'overflow-x': 'hidden',
495
+ 'margin-top': "-" + moveCalc + 'px'
496
+ });
497
+ }
498
+
499
+ this.$container.append(this.$ul);
500
+ },
501
+
502
+ /**
503
+ * Build the dropdown options and binds all nessecary events.
504
+ *
505
+ * Uses createDivider and createOptionValue to create the necessary options.
506
+ */
507
+ buildDropdownOptions: function() {
508
+
509
+ this.$select.children().each($.proxy(function(index, element) {
510
+
511
+ var $element = $(element);
512
+ // Support optgroups and options without a group simultaneously.
513
+ var tag = $element.prop('tagName')
514
+ .toLowerCase();
515
+
516
+ if ($element.prop('value') === this.options.selectAllValue) {
517
+ return;
518
+ }
519
+
520
+ if (tag === 'optgroup') {
521
+ this.createOptgroup(element);
522
+ }
523
+ else if (tag === 'option') {
524
+
525
+ if ($element.data('role') === 'divider') {
526
+ this.createDivider();
527
+ }
528
+ else {
529
+ this.createOptionValue(element);
530
+ }
531
+
532
+ }
533
+
534
+ // Other illegal tags will be ignored.
535
+ }, this));
536
+
537
+ // Bind the change event on the dropdown elements.
538
+ $('li input', this.$ul).on('change', $.proxy(function(event) {
539
+ var $target = $(event.target);
540
+
541
+ var checked = $target.prop('checked') || false;
542
+ var isSelectAllOption = $target.val() === this.options.selectAllValue;
543
+
544
+ // Apply or unapply the configured selected class.
545
+ if (this.options.selectedClass) {
546
+ if (checked) {
547
+ $target.closest('li')
548
+ .addClass(this.options.selectedClass);
549
+ }
550
+ else {
551
+ $target.closest('li')
552
+ .removeClass(this.options.selectedClass);
553
+ }
554
+ }
555
+
556
+ // Get the corresponding option.
557
+ var value = $target.val();
558
+ var $option = this.getOptionByValue(value);
559
+
560
+ var $optionsNotThis = $('option', this.$select).not($option);
561
+ var $checkboxesNotThis = $('input', this.$container).not($target);
562
+
563
+ if (isSelectAllOption) {
564
+ if (checked) {
565
+ this.selectAll(this.options.selectAllJustVisible);
566
+ }
567
+ else {
568
+ this.deselectAll(this.options.selectAllJustVisible);
569
+ }
570
+ }
571
+ else {
572
+ if (checked) {
573
+ $option.prop('selected', true);
574
+
575
+ if (this.options.multiple) {
576
+ // Simply select additional option.
577
+ $option.prop('selected', true);
578
+ }
579
+ else {
580
+ // Unselect all other options and corresponding checkboxes.
581
+ if (this.options.selectedClass) {
582
+ $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass);
583
+ }
584
+
585
+ $($checkboxesNotThis).prop('checked', false);
586
+ $optionsNotThis.prop('selected', false);
587
+
588
+ // It's a single selection, so close.
589
+ this.$button.click();
590
+ }
591
+
592
+ if (this.options.selectedClass === "active") {
593
+ $optionsNotThis.closest("a").css("outline", "");
594
+ }
595
+ }
596
+ else {
597
+ // Unselect option.
598
+ $option.prop('selected', false);
599
+ }
600
+
601
+ // To prevent select all from firing onChange: #575
602
+ this.options.onChange($option, checked);
603
+ }
604
+
605
+ this.$select.change();
606
+
607
+ this.updateButtonText();
608
+ this.updateSelectAll();
609
+
610
+ if(this.options.preventInputChangeEvent) {
611
+ return false;
612
+ }
613
+ }, this));
614
+
615
+ $('li a', this.$ul).on('mousedown', function(e) {
616
+ if (e.shiftKey) {
617
+ // Prevent selecting text by Shift+click
618
+ return false;
619
+ }
620
+ });
621
+
622
+ $('li a', this.$ul).on('touchstart click', $.proxy(function(event) {
623
+ event.stopPropagation();
624
+
625
+ var $target = $(event.target);
626
+
627
+ if (event.shiftKey && this.options.multiple) {
628
+ if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431)
629
+ event.preventDefault();
630
+ $target = $target.find("input");
631
+ $target.prop("checked", !$target.prop("checked"));
632
+ }
633
+ var checked = $target.prop('checked') || false;
634
+
635
+ if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range
636
+ var from = $target.closest("li").index();
637
+ var to = this.lastToggledInput.closest("li").index();
638
+
639
+ if (from > to) { // Swap the indices
640
+ var tmp = to;
641
+ to = from;
642
+ from = tmp;
643
+ }
644
+
645
+ // Make sure we grab all elements since slice excludes the last index
646
+ ++to;
647
+
648
+ // Change the checkboxes and underlying options
649
+ var range = this.$ul.find("li").slice(from, to).find("input");
650
+
651
+ range.prop('checked', checked);
652
+
653
+ if (this.options.selectedClass) {
654
+ range.closest('li')
655
+ .toggleClass(this.options.selectedClass, checked);
656
+ }
657
+
658
+ for (var i = 0, j = range.length; i < j; i++) {
659
+ var $checkbox = $(range[i]);
660
+
661
+ var $option = this.getOptionByValue($checkbox.val());
662
+
663
+ $option.prop('selected', checked);
664
+ }
665
+ }
666
+
667
+ // Trigger the select "change" event
668
+ $target.trigger("change");
669
+ }
670
+
671
+ // Remembers last clicked option
672
+ if($target.is("input") && !$target.closest("li").is(".multiselect-item")){
673
+ this.lastToggledInput = $target;
674
+ }
675
+
676
+ $target.blur();
677
+ }, this));
678
+
679
+ // Keyboard support.
680
+ this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) {
681
+ if ($('input[type="text"]', this.$container).is(':focus')) {
682
+ return;
683
+ }
684
+
685
+ if (event.keyCode === 9 && this.$container.hasClass('open')) {
686
+ this.$button.click();
687
+ }
688
+ else {
689
+ var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible");
690
+
691
+ if (!$items.length) {
692
+ return;
693
+ }
694
+
695
+ var index = $items.index($items.filter(':focus'));
696
+
697
+ // Navigation up.
698
+ if (event.keyCode === 38 && index > 0) {
699
+ index--;
700
+ }
701
+ // Navigate down.
702
+ else if (event.keyCode === 40 && index < $items.length - 1) {
703
+ index++;
704
+ }
705
+ else if (!~index) {
706
+ index = 0;
707
+ }
708
+
709
+ var $current = $items.eq(index);
710
+ $current.focus();
711
+
712
+ if (event.keyCode === 32 || event.keyCode === 13) {
713
+ var $checkbox = $current.find('input');
714
+
715
+ $checkbox.prop("checked", !$checkbox.prop("checked"));
716
+ $checkbox.change();
717
+ }
718
+
719
+ event.stopPropagation();
720
+ event.preventDefault();
721
+ }
722
+ }, this));
723
+
724
+ if(this.options.enableClickableOptGroups && this.options.multiple) {
725
+ $('li.multiselect-group', this.$ul).on('click', $.proxy(function(event) {
726
+ event.stopPropagation();
727
+ console.log('test');
728
+ var group = $(event.target).parent();
729
+
730
+ // Search all option in optgroup
731
+ var $options = group.nextUntil('li.multiselect-group');
732
+ var $visibleOptions = $options.filter(":visible:not(.disabled)");
733
+
734
+ // check or uncheck items
735
+ var allChecked = true;
736
+ var optionInputs = $visibleOptions.find('input');
737
+ var values = [];
738
+
739
+ optionInputs.each(function() {
740
+ allChecked = allChecked && $(this).prop('checked');
741
+ values.push($(this).val());
742
+ });
743
+
744
+ if (!allChecked) {
745
+ this.select(values, false);
746
+ }
747
+ else {
748
+ this.deselect(values, false);
749
+ }
750
+
751
+ this.options.onChange(optionInputs, !allChecked);
752
+ }, this));
753
+ }
754
+
755
+ if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
756
+ $("li.multiselect-group input", this.$ul).off();
757
+ $("li.multiselect-group", this.$ul).siblings().not("li.multiselect-group, li.multiselect-all", this.$ul).each( function () {
758
+ $(this).toggleClass('hidden', true);
759
+ });
760
+
761
+ $("li.multiselect-group", this.$ul).on("click", $.proxy(function(group) {
762
+ group.stopPropagation();
763
+ }, this));
764
+
765
+ $("li.multiselect-group > a > b", this.$ul).on("click", $.proxy(function(t) {
766
+ t.stopPropagation();
767
+ var n = $(t.target).closest('li');
768
+ var r = n.nextUntil("li.multiselect-group");
769
+ var i = true;
770
+
771
+ r.each(function() {
772
+ i = i && $(this).hasClass('hidden');
773
+ });
774
+
775
+ r.toggleClass('hidden', !i);
776
+ }, this));
777
+
778
+ $("li.multiselect-group > a > input", this.$ul).on("change", $.proxy(function(t) {
779
+ t.stopPropagation();
780
+ var n = $(t.target).closest('li');
781
+ var r = n.nextUntil("li.multiselect-group", ':not(.disabled)');
782
+ var s = r.find("input");
783
+
784
+ var i = true;
785
+ s.each(function() {
786
+ i = i && $(this).prop("checked");
787
+ });
788
+
789
+ s.prop("checked", !i).trigger("change");
790
+ }, this));
791
+
792
+ // Set the initial selection state of the groups.
793
+ $('li.multiselect-group', this.$ul).each(function() {
794
+ var r = $(this).nextUntil("li.multiselect-group", ':not(.disabled)');
795
+ var s = r.find("input");
796
+
797
+ var i = true;
798
+ s.each(function() {
799
+ i = i && $(this).prop("checked");
800
+ });
801
+
802
+ $(this).find('input').prop("checked", i);
803
+ });
804
+
805
+ // Update the group checkbox based on new selections among the
806
+ // corresponding children.
807
+ $("li input", this.$ul).on("change", $.proxy(function(t) {
808
+ t.stopPropagation();
809
+ var n = $(t.target).closest('li');
810
+ var r1 = n.prevUntil("li.multiselect-group", ':not(.disabled)');
811
+ var r2 = n.nextUntil("li.multiselect-group", ':not(.disabled)');
812
+ var s1 = r1.find("input");
813
+ var s2 = r2.find("input");
814
+
815
+ var i = $(t.target).prop('checked');
816
+ s1.each(function() {
817
+ i = i && $(this).prop("checked");
818
+ });
819
+
820
+ s2.each(function() {
821
+ i = i && $(this).prop("checked");
822
+ });
823
+
824
+ n.prevAll('.multiselect-group').find('input').prop('checked', i);
825
+ }, this));
826
+
827
+ $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea');
828
+ $("li.multiselect-group > a, li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px');
829
+ $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px');
830
+ }
831
+ },
832
+
833
+ /**
834
+ * Create an option using the given select option.
835
+ *
836
+ * @param {jQuery} element
837
+ */
838
+ createOptionValue: function(element) {
839
+ var $element = $(element);
840
+ if ($element.is(':selected')) {
841
+ $element.prop('selected', true);
842
+ }
843
+
844
+ // Support the label attribute on options.
845
+ var label = this.options.optionLabel(element);
846
+ var classes = this.options.optionClass(element);
847
+ var value = $element.val();
848
+ var inputType = this.options.multiple ? "checkbox" : "radio";
849
+
850
+ var $li = $(this.options.templates.li);
851
+ var $label = $('label', $li);
852
+ $label.addClass(inputType);
853
+ $li.addClass(classes);
854
+
855
+ if (this.options.enableHTML) {
856
+ $label.html(" " + label);
857
+ }
858
+ else {
859
+ $label.text(" " + label);
860
+ }
861
+
862
+ var $checkbox = $('<input/>').attr('type', inputType);
863
+
864
+ if (this.options.checkboxName) {
865
+ $checkbox.attr('name', this.options.checkboxName);
866
+ }
867
+ $label.prepend($checkbox);
868
+
869
+ var selected = $element.prop('selected') || false;
870
+ $checkbox.val(value);
871
+
872
+ if (value === this.options.selectAllValue) {
873
+ $li.addClass("multiselect-item multiselect-all");
874
+ $checkbox.parent().parent()
875
+ .addClass('multiselect-all');
876
+ }
877
+
878
+ $label.attr('title', $element.attr('title'));
879
+
880
+ this.$ul.append($li);
881
+
882
+ if ($element.is(':disabled')) {
883
+ $checkbox.attr('disabled', 'disabled')
884
+ .prop('disabled', true)
885
+ .closest('a')
886
+ .attr("tabindex", "-1")
887
+ .closest('li')
888
+ .addClass('disabled');
889
+ }
890
+
891
+ $checkbox.prop('checked', selected);
892
+
893
+ if (selected && this.options.selectedClass) {
894
+ $checkbox.closest('li')
895
+ .addClass(this.options.selectedClass);
896
+ }
897
+ },
898
+
899
+ /**
900
+ * Creates a divider using the given select option.
901
+ *
902
+ * @param {jQuery} element
903
+ */
904
+ createDivider: function(element) {
905
+ var $divider = $(this.options.templates.divider);
906
+ this.$ul.append($divider);
907
+ },
908
+
909
+ /**
910
+ * Creates an optgroup.
911
+ *
912
+ * @param {jQuery} group
913
+ */
914
+ createOptgroup: function(group) {
915
+ if (this.options.enableCollapsibleOptGroups && this.options.multiple) {
916
+ var label = $(group).attr("label");
917
+ var value = $(group).attr("value");
918
+ var r = $('<li class="multiselect-item multiselect-group"><a href="javascript:void(0);"><input type="checkbox" value="' + value + '"/><b> ' + label + '<b class="caret"></b></b></a></li>');
919
+
920
+ if (this.options.enableClickableOptGroups) {
921
+ r.addClass("multiselect-group-clickable")
922
+ }
923
+ this.$ul.append(r);
924
+ if ($(group).is(":disabled")) {
925
+ r.addClass("disabled")
926
+ }
927
+ $("option", group).each($.proxy(function($, group) {
928
+ this.createOptionValue(group)
929
+ }, this))
930
+ }
931
+ else {
932
+ var groupName = $(group).prop('label');
933
+
934
+ // Add a header for the group.
935
+ var $li = $(this.options.templates.liGroup);
936
+
937
+ if (this.options.enableHTML) {
938
+ $('label', $li).html(groupName);
939
+ }
940
+ else {
941
+ $('label', $li).text(groupName);
942
+ }
943
+
944
+ if (this.options.enableClickableOptGroups) {
945
+ $li.addClass('multiselect-group-clickable');
946
+ }
947
+
948
+ this.$ul.append($li);
949
+
950
+ if ($(group).is(':disabled')) {
951
+ $li.addClass('disabled');
952
+ }
953
+
954
+ // Add the options of the group.
955
+ $('option', group).each($.proxy(function(index, element) {
956
+ this.createOptionValue(element);
957
+ }, this));
958
+ }
959
+ },
960
+
961
+ /**
962
+ * Build the select all.
963
+ *
964
+ * Checks if a select all has already been created.
965
+ */
966
+ buildSelectAll: function() {
967
+ if (typeof this.options.selectAllValue === 'number') {
968
+ this.options.selectAllValue = this.options.selectAllValue.toString();
969
+ }
970
+
971
+ var alreadyHasSelectAll = this.hasSelectAll();
972
+
973
+ if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple
974
+ && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) {
975
+
976
+ // Check whether to add a divider after the select all.
977
+ if (this.options.includeSelectAllDivider) {
978
+ this.$ul.prepend($(this.options.templates.divider));
979
+ }
980
+
981
+ var $li = $(this.options.templates.li);
982
+ $('label', $li).addClass("checkbox");
983
+
984
+ if (this.options.enableHTML) {
985
+ $('label', $li).html(" " + this.options.selectAllText);
986
+ }
987
+ else {
988
+ $('label', $li).text(" " + this.options.selectAllText);
989
+ }
990
+
991
+ if (this.options.selectAllName) {
992
+ $('label', $li).prepend('<input type="checkbox" name="' + this.options.selectAllName + '" />');
993
+ }
994
+ else {
995
+ $('label', $li).prepend('<input type="checkbox" />');
996
+ }
997
+
998
+ var $checkbox = $('input', $li);
999
+ $checkbox.val(this.options.selectAllValue);
1000
+
1001
+ $li.addClass("multiselect-item multiselect-all");
1002
+ $checkbox.parent().parent()
1003
+ .addClass('multiselect-all');
1004
+
1005
+ this.$ul.prepend($li);
1006
+
1007
+ $checkbox.prop('checked', false);
1008
+ }
1009
+ },
1010
+
1011
+ /**
1012
+ * Builds the filter.
1013
+ */
1014
+ buildFilter: function() {
1015
+
1016
+ // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
1017
+ if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
1018
+ var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
1019
+
1020
+ if (this.$select.find('option').length >= enableFilterLength) {
1021
+
1022
+ this.$filter = $(this.options.templates.filter);
1023
+ $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder);
1024
+
1025
+ // Adds optional filter clear button
1026
+ if(this.options.includeFilterClearBtn){
1027
+ var clearBtn = $(this.options.templates.filterClearBtn);
1028
+ clearBtn.on('click', $.proxy(function(event){
1029
+ clearTimeout(this.searchTimeout);
1030
+ this.$filter.find('.multiselect-search').val('');
1031
+ $('li', this.$ul).show().removeClass("filter-hidden");
1032
+ this.updateSelectAll();
1033
+ }, this));
1034
+ this.$filter.find('.input-group').append(clearBtn);
1035
+ }
1036
+
1037
+ this.$ul.prepend(this.$filter);
1038
+
1039
+ this.$filter.val(this.query).on('click', function(event) {
1040
+ event.stopPropagation();
1041
+ }).on('input keydown', $.proxy(function(event) {
1042
+ // Cancel enter key default behaviour
1043
+ if (event.which === 13) {
1044
+ event.preventDefault();
1045
+ }
1046
+
1047
+ // This is useful to catch "keydown" events after the browser has updated the control.
1048
+ clearTimeout(this.searchTimeout);
1049
+
1050
+ this.searchTimeout = this.asyncFunction($.proxy(function() {
1051
+
1052
+ if (this.query !== event.target.value) {
1053
+ this.query = event.target.value;
1054
+
1055
+ var currentGroup, currentGroupVisible;
1056
+ $.each($('li', this.$ul), $.proxy(function(index, element) {
1057
+ var value = $('input', element).length > 0 ? $('input', element).val() : "";
1058
+ var text = $('label', element).text();
1059
+
1060
+ var filterCandidate = '';
1061
+ if ((this.options.filterBehavior === 'text')) {
1062
+ filterCandidate = text;
1063
+ }
1064
+ else if ((this.options.filterBehavior === 'value')) {
1065
+ filterCandidate = value;
1066
+ }
1067
+ else if (this.options.filterBehavior === 'both') {
1068
+ filterCandidate = text + '\n' + value;
1069
+ }
1070
+
1071
+ if (value !== this.options.selectAllValue && text) {
1072
+
1073
+ // By default lets assume that element is not
1074
+ // interesting for this search.
1075
+ var showElement = false;
1076
+
1077
+ if (this.options.enableCaseInsensitiveFiltering) {
1078
+ filterCandidate = filterCandidate.toLowerCase();
1079
+ this.query = this.query.toLowerCase();
1080
+ }
1081
+
1082
+ if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') {
1083
+ var valueToMatch = filterCandidate.trim().substring(0, this.query.length);
1084
+ if (this.query.indexOf(valueToMatch) > -1) {
1085
+ showElement = true;
1086
+ }
1087
+ }
1088
+ else if (filterCandidate.indexOf(this.query) > -1) {
1089
+ showElement = true;
1090
+ }
1091
+
1092
+ // Toggle current element (group or group item) according to showElement boolean.
1093
+ $(element).toggle(showElement).toggleClass('filter-hidden', !showElement);
1094
+
1095
+ // Differentiate groups and group items.
1096
+ if ($(element).hasClass('multiselect-group')) {
1097
+ // Remember group status.
1098
+ currentGroup = element;
1099
+ currentGroupVisible = showElement;
1100
+ }
1101
+ else {
1102
+ // Show group name when at least one of its items is visible.
1103
+ if (showElement) {
1104
+ $(currentGroup).show().removeClass('filter-hidden');
1105
+ }
1106
+
1107
+ // Show all group items when group name satisfies filter.
1108
+ if (!showElement && currentGroupVisible) {
1109
+ $(element).show().removeClass('filter-hidden');
1110
+ }
1111
+ }
1112
+ }
1113
+ }, this));
1114
+ }
1115
+
1116
+ this.updateSelectAll();
1117
+ }, this), 300, this);
1118
+ }, this));
1119
+ }
1120
+ }
1121
+ },
1122
+
1123
+ /**
1124
+ * Unbinds the whole plugin.
1125
+ */
1126
+ destroy: function() {
1127
+ this.$container.remove();
1128
+ this.$select.show();
1129
+ this.$select.data('multiselect', null);
1130
+ },
1131
+
1132
+ /**
1133
+ * Refreshs the multiselect based on the selected options of the select.
1134
+ */
1135
+ refresh: function () {
1136
+ var inputs = $.map($('li input', this.$ul), $);
1137
+
1138
+ $('option', this.$select).each($.proxy(function (index, element) {
1139
+ var $elem = $(element);
1140
+ var value = $elem.val();
1141
+ var $input;
1142
+ for (var i = inputs.length; 0 < i--; /**/) {
1143
+ if (value !== ($input = inputs[i]).val())
1144
+ continue; // wrong li
1145
+
1146
+ if ($elem.is(':selected')) {
1147
+ $input.prop('checked', true);
1148
+
1149
+ if (this.options.selectedClass) {
1150
+ $input.closest('li')
1151
+ .addClass(this.options.selectedClass);
1152
+ }
1153
+ }
1154
+ else {
1155
+ $input.prop('checked', false);
1156
+
1157
+ if (this.options.selectedClass) {
1158
+ $input.closest('li')
1159
+ .removeClass(this.options.selectedClass);
1160
+ }
1161
+ }
1162
+
1163
+ if ($elem.is(":disabled")) {
1164
+ $input.attr('disabled', 'disabled')
1165
+ .prop('disabled', true)
1166
+ .closest('li')
1167
+ .addClass('disabled');
1168
+ }
1169
+ else {
1170
+ $input.prop('disabled', false)
1171
+ .closest('li')
1172
+ .removeClass('disabled');
1173
+ }
1174
+ break; // assumes unique values
1175
+ }
1176
+ }, this));
1177
+
1178
+ this.updateButtonText();
1179
+ this.updateSelectAll();
1180
+ },
1181
+
1182
+ /**
1183
+ * Select all options of the given values.
1184
+ *
1185
+ * If triggerOnChange is set to true, the on change event is triggered if
1186
+ * and only if one value is passed.
1187
+ *
1188
+ * @param {Array} selectValues
1189
+ * @param {Boolean} triggerOnChange
1190
+ */
1191
+ select: function(selectValues, triggerOnChange) {
1192
+ if(!$.isArray(selectValues)) {
1193
+ selectValues = [selectValues];
1194
+ }
1195
+
1196
+ for (var i = 0; i < selectValues.length; i++) {
1197
+ var value = selectValues[i];
1198
+
1199
+ if (value === null || value === undefined) {
1200
+ continue;
1201
+ }
1202
+
1203
+ var $option = this.getOptionByValue(value);
1204
+ var $checkbox = this.getInputByValue(value);
1205
+
1206
+ if($option === undefined || $checkbox === undefined) {
1207
+ continue;
1208
+ }
1209
+
1210
+ if (!this.options.multiple) {
1211
+ this.deselectAll(false);
1212
+ }
1213
+
1214
+ if (this.options.selectedClass) {
1215
+ $checkbox.closest('li')
1216
+ .addClass(this.options.selectedClass);
1217
+ }
1218
+
1219
+ $checkbox.prop('checked', true);
1220
+ $option.prop('selected', true);
1221
+
1222
+ if (triggerOnChange) {
1223
+ this.options.onChange($option, true);
1224
+ }
1225
+ }
1226
+
1227
+ this.updateButtonText();
1228
+ this.updateSelectAll();
1229
+ },
1230
+
1231
+ /**
1232
+ * Clears all selected items.
1233
+ */
1234
+ clearSelection: function () {
1235
+ this.deselectAll(false);
1236
+ this.updateButtonText();
1237
+ this.updateSelectAll();
1238
+ },
1239
+
1240
+ /**
1241
+ * Deselects all options of the given values.
1242
+ *
1243
+ * If triggerOnChange is set to true, the on change event is triggered, if
1244
+ * and only if one value is passed.
1245
+ *
1246
+ * @param {Array} deselectValues
1247
+ * @param {Boolean} triggerOnChange
1248
+ */
1249
+ deselect: function(deselectValues, triggerOnChange) {
1250
+ if(!$.isArray(deselectValues)) {
1251
+ deselectValues = [deselectValues];
1252
+ }
1253
+
1254
+ for (var i = 0; i < deselectValues.length; i++) {
1255
+ var value = deselectValues[i];
1256
+
1257
+ if (value === null || value === undefined) {
1258
+ continue;
1259
+ }
1260
+
1261
+ var $option = this.getOptionByValue(value);
1262
+ var $checkbox = this.getInputByValue(value);
1263
+
1264
+ if($option === undefined || $checkbox === undefined) {
1265
+ continue;
1266
+ }
1267
+
1268
+ if (this.options.selectedClass) {
1269
+ $checkbox.closest('li')
1270
+ .removeClass(this.options.selectedClass);
1271
+ }
1272
+
1273
+ $checkbox.prop('checked', false);
1274
+ $option.prop('selected', false);
1275
+
1276
+ if (triggerOnChange) {
1277
+ this.options.onChange($option, false);
1278
+ }
1279
+ }
1280
+
1281
+ this.updateButtonText();
1282
+ this.updateSelectAll();
1283
+ },
1284
+
1285
+ /**
1286
+ * Selects all enabled & visible options.
1287
+ *
1288
+ * If justVisible is true or not specified, only visible options are selected.
1289
+ *
1290
+ * @param {Boolean} justVisible
1291
+ * @param {Boolean} triggerOnSelectAll
1292
+ */
1293
+ selectAll: function (justVisible, triggerOnSelectAll) {
1294
+ justVisible = (this.options.enableCollapsibleOptGroups && this.options.multiple) ? false : justVisible;
1295
+
1296
+ var justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1297
+ var allCheckboxes = $("li input[type='checkbox']:enabled", this.$ul);
1298
+ var visibleCheckboxes = allCheckboxes.filter(":visible");
1299
+ var allCheckboxesCount = allCheckboxes.length;
1300
+ var visibleCheckboxesCount = visibleCheckboxes.length;
1301
+
1302
+ if(justVisible) {
1303
+ visibleCheckboxes.prop('checked', true);
1304
+ $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").addClass(this.options.selectedClass);
1305
+ }
1306
+ else {
1307
+ allCheckboxes.prop('checked', true);
1308
+ $("li:not(.divider):not(.disabled)", this.$ul).addClass(this.options.selectedClass);
1309
+ }
1310
+
1311
+ if (allCheckboxesCount === visibleCheckboxesCount || justVisible === false) {
1312
+ $("option:not([data-role='divider']):enabled", this.$select).prop('selected', true);
1313
+ }
1314
+ else {
1315
+ var values = visibleCheckboxes.map(function() {
1316
+ return $(this).val();
1317
+ }).get();
1318
+
1319
+ $("option:enabled", this.$select).filter(function(index) {
1320
+ return $.inArray($(this).val(), values) !== -1;
1321
+ }).prop('selected', true);
1322
+ }
1323
+
1324
+ if (triggerOnSelectAll) {
1325
+ this.options.onSelectAll();
1326
+ }
1327
+ },
1328
+
1329
+ /**
1330
+ * Deselects all options.
1331
+ *
1332
+ * If justVisible is true or not specified, only visible options are deselected.
1333
+ *
1334
+ * @param {Boolean} justVisible
1335
+ */
1336
+ deselectAll: function (justVisible) {
1337
+ justVisible = (this.options.enableCollapsibleOptGroups && this.options.multiple) ? false : justVisible;
1338
+ justVisible = typeof justVisible === 'undefined' ? true : justVisible;
1339
+
1340
+ if(justVisible) {
1341
+ var visibleCheckboxes = $("li input[type='checkbox']:not(:disabled)", this.$ul).filter(":visible");
1342
+ visibleCheckboxes.prop('checked', false);
1343
+
1344
+ var values = visibleCheckboxes.map(function() {
1345
+ return $(this).val();
1346
+ }).get();
1347
+
1348
+ $("option:enabled", this.$select).filter(function(index) {
1349
+ return $.inArray($(this).val(), values) !== -1;
1350
+ }).prop('selected', false);
1351
+
1352
+ if (this.options.selectedClass) {
1353
+ $("li:not(.divider):not(.disabled)", this.$ul).filter(":visible").removeClass(this.options.selectedClass);
1354
+ }
1355
+ }
1356
+ else {
1357
+ $("li input[type='checkbox']:enabled", this.$ul).prop('checked', false);
1358
+ $("option:enabled", this.$select).prop('selected', false);
1359
+
1360
+ if (this.options.selectedClass) {
1361
+ $("li:not(.divider):not(.disabled)", this.$ul).removeClass(this.options.selectedClass);
1362
+ }
1363
+ }
1364
+ },
1365
+
1366
+ /**
1367
+ * Rebuild the plugin.
1368
+ *
1369
+ * Rebuilds the dropdown, the filter and the select all option.
1370
+ */
1371
+ rebuild: function() {
1372
+ this.$ul.html('');
1373
+
1374
+ // Important to distinguish between radios and checkboxes.
1375
+ this.options.multiple = this.$select.attr('multiple') === "multiple";
1376
+
1377
+ this.buildSelectAll();
1378
+ this.buildDropdownOptions();
1379
+ this.buildFilter();
1380
+
1381
+ this.updateButtonText();
1382
+ this.updateSelectAll(true);
1383
+
1384
+ if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) {
1385
+ this.disable();
1386
+ }
1387
+ else {
1388
+ this.enable();
1389
+ }
1390
+
1391
+ if (this.options.dropRight) {
1392
+ this.$ul.addClass('pull-right');
1393
+ }
1394
+ },
1395
+
1396
+ /**
1397
+ * The provided data will be used to build the dropdown.
1398
+ */
1399
+ dataprovider: function(dataprovider) {
1400
+
1401
+ var groupCounter = 0;
1402
+ var $select = this.$select.empty();
1403
+
1404
+ $.each(dataprovider, function (index, option) {
1405
+ var $tag;
1406
+
1407
+ if ($.isArray(option.children)) { // create optiongroup tag
1408
+ groupCounter++;
1409
+
1410
+ $tag = $('<optgroup/>').attr({
1411
+ label: option.label || 'Group ' + groupCounter,
1412
+ disabled: !!option.disabled
1413
+ });
1414
+
1415
+ forEach(option.children, function(subOption) { // add children option tags
1416
+ $tag.append($('<option/>').attr({
1417
+ value: subOption.value,
1418
+ label: subOption.label || subOption.value,
1419
+ title: subOption.title,
1420
+ selected: !!subOption.selected,
1421
+ disabled: !!subOption.disabled
1422
+ }));
1423
+ });
1424
+ }
1425
+ else {
1426
+ $tag = $('<option/>').attr({
1427
+ value: option.value,
1428
+ label: option.label || option.value,
1429
+ title: option.title,
1430
+ class: option.class,
1431
+ selected: !!option.selected,
1432
+ disabled: !!option.disabled
1433
+ });
1434
+ $tag.text(option.label || option.value);
1435
+ }
1436
+
1437
+ $select.append($tag);
1438
+ });
1439
+
1440
+ this.rebuild();
1441
+ },
1442
+
1443
+ /**
1444
+ * Enable the multiselect.
1445
+ */
1446
+ enable: function() {
1447
+ this.$select.prop('disabled', false);
1448
+ this.$button.prop('disabled', false)
1449
+ .removeClass('disabled');
1450
+ },
1451
+
1452
+ /**
1453
+ * Disable the multiselect.
1454
+ */
1455
+ disable: function() {
1456
+ this.$select.prop('disabled', true);
1457
+ this.$button.prop('disabled', true)
1458
+ .addClass('disabled');
1459
+ },
1460
+
1461
+ /**
1462
+ * Set the options.
1463
+ *
1464
+ * @param {Array} options
1465
+ */
1466
+ setOptions: function(options) {
1467
+ this.options = this.mergeOptions(options);
1468
+ },
1469
+
1470
+ /**
1471
+ * Merges the given options with the default options.
1472
+ *
1473
+ * @param {Array} options
1474
+ * @returns {Array}
1475
+ */
1476
+ mergeOptions: function(options) {
1477
+ return $.extend(true, {}, this.defaults, this.options, options);
1478
+ },
1479
+
1480
+ /**
1481
+ * Checks whether a select all checkbox is present.
1482
+ *
1483
+ * @returns {Boolean}
1484
+ */
1485
+ hasSelectAll: function() {
1486
+ return $('li.multiselect-all', this.$ul).length > 0;
1487
+ },
1488
+
1489
+ /**
1490
+ * Updates the select all checkbox based on the currently displayed and selected checkboxes.
1491
+ */
1492
+ updateSelectAll: function(notTriggerOnSelectAll) {
1493
+ if (this.hasSelectAll()) {
1494
+ var allBoxes = $("li:not(.multiselect-item):not(.filter-hidden) input:enabled", this.$ul);
1495
+ var allBoxesLength = allBoxes.length;
1496
+ var checkedBoxesLength = allBoxes.filter(":checked").length;
1497
+ var selectAllLi = $("li.multiselect-all", this.$ul);
1498
+ var selectAllInput = selectAllLi.find("input");
1499
+
1500
+ if (checkedBoxesLength > 0 && checkedBoxesLength === allBoxesLength) {
1501
+ selectAllInput.prop("checked", true);
1502
+ selectAllLi.addClass(this.options.selectedClass);
1503
+ this.options.onSelectAll(true);
1504
+ }
1505
+ else {
1506
+ selectAllInput.prop("checked", false);
1507
+ selectAllLi.removeClass(this.options.selectedClass);
1508
+ if (checkedBoxesLength === 0) {
1509
+ if (!notTriggerOnSelectAll) {
1510
+ this.options.onSelectAll(false);
1511
+ }
1512
+ }
1513
+ }
1514