PageSourceSearch

https://oceanfix.ca/js/jquery.multiselect.js

js oceanfix.ca collected 2026-10-08 02:27:45 UTC 22,522 bytes, 549 lines download raw bytes

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.