You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

635 lines
25 KiB

7 years ago
  1. /**
  2. * bootstrap-multiselect.js
  3. * https://github.com/davidstutz/bootstrap-multiselect
  4. *
  5. * Copyright 2012, 2013 David Stutz
  6. *
  7. * Dual licensed under the BSD-3-Clause and the Apache License, Version 2.0.
  8. * See the README.
  9. */
  10. !function($) {
  11. "use strict";// jshint ;_;
  12. if (typeof ko != 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) {
  13. ko.bindingHandlers.multiselect = {
  14. init : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {},
  15. update : function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
  16. var ms = $(element).data('multiselect');
  17. if (!ms) {
  18. $(element).multiselect(ko.utils.unwrapObservable(valueAccessor()));
  19. }
  20. else if (allBindingsAccessor().options && allBindingsAccessor().options().length !== ms.originalOptions.length) {
  21. ms.updateOriginalOptions();
  22. $(element).multiselect('rebuild');
  23. }
  24. }
  25. };
  26. }
  27. function Multiselect(select, options) {
  28. this.options = this.mergeOptions(options);
  29. this.$select = $(select);
  30. // Initialization.
  31. // We have to clone to create a new reference.
  32. this.originalOptions = this.$select.clone()[0].options;
  33. this.query = '';
  34. this.searchTimeout = null;
  35. this.options.multiple = this.$select.attr('multiple') == "multiple";
  36. this.options.onChange = $.proxy(this.options.onChange, this);
  37. // Build select all if enabled.
  38. this.buildContainer();
  39. this.buildButton();
  40. this.buildSelectAll();
  41. this.buildDropdown();
  42. this.buildDropdownOptions();
  43. this.buildFilter();
  44. this.updateButtonText();
  45. this.$select.hide().after(this.$container);
  46. };
  47. Multiselect.prototype = {
  48. // Default options.
  49. defaults: {
  50. // Default text function will either print 'None selected' in case no
  51. // option is selected, or a list of the selected options up to a length of 3 selected options.
  52. // If more than 3 options are selected, the number of selected options is printed.
  53. buttonText: function(options, select) {
  54. if (options.length == 0) {
  55. return this.nonSelectedText + ' <b class="caret"></b>';
  56. }
  57. else {
  58. if (options.length > 3) {
  59. return options.length + ' ' + this.nSelectedText + ' <b class="caret"></b>';
  60. }
  61. else {
  62. var selected = '';
  63. options.each(function() {
  64. var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).html();
  65. selected += label + ', ';
  66. });
  67. return selected.substr(0, selected.length - 2) + ' <b class="caret"></b>';
  68. }
  69. }
  70. },
  71. // Like the buttonText option to update the title of the button.
  72. buttonTitle: function(options, select) {
  73. var selected = '';
  74. options.each(function () {
  75. selected += $(this).text() + ', ';
  76. });
  77. return selected.substr(0, selected.length - 2);
  78. },
  79. // Is triggered on change of the selected options.
  80. onChange : function(option, checked) {
  81. },
  82. buttonClass: 'btn',
  83. dropRight: false,
  84. selectedClass: 'active',
  85. buttonWidth: 'auto',
  86. buttonContainer: '<div class="btn-group" />',
  87. // Maximum height of the dropdown menu.
  88. // If maximum height is exceeded a scrollbar will be displayed.
  89. maxHeight: false,
  90. includeSelectAllOption: false,
  91. selectAllText: ' Select all',
  92. selectAllValue: 'multiselect-all',
  93. enableFiltering: false,
  94. enableCaseInsensitiveFiltering: false,
  95. filterPlaceholder: 'Search',
  96. // possible options: 'text', 'value', 'both'
  97. filterBehavior: 'text',
  98. preventInputChangeEvent: false,
  99. nonSelectedText: 'None selected',
  100. nSelectedText: 'selected'
  101. },
  102. // Templates.
  103. templates: {
  104. button: '<button type="button" class="multiselect dropdown-toggle" data-toggle="dropdown"></button>',
  105. ul: '<ul class="multiselect-container dropdown-menu"></ul>',
  106. 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>',
  107. li: '<li><a href="javascript:void(0);"><label><input /></label></a></li>',
  108. liGroup: '<li><label class="multiselect-group"></label></li>'
  109. },
  110. constructor: Multiselect,
  111. buildContainer: function() {
  112. this.$container = $(this.options.buttonContainer);
  113. },
  114. buildButton: function() {
  115. // Build button.
  116. this.$button = $(this.templates.button).addClass(this.options.buttonClass);
  117. // Adopt active state.
  118. if (this.$select.attr('disabled') == undefined) {
  119. this.$button.removeClass('disabled');
  120. }
  121. else {
  122. this.$button.addClass('disabled');
  123. }
  124. // Manually add button width if set.
  125. if (this.options.buttonWidth) {
  126. this.$button.css({
  127. 'width' : this.options.buttonWidth
  128. });
  129. }
  130. // Keep the tab index from the select.
  131. var tabindex = this.$select.attr('tabindex');
  132. if (tabindex) {
  133. this.$button.attr('tabindex', tabindex);
  134. }
  135. this.$container.prepend(this.$button)
  136. },
  137. // Build dropdown container ul.
  138. buildDropdown: function() {
  139. // Build ul.
  140. this.$ul = $(this.templates.ul);
  141. if (this.options.dropRight) {
  142. this.$ul.addClass('pull-right');
  143. }
  144. // Set max height of dropdown menu to activate auto scrollbar.
  145. if (this.options.maxHeight) {
  146. // TODO: Add a class for this option to move the css declarations.
  147. this.$ul.css({
  148. 'max-height': this.options.maxHeight + 'px',
  149. 'overflow-y': 'auto',
  150. 'overflow-x': 'hidden'
  151. });
  152. }
  153. this.$container.append(this.$ul)
  154. },
  155. // Build the dropdown and bind event handling.
  156. buildDropdownOptions: function() {
  157. this.$select.children().each($.proxy(function(index, element) {
  158. // Support optgroups and options without a group simultaneously.
  159. var tag = $(element).prop('tagName').toLowerCase();
  160. if (tag == 'optgroup') {
  161. this.createOptgroup(element);
  162. }
  163. else if (tag == 'option') {
  164. this.createOptionValue(element);
  165. }
  166. // Other illegal tags will be ignored.
  167. }, this));
  168. // Bind the change event on the dropdown elements.
  169. $('li input', this.$ul).on('change', $.proxy(function(event) {
  170. var checked = $(event.target).prop('checked') || false;
  171. var isSelectAllOption = $(event.target).val() == this.options.selectAllValue;
  172. // Apply or unapply the configured selected class.
  173. if (this.options.selectedClass) {
  174. if (checked) {
  175. $(event.target).parents('li').addClass(this.options.selectedClass);
  176. }
  177. else {
  178. $(event.target).parents('li').removeClass(this.options.selectedClass);
  179. }
  180. }
  181. // Get the corresponding option.
  182. var value = $(event.target).val();
  183. var $option = this.getOptionByValue(value);
  184. var $optionsNotThis = $('option', this.$select).not($option);
  185. var $checkboxesNotThis = $('input', this.$container).not($(event.target));
  186. // Toggle all options if the select all option was changed.
  187. if (isSelectAllOption) {
  188. $checkboxesNotThis.filter(function() {
  189. return $(this).is(':checked') != checked;
  190. }).trigger('click');
  191. }
  192. if (checked) {
  193. $option.prop('selected', true);
  194. if (this.options.multiple) {
  195. // Simply select additional option.
  196. $option.attr('selected', 'selected');
  197. }
  198. else {
  199. // Unselect all other options and corresponding checkboxes.
  200. if (this.options.selectedClass) {
  201. $($checkboxesNotThis).parents('li').removeClass(this.options.selectedClass);
  202. }
  203. $($checkboxesNotThis).prop('checked', false);
  204. $optionsNotThis.removeAttr('selected').prop('selected', false);
  205. // It's a single selection, so close.
  206. this.$button.click();
  207. }
  208. if (this.options.selectedClass == "active") {
  209. $optionsNotThis.parents("a").css("outline", "");
  210. }
  211. }
  212. else {
  213. // Unselect option.
  214. $option.removeAttr('selected').prop('selected', false);
  215. }
  216. this.updateButtonText();
  217. this.options.onChange($option, checked);
  218. this.$select.change();
  219. if(this.options.preventInputChangeEvent) {
  220. return false;
  221. }
  222. }, this));
  223. $('li a', this.$ul).on('touchstart click', function(event) {
  224. event.stopPropagation();
  225. $(event.target).blur();
  226. });
  227. // Keyboard support.
  228. this.$container.on('keydown', $.proxy(function(event) {
  229. if ($('input[type="text"]', this.$container).is(':focus')) {
  230. return;
  231. }
  232. if ((event.keyCode == 9 || event.keyCode == 27) && this.$container.hasClass('open')) {
  233. // Close on tab or escape.
  234. this.$button.click();
  235. }
  236. else {
  237. var $items = $(this.$container).find("li:not(.divider):visible a");
  238. if (!$items.length) {
  239. return;
  240. }
  241. var index = $items.index($items.filter(':focus'));
  242. // Navigation up.
  243. if (event.keyCode == 38 && index > 0) {
  244. index--;
  245. }
  246. // Navigate down.
  247. else if (event.keyCode == 40 && index < $items.length - 1) {
  248. index++;
  249. }
  250. else if (!~index) {
  251. index = 0;
  252. }
  253. var $current = $items.eq(index);
  254. $current.focus();
  255. if (event.keyCode == 32 || event.keyCode == 13) {
  256. var $checkbox = $current.find('input');
  257. $checkbox.prop("checked", !$checkbox.prop("checked"));
  258. $checkbox.change();
  259. }
  260. event.stopPropagation();
  261. event.preventDefault();
  262. }
  263. }, this));
  264. },
  265. // Will build an dropdown element for the given option.
  266. createOptionValue: function(element) {
  267. if ($(element).is(':selected')) {
  268. $(element).attr('selected', 'selected').prop('selected', true);
  269. }
  270. // Support the label attribute on options.
  271. var label = $(element).attr('label') || $(element).html();
  272. var value = $(element).val();
  273. var inputType = this.options.multiple ? "checkbox" : "radio";
  274. var $li = $(this.templates.li);
  275. $('label', $li).addClass(inputType);
  276. $('input', $li).attr('type', inputType);
  277. var selected = $(element).prop('selected') || false;
  278. var $checkbox = $('input', $li);
  279. $checkbox.val(value);
  280. if (value == this.options.selectAllValue) {
  281. $checkbox.parent().parent().addClass('multiselect-all');
  282. }
  283. $('label', $li).append(" " + label);
  284. this.$ul.append($li);
  285. if ($(element).is(':disabled')) {
  286. $checkbox.attr('disabled', 'disabled').prop('disabled', true).parents('li').addClass('disabled');
  287. }
  288. $checkbox.prop('checked', selected);
  289. if (selected && this.options.selectedClass) {
  290. $checkbox.parents('li').addClass(this.options.selectedClass);
  291. }
  292. },
  293. // Create optgroup.
  294. createOptgroup: function(group) {
  295. var groupName = $(group).prop('label');
  296. // Add a header for the group.
  297. var $li = $(this.templates.liGroup);
  298. $('label', $li).text(groupName);
  299. this.$ul.append($li);
  300. // Add the options of the group.
  301. $('option', group).each($.proxy(function(index, element) {
  302. this.createOptionValue(element);
  303. }, this));
  304. },
  305. // Add the select all option to the select.
  306. buildSelectAll: function() {
  307. var alreadyHasSelectAll = this.$select[0][0] ? this.$select[0][0].value == this.options.selectAllValue : false;
  308. console.log(this.options);
  309. // If options.includeSelectAllOption === true, add the include all checkbox.
  310. if (this.options.includeSelectAllOption && this.options.multiple && !alreadyHasSelectAll) {
  311. this.$select.prepend('<option value="' + this.options.selectAllValue + '">' + this.options.selectAllText + '</option>');
  312. }
  313. },
  314. // Build and bind filter.
  315. buildFilter: function() {
  316. // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength.
  317. if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) {
  318. var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering);
  319. if (this.$select.find('option').length >= enableFilterLength) {
  320. this.$filter = $(this.templates.filter).attr('placeholder', this.options.filterPlaceholder);
  321. this.$ul.prepend(this.$filter);
  322. this.$filter.val(this.query).on('click', function(event) {
  323. event.stopPropagation();
  324. }).on('keydown', $.proxy(function(event) {
  325. // This is useful to catch "keydown" events after the browser has updated the control.
  326. clearTimeout(this.searchTimeout);
  327. this.searchTimeout = this.asyncFunction($.proxy(function() {
  328. if (this.query != event.target.value) {
  329. this.query = event.target.value;
  330. $.each($('li', this.$ul), $.proxy(function(index, element) {
  331. var value = $('input', element).val();
  332. if (value != this.options.selectAllValue) {
  333. var text = $('label', element).text();
  334. var value = $('input', element).val();
  335. if (value && text && value != this.options.selectAllValue) {
  336. // by default lets assume that element is not
  337. // interesting for this search
  338. var showElement = false;
  339. var filterCandidate = '';
  340. if ((this.options.filterBehavior == 'text' || this.options.filterBehavior == 'both')) {
  341. filterCandidate = text;
  342. }
  343. if ((this.options.filterBehavior == 'value' || this.options.filterBehavior == 'both')) {
  344. filterCandidate = value;
  345. }
  346. if (this.options.enableCaseInsensitiveFiltering && filterCandidate.toLowerCase().indexOf(this.query.toLowerCase()) > -1) {
  347. showElement = true;
  348. }
  349. else if (filterCandidate.indexOf(this.query) > -1) {
  350. showElement = true;
  351. }
  352. if (showElement) {
  353. $(element).show();
  354. }
  355. else {
  356. $(element).hide();
  357. }
  358. }
  359. }
  360. }, this));
  361. }
  362. }, this), 300, this);
  363. }, this));
  364. }
  365. }
  366. },
  367. // Destroy - unbind - the plugin.
  368. destroy: function() {
  369. this.$container.remove();
  370. this.$select.show();
  371. },
  372. // Refreshs the checked options based on the current state of the select.
  373. refresh: function() {
  374. $('option', this.$select).each($.proxy(function(index, element) {
  375. var $input = $('li input', this.$ul).filter(function() {
  376. return $(this).val() == $(element).val();
  377. });
  378. if ($(element).is(':selected')) {
  379. $input.prop('checked', true);
  380. if (this.options.selectedClass) {
  381. $input.parents('li').addClass(this.options.selectedClass);
  382. }
  383. }
  384. else {
  385. $input.prop('checked', false);
  386. if (this.options.selectedClass) {
  387. $input.parents('li').removeClass(this.options.selectedClass);
  388. }
  389. }
  390. if ($(element).is(":disabled")) {
  391. $input.attr('disabled', 'disabled').prop('disabled', true).parents('li').addClass('disabled');
  392. }
  393. else {
  394. $input.removeAttr('disabled').prop('disabled', false).parents('li').removeClass('disabled');
  395. }
  396. }, this));
  397. this.updateButtonText();
  398. },
  399. // Select an option by its value or multiple options using an array of values.
  400. select: function(selectValues) {
  401. if(selectValues && !$.isArray(selectValues)) {
  402. selectValues = [selectValues];
  403. }
  404. for (var i = 0; i < selectValues.length; i++) {
  405. var value = selectValues[i];
  406. var $option = this.getOptionByValue(value);
  407. var $checkbox = this.getInputByValue(value);
  408. if (this.options.selectedClass) {
  409. $checkbox.parents('li').addClass(this.options.selectedClass);
  410. }
  411. $checkbox.prop('checked', true);
  412. $option.attr('selected', 'selected').prop('selected', true);
  413. this.options.onChange($option, true);
  414. }
  415. this.updateButtonText();
  416. },
  417. // Deselect an option by its value or using an array of values.
  418. deselect: function(deselectValues) {
  419. if(deselectValues && !$.isArray(deselectValues)) {
  420. deselectValues = [deselectValues];
  421. }
  422. for (var i = 0; i < deselectValues.length; i++) {
  423. var value = deselectValues[i];
  424. var $option = this.getOptionByValue(value);
  425. var $checkbox = this.getInputByValue(value);
  426. if (this.options.selectedClass) {
  427. $checkbox.parents('li').removeClass(this.options.selectedClass);
  428. }
  429. $checkbox.prop('checked', false);
  430. $option.removeAttr('selected').prop('selected', false);
  431. this.options.onChange($option, false);
  432. }
  433. this.updateButtonText();
  434. },
  435. // Rebuild the whole dropdown menu.
  436. rebuild: function() {
  437. this.$ul.html('');
  438. // Remove select all option in select.
  439. $('option[value="' + this.options.selectAllValue + '"]', this.$select).remove();
  440. // Important to distinguish between radios and checkboxes.
  441. this.options.multiple = this.$select.attr('multiple') == "multiple";
  442. this.buildSelectAll();
  443. this.buildDropdownOptions();
  444. this.updateButtonText();
  445. this.buildFilter();
  446. },
  447. // Build select using the given data as options.
  448. dataprovider: function(dataprovider) {
  449. var optionDOM = "";
  450. dataprovider.forEach(function (option) {
  451. optionDOM += '<option value="' + option.value + '">' + option.label + '</option>';
  452. });
  453. this.$select.html(optionDOM);
  454. this.rebuild();
  455. },
  456. // Set options.
  457. setOptions: function(options) {
  458. this.options = this.mergeOptions(options);
  459. },
  460. // Get options by merging defaults and given options.
  461. mergeOptions: function(options) {
  462. return $.extend({}, this.defaults, options);
  463. },
  464. // Update button text and button title.
  465. updateButtonText: function() {
  466. var options = this.getSelected();
  467. // First update the displayed button text.
  468. $('button', this.$container).html(this.options.buttonText(options, this.$select));
  469. // Now update the title attribute of the button.
  470. $('button', this.$container).attr('title', this.options.buttonTitle(options, this.$select));
  471. },
  472. // Get all selected options.
  473. getSelected: function() {
  474. return $('option:selected[value!="' + this.options.selectAllValue + '"]', this.$select);
  475. },
  476. // Get the corresponding option by ts value.
  477. getOptionByValue: function(value) {
  478. return $('option', this.$select).filter(function() {
  479. return $(this).val() == value;
  480. });
  481. },
  482. // Get an input in the dropdown by its value.
  483. getInputByValue: function(value) {
  484. return $('li input', this.$ul).filter(function() {
  485. return $(this).val() == value;
  486. });
  487. },
  488. updateOriginalOptions: function() {
  489. this.originalOptions = this.$select.clone()[0].options;
  490. },
  491. asyncFunction: function(callback, timeout, self) {
  492. var args = Array.prototype.slice.call(arguments, 3);
  493. return setTimeout(function() {
  494. callback.apply(self || window, args);
  495. }, timeout);
  496. }
  497. };
  498. $.fn.multiselect = function(option, parameter) {
  499. return this.each(function() {
  500. var data = $(this).data('multiselect'), options = typeof option == 'object' && option;
  501. // Initialize the multiselect.
  502. if (!data) {
  503. $(this).data('multiselect', ( data = new Multiselect(this, options)));
  504. }
  505. // Call multiselect method.
  506. if ( typeof option == 'string') {
  507. data[option](parameter);
  508. }
  509. });
  510. };
  511. $.fn.multiselect.Constructor = Multiselect;
  512. // Automatically init selects by their data-role.
  513. $(function() {
  514. $("select[data-role=multiselect]").multiselect();
  515. });
  516. }(window.jQuery);