bootstrap-multiselect.js 52 KB

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