86

I thought this would be a simple hack, but I've now been searching for hours and can't seen to find the right search term. I want to have an ordinary multiple select box (<select multiple="multiple">) except I don't want the user to have to hold down the control key to make multiple selections.

In other words, I want a left click to toggle the <option> element that's under the cursor without changing any of the others. In other other words, I want something that looks like a combo list box but behaves like a group of check boxes.

Can anybody suggest a simple way to do this in Javascript? Thanks.

ChrisW
  • 4,970
  • 7
  • 55
  • 92
bokov
  • 3,444
  • 2
  • 31
  • 49
  • If you don't mind changing your markup, you can build a list of checkboxes & labels, then hide the checkbox, and style the label (when it's input is checked) to have a similar visual appearance to ` – KyleMit Mar 23 '17 at 19:26

6 Answers6

126

Check this fiddle: http://jsfiddle.net/xQqbR/1022/

You basically need to override the mousedown event for each <option> and toggle the selected property there.

$('option').mousedown(function(e) {
    e.preventDefault();
    $(this).prop('selected', !$(this).prop('selected'));
    return false;
});

For simplicity, I've given 'option' as the selector above. You can fine tune it to match <option>s under specific <select> element(s). For ex: $('#mymultiselect option')

techfoobar
  • 65,616
  • 14
  • 114
  • 135
  • 2
    You could use `! this.selected` there. – alex Dec 27 '11 at 06:24
  • Aha! The preventDefault() method is what I think I was missing. I'll try it and report back. – bokov Dec 27 '11 at 06:26
  • Like alex said, you can use `this.selected` instead of `$(this).prop('selected')`. – nnnnnn Dec 27 '11 at 06:56
  • Sorry to be dense, but should I put the above code inside the usual ` – bokov Dec 27 '11 at 07:03
  • 1
    This is dependent on jQuery. Do make sure you execute the above code once your DOM elements are loaded and ready to bind to events. A good place to put it would would be inside your `$(document).ready(...)` – techfoobar Dec 27 '11 at 07:17
  • 1
    Doesn't work properly. It unselects all options once you drag the mouse. – evilReiko May 27 '15 at 06:30
  • 5
    Very good solution. The only downside I noticed is that the event prevention works in a way that the parent select is not focused on, resulting in a different look and feel (grey instead of blue background of selected options). In order to preserve the look and feel I focused on the select box manually. $(this.parentElement).focus(); – Uluaiv Jul 16 '16 at 16:18
  • 1
    As everyone else has stated, to simplify it would just be ```this.selected = !this.selected;``` There is really no need to use jQuery – doz87 Feb 23 '17 at 03:49
  • As pointed out in [this question](http://stackoverflow.com/q/21797291/1366033), both IE and FF have very limited support for firing mousedown events on option elements, which will heavily restrict these types of solutions that rely on extending the default ` – KyleMit Mar 22 '17 at 17:23
  • 2
    When the select have scrollbar, it will **jump** to first selected option, it's a flicker, how to solve this problem? – James Yang Sep 05 '17 at 13:38
  • You need to set option.parentElement.focus(); too, because this doesn't get set when you write e.preventDefault... – Stefan Steiger May 22 '18 at 06:59
  • 1
    you may want to change: `$(this).attr('selected', !$(this).prop('selected'));` to `$(this).attr('selected', !$(this).prop('selected')).change();` so if you have events attached to the select box they are triggered – nzn Aug 14 '21 at 19:08
  • This does what was asked, it does however also eliminate the possibility to select several by holding SHIFT (or whatever is way to do that on your platform). You might be able to expand this solution to make it posssible to select several with SHIFT. – Sybille Peters Nov 01 '22 at 09:12
22

Had to solve this problem myself and noticed the bugged behavior a simple interception of the mousedown and setting the attribute would have, so made a override of the select element and it works good.

jsFiddle: http://jsfiddle.net/51p7ocLw/

Note: This code does fix buggy behavior by replacing the select element in the DOM. This is a bit agressive and will break event handlers you might have attached to the element.

window.onmousedown = function (e) {
    var el = e.target;
    if (el.tagName.toLowerCase() == 'option' && el.parentNode.hasAttribute('multiple')) {
        e.preventDefault();

        // toggle selection
        if (el.hasAttribute('selected')) el.removeAttribute('selected');
        else el.setAttribute('selected', '');

        // hack to correct buggy behavior
        var select = el.parentNode.cloneNode(true);
        el.parentNode.parentNode.replaceChild(select, el.parentNode);
    }
}
<h4>From</h4>

<div>
    <select name="sites-list" size="7" multiple>
        <option value="site-1">SITE</option>
        <option value="site-2" selected>SITE</option>
        <option value="site-3">SITE</option>
        <option value="site-4">SITE</option>
        <option value="site-5">SITE</option>
        <option value="site-6" selected>SITE</option>
        <option value="site-7">SITE</option>
        <option value="site-8">SITE</option>
        <option value="site-9">SITE</option>
    </select>
</div>
Community
  • 1
  • 1
Sergio
  • 28,539
  • 11
  • 85
  • 132
  • 1
    This works for me, but selecting does not make the form dirty (`e.preventDefault()`). Just something to have in mind. – Hanlet Escaño Aug 17 '16 at 16:22
  • Please explain what you mean by "buggy behavior" – Kelly Elton Sep 13 '18 at 19:55
  • What does `e.preventDefault()` do? – Marvin Oct 10 '18 at 17:00
  • This is a great answer. Except when the options are listen under other parent node type than select. When the options are wrapped with optgroup this code fails. Instead of using `el.parentNode.hasAttribute('multiple')` use `el.closest('select').hasAttribute('multiple')` – Jay Apr 04 '22 at 16:16
10

techfoobar's answer is buggy, it unselects all options if you drag the mouse.

Sergio's answer is interesting, but cloning and removing events-bound to a dropdown is not a nice thing.

Try this answer.

Note: Doesn't work on Firefox, but works perfectly on Safari/Chrome/Opera. (I didn't test it on IE)

EDIT (2020)

After 5 years since my original answer, I think best practice here is to replace the dropdown with checkboxes. Think about it, that's the main reason why checkboxes exist in the first place, and it works nicely with old browsers like IE & modern mobiles without any custom JS to handle all the wacky scenarios.

evilReiko
  • 19,501
  • 24
  • 86
  • 102
5

Necromancing.
The selected answer without jQuery.
Also, it missed setting the focus when an option is clicked, because you have to do this yourself, if you write e.preventDefault...
Forgetting to do focus would affect CSS-styling, e.g. bootstrap, etc.

var options = [].slice.call(document.querySelectorAll("option"));

options.forEach(function (element)
{
    // console.log("element", element);
    element.addEventListener("mousedown", 
        function (e)
        {
            e.preventDefault();
            element.parentElement.focus();
            this.selected = !this.selected;
            return false;
        }
        , false
    );
});
Stefan Steiger
  • 78,642
  • 66
  • 377
  • 442
1

I had same problem today, generally the advice is to use a list of hidden checkboxes and emulate the behavior via css, in this way is more easy to manage but in my case i don't want to modify html.

At the moment i've tested this code only with google chrome, i don't know if works with other browser but it should:

var changed;
$('select[multiple="multiple"]').change(function(e) {
    var select = $(this);
    var list = select.data('prevstate');
    var val = select.val();
    if (list == null) {
        list = val;
    } else if (val.length == 1) {
        val = val.pop();
        var pos = list.indexOf(val);
        if (pos == -1)
            list.push(val);
        else
            list.splice(pos, 1);
    } else {
        list = val;
    }
    select.val(list);
    select.data('prevstate', list);
    changed = true;
}).find('option').click(function() {
    if (!changed){
        $(this).parent().change();
    }
    changed = false;
});

Of course suggestions are welcome but I have not found another way

Marino Di Clemente
  • 3,120
  • 1
  • 23
  • 24
0

Reusable and Pure JavaScript Solution

const multiSelectWithoutCtrl = ( elemSelector ) => {
  
  let options = document.querySelectorAll(`${elemSelector} option`);
  
  options.forEach(function (element) {
      element.addEventListener("mousedown", 
          function (e) {
              e.preventDefault();
              element.parentElement.focus();
              this.selected = !this.selected;
              return false;
          }, false );
  });

}


multiSelectWithoutCtrl('#mySelectInput') /* Can use ID or Class */
option {
font-size: 20px;
padding: 10px 20px;
}
<select multiple id="mySelectInput" class="form-control">
  <option> Apple</option>
  <option> Banana</option>
  <option> Pineapple</option>
  <option> Watermelon</option>
</select>
GMKHussain
  • 3,342
  • 1
  • 21
  • 19