1// jquery.multi-select.js 2// by mySociety 3// https://github.com/mysociety/jquery-multi-select 4 5;(function($) { 6 7 "use strict"; 8 9 var pluginName = "multiSelect", 10 defaults = { 11 'containerHTML': '<div class="multi-select-container">', 12 'menuHTML': '<div class="multi-select-menu">', 13 'buttonHTML': '<span class="multi-select-button">', 14 'menuItemsHTML': '<div class="multi-select-menuitems">', 15 'menuItemHTML': '<label class="multi-select-menuitem">', 16 'presetsHTML': '<div class="multi-select-presets">', 17 'modalHTML': undefined, 18 'menuItemTitleClass': 'multi-select-menuitem--titled', 19 'activeClass': 'multi-select-container--open', 20 'noneText': 'All', 21 'allText': undefined, 22 'presets': undefined, 23 'positionedMenuClass': 'multi-select-container--positioned', 24 'positionMenuWithin': undefined, 25 'viewportBottomGutter': 20, 26 'menuMinHeight': 200 27 }; 28 29 /** 30 * @constructor 31 */ 32 function MultiSelect(element, options) { 33 this.element = element; 34 this.$element = $(element); 35 this.settings = $.extend( {}, defaults, options ); 36 this._defaults = defaults; 37 this._name = pluginName; 38 this.init(); 39 } 40 41 function arraysAreEqual(array1, array2) { 42 if ( array1.length != array2.length ){ 43 return false; 44 } 45 46 array1.sort(); 47 array2.sort(); 48 49 for ( var i = 0; i < array1.length; i++ ){ 50 if ( array1[i] !== array2[i] ){ 51 return false; 52 } 53 } 54 55 return true; 56 } 57 58 $.extend(MultiSelect.prototype, { 59 60 init: function() { 61 this.checkSuitableInput(); 62 this.findLabels(); 63 this.constructContainer(); 64 this.constructButton(); 65 this.constructMenu(); 66 this.constructModal(); 67 68 this.setUpBodyClickListener(); 69 this.setUpLabelsClickListener(); 70 71 this.$element.hide(); 72 }, 73 74 checkSuitableInput: function(text) { 75 if ( this.$element.is('select[multiple]') === false ) { 76 throw new Error('$.multiSelect only works on <select multiple> elements'); 77 } 78 }, 79 80 findLabels: function() { 81 this.$labels = $('label[for="' + this.$element.attr('id') + '"]'); 82 }, 83 84 constructContainer: function() { 85 this.$container = $(this.settings['containerHTML']); 86 this.$element.data('multi-select-container', this.$container); 87 this.$container.insertAfter(this.$element); 88 }, 89 90 constructButton: function() { 91 var _this = this; 92 this.$button = $(this.settings['buttonHTML']); 93 this.$button.attr({ 94 'role': 'button', 95 'aria-haspopup': 'true', 96 'tabindex': 0, 97 'aria-label': this.$labels.eq(0).text() 98 }) 99 .on('keydown.multiselect', function(e) { 100 var key = e.which; 101 var returnKey = 13; 102 var escapeKey = 27; 103 var spaceKey = 32; 104 var downArrow = 40; 105 if ((key === returnKey) || (key === spaceKey)) { 106 e.preventDefault(); 107 _this.$button.click(); 108 } else if (key === downArrow) { 109 e.preventDefault(); 110 _this.menuShow(); 111 var group = _this.$presets || _this.$menuItems; 112 group.children(":first").focus(); 113 } else if (key === escapeKey) { 114 _this.menuHide(); 115 } 116 }).on('click.multiselect', function(e) { 117 _this.menuToggle(); 118 }) 119 .appendTo(this.$container); 120 121 this.$element.on('change.multiselect', function() { 122 _this.updateButtonContents(); 123 }); 124 125 this.updateButtonContents(); 126 }, 127 128 updateButtonContents: function() { 129 var _this = this; 130 var options = []; 131 var selected = []; 132 133 this.$element.find('option').each(function() { 134 var text = /** @type string */ ($(this).text()); 135 options.push(text); 136 if ($(this).is(':selected')) { 137 selected.push( $.trim(text) ); 138 } 139 }); 140 141 this.$button.empty(); 142 143 if (selected.length == 0) { 144 this.$button.text( this.settings['noneText'] ); 145 } else if ( (selected.length === options.length) && this.settings['allText']) { 146 this.$button.text( this.settings['allText'] ); 147 } else { 148 this.$button.text( selected.join(', ') ); 149 } 150 }, 151 152 constructMenu: function() { 153 var _this = this; 154 155 this.$menu = $(this.settings['menuHTML']); 156 this.$menu.attr({ 157 'role': 'menu' 158 }).on('keyup.multiselect', function(e){ 159 var key = e.which; 160 var escapeKey = 27; 161 if (key === escapeKey) { 162 _this.menuHide(); 163 _this.$button.focus(); 164 } 165 }) 166 .appendTo(this.$container); 167 168 this.constructMenuItems(); 169 170 if ( this.settings['presets'] ) { 171 this.constructPresets(); 172 } 173 }, 174 175 constructMenuItems: function() { 176 var _this = this; 177 178 this.$menuItems = $(this.settings['menuItemsHTML']); 179 this.$menu.append(this.$menuItems); 180 181 this.$element.on('change.multiselect', function(e, internal) { 182 // Don't need to update the menu items if this 183 // change event was fired by our tickbox handler. 184 if(internal !== true){ 185 _this.updateMenuItems(); 186 } 187 }); 188 189 this.updateMenuItems(); 190 }, 191 192 updateMenuItems: function() { 193 var _this = this; 194 this.$menuItems.empty(); 195 196 this.$element.children('optgroup,option').each(function(index, element) { 197 var $item; 198 if (element.nodeName === 'OPTION') { 199 $item = _this.constructMenuItem($(element), index); 200 _this.$menuItems.append($item); 201 } else { 202 _this.constructMenuItemsGroup($(element), index); 203 } 204 }); 205 }, 206 207 upDown: function(type, e) { 208 var key = e.which; 209 var upArrow = 38; 210 var downArrow = 40; 211 212 if (key === upArrow) { 213 e.preventDefault(); 214 var prev = $(e.currentTarget).prev(); 215 if (prev.length) { 216 prev.focus(); 217 } else if (this.$presets && type === 'menuitem') { 218 this.$presets.children(':last').focus(); 219 } else { 220 this.$button.focus(); 221 } 222 } else if (key === downArrow) { 223 e.preventDefault(); 224 var next = $(e.currentTarget).next(); 225 if (next.length || type === 'menuitem') { 226 next.focus(); 227 } else { 228 this.$menuItems.children(':first').focus(); 229 } 230 } 231 }, 232 233 constructPresets: function() { 234 var _this = this; 235 this.$presets = $(this.settings['presetsHTML']); 236 this.$menu.prepend(this.$presets); 237 238 $.each(this.settings['presets'], function(i, preset){ 239 var unique_id = _this.$element.attr('name') + '_preset_' + i; 240 var $item = $(_this.settings['menuItemHTML']) 241 .attr({ 242 'for': unique_id, 243 'role': 'menuitem' 244 }) 245 .text(' ' + preset.name) 246 .on('keydown.multiselect', _this.upDown.bind(_this, 'preset')) 247 .appendTo(_this.$presets); 248 249 var $input = $('<input>') 250 .attr({ 251 'type': 'radio', 252 'name': _this.$element.attr('name') + '_presets', 253 'id': unique_id 254 }) 255 .prependTo($item); 256 257 $input.on('change.multiselect', function(){ 258 _this.$element.val(preset.options); 259 _this.$element.trigger('change'); 260 }); 261 }); 262 263 this.$element.on('change.multiselect', function() { 264 _this.updatePresets(); 265 }); 266 267 this.updatePresets(); 268 }, 269 270 updatePresets: function() { 271 var _this = this; 272 273 $.each(this.settings['presets'], function(i, preset){ 274 var unique_id = _this.$element.attr('name') + '_preset_' + i; 275 var $input = _this.$presets.find('#' + unique_id); 276 277 if ( arraysAreEqual(preset.options || [], _this.$element.val() || []) ){ 278 $input.prop('checked', true); 279 } else { 280 $input.prop('checked', false); 281 } 282 }); 283 }, 284 285 constructMenuItemsGroup: function($optgroup, optgroup_index) { 286 var _this = this; 287 288 $optgroup.children('option').each(function(option_index, option) { 289 var $item = _this.constructMenuItem($(option), optgroup_index + '_' + option_index); 290 var cls = _this.settings['menuItemTitleClass']; 291 if (option_index !== 0) { 292 cls += 'sr'; 293 } 294 $item.addClass(cls).attr('data-group-title', $optgroup.attr('label')); 295 _this.$menuItems.append($item); 296 }); 297 }, 298 299 constructMenuItem: function($option, option_index) { 300 var unique_id = this.$element.attr('name') + '_' + option_index; 301 var $item = $(this.settings['menuItemHTML']) 302 .attr({ 303 'for': unique_id, 304 'role': 'menuitem' 305 }) 306 .on('keydown.multiselect', this.upDown.bind(this, 'menuitem')) 307 .text(' ' + $option.text()); 308 309 var $input = $('<input>') 310 .attr({ 311 'type': 'checkbox', 312 'id': unique_id, 313 'value': $option.val() 314 }) 315 .prependTo($item); 316 317 if ( $option.is(':disabled') ) { 318 $input.attr('disabled', 'disabled'); 319 } 320 if ( $option.is(':selected') ) { 321 $input.prop('checked', 'checked'); 322 } 323 324 $input.on('change.multiselect', function() { 325 if ($(this).prop('checked')) { 326 $option.prop('selected', true); 327 } else { 328 $option.prop('selected', false); 329 } 330 331 // .prop() on its own doesn't generate a change event. 332 // Other plugins might want to do stuff onChange. 333 $option.trigger('change', [true]); 334 }); 335 336 return $item; 337 }, 338 339 constructModal: function() { 340 var _this = this; 341 342 if (this.settings['modalHTML']) { 343 this.$modal = $(this.settings['modalHTML']); 344 this.$modal.on('click.multiselect', function(){ 345 _this.menuHide(); 346 }) 347 this.$modal.insertBefore(this.$menu); 348 } 349 }, 350 351 setUpBodyClickListener: function() { 352 var _this = this; 353 354 // Hide the $menu when you click outside of it. 355 $('html').on('click.multiselect', function(){ 356 _this.menuHide(); 357 }); 358 359 // Stop click events from inside the $button or $menu from 360 // bubbling up to the body and closing the menu! 361 this.$container.on('click.multiselect', function(e){ 362 e.stopPropagation(); 363 }); 364 }, 365 366 setUpLabelsClickListener: function() { 367 var _this = this; 368 this.$labels.on('click.multiselect', function(e) { 369 e.preventDefault(); 370 e.stopPropagation(); 371 _this.menuToggle(); 372 }); 373 }, 374 375 menuShow: function() { 376 $('html').trigger('click.multiselect'); // Close any other open menus 377 this.$container.addClass(this.settings['activeClass']); 378 379 if ( this.settings['positionMenuWithin'] && this.settings['positionMenuWithin'] instanceof $ ) { 380 var menuLeftEdge = this.$menu.offset().left + this.$menu.outerWidth(); 381 var withinLeftEdge = this.settings['positionMenuWithin'].offset().left + 382 this.settings['positionMenuWithin'].outerWidth(); 383 384 if ( menuLeftEdge > withinLeftEdge ) { 385 this.$menu.css( 'width', (withinLeftEdge - this.$menu.offset().left) ); 386 this.$container.addClass(this.settings['positionedMenuClass']); 387 } 388 } 389 390 var menuBottom = this.$menu.offset().top + this.$menu.outerHeight(); 391 var viewportBottom = $(window).scrollTop() + $(window).height(); 392 if ( menuBottom > viewportBottom - this.settings['viewportBottomGutter'] ) { 393 this.$menu.css({ 394 'maxHeight': Math.max( 395 viewportBottom - this.settings['viewportBottomGutter'] - this.$menu.offset().top, 396 this.settings['menuMinHeight'] 397 ), 398 'overflow': 'scroll' 399 }); 400 } else { 401 this.$menu.css({ 402 'maxHeight': '', 403 'overflow': '' 404 }); 405 } 406 }, 407 408 menuHide: function() { 409 this.$container.removeClass(this.settings['activeClass']); 410 this.$container.removeClass(this.settings['positionedMenuClass']); 411 this.$menu.css('width', 'auto'); 412 }, 413 414 menuToggle: function() { 415 if ( this.$container.hasClass(this.settings['activeClass']) ) { 416 this.menuHide(); 417 } else { 418 this.menuShow(); 419 } 420 } 421 422 }); 423 424 $.fn[ pluginName ] = function(options) { 425 return this.each(function() { 426 if ( !$.data(this, "plugin_" + pluginName) ) { 427 $.data(this, "plugin_" + pluginName, 428 new MultiSelect(this, options) ); 429 } 430 }); 431 }; 432 433})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.