nobleclem / jQuery-MultiSelect

Turn a multiselect list into a nice and easy to use list with checkboxes.
https://springstubbe.us/projects/jquery-multiselect/
MIT License
289 stars 216 forks source link

Potential contribution #137

Closed SB401 closed 5 years ago

SB401 commented 6 years ago

Hi Patrick,

I have been using your multiselect on a couple of projects know and I absolutely love it. However, I had to make a few addition to your code to suit my needs and I am happy to give you the modified code as a contribution. I am a self thought coder and my codes are a bit messy but they work.

Additional features are:-

addfood is a javascript function that runs when you click the button which is created in .ms-options when addnewitems is set to true

Below is the modified code with comments to show where I have made the modifications:

/*
 * Display a nice easy to use multiselect list
 * @Version: 2.4.11
 * @Author: Patrick Springstubbe
 * @Contact: @JediNobleclem
 * @Website: springstubbe.us
 * @Source: https://github.com/nobleclem/jQuery-MultiSelect
 *
 * Usage:
 *     $('select[multiple]').multiselect();
 *     $('select[multiple]').multiselect({ texts: { placeholder: 'Select options' } });
 *     $('select[multiple]').multiselect('reload');
 *     $('select[multiple]').multiselect( 'loadOptions', [{
 *         name   : 'Option Name 1',
 *         value  : 'option-value-1',
 *         checked: false,
 *         attributes : {
 *             custom1: 'value1',
 *             custom2: 'value2'
 *         }
 *     },{
 *         name   : 'Option Name 2',
 *         value  : 'option-value-2',
 *         checked: false,
 *         attributes : {
 *             custom1: 'value1',
 *             custom2: 'value2'
 *         }
 *     }]);
 *
 */
