jquery.multi-select.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. // jquery.multi-select.js
  2. // by mySociety
  3. // https://github.com/mysociety/jquery-multi-select
  4. ;(function($) {
  5. "use strict";
  6. var pluginName = "multiSelect",
  7. defaults = {
  8. 'containerHTML': '<div class="multi-select-container">',
  9. 'menuHTML': '<div class="multi-select-menu">',
  10. 'buttonHTML': '<span class="multi-select-button">',
  11. 'menuItemsHTML': '<div class="multi-select-menuitems">',
  12. 'menuItemHTML': '<label class="multi-select-menuitem">',
  13. 'presetsHTML': '<div class="multi-select-presets">',
  14. 'activeClass': 'multi-select-container--open',
  15. 'noneText': '-- Select --',
  16. 'allText': undefined,
  17. 'presets': undefined,
  18. 'positionedMenuClass': 'multi-select-container--positioned',
  19. 'positionMenuWithin': undefined,
  20. 'viewportBottomGutter': 20,
  21. 'menuMinHeight': 200
  22. };
  23. /**
  24. * @constructor
  25. */
  26. function MultiSelect(element, options) {
  27. this.element = element;
  28. this.$element = $(element);
  29. this.settings = $.extend( {}, defaults, options );
  30. this._defaults = defaults;
  31. this._name = pluginName;
  32. this.init();
  33. }
  34. function arraysAreEqual(array1, array2) {
  35. if ( array1.length != array2.length ){
  36. return false;
  37. }
  38. array1.sort();
  39. array2.sort();
  40. for ( var i = 0; i < array1.length; i++ ){
  41. if ( array1[i] !== array2[i] ){
  42. return false;
  43. }
  44. }
  45. return true;
  46. }
  47. $.extend(MultiSelect.prototype, {
  48. init: function() {
  49. this.checkSuitableInput();
  50. this.findLabels();
  51. this.constructContainer();
  52. this.constructButton();
  53. this.constructMenu();
  54. this.setUpBodyClickListener();
  55. this.setUpLabelsClickListener();
  56. this.$element.hide();
  57. },
  58. checkSuitableInput: function(text) {
  59. if ( this.$element.is('select[multiple]') === false ) {
  60. throw new Error('$.multiSelect only works on <select multiple> elements');
  61. }
  62. },
  63. findLabels: function() {
  64. this.$labels = $('label[for="' + this.$element.attr('id') + '"]');
  65. },
  66. constructContainer: function() {
  67. this.$container = $(this.settings['containerHTML']);
  68. this.$element.data('multi-select-container', this.$container);
  69. this.$container.insertAfter(this.$element);
  70. },
  71. constructButton: function() {
  72. var _this = this;
  73. this.$button = $(this.settings['buttonHTML']);
  74. this.$button.attr({
  75. 'role': 'button',
  76. 'aria-haspopup': 'true',
  77. 'tabindex': 0,
  78. 'aria-label': this.$labels.eq(0).text()
  79. })
  80. .on('keydown.multiselect', function(e) {
  81. var key = e.which;
  82. var returnKey = 13;
  83. var spaceKey = 32;
  84. if ((key === returnKey) || (key === spaceKey)) {
  85. _this.$button.click();
  86. }
  87. }).on('click.multiselect', function(e) {
  88. _this.menuToggle();
  89. })
  90. .appendTo(this.$container);
  91. this.$element.on('change.multiselect', function() {
  92. _this.updateButtonContents();
  93. });
  94. this.updateButtonContents();
  95. },
  96. updateButtonContents: function() {
  97. var _this = this;
  98. var options = [];
  99. var selected = [];
  100. this.$element.children('option').each(function() {
  101. var text = /** @type string */ ($(this).text());
  102. options.push(text);
  103. if ($(this).is(':selected')) {
  104. selected.push( $.trim(text) );
  105. }
  106. });
  107. this.$button.empty();
  108. if (selected.length == 0) {
  109. this.$button.text( this.settings['noneText'] );
  110. } else if ( (selected.length === options.length) && this.settings['allText']) {
  111. this.$button.text( this.settings['allText'] );
  112. } else {
  113. this.$button.text( selected.join(', ') );
  114. }
  115. },
  116. constructMenu: function() {
  117. var _this = this;
  118. this.$menu = $(this.settings['menuHTML']);
  119. this.$menu.attr({
  120. 'role': 'menu'
  121. }).on('keyup.multiselect', function(e){
  122. var key = e.which;
  123. var escapeKey = 27;
  124. if (key === escapeKey) {
  125. _this.menuHide();
  126. }
  127. })
  128. .appendTo(this.$container);
  129. this.constructMenuItems();
  130. if ( this.settings['presets'] ) {
  131. this.constructPresets();
  132. }
  133. },
  134. constructMenuItems: function() {
  135. var _this = this;
  136. this.$menuItems = $(this.settings['menuItemsHTML']);
  137. this.$menu.append(this.$menuItems);
  138. this.$element.on('change.multiselect', function(e, internal) {
  139. // Don't need to update the menu items if this
  140. // change event was fired by our tickbox handler.
  141. if(internal !== true){
  142. _this.updateMenuItems();
  143. }
  144. });
  145. this.updateMenuItems();
  146. },
  147. updateMenuItems: function() {
  148. var _this = this;
  149. this.$menuItems.empty();
  150. this.$element.children('option').each(function(option_index, option) {
  151. var $item = _this.constructMenuItem($(option), option_index);
  152. _this.$menuItems.append($item);
  153. });
  154. },
  155. constructPresets: function() {
  156. var _this = this;
  157. this.$presets = $(this.settings['presetsHTML']);
  158. this.$menu.prepend(this.$presets);
  159. $.each(this.settings['presets'], function(i, preset){
  160. var unique_id = _this.$element.attr('name') + '_preset_' + i;
  161. var $item = $(_this.settings['menuItemHTML'])
  162. .attr({
  163. 'for': unique_id,
  164. 'role': 'menuitem'
  165. })
  166. .text(' ' + preset.name)
  167. .appendTo(_this.$presets);
  168. var $input = $('<input>')
  169. .attr({
  170. 'type': 'radio',
  171. 'name': _this.$element.attr('name') + '_presets',
  172. 'id': unique_id
  173. })
  174. .prependTo($item);
  175. $input.on('change.multiselect', function(){
  176. _this.$element.val(preset.options);
  177. _this.$element.trigger('change');
  178. });
  179. });
  180. this.$element.on('change.multiselect', function() {
  181. _this.updatePresets();
  182. });
  183. this.updatePresets();
  184. },
  185. updatePresets: function() {
  186. var _this = this;
  187. $.each(this.settings['presets'], function(i, preset){
  188. var unique_id = _this.$element.attr('name') + '_preset_' + i;
  189. var $input = _this.$presets.find('#' + unique_id);
  190. if ( arraysAreEqual(preset.options || [], _this.$element.val() || []) ){
  191. $input.prop('checked', true);
  192. } else {
  193. $input.prop('checked', false);
  194. }
  195. });
  196. },
  197. constructMenuItem: function($option, option_index) {
  198. var unique_id = this.$element.attr('name') + '_' + option_index;
  199. var $item = $(this.settings['menuItemHTML'])
  200. .attr({
  201. 'for': unique_id,
  202. 'role': 'menuitem'
  203. })
  204. .text(' ' + $option.text());
  205. var $input = $('<input>')
  206. .attr({
  207. 'type': 'checkbox',
  208. 'id': unique_id,
  209. 'value': $option.val()
  210. })
  211. .prependTo($item);
  212. if ( $option.is(':disabled') ) {
  213. $input.attr('disabled', 'disabled');
  214. }
  215. if ( $option.is(':selected') ) {
  216. $input.prop('checked', 'checked');
  217. }
  218. $input.on('change.multiselect', function() {
  219. if ($(this).prop('checked')) {
  220. $option.prop('selected', true);
  221. } else {
  222. $option.prop('selected', false);
  223. }
  224. // .prop() on its own doesn't generate a change event.
  225. // Other plugins might want to do stuff onChange.
  226. $option.trigger('change', [true]);
  227. });
  228. return $item;
  229. },
  230. setUpBodyClickListener: function() {
  231. var _this = this;
  232. // Hide the $menu when you click outside of it.
  233. $('html').on('click.multiselect', function(){
  234. _this.menuHide();
  235. });
  236. // Stop click events from inside the $button or $menu from
  237. // bubbling up to the body and closing the menu!
  238. this.$container.on('click.multiselect', function(e){
  239. e.stopPropagation();
  240. });
  241. },
  242. setUpLabelsClickListener: function() {
  243. var _this = this;
  244. this.$labels.on('click.multiselect', function(e) {
  245. e.preventDefault();
  246. e.stopPropagation();
  247. _this.menuToggle();
  248. });
  249. },
  250. menuShow: function() {
  251. $('html').trigger('click.multiselect'); // Close any other open menus
  252. this.$container.addClass(this.settings['activeClass']);
  253. if ( this.settings['positionMenuWithin'] && this.settings['positionMenuWithin'] instanceof $ ) {
  254. var menuLeftEdge = this.$menu.offset().left + this.$menu.outerWidth();
  255. var withinLeftEdge = this.settings['positionMenuWithin'].offset().left +
  256. this.settings['positionMenuWithin'].outerWidth();
  257. if ( menuLeftEdge > withinLeftEdge ) {
  258. this.$menu.css( 'width', (withinLeftEdge - this.$menu.offset().left) );
  259. this.$container.addClass(this.settings['positionedMenuClass']);
  260. }
  261. }
  262. var menuBottom = this.$menu.offset().top + this.$menu.outerHeight();
  263. var viewportBottom = $(window).scrollTop() + $(window).height();
  264. if ( menuBottom > viewportBottom - this.settings['viewportBottomGutter'] ) {
  265. this.$menu.css({
  266. 'maxHeight': Math.max(
  267. viewportBottom - this.settings['viewportBottomGutter'] - this.$menu.offset().top,
  268. this.settings['menuMinHeight']
  269. ),
  270. 'overflow': 'scroll'
  271. });
  272. } else {
  273. this.$menu.css({
  274. 'maxHeight': '',
  275. 'overflow': ''
  276. });
  277. }
  278. },
  279. menuHide: function() {
  280. this.$container.removeClass(this.settings['activeClass']);
  281. this.$container.removeClass(this.settings['positionedMenuClass']);
  282. this.$menu.css('width', 'auto');
  283. },
  284. menuToggle: function() {
  285. if ( this.$container.hasClass(this.settings['activeClass']) ) {
  286. this.menuHide();
  287. } else {
  288. this.menuShow();
  289. }
  290. }
  291. });
  292. $.fn[ pluginName ] = function(options) {
  293. return this.each(function() {
  294. if ( !$.data(this, "plugin_" + pluginName) ) {
  295. $.data(this, "plugin_" + pluginName,
  296. new MultiSelect(this, options) );
  297. }
  298. });
  299. };
  300. })(jQuery);