Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Select all options in optgroup

I have a select element with grouped options. I need to select (or deselect) all options in an optgroup when an option is clicked. I also need to be able to have multiple optgroups being selected at once.

The way I wish it to work is this:

  • If nothing is selected, I want to click one option and get all of the options in the same optgroup selected.
  • If one or more optgroups are already selected, I want to click one option in another optgroup and have all those options selected instead.
  • If one or more optgroups are already selected, I want to be able to Ctrl-click an option in a non-selected optgroup and have all of the options in that optgroup selected too.
  • If one or more optgroups are already selected, I want to be able to Ctrl-click an option in a selected optgroup and have all the options in that group deselected.

Looking at other answers on Stack Overflow I created the following:

HTML:

<select multiple="multiple" size="10">
    <optgroup label="Queen">
        <option value="Mercury">Freddie</option>
        <option value="May">Brian</option>
        <option value="Taylor">Roger</option>
        <option value="Deacon">John</option>
    </optgroup>
    <optgroup label="Pink Floyd">
        <option value="Waters">Roger</option>
        <option value="Gilmour">David</option>    
        <option value="Mason">Nick</option>                
        <option value="Wright">Richard</option>
    </optgroup>
</select>

jQuery:

$('select').click(selectSiblings);
function selectSiblings(ev) {
    var clickedOption = $(ev.target);
    var siblings = clickedOption.siblings();
    if (clickedOption.is(":selected")) {
        siblings.attr("selected", "selected");
    } else {
        siblings.removeAttr("selected");
    }
}​

I made an example here: http://jsfiddle.net/mflodin/Ndkct/ (Sadly jsFiddle doesn't seem to support IE8 any more.)

This works as expected in Firefox (16.0), but in IE8 it doesn't work at all. From other answers I have found out that IE8 can't handle the click event on optgroup or option, which is the reason I bind it to the select and then use $(ev.target). But in IE8 the $(ev.target) still points to the entire select and not to the option that was clicked. How can I find out which option (or containing optgroup) that was clicked and whether it was selected or deselected?

Another unexpected behaviour, but minor in comparison, is that in Chrome (20.0) the deselect doesn't happen until the mouse leaves the select. Does anybody know a workaround for this? ​

like image 437
mflodin Avatar asked Oct 24 '12 10:10

mflodin


2 Answers

not an answer to the specific question of select option cross browser issues, I think that's been answered already, but a solution to the problem of selecting the data and passing it back to the server would be to change your mark up to something which better suits multi selection

if you use checkboxes instead, you can put them in a list and add enhanching js to select all and deselect all

http://jsfiddle.net/Ndkct/43/

<dl>
<dt>Queen</dt>
<dd>
    <ul>
        <li>
            <input id="Mercury" name="bandmembers" value="Mercury" type="checkbox" />
            <label for="Mercury">Freddie</label>
        </li>    
        <li>
            <input id="May" name="bandmembers" value="May" type="checkbox" />
            <label for="May">Brian</label>
        </li>    
        <li>
            <input id="Taylor" name="bandmembers" value="Taylor" type="checkbox" />
            <label for="Taylor">Roger</label>
        </li>    
        <li>
            <input id="Deacon" name="bandmembers" value="Deacon" type="checkbox" />
            <label for="Deacon">John</label>
        </li>    
    </ul>
</dd>
<dt>Pink Floyd</dt> ...

The js is now simple enough

$("dt")
    .css("cursor","pointer")
    .click(function(){
        var $el = $(this).next();
        var $checks = $el.find(":checkbox");
        if($checks.is(":not(:checked)")){
           $checks.attr("checked",true); 
        }else{
           $checks.attr("checked",false); 
        }
    });

(this fiddle http://jsfiddle.net/Ndkct/44/ adds a parent checkbox to make it more usable)

like image 184
Anthony Johnston Avatar answered Sep 19 '22 11:09

Anthony Johnston


Since Internet Explorer as of version 10 still does not support any useful events on optgroup or option, any cross-browser solution to this issue can't depend on direct events from those elements.

Note: MSDN documentation claims that optgroup supports click events, but as of IE 10 that still does not seem to be a reality: http://msdn.microsoft.com/en-us/library/ie/ms535876(v=vs.85).aspx

I tested the following in Chrome 27, Firefox 20, Safari 6, IE 9, and IE 10 (I don't have access to IE 8 currently).

Here's an example you can play with: http://jsfiddle.net/potatosalad/Ndkct/38/

// Detect toggle key (toggle selection)
$('select')
    .on('mousedown', function(event) {
        // toggleKey = Command for Mac OS X; Control for others
        var toggleKey = (!event.metaKey && event.ctrlKey && !(/Mac OS/.test(navigator.userAgent))) ? event.ctrlKey : event.metaKey;
        if (toggleKey === true) {
            $(this).data('toggleKey', true);
        }
        $(this).data('_mousedown', true);
    })
    .on('mouseup', function() {
        $(this).data('_mousedown', null);
    });
// Chrome fix for mouseup outside select
$(document).on('mouseup', ':not(select)', function() {
    var $select = $('select');
    if ($select.data('_mousedown') === true) {
        $select.data('_mousedown', null);
        $select.trigger('change');
    }
});

$('select')
    .on('focus',  storeState)
    .on('change', changeState);

function storeState() {
    $(this).data('previousGroup', $('option:selected', this).closest('optgroup'));
    $(this).data('previousVal',   $(this).val());
};

function changeState() {
    var select = this,
        args   = Array.prototype.slice.call(arguments);

    var previousGroup = $(select).data('previousGroup'),
        previousVal   = $(select).data('previousVal');
    var currentGroup  = null,
        currentVal    = $(select).val();

    var selected = $('option:selected', select),
        groups   = selected.closest('optgroup');

    console.log("[change] %o -> %o", previousVal, currentVal);

    if ((previousVal === currentVal) && (groups && groups.length == 1)) {
        // selected options have not changed
        // AND
        // only 1 optgroup is selected
        // -> do nothing
    } else if (currentVal === null || currentVal.length === 0) {
        // zero options selected
        // -> store state and do nothing
        storeState.apply(select, args);
    } else { // a select/deselect change has occurred
        if ($(select).data('toggleKey') === true
            && (previousVal !== null && previousGroup !== null && groups !== null)
            && (previousVal.length > currentVal.length)
            && (previousGroup.length === groups.length)
            && (previousGroup.get(0) === groups.get(0))) {
            // options within the same optgroup have been deselected
            // -> deselect all and store state
            $(select).val(null);
            storeState.apply(select, args);
        } else if (currentVal.length === 1) {
            // single option selected
            // -> use groups
            currentGroup = groups;
        } else if (groups.length === 1) {
            // multiple options selected within same optgroup
            // -> use groups
            currentGroup = groups;
        } else {
            // multiple options selected spanning multiple optgroups
            // -> use last optgroup
            currentGroup = groups.last();
        }
    }

    $(select).data('toggleKey', null);

    if (currentGroup !== null) {
        $('optgroup', select).not(currentGroup).children(':selected').attr('selected', false);
        $(currentGroup).children(':not(:selected)').attr('selected', true);
        storeState.apply(select, args);
    }
};

It works by storing the previously selected group and options on the select element and determining which optgroup should be selected (or deselected).

Some behaviors can be changed based on the developers needs. For example in the case of selecting multiple options that span multiple optgroups (by clicking and dragging down as seen in screenshot):

multiple options selected spanning multiple optgroups

The default conflict resolution for this case is to always use the last optgroup, but could be changed to always use the first one, or the optgroup with the largest number of already selected options.

like image 28
potatosalad Avatar answered Sep 19 '22 11:09

potatosalad