(function($) {
    var defaults = {
        columns: 1, // how many columns should be use to show options
        search: false, // include option search box

        // search filter options
        searchOptions: {
            delay: 250, // time (in ms) between keystrokes until search happens
            showOptGroups: false, // show option group titles if no options remaining
            searchText: true, // search within the text
            searchValue: false, // search within the value
            onSearch: function(element) {}, // fires on keyup before search on options happens

        },

        // plugin texts
        texts: {
            placeholder: 'Select options', // text to use in dummy input
            search: 'Search', // search input placeholder text
            selectedOptions: ' selected', // selected suffix text
            selectAll: 'Select all', // select all text
            unselectAll: 'Unselect all', // unselect all text
            noneSelected: 'None Selected' // None selected text
        },

        // general options
        selectAll: false, // add select all option
        selectGroup: false, // select entire optgroup
        minHeight: 200, // minimum height of option overlay
        maxHeight: null, // maximum height of option overlay
        maxWidth: null, // maximum width of option overlay (or selector)
        maxPlaceholderWidth: null, // maximum width of placeholder button
        maxPlaceholderOpts: 10, // maximum number of placeholder options to show until "# selected" shown instead
        showCheckbox: true, // display the checkbox to the user
        checkboxAutoFit: false, // auto calc checkbox padding
        optionAttributes: [], // attributes to copy to the checkbox from the option element

        // Callbacks
        onLoad: function(element) {}, // fires at end of list initialization
        onOptionClick: function(element, option) {}, // fires when an option is clicked
        onControlClose: function(element) {}, // fires when the options list is closed
        onSelectAll: function(element, selected) {}, // fires when (un)select all is clicked

        // @NOTE: these are for future development
        minSelect: false, // minimum number of items that can be selected
        maxSelect: false, // maximum number of items that can be selected
    };

    var msCounter = 1;

    // FOR LEGACY BROWSERS (talking to you IE8)
    if (typeof Array.prototype.map !== 'function') {
        Array.prototype.map = function(callback, thisArg) {
            if (typeof thisArg === 'undefined') {
                thisArg = this;
            }

            return $.isArray(thisArg) ? $.map(thisArg, callback) : [];
        };
    }
    if (typeof String.prototype.trim !== 'function') {
        String.prototype.trim = function() {
            return this.replace(/^\s+|\s+$/g, '');
        };
    }

    function MultiSelect(element, options) {
        this.element = element;
        this.options = $.extend(true, {}, defaults, options);
        this.updateSelectAll = true;
        this.updatePlaceholder = true;

        /* Make sure its a multiselect list */
        if (!$(this.element).attr('multiple')) {
            throw new Error('[jQuery-MultiSelect] Select list must be a multiselect list in order to use this plugin');
        }

        /* Options validation checks */
        if (this.options.search) {
            if (!this.options.searchOptions.searchText && !this.options.searchOptions.searchValue) {
                throw new Error('[jQuery-MultiSelect] Either searchText or searchValue should be true.');
            }
        }

        /** BACKWARDS COMPATIBILITY **/
        if ('placeholder' in this.options) {
            this.options.texts.placeholder = this.options.placeholder;
            delete this.options.placeholder;
        }
        if ('default' in this.options.searchOptions) {
            this.options.texts.search = this.options.searchOptions['default'];
            delete this.options.searchOptions['default'];
        }
        /** END BACKWARDS COMPATIBILITY **/

        // load this instance
        this.load();
    }

    MultiSelect.prototype = {
        /* LOAD CUSTOM MULTISELECT DOM/ACTIONS */
        load: function() {
            var instance = this;

            // make sure this is a select list and not loaded
            if ((instance.element.nodeName != 'SELECT') || $(instance.element).hasClass('jqmsLoaded')) {
                return true;
            }

            // sanity check so we don't double load on a select element
            $(instance.element).addClass('jqmsLoaded').data('plugin_multiselect-instance', instance);

            // add option container
            $(instance.element).after('<div class="ms-options-wrap"><button type="button"><span>None Selected</span></button><div class="ms-options"><ul></ul></div></div>');

            var placeholder = $(instance.element).next('.ms-options-wrap').find('> button:first-child');
            var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options');
            var optionsList = optionsWrap.find('> ul');

            // don't show checkbox (add class for css to hide checkboxes)
            if (!instance.options.showCheckbox) {
                optionsWrap.addClass('hide-checkbox');
            } else if (instance.options.checkboxAutoFit) {
                optionsWrap.addClass('checkbox-autofit');
            }

            // check if list is disabled
            if ($(instance.element).prop('disabled')) {
                placeholder.prop('disabled', true);
            }

            // set placeholder maxWidth
            if (instance.options.maxPlaceholderWidth) {
                placeholder.css('maxWidth', instance.options.maxPlaceholderWidth);
            }

            // override with user defined maxHeight
            if (instance.options.maxHeight) {
                var maxHeight = instance.options.maxHeight;
            } else {
                // cacl default maxHeight
                var maxHeight = ($(window).height() - optionsWrap.offset().top + $(window).scrollTop() - 20);
            }

            // maxHeight cannot be less than options.minHeight
            maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxHeight;

            optionsWrap.css({
                maxWidth: instance.options.maxWidth,
                minHeight: instance.options.minHeight,
                maxHeight: maxHeight,
            });

            // isolate options scroll
            // @source: https://github.com/nobleclem/jQuery-IsolatedScroll
            optionsWrap.bind('touchmove mousewheel DOMMouseScroll', function(e) {
                if (($(this).outerHeight() < $(this)[0].scrollHeight)) {
                    var e0 = e.originalEvent,
                        delta = e0.wheelDelta || -e0.detail;

                    if (($(this).outerHeight() + $(this)[0].scrollTop) > $(this)[0].scrollHeight) {
                        e.preventDefault();
                        this.scrollTop += (delta < 0 ? 1 : -1);
                    }
                }
            });

            // hide options menus if click happens off of the list placeholder button
            $(document).off('click.ms-hideopts').on('click.ms-hideopts', function(event) {
                if (!$(event.target).closest('.ms-options-wrap').length) {
                    $('.ms-options-wrap.ms-active > .ms-options').each(function() {
                        $(this).closest('.ms-options-wrap').removeClass('ms-active');

                        //var thisInst = $(this).parent().prev('.jqmsLoaded').data('plugin_multiselect-instance');
                        //sheils modified above
                        var thisInst = $(this).parent().siblings(".jqmsLoaded").data('plugin_multiselect-instance');

                        // USER CALLBACK
                        if (typeof thisInst.options.onControlClose == 'function') {
                            thisInst.options.onControlClose(thisInst.element);
                        }
                    });
                }
                // hide open option lists if escape key pressed
            }).bind('keydown', function(event) {
                if ((event.keyCode || event.which) == 27) { // esc key
                    $(this).trigger('click.ms-hideopts');
                }
            });

            // handle pressing enter|space while tabbing through
            placeholder.bind('keydown', function(event) {
                var code = (event.keyCode || event.which);
                if ((code == 13) || (code == 32)) { // enter OR space
                    placeholder.trigger('mousedown');
                }
            });

            // disable button action
            placeholder.bind('mousedown', function(event) {
                // ignore if its not a left click
                if (event.which && (event.which != 1)) {
                    return true;
                }

                // hide other menus before showing this one
                $('.ms-options-wrap.ms-active > .ms-options').each(function() {
                    if ($(this).parent().prev()[0] != optionsWrap.parent().prev()[0]) {
                        $(this).closest('.ms-options-wrap').removeClass('ms-active');

                        var thisInst = $(this).parent().prev('.jqmsLoaded').data('plugin_multiselect-instance');

                        // USER CALLBACK
                        if (typeof thisInst.options.onControlClose == 'function') {
                            thisInst.options.onControlClose(thisInst.element);
                        }
                    }
                });

                // show/hide options
                optionsWrap.closest('.ms-options-wrap').toggleClass('ms-active');

                // recalculate height
                if (optionsWrap.closest('.ms-options-wrap').hasClass('ms-active')) {
                    optionsWrap.css('maxHeight', '');

                    // override with user defined maxHeight
                    if (instance.options.maxHeight) {
                        var maxHeight = instance.options.maxHeight;
                    } else {
                        // cacl default maxHeight
                        var maxHeight = ($(window).height() - optionsWrap.offset().top + $(window).scrollTop() - 20);
                    }

                    if (maxHeight) {
                        // maxHeight cannot be less than options.minHeight
                        maxHeight = maxHeight < instance.options.minHeight ? instance.options.minHeight : maxHeight;

                        optionsWrap.css('maxHeight', maxHeight);
                    }
                } else if (typeof instance.options.onControlClose == 'function') {
                    instance.options.onControlClose(instance.element);
                }
            }).click(function(event) {
                event.preventDefault();
            });

            // add placeholder copy
            if (instance.options.texts.placeholder) {
                placeholder.find('span').text(instance.options.texts.placeholder);
            }

            // add search box

            if (instance.options.search) {
                optionsList.before('<div class="ms-search"><input type="text" value="" placeholder="' + instance.options.texts.search + '" /></div>');

                //Sheils modification---------------------------------------------------------------------
                //add ADD New Option Button

                if (instance.options.searchOptions.addnewitems) {
                    optionsList.before('<input id="ms-addnew" type="button">');

                }

                var addnewbutton = optionsWrap.find('#ms-addnew');
                //addnewbutton.hide();
                //-------------------------------------------------------------------
                var search = optionsWrap.find('.ms-search input');
                search.on('keyup', function() {
                    // ignore keystrokes that don't make a difference
                    if ($(this).data('lastsearch') == $(this).val()) {
                        return true;
                    }

                    // pause timeout
                    if ($(this).data('searchTimeout')) {
                        clearTimeout($(this).data('searchTimeout'));
                    }

                    var thisSearchElem = $(this);

                    $(this).data('searchTimeout', setTimeout(function() {
                        thisSearchElem.data('lastsearch', thisSearchElem.val());

                        // USER CALLBACK
                        if (typeof instance.options.searchOptions.onSearch == 'function') {
                            instance.options.searchOptions.onSearch(instance.element);
                        }

                        // search non optgroup li's
                        var searchString = $.trim(search.val().toLowerCase());
                        if (searchString) {
                            optionsList.find('li[data-search-term*="' + searchString + '"]:not(.optgroup)').removeClass('ms-hidden');
                            optionsList.find('li:not([data-search-term*="' + searchString + '"], .optgroup)').addClass('ms-hidden');
                        } else {
                            optionsList.find('.ms-hidden').removeClass('ms-hidden');
                        }

                        //Sheils modification -----------------------------------------------------------

                        addnewbutton.val("Add '" + searchString + "' to list")
                        document.getElementById("ms-addnew").setAttribute("onclick", instance.options.searchOptions.addnewitems + "('" + searchString + "'," + instance.options.singleselect + ",'#" + instance.element.id + "')");

                        //----------------------------------------------------------------------------------

                        // show/hide optgroups based on if there are items visible within
                        if (!instance.options.searchOptions.showOptGroups) {
                            optionsList.find('.optgroup').each(function() {
                                if ($(this).find('li:not(.ms-hidden)').length) {
                                    $(this).show();
                                } else {
                                    $(this).hide();
                                }
                            });
                        }

                        instance._updateSelectAllText();
                    }, instance.options.searchOptions.delay));
                });
            }

            // add global select all options
            if (instance.options.selectAll) {
                optionsList.before('<a href="#" class="ms-selectall global">' + instance.options.texts.selectAll + '</a>');
            }

            // handle select all option
            optionsWrap.on('click', '.ms-selectall', function(event) {
                event.preventDefault();

                instance.updateSelectAll = false;
                instance.updatePlaceholder = false;

                var select = optionsWrap.parent().prev();

                if ($(this).hasClass('global')) {
                    // check if any options are not selected if so then select them
                    if (optionsList.find('li:not(.optgroup, .selected, .ms-hidden)').length) {
                        // get unselected vals, mark as selected, return val list
                        optionsList.find('li:not(.optgroup, .selected, .ms-hidden)').addClass('selected');
                        optionsList.find('li.selected input[type="checkbox"]:not(:disabled)').prop('checked', true);
                    }
                    // deselect everything
                    else {
                        optionsList.find('li:not(.optgroup, .ms-hidden).selected').removeClass('selected');
                        optionsList.find('li:not(.optgroup, .ms-hidden, .selected) input[type="checkbox"]:not(:disabled)').prop('checked', false);
                    }
                } else if ($(this).closest('li').hasClass('optgroup')) {
                    var optgroup = $(this).closest('li.optgroup');

                    // check if any selected if so then select them
                    if (optgroup.find('li:not(.selected, .ms-hidden)').length) {
                        optgroup.find('li:not(.selected, .ms-hidden)').addClass('selected');
                        optgroup.find('li.selected input[type="checkbox"]:not(:disabled)').prop('checked', true);
                    }
                    // deselect everything
                    else {
                        optgroup.find('li:not(.ms-hidden).selected').removeClass('selected');
                        optgroup.find('li:not(.ms-hidden, .selected) input[type="checkbox"]:not(:disabled)').prop('checked', false);
                    }
                }

                var vals = [];
                optionsList.find('li.selected input[type="checkbox"]').each(function() {
                    vals.push($(this).val());
                });
                select.val(vals).trigger('change');

                instance.updateSelectAll = true;
                instance.updatePlaceholder = true;

                // USER CALLBACK
                if (typeof instance.options.onSelectAll == 'function') {
                    instance.options.onSelectAll(instance.element, vals.length);
                }

                instance._updateSelectAllText();
                instance._updatePlaceholderText();
            });

            // add options to wrapper
            var options = [];
            $(instance.element).children().each(function() {
                if (this.nodeName == 'OPTGROUP') {
                    var groupOptions = [];

                    $(this).children('option').each(function() {
                        var thisOptionAtts = {};
                        for (var i = 0; i < instance.options.optionAttributes.length; i++) {
                            var thisOptAttr = instance.options.optionAttributes[i];

                            if ($(this).attr(thisOptAttr) !== undefined) {
                                thisOptionAtts[thisOptAttr] = $(this).attr(thisOptAttr);
                            }
                        }

                        groupOptions.push({
                            name: $(this).text(),
                            value: $(this).val(),
                            checked: $(this).prop('selected'),
                            attributes: thisOptionAtts
                        });
                    });

                    options.push({
                        label: $(this).attr('label'),
                        options: groupOptions
                    });
                } else if (this.nodeName == 'OPTION') {
                    var thisOptionAtts = {};
                    for (var i = 0; i < instance.options.optionAttributes.length; i++) {
                        var thisOptAttr = instance.options.optionAttributes[i];

                        if ($(this).attr(thisOptAttr) !== undefined) {
                            thisOptionAtts[thisOptAttr] = $(this).attr(thisOptAttr);
                        }
                    }

                    options.push({
                        name: $(this).text(),
                        value: $(this).val(),
                        checked: $(this).prop('selected'),
                        attributes: thisOptionAtts
                    });
                } else {
                    // bad option
                    return true;
                }
            });
            instance.loadOptions(options, true, false);

            // update un/select all logic
            instance._updateSelectAllText(false);

            // BIND SELECT ACTION
            optionsWrap.on('click', 'input[type="checkbox"]', function() {
                $(this).closest('li').toggleClass('selected');

                //var select = optionsWrap.parent().prev();
                //sheils modified above
                var select = optionsWrap.parent().siblings("select")

                // toggle clicked option
                select.find('option[value="' + $(this).val() + '"]').prop(
                    'selected', $(this).is(':checked')
                ).closest('select').trigger('change');

                // USER CALLBACK
                if (typeof instance.options.onOptionClick == 'function') {
                    instance.options.onOptionClick(instance.element, this);
                }

                instance._updateSelectAllText();
                instance._updatePlaceholderText();
            });

            // BIND FOCUS EVENT
            optionsWrap.on('focusin', 'input[type="checkbox"]', function() {
                $(this).closest('label').addClass('focused');
            }).on('focusout', 'input[type="checkbox"]', function() {
                $(this).closest('label').removeClass('focused');
            });

            // USER CALLBACK
            if (typeof instance.options.onLoad === 'function') {
                instance.options.onLoad(instance.element);
            }

            // hide native select list
            $(instance.element).hide();
        },

        /* LOAD SELECT OPTIONS */
        loadOptions: function(options, overwrite, updateSelect) {
            overwrite = (typeof overwrite == 'boolean') ? overwrite : true;
            updateSelect = (typeof updateSelect == 'boolean') ? updateSelect : true;

            var instance = this;
            var optionsList = $(instance.element).next('.ms-options-wrap').find('> .ms-options > ul');
            var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options');
            var select = optionsWrap.parent().prev();

            if (overwrite) {
                optionsList.find('> li').remove();

                if (updateSelect) {
                    select.find('> *').remove();
                }
            }

            var containers = [];
            for (var key in options) {
                // Prevent prototype methods injected into options from being iterated over.
                if (!options.hasOwnProperty(key)) {
                    continue;
                }

                var thisOption = options[key];
                var container = $('<li/>');
                var appendContainer = true;

                // OPTION
                if (thisOption.hasOwnProperty('value')) {
                    if (instance.options.showCheckbox && instance.options.checkboxAutoFit) {
                        container.addClass('ms-reflow');
                    }

                    // add option to ms dropdown
                    instance._addOption(container, thisOption);

                    if (updateSelect) {
                        var selOption = $('<option value="' + thisOption.value + '">' + thisOption.name + '</option>');

                        // add custom user attributes
                        if (thisOption.hasOwnProperty('attributes') && Object.keys(thisOption.attributes).length) {
                            selOption.attr(thisOption.attributes);
                        }

                        // mark option as selected
                        if (thisOption.checked) {
                            selOption.prop('selected', true);
                        }

                        select.append(selOption);
                    }
                }
                // OPTGROUP
                else if (thisOption.hasOwnProperty('options')) {
                    var optGroup = $('<optgroup label="' + thisOption.label + '"></optgroup>');

                    optionsList.find('> li.optgroup > span.label').each(function() {
                        if ($(this).text() == thisOption.label) {
                            container = $(this).closest('.optgroup');
                            appendContainer = false;
                        }
                    });

                    // prepare to append optgroup to select element
                    if (updateSelect) {
                        if (select.find('optgroup[label="' + thisOption.label + '"]').length) {
                            optGroup = select.find('optgroup[label="' + thisOption.label + '"]');
                        } else {
                            select.append(optGroup);
                        }
                    }

                    // setup container
                    if (appendContainer) {
                        container.addClass('optgroup');
                        container.append('<span class="label">' + thisOption.label + '</span>');
                        container.find('> .label').css({
                            clear: 'both'
                        });

                        // add select all link
                        if (instance.options.selectGroup) {
                            container.append('<a href="#" class="ms-selectall">' + instance.options.texts.selectAll + '</a>');
                        }

                        container.append('<ul/>');
                    }

                    for (var gKey in thisOption.options) {
                        // Prevent prototype methods injected into options from
                        // being iterated over.
                        if (!thisOption.options.hasOwnProperty(gKey)) {
                            continue;
                        }

                        var thisGOption = thisOption.options[gKey];
                        var gContainer = $('<li/>');
                        if (instance.options.showCheckbox && instance.options.checkboxAutoFit) {
                            gContainer.addClass('ms-reflow');
                        }

                        // no clue what this is we hit (skip)
                        if (!thisGOption.hasOwnProperty('value')) {
                            continue;
                        }

                        instance._addOption(gContainer, thisGOption);

                        container.find('> ul').append(gContainer);

                        // add option to optgroup in select element
                        if (updateSelect) {
                            var selOption = $('<option value="' + thisGOption.value + '">' + thisGOption.name + '</option>');

                            // add custom user attributes
                            if (thisGOption.hasOwnProperty('attributes') && Object.keys(thisGOption.attributes).length) {
                                selOption.attr(thisGOption.attributes);
                            }

                            // mark option as selected
                            if (thisGOption.checked) {
                                selOption.prop('selected', true);
                            }

                            optGroup.append(selOption);
                        }
                    }
                } else {
                    // no clue what this is we hit (skip)
                    continue;
                }

                if (appendContainer) {
                    containers.push(container);
                }
            }
            optionsList.append(containers);

            // pad out label for room for the checkbox
            if (instance.options.checkboxAutoFit && instance.options.showCheckbox && !optionsWrap.hasClass('hide-checkbox')) {
                var chkbx = optionsList.find('.ms-reflow:eq(0) input[type="checkbox"]');
                if (chkbx.length) {
                    var checkboxWidth = chkbx.outerWidth();
                    checkboxWidth = checkboxWidth ? checkboxWidth : 15;

                    optionsList.find('.ms-reflow label').css(
                        'padding-left',
                        (parseInt(chkbx.closest('label').css('padding-left')) * 2) + checkboxWidth
                    );

                    optionsList.find('.ms-reflow').removeClass('ms-reflow');
                }
            }

            // update placeholder text
            instance._updatePlaceholderText();

            // RESET COLUMN STYLES
            optionsWrap.find('ul').css({
                'column-count': '',
                'column-gap': '',
                '-webkit-column-count': '',
                '-webkit-column-gap': '',
                '-moz-column-count': '',
                '-moz-column-gap': ''
            });

            // COLUMNIZE
            if (select.find('optgroup').length) {
                // float non grouped options
                optionsList.find('> li:not(.optgroup)').css({
                    'float': 'left',
                    width: (100 / instance.options.columns) + '%'
                });

                // add CSS3 column styles
                optionsList.find('li.optgroup').css({
                    clear: 'both'
                }).find('> ul').css({
                    'column-count': instance.options.columns,
                    'column-gap': 0,
                    '-webkit-column-count': instance.options.columns,
                    '-webkit-column-gap': 0,
                    '-moz-column-count': instance.options.columns,
                    '-moz-column-gap': 0
                });

                // for crappy IE versions float grouped options
                if (this._ieVersion() && (this._ieVersion() < 10)) {
                    optionsList.find('li.optgroup > ul > li').css({
                        'float': 'left',
                        width: (100 / instance.options.columns) + '%'
                    });
                }
            } else {
                // add CSS3 column styles
                optionsList.css({
                    'column-count': instance.options.columns,
                    'column-gap': 0,
                    '-webkit-column-count': instance.options.columns,
                    '-webkit-column-gap': 0,
                    '-moz-column-count': instance.options.columns,
                    '-moz-column-gap': 0
                });

                // for crappy IE versions float grouped options
                if (this._ieVersion() && (this._ieVersion() < 10)) {
                    optionsList.find('> li').css({
                        'float': 'left',
                        width: (100 / instance.options.columns) + '%'
                    });
                }
            }

            //Sheils-------------------------------------------------------------
            //Create single select behaviour
            if (instance.options.singleselect) {

                var ckbox = $('.ms-options input:checkbox');
                var $elementid = '#' + instance.element.id

                $($elementid).closest('div').find('input:checkbox').on('click', function() {

                    if (ckbox.is(':checked')) {
                        $($elementid).val($(this).val())
                        $(this).closest('div').find('input:checkbox').prop('checked', false)
                        $(this).prop('checked', true)

                    }

                });

            }

            //-------------------------------------------------------------
        },

        /* UPDATE MULTISELECT CONFIG OPTIONS */
        settings: function(options) {
            this.options = $.extend(true, {}, this.options, options);
            this.reload();
        },

        /* RESET THE DOM */
        unload: function() {
            $(this.element).next('.ms-options-wrap').remove();
            $(this.element).show(function() {
                $(this).css('display', '').removeClass('jqmsLoaded');
            });
        },

        /* RELOAD JQ MULTISELECT LIST */
        reload: function() {
            // remove existing options
            while($(this.element).siblings(".ms-options-wrap").length){//loop inserted by sheils

            $(this.element).siblings('.ms-options-wrap').remove();

            }

            $(this.element).removeClass('jqmsLoaded');

            // load element
            this.load();
        },

        // RESET BACK TO DEFAULT VALUES & RELOAD
        reset: function() {
            var defaultVals = [];
            $(this.element).find('option').each(function() {
                if ($(this).prop('defaultSelected')) {
                    defaultVals.push($(this).val());
                }
            });

            $(this.element).val(defaultVals);

            this.reload();
        },

        disable: function(status) {
            status = (typeof status === 'boolean') ? status : true;
            $(this.element).prop('disabled', status);
            $(this.element).next('.ms-options-wrap').find('button:first-child')
                .prop('disabled', status);
        },

        /** PRIVATE FUNCTIONS **/
        // update the un/select all texts based on selected options and visibility
        _updateSelectAllText: function(visibleOnly) {
            if (!this.updateSelectAll) {
                return;
            }

            if (typeof visibleOnly !== 'boolean') {
                visibleOnly = true;
            }

            var instance = this;

            // select all not used at all so just do nothing
            if (!instance.options.selectAll && !instance.options.selectGroup) {
                return;
            }

            var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options');

            // update un/select all text
            optionsWrap.find('.ms-selectall').each(function() {
                var unselected = $(this).parent().find('li:not(.optgroup,.selected,.ms-hidden)');

                $(this).text(
                    unselected.length ? instance.options.texts.selectAll : instance.options.texts.unselectAll
                );
            });
        },

        // update selected placeholder text
        _updatePlaceholderText: function() {
            if (!this.updatePlaceholder) {
                return;
            }

            var instance = this;
            //var placeholder = $(instance.element).next('.ms-options-wrap').find('> button:first-child');
            //sheils modified above used siblings instead of Next
            var placeholder = $(instance.element).siblings('.ms-options-wrap').find('> button:first-child');

            var placeholderTxt = placeholder.find('span');
            //var optionsWrap = $(instance.element).next('.ms-options-wrap').find('> .ms-options');
            //sheils modified above used siblings instead of Next
            var optionsWrap = $(instance.element).siblings('.ms-options-wrap').find('> .ms-options');

            //var select = optionsWrap.parent().prev();
            //sheils modified above used siblings("select") instead of prev()
            var select = optionsWrap.parent().siblings("select");

            var selectVals = select.val() ? select.val() : [];

            // if there are disabled options get those values as well
            if (select.find('option:selected:disabled').length) {
                selectVals = [];
                select.find('option:selected').each(function() {
                    selectVals.push($(this).val());
                });
            }

            // get selected options
            var selOpts = [];
            for (var key in selectVals) {
                selOpts.push(
                    $.trim(select.find('option[value="' + selectVals[key] + '"]').text())
                );

                if (selOpts.length >= instance.options.maxPlaceholderOpts) {
                    break;
                }
            }

            // UPDATE PLACEHOLDER TEXT WITH OPTIONS SELECTED
            placeholderTxt.text(selOpts.join(', '));

            if (selOpts.length) {
                optionsWrap.closest('.ms-options-wrap').addClass('ms-has-selections');
            } else {
                optionsWrap.closest('.ms-options-wrap').removeClass('ms-has-selections');
            }

            // replace placeholder text
            if (!selOpts.length) {
                placeholderTxt.text(instance.options.texts.placeholder);
            }
            // if copy is larger than button width use "# selected"
            else if ((placeholderTxt.width() > placeholder.width()) || (selOpts.length != selectVals.length)) {
                placeholderTxt.text(select.val().length + instance.options.texts.selectedOptions);
            }
        },

        // Add option to the custom dom list
        _addOption: function(container, option) {
            var instance = this;
            var thisOption = $('<label/>', {
                for: 'ms-opt-' + msCounter,
                text: option.name
            });

            var thisCheckbox = $('<input>', {
                type: 'checkbox',
                title: option.name,
                id: 'ms-opt-' + msCounter,
                value: option.value
            });

            // add user defined attributes
            if (option.hasOwnProperty('attributes') && Object.keys(option.attributes).length) {
                thisCheckbox.attr(option.attributes);
            }

            if (option.checked) {
                container.addClass('default selected');
                thisCheckbox.prop('checked', true);
            }

            thisOption.prepend(thisCheckbox);

            var searchTerm = '';
            if (instance.options.searchOptions.searchText) {
                searchTerm += ' ' + option.name.toLowerCase();
            }
            if (instance.options.searchOptions.searchValue) {
                searchTerm += ' ' + option.value.toLowerCase();
            }

            container.attr('data-search-term', $.trim(searchTerm)).prepend(thisOption);

            msCounter = msCounter + 1;
        },

        // check ie version
        _ieVersion: function() {
            var myNav = navigator.userAgent.toLowerCase();
            return (myNav.indexOf('msie') != -1) ? parseInt(myNav.split('msie')[1]) : false;
        }
    };

    // ENABLE JQUERY PLUGIN FUNCTION
    $.fn.multiselect = function(options) {
        if (!this.length) {
            return;
        }

        var args = arguments;
        var ret;

        // menuize each list
        if ((options === undefined) || (typeof options === 'object')) {
            return this.each(function() {
                if (!$.data(this, 'plugin_multiselect')) {
                    $.data(this, 'plugin_multiselect', new MultiSelect(this, options));
                }
            });
        } else if ((typeof options === 'string') && (options[0] !== '_') && (options !== 'init')) {
            this.each(function() {
                var instance = $.data(this, 'plugin_multiselect');

                if (instance instanceof MultiSelect && typeof instance[options] === 'function') {
                    ret = instance[options].apply(instance, Array.prototype.slice.call(args, 1));
                }

                // special destruct handler
                if (options === 'unload') {
                    $.data(this, 'plugin_multiselect', null);
                }
            });

            return ret;
        }
    };
}
(jQuery));
nobleclem commented 6 years ago

So the add option functionality already exists. You would want to use the loadOptions method. There is an example "DYNAMICALLY ADD OPTION TO LIST" on my demo site.

The ability to limit to a single option selected can be done using the onOptionClick callback. There is an example in issue #43.

As far as jQuery Validate I am not familiar with it nor am I sure what changes within the code refers to it. Is that the changes from the .prev & .next to .siblings? Maybe you can elaborate a little more?

FYI: I am usually hesitant on adding new "features" to this plugin. My thought process for this plugin is something that is simple but give the developer the ability to customize as they require. I had looked at many other select plugins but they were usually overbuilt for my needs.

SB401 commented 6 years ago

I first could not get jQuery Validate to work when using your multiselect. I ask for assistance on stackoverflow (https://stackoverflow.com/questions/51462649/how-to-use-jquery-validation-together-with-jquery-multiselect-plugin) and was advised to set ignore: ':hidden:not("#mySelect")'. This worked to some extent. But I noticed that the div create by jQuery Validate lodged between the select and .ms-options div preventing the .prev & .next from working. .siblings seemed to do the trick.

As regards add option I wanted to add option when not available from search. The value in the search box is then added to the option. The add option button is also inside the .ms-options. See example on the site I am currently developing (http://wowislandcharter.com/roit/)

As I said before your multiselect is great out of the box. I just posted my modification just in case it may be of some use.

Cheers

nobleclem commented 6 years ago

So I fixed the issue with the $.prev() & $.next() usage that was causing your jQuery validate bug. I opted to make a slight alteration as I want to guarantee that the proper sibling is targeted and not that of another list.

I am not sure I want to add the add item functionality, however I feel like it might be able to be added on your end using the onLoad callback.

SB401 commented 6 years ago

Thanks for addressing this issue. You are a champion.