1/** 2 * Display a nice easy to use multiselect list 3 * @Version: 2.0 4 * @Author: Patrick Springstubbe 5 * @Contact: @JediNobleclem 6 * @Website: springstubbe.us 7 * @Source: https://github.com/nobleclem/jQuery-MultiSelect 8 * @Notes: If select list is hidden on page load use the jquery.actual plugin 9 * to resolve issues with preselected items placeholder text 10 * https://github.com/dreamerslab/jquery.actual 11 * 12 * Usage: 13 * $('select[multiple]').multiselect(); 14 * $('select[multiple]').multiselect({ placeholder: 'Select options' }); 15 * $('select[multiple]').multiselect('reload'); 16 * $('select[multiple]').multiselect( 'loadOption', [{ 17 * name : 'Option Name 1', 18 * value : 'option-value-1', 19 * checked: false 20 * },{ 21 * name : 'Option Name 2', 22 * value : 'option-value-2', 23 * checked: false 24 * }]); 25 * 26 **/ 27(function($){ 28 var defaults = { 29 placeholder : 'Select options', // text to use in dummy input 30 columns : 1, // how many columns should be use to show options 31 search : false, // include option search box 32 // search filter options 33 searchOptions : { 34 'default' : 'Search', // search input placeholder text 35 showOptGroups: false, // show option group titles if no options remaining 36 onSearch : function( element ){} // fires on keyup before search on options happens 37 }, 38 selectAll : false, // add select all option 39 selectGroup : false, // select entire optgroup 40 minHeight : 200, // minimum height of option overlay 41 maxHeight : null, // maximum height of option overlay 42 showCheckbox : true, // display the checkbox to the user 43 jqActualOpts : {}, // options for jquery.actual 44 45 // Callbacks 46 onLoad : function( element ) { // fires at end of list initialization 47 $(element).hide(); 48 }, 49 onOptionClick : function( element, option ){}, // fires when an option is clicked 50 51 // @NOTE: these are for future development 52 maxWidth : null, // maximum width of option overlay (or selector) 53 minSelect : false, // minimum number of items that can be selected 54 maxSelect : false, // maximum number of items that can be selected 55 }; 56 57 var msCounter = 1; 58 59 function MultiSelect( element, options ) 60 { 61 this.element = element; 62 this.options = $.extend( {}, defaults, options ); 63 this.load(); 64 } 65 66 MultiSelect.prototype = { 67 /* LOAD CUSTOM MULTISELECT DOM/ACTIONS */ 68 load: function() { 69 var instance = this; 70 71 // make sure this is a select list and not loaded 72 if( (instance.element.nodeName != 'SELECT') || $(instance.element).hasClass('jqmsLoaded') ) { 73 return true; 74 } 75 76 // sanity check so we don't double load on a select element 77 $(instance.element).addClass('jqmsLoaded'); 78 79 // add option container 80 $(instance.element).after('<div class="ms-options-wrap"><button>None Selected</button><div class="ms-options"><ul></ul></div></div>'); 81 var placeholder = $(instance.element).next('.ms-options-wrap').find('> button:first-child'); 82 var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options'); 83 var optionsList = optionsWrap.find('> ul'); 84 var hasOptGroup = $(instance.element).find('optgroup').length ? true : false; 85 86 var maxWidth = null; 87 if( typeof instance.options.width == 'number' ) { 88 optionsWrap.parent().css( 'position', 'relative' ); 89 maxWidth = instance.options.width; 90 } 91 else if( typeof instance.options.width == 'string' ) { 92 $( instance.options.width ).css( 'position', 'relative' ); 93 maxWidth = '100%'; 94 } 95 else { 96 optionsWrap.parent().css( 'position', 'relative' ); 97 } 98 99 var maxHeight = ($(window).height() - optionsWrap.offset().top - 20); 100 if( instance.options.maxHeight ) { 101 maxHeight = ($(window).height() - optionsWrap.offset().top - 20); 102 maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxheight; 103 } 104 105 maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxHeight; 106 107 optionsWrap.css({ 108 maxWidth : maxWidth, 109 minHeight: instance.options.minHeight, 110 maxHeight: maxHeight, 111 overflow : 'auto' 112 }).hide(); 113 114 // isolate options scroll 115 // @source: https://github.com/nobleclem/jQuery-IsolatedScroll 116 optionsWrap.bind( 'touchmove mousewheel DOMMouseScroll', function ( e ) { 117 if( ($(this).outerHeight() < $(this)[0].scrollHeight) ) { 118 var e0 = e.originalEvent, 119 delta = e0.wheelDelta || -e0.detail; 120 121 if( ($(this).outerHeight() + $(this)[0].scrollTop) > $(this)[0].scrollHeight ) { 122 e.preventDefault(); 123 this.scrollTop += ( delta < 0 ? 1 : -1 ); 124 } 125 } 126 }); 127 128 // hide options menus if click happens off of the list placeholder button 129 $(document).off('click.ms-hideopts').on('click.ms-hideopts', function( event ){ 130 if( !$(event.target).closest('.ms-options-wrap').length ) { 131 $('.ms-options-wrap > .ms-options:visible').hide(); 132 } 133 }); 134 135 // disable button action 136 placeholder.bind('mousedown',function( event ){ 137 // ignore if its not a left click 138 if( event.which != 1 ) { 139 return true; 140 } 141 142 // hide other menus before showing this one 143 $('.ms-options-wrap > .ms-options:visible').each(function(){ 144 if( $(this).parent().prev()[0] != optionsWrap.parent().prev()[0] ) { 145 $(this).hide(); 146 } 147 }); 148 149 // show/hide options 150 optionsWrap.toggle(); 151 152 // recalculate height 153 if( optionsWrap.is(':visible') ) { 154 optionsWrap.css( 'maxHeight', '' ); 155 156 var maxHeight = ($(window).height() - optionsWrap.offset().top - 20); 157 if( instance.options.maxHeight ) { 158 maxHeight = ($(window).height() - optionsWrap.offset().top - 20); 159 maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxheight; 160 } 161 maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxHeight; 162 163 optionsWrap.css( 'maxHeight', maxHeight ); 164 } 165 }).click(function( event ){ event.preventDefault(); }); 166 167 // add placeholder copy 168 if( instance.options.placeholder ) { 169 placeholder.text( instance.options.placeholder ); 170 } 171 172 // add search box 173 if( instance.options.search ) { 174 optionsList.before('<div class="ms-search"><input type="text" value="" placeholder="'+ instance.options.searchOptions['default'] +'" /></div>'); 175 176 var search = optionsWrap.find('.ms-search input'); 177 search.on('keyup', function(){ 178 // ignore keystrokes that don't make a difference 179 if( $(this).data('lastsearch') == $(this).val() ) { 180 return true; 181 } 182 183 $(this).data('lastsearch', $(this).val() ); 184 185 // USER CALLBACK 186 if( typeof instance.options.searchOptions.onSearch == 'function' ) { 187 instance.options.searchOptions.onSearch( instance.element ); 188 } 189 190 // search non optgroup li's 191 optionsList.find('li:not(.optgroup)').each(function(){ 192 var optText = $(this).text(); 193 194 // show option if string exists 195 if( optText.toLowerCase().indexOf( search.val().toLowerCase() ) > -1 ) { 196 $(this).show(); 197 } 198 // don't hide selected items 199 else if( !$(this).hasClass('selected') ) { 200 $(this).hide(); 201 } 202 203 // hide / show optgroups depending on if options within it are visible 204 if( !instance.options.searchOptions.showOptGroups && $(this).closest('li.optgroup') ) { 205 $(this).closest('li.optgroup').show(); 206 207 if( $(this).closest('li.optgroup').find('li:visible').length ) { 208 $(this).closest('li.optgroup').show(); 209 } 210 else { 211 $(this).closest('li.optgroup').hide(); 212 } 213 } 214 }); 215 }); 216 } 217 218 // add global select all options 219 if( instance.options.selectAll ) { 220 optionsList.before('<a href="#" class="ms-selectall global">Select all</a>'); 221 } 222 223 // handle select all option 224 optionsWrap.on('click', '.ms-selectall', function( event ){ 225 event.preventDefault(); 226 227 if( $(this).hasClass('global') ) { 228 // check if any selected if so then select them 229 if( optionsList.find('li:not(.optgroup)').filter(':not(.selected)').length ) { 230 optionsList.find('li:not(.optgroup)').filter(':not(.selected)').find('input[type="checkbox"]').trigger('click'); 231 } 232 // deselect everything 233 else { 234 optionsList.find('li:not(.optgroup).selected input[type="checkbox"]').trigger('click'); 235 } 236 } 237 else if( $(this).closest('li').hasClass('optgroup') ) { 238 var optgroup = $(this).closest('li.optgroup'); 239 240 // check if any selected if so then select them 241 if( optgroup.find('li:not(.selected)').length ) { 242 optgroup.find('li:not(.selected) input[type="checkbox"]').trigger('click'); 243 } 244 // deselect everything 245 else { 246 optgroup.find('li.selected input[type="checkbox"]').trigger('click'); 247 } 248 } 249 }); 250 251 // add options to wrapper 252 var options = []; 253 $(instance.element).children().each(function(){ 254 if( this.nodeName == 'OPTGROUP' ) { 255 var groupOptions = []; 256 257 $(this).children('option').each(function(){ 258 groupOptions[ $(this).val() ] = { 259 name : $(this).text(), 260 value : $(this).val(), 261 checked: $(this).prop( 'selected' ) 262 }; 263 }); 264 265 options.push({ 266 label : $(this).attr('label'), 267 options: groupOptions 268 }); 269 } 270 else if( this.nodeName == 'OPTION' ) { 271 options.push({ 272 name : $(this).text(), 273 value : $(this).val(), 274 checked: $(this).prop( 'selected' ) 275 }); 276 } 277 else { 278 // bad option 279 return true; 280 } 281 }); 282 instance.loadOptions( options ); 283 284 // COLUMNIZE 285 if( hasOptGroup ) { 286 // float non grouped options 287 optionsList.find('> li:not(.optgroup)').css({ 288 float: 'left', 289 width: (100 / instance.options.columns) +'%' 290 }); 291 292 // add CSS3 column styles 293 optionsList.find('li.optgroup').css({ 294 clear: 'both' 295 }).find('> ul').css({ 296 'column-count' : instance.options.columns, 297 'column-gap' : 0,
298 '-webkit-column-count': instance.options.columns, 299 '-webkit-column-gap' : 0, 300 '-moz-column-count' : instance.options.columns, 301 '-moz-column-gap' : 0 302 }); 303 304 // for crappy IE versions float grouped options 305 if( this._ieVersion() && (this._ieVersion() < 10) ) { 306 optionsList.find('li.optgroup > ul > li').css({ 307 float: 'left', 308 width: (100 / instance.options.columns) +'%' 309 }); 310 } 311 } 312 else { 313 // add CSS3 column styles 314 optionsList.css({ 315 'column-count' : instance.options.columns, 316 'column-gap' : 0, 317 '-webkit-column-count': instance.options.columns, 318 '-webkit-column-gap' : 0, 319 '-moz-column-count' : instance.options.columns, 320 '-moz-column-gap' : 0 321 }); 322 323 // for crappy IE versions float grouped options 324 if( this._ieVersion() && (this._ieVersion() < 10) ) { 325 optionsList.find('> li').css({ 326 float: 'left', 327 width: (100 / instance.options.columns) +'%' 328 }); 329 } 330 } 331 332 // BIND SELECT ACTION 333 optionsWrap.on( 'click', 'input[type="checkbox"]', function(){ 334 $(this).closest( 'li' ).toggleClass( 'selected' ); 335 336 var select = optionsWrap.parent().prev(); 337 338 // toggle clicked option 339 select.find('option[value="'+ $(this).val() +'"]').prop( 340 'selected', $(this).is(':checked') 341 ).closest('select').trigger('change'); 342 343 if( typeof instance.options.onOptionClick == 'function' ) { 344 instance.options.onOptionClick(); 345 } 346 347 instance._updatePlaceholderText(); 348 }); 349 350 // hide native select list 351 if( typeof instance.options.onLoad === 'function' ) { 352 instance.options.onLoad( instance.element ); 353 } 354 else { 355 $(instance.element).hide(); 356 } 357 }, 358 359 /* LOAD SELECT OPTIONS */ 360 loadOptions: function( options, overwrite ) { 361 overwrite = (typeof overwrite == 'boolean') ? overwrite : true; 362 363 var instance = this; 364 var optionsList = $(instance.element).next('.ms-options-wrap').find('> .ms-options > ul'); 365 366 if( overwrite ) { 367 optionsList.find('> li').remove(); 368 } 369 370 for( var key in options ) { 371 var thisOption = options[ key ]; 372 var container = $('<li></li>'); 373 374 // optgroup 375 if( thisOption.hasOwnProperty('options') ) { 376 container.addClass('optgroup'); 377 container.append('<span class="label">'+ thisOption.label +'</span>'); 378 container.find('> .label').css({ 379 clear: 'both' 380 }); 381 382 if( instance.options.selectGroup ) { 383 container.append('<a href="#" class="ms-selectall">Select all</a>') 384 } 385 386 container.append('<ul></ul>'); 387 388 for( var gKey in thisOption.options ) { 389 var thisGOption = thisOption.options[ gKey ]; 390 var gContainer = $('<li></li>').addClass('ms-reflow'); 391 392 instance._addOption( gContainer, thisGOption ); 393 394 container.find('> ul').append( gContainer ); 395 } 396 } 397 // option 398 else if( thisOption.hasOwnProperty('value') ) { 399 container.addClass('ms-reflow') 400 401 instance._addOption( container, thisOption ); 402 } 403 404 optionsList.append( container ); 405 } 406 407 optionsList.find('.ms-reflow input[type="checkbox"]').each(function( idx ){ 408 if( $(this).css('display').match(/block$/) ) { 409 var checkboxWidth = $(this).outerWidth(); 410 checkboxWidth = checkboxWidth ? checkboxWidth : 15; 411 412 $(this).closest('label').css( 413 'padding-left', 414 (parseInt( $(this).closest('label').css('padding-left') ) * 2) + checkboxWidth 415 ); 416 417 $(this).closest('.ms-reflow').removeClass('ms-reflow'); 418 } 419 }); 420 421 instance._updatePlaceholderText(); 422 }, 423 424 /* RESET THE DOM */ 425 unload: function() { 426 $(this.element).next('.ms-options-wrap').remove(); 427 $(this.element).show(function(){ 428 $(this).css('display','').removeClass('jqmsLoaded'); 429 }); 430 }, 431 432 /* RELOAD JQ MULTISELECT LIST */ 433 reload: function() { 434 // remove existing options 435 $(this.element).next('.ms-options-wrap').remove(); 436 $(this.element).removeClass('jqmsLoaded'); 437 438 // load element 439 this.load(); 440 }, 441 442 /** PRIVATE FUNCTIONS **/ 443 // update selected placeholder text 444 _updatePlaceholderText: function(){ 445 return false;
446 var instance = this; 447 var placeholder = $(instance.element).next('.ms-options-wrap').find('> button:first-child'); 448 var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options'); 449 var select = optionsWrap.parent().prev(); 450 451 // get selected options 452 var selOpts = []; 453 select.find('option:selected').each(function(){ 454 selOpts.push( $(this).text() ); 455 }); 456 457 // UPDATE PLACEHOLDER TEXT WITH OPTIONS SELECTED 458 placeholder.text( selOpts.join( ', ' ) ); 459 var copy = placeholder.clone().css({ 460 display : 'inline', 461 width : 'auto', 462 visibility: 'hidden' 463 }).appendTo( optionsWrap.parent() ); 464 465 // if the jquery.actual plugin is loaded use it to get the widths 466 var copyWidth = (typeof $.fn.actual !== 'undefined') ? copy.actual( 'width', instance.options.jqActualOpts ) : copy.width(); 467 var placeWidth = (typeof $.fn.actual !== 'undefined') ? placeholder.actual( 'width', instance.options.jqActualOpts ) : placeholder.width(); 468 469 // if copy is larger than button width use "# selected" 470 if( copyWidth > placeWidth ) { 471 placeholder.text( selOpts.length +' selected' ); 472 } 473 // if options selected then use those 474 else if( selOpts.length ) { 475 placeholder.text( selOpts.join( ', ' ) ); 476 } 477 // replace placeholder text 478 else { 479 placeholder.text( instance.options.placeholder ); 480 } 481 482 // remove dummy element 483 copy.remove(); 484 }, 485 486 // Add option to the custom dom list 487 _addOption: function( container, option ) { 488 container.text( option.name ); 489 container.prepend( 490 $('<input type="checkbox" value="" title="" />') 491 .val( option.value ) 492 .attr( 'title', option.name ) 493 .attr( 'id', 'ms-opt-'+ msCounter ) 494 ); 495 496 if( option.checked ) { 497 container.addClass('default'); 498 container.addClass('selected'); 499 container.find( 'input[type="checkbox"]' ).prop( 'checked', true ); 500 } 501 502 var label = $('<label></label>').attr( 'for', 'ms-opt-'+ msCounter ); 503 container.wrapInner( label ); 504 505 506 if( !this.options.showCheckbox ) { 507 container.find('input[id="ms-opt-'+ msCounter +'"]').hide(); 508 } 509 510 msCounter = msCounter + 1; 511 }, 512 513 // check ie version 514 _ieVersion: function() { 515 var myNav = navigator.userAgent.toLowerCase(); 516 return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false; 517 } 518 }; 519 520 // ENABLE JQUERY PLUGIN FUNCTION 521 $.fn.multiselect = function( options ){ 522 var args = arguments; 523 var ret; 524 525 // menuize each list 526 if( (options === undefined) || (typeof options === 'object') ) { 527 return this.each(function(){ 528 if( !$.data( this, 'plugin_multiselect' ) ) { 529 $.data( this, 'plugin_multiselect', new MultiSelect( this, options ) ); 530 } 531 }); 532 } else if( (typeof options === 'string') && (options[0] !== '_') && (options !== 'init') ) { 533 this.each(function(){ 534 var instance = $.data( this, 'plugin_multiselect' ); 535 536 if( instance instanceof MultiSelect && typeof instance[ options ] === 'function' ) { 537 ret = instance[ options ].apply( instance, Array.prototype.slice.call( args, 1 ) ); 538 } 539 540 // special destruct handler 541 if( options === 'unload' ) { 542 $.data( this, 'plugin_multiselect', null ); 543 } 544 }); 545 546 return ret; 547 } 548 }; 549}(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.