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