Няма описание

bootstrap-multiselect.js 53KB

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