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 : true, // 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('<input type="checkbox" class="ms-selectall global" id="manu_selectall" title="Select All"><a href="#" class="ms-selectall global">Select all</a><button type="button" id="manu_search_button" style="width: 15%;float: right;"> <i class="fa fa-search"></i> </button>'); 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 $("#manu_selectall").prop("checked", true); 232 } 233 // deselect everything 234 else { 235 optionsList.find('li:not(.optgroup).selected input[type="checkbox"]').trigger('click'); 236 $("#manu_selectall").prop("checked", false); 237 } 238 } 239 else if( $(this).closest('li').hasClass('optgroup') ) { 240 var optgroup = $(this).closest('li.optgroup'); 241 242 // check if any selected if so then select them 243 if( optgroup.find('li:not(.selected)').length ) { 244 optgroup.find('li:not(.selected) input[type="checkbox"]').trigger('click'); 245 } 246 // deselect everything 247 else { 248 optgroup.find('li.selected input[type="checkbox"]').trigger('click'); 249 } 250 } 251 }); 252 253 // add options to wrapper 254 var options = []; 255 $(instance.element).children().each(function(){ 256 if( this.nodeName == 'OPTGROUP' ) { 257 var groupOptions = []; 258 259 $(this).children('option').each(function(){ 260 groupOptions[ $(this).val() ] = { 261 name : $(this).text(), 262 value : $(this).val(), 263 checked: $(this).prop( 'selected' ) 264 }; 265 }); 266 267 options.push({ 268 label : $(this).attr('label'), 269 options: groupOptions 270 }); 271 } 272 else if( this.nodeName == 'OPTION' ) { 273 options.push({ 274 name : $(this).text(), 275 value : $(this).val(), 276 checked: $(this).prop( 'selected' ) 277 }); 278 } 279 else { 280 // bad option 281 return true; 282 } 283 }); 284 instance.loadOptions( options ); 285 286 // COLUMNIZE 287 if( hasOptGroup ) { 288 // float non grouped options 289 optionsList.find('> li:not(.optgroup)').css({ 290 float: 'left', 291 width: (100 / instance.options.columns) +'%' 292 }); 293 294 // add CSS3 column styles 295 optionsList.find('li.optgroup').css({ 296 clear: 'both' 297 }).find('> ul').css({ 298 'column-count' : instance.options.columns, 299 'column-gap' : 0,
300 '-webkit-column-count': instance.options.columns, 301 '-webkit-column-gap' : 0, 302 '-moz-column-count' : instance.options.columns, 303 '-moz-column-gap' : 0 304 }); 305 306 // for crappy IE versions float grouped options 307 if( this._ieVersion() && (this._ieVersion() < 10) ) { 308 optionsList.find('li.optgroup > ul > li').css({ 309 float: 'left', 310 width: (100 / instance.options.columns) +'%' 311 }); 312 } 313 } 314 else { 315 // add CSS3 column styles 316 optionsList.css({ 317 'column-count' : instance.options.columns, 318 'column-gap' : 0, 319 '-webkit-column-count': instance.options.columns, 320 '-webkit-column-gap' : 0, 321 '-moz-column-count' : instance.options.columns, 322 '-moz-column-gap' : 0 323 }); 324 325 // for crappy IE versions float grouped options 326 if( this._ieVersion() && (this._ieVersion() < 10) ) { 327 optionsList.find('> li').css({ 328 float: 'left', 329 width: (100 / instance.options.columns) +'%' 330 }); 331 } 332 } 333 334 // BIND SELECT ACTION 335 optionsWrap.on( 'click', 'input[type="checkbox"]', function(){ 336 $(this).closest( 'li' ).toggleClass( 'selected' ); 337 338 var select = optionsWrap.parent().prev(); 339 340 // toggle clicked option 341 select.find('option[value="'+ $(this).val() +'"]').prop( 342 'selected', $(this).is(':checked') 343 ).closest('select').trigger('change'); 344 345 if( typeof instance.options.onOptionClick == 'function' ) { 346 instance.options.onOptionClick(); 347 } 348 349 instance._updatePlaceholderText(); 350 }); 351 352 // hide native select list 353 if( typeof instance.options.onLoad === 'function' ) { 354 instance.options.onLoad( instance.element ); 355 } 356 else { 357 $(instance.element).hide(); 358 } 359 }, 360 361 /* LOAD SELECT OPTIONS */ 362 loadOptions: function( options, overwrite ) { 363 overwrite = (typeof overwrite == 'boolean') ? overwrite : true; 364 365 var instance = this; 366 var optionsList = $(instance.element).next('.ms-options-wrap').find('> .ms-options > ul'); 367 368 if( overwrite ) { 369 optionsList.find('> li').remove(); 370 } 371 372 for( var key in options ) { 373 var thisOption = options[ key ]; 374 var container = $('<li></li>'); 375 376 // optgroup 377 if( thisOption.hasOwnProperty('options') ) { 378 container.addClass('optgroup'); 379 container.append('<span class="label">'+ thisOption.label +'</span>'); 380 container.find('> .label').css({ 381 clear: 'both' 382 }); 383 384 if( instance.options.selectGroup ) { 385 container.append('<a href="#" class="ms-selectall">Select all</a>') 386 } 387 388 container.append('<ul></ul>'); 389 390 for( var gKey in thisOption.options ) { 391 var thisGOption = thisOption.options[ gKey ]; 392 var gContainer = $('<li></li>').addClass('ms-reflow'); 393 394 instance._addOption( gContainer, thisGOption ); 395 396 container.find('> ul').append( gContainer ); 397 } 398 } 399 // option 400 else if( thisOption.hasOwnProperty('value') ) { 401 container.addClass('ms-reflow') 402 403 instance._addOption( container, thisOption ); 404 } 405 406 optionsList.append( container ); 407 } 408 409 optionsList.find('.ms-reflow input[type="checkbox"]').each(function( idx ){ 410 if( $(this).css('display').match(/block$/) ) { 411 var checkboxWidth = $(this).outerWidth(); 412 checkboxWidth = checkboxWidth ? checkboxWidth : 15; 413 414 $(this).closest('label').css( 415 'padding-left', 416 (parseInt( $(this).closest('label').css('padding-left') ) * 2) + checkboxWidth 417 ); 418 419 $(this).closest('.ms-reflow').removeClass('ms-reflow'); 420 } 421 }); 422 423 instance._updatePlaceholderText(); 424 }, 425 426 /* RESET THE DOM */ 427 unload: function() { 428 $(this.element).next('.ms-options-wrap').remove(); 429 $(this.element).show(function(){ 430 $(this).css('display','').removeClass('jqmsLoaded'); 431 }); 432 }, 433 434 /* RELOAD JQ MULTISELECT LIST */ 435 reload: function() { 436 // remove existing options 437 $(this.element).next('.ms-options-wrap').remove(); 438 $(this.element).removeClass('jqmsLoaded'); 439 440 // load element 441 this.load(); 442 }, 443 444 /** PRIVATE FUNCTIONS **/ 445 // update selected placeholder text 446 _updatePlaceholderText: function(){ 447 return false;
448 var instance = this; 449 var placeholder = $(instance.element).next('.ms-options-wrap').find('> button:first-child'); 450 var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options'); 451 var select = optionsWrap.parent().prev(); 452 453 // get selected options 454 var selOpts = []; 455 select.find('option:selected').each(function(){ 456 selOpts.push( $(this).text() ); 457 }); 458 459 // UPDATE PLACEHOLDER TEXT WITH OPTIONS SELECTED 460 placeholder.text( selOpts.join( ', ' ) ); 461 var copy = placeholder.clone().css({ 462 display : 'inline', 463 width : 'auto', 464 visibility: 'hidden' 465 }).appendTo( optionsWrap.parent() ); 466 467 // if the jquery.actual plugin is loaded use it to get the widths 468 var copyWidth = (typeof $.fn.actual !== 'undefined') ? copy.actual( 'width', instance.options.jqActualOpts ) : copy.width(); 469 var placeWidth = (typeof $.fn.actual !== 'undefined') ? placeholder.actual( 'width', instance.options.jqActualOpts ) : placeholder.width(); 470 471 // if copy is larger than button width use "# selected" 472 if( copyWidth > placeWidth ) { 473 placeholder.text( selOpts.length +' selected' ); 474 } 475 // if options selected then use those 476 else if( selOpts.length ) { 477 placeholder.text( selOpts.join( ', ' ) ); 478 } 479 // replace placeholder text 480 else { 481 placeholder.text( instance.options.placeholder ); 482 } 483 484 // remove dummy element 485 copy.remove(); 486 }, 487 488 // Add option to the custom dom list 489 _addOption: function( container, option ) { 490 container.text( option.name ); 491 container.prepend( 492 $('<input type="checkbox" value="" title="" />') 493 .val( option.value ) 494 .attr( 'title', option.name ) 495 .attr( 'id', 'ms-opt-'+ msCounter ) 496 ); 497 498 if( option.checked ) { 499 container.addClass('default'); 500 container.addClass('selected'); 501 container.find( 'input[type="checkbox"]' ).prop( 'checked', true ); 502 } 503 504 var label = $('<label></label>').attr( 'for', 'ms-opt-'+ msCounter ); 505 container.wrapInner( label ); 506 507 508 if( !this.options.showCheckbox ) { 509 container.find('input[id="ms-opt-'+ msCounter +'"]').hide(); 510 } 511 512 msCounter = msCounter + 1; 513 }, 514 515 // check ie version 516 _ieVersion: function() { 517 var myNav = navigator.userAgent.toLowerCase(); 518 return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false; 519 } 520 }; 521 522 // ENABLE JQUERY PLUGIN FUNCTION 523 $.fn.multiselect = function( options ){ 524 var args = arguments; 525 var ret; 526 527 // menuize each list 528 if( (options === undefined) || (typeof options === 'object') ) { 529 return this.each(function(){ 530 if( !$.data( this, 'plugin_multiselect' ) ) { 531 $.data( this, 'plugin_multiselect', new MultiSelect( this, options ) ); 532 } 533 }); 534 } else if( (typeof options === 'string') && (options[0] !== '_') && (options !== 'init') ) { 535 this.each(function(){ 536 var instance = $.data( this, 'plugin_multiselect' ); 537 538 if( instance instanceof MultiSelect && typeof instance[ options ] === 'function' ) { 539 ret = instance[ options ].apply( instance, Array.prototype.slice.call( args, 1 ) ); 540 } 541 542 // special destruct handler 543 if( options === 'unload' ) { 544 $.data( this, 'plugin_multiselect', null ); 545 } 546 }); 547 548 return ret; 549 } 550 }; 551}(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.