Convert jQuery code to plain JavaScript for Multi-Selects without Ctrl-click

tel2
tel2 used Ask the Experts™
Hi JavaScript Experts,

I’m trialing some code to make it so HTML <select multiple> controls don’t need Ctrl to be held down while clicking, to select/deselect options without deselecting all other selected options.  I picked up this solution from the JSFiddle in the accepted solution from here.

I don’t know much JavaScript, but I guessed how to change that JSFiddle so it respected disabled options, by simply adding a condition, and here’s the resulting JSFiddle including that change.

Now I’m wanting to see if I could have a plain JavaScript (i.e. without jQuery) version of that, so I can compare performance, because I don’t (yet) need jQuery for anything else in my Perl web application and if a plain JavaScript version works out to be lighter, I’ll use it, because page load speed is important for this light application.  I know jQuery caches, but it will reload during refreshes.  So could someone please convert it for me?  If it's going to be too long/hard to convert while retaining a good level of browser compatibility, let me know.

Also, the code seems to work on Firefox, Chrome, Opera, Edge, but not IE11 for some reason (i.e. Ctrl click is still required).  Any ideas why or how to fix that?  If not, I’m not too worried, because I hear IE’s days are numbered.

Here's the code for your convenience:
$('option').mousedown(function(e) {
    e.preventDefault();
    var originalScrollTop = $(this).parent().scrollTop();
    console.log(originalScrollTop);
    if (!$(this).prop('disabled')) {
      $(this).prop('selected', $(this).prop('selected') ? false : true);
    }
    var self = this;
    $(this).parent().focus();
    setTimeout(function() {
        $(self).parent().scrollTop(originalScrollTop);
    }, 0);
    
    return false;
});

Open in new window


Also, what are my options for how/where to run this code on the webpage?  Currently I've got it in the <head> in a window.onload function something like this in my application:
  <script src='resources/jquery-4.3.1.slim.min.js'></script>
  <script>
    window.onload=function() {
      $('option').mousedown(function(e) {
        ...etc.. (see snippet above)
      }
    }
  </script>
</head>

Open in new window

The above placement seems to work, but just wondering whether that's best and what my options are.

Thanks.
tel2
Comment
Watch Question

Do more with

Expert Office
EXPERT OFFICE® is a registered trademark of EXPERTS EXCHANGE®
Most Valuable Expert 2017
Distinguished Expert 2018
I wouldn't use onload as you have it. Why? because the next bit of code that assigns onload will wipe out your function. Rather use addEventListner.

IE11 - this is a non-standards compliant browser - personally I don't support it.

Here is my stab at it
<script>
// Bind to onload
window.addEventListener('load',function() {

  // Get the target select we are interested in - we don't necessarily want to bind to all options on the page
  var ts = document.getElementById('targetSelect');

  // Get the options
  var options = ts.getElementsByTagName('option'); // CORRECTED

  // Iterate over options and bind event Handler
  for(var i = 0; i < options.length; i++) {
    bindSelectHandler(options[i]);
  };
});

// Event handler function
function bindSelectHandler(opt) {
  opt.addEventListener('mousedown', function(e) {
    e.preventDefault();

    var parent = opt.parentElement
    var originalScrollTop = parent.scrollTop;

    // opt is passed into the function by reference so we can use that
    // parameter rather than having to get from the event parameter
    if (!opt.disabled) {
      opt.selected = !opt.selected;
    }

    opt.focus();
    setTimeout(function() {
      parent.scrollTop = originalScrollTop;
    }, 0);
  });
}
</script>

Open in new window


You can see a working sample here

UPDATED
Changed line 9 to get options relative to the parent rather than the document as per comment by tel2 further down
Sorry to shout, but IMPRESSIVE!
Thank's for that amazing work, Julian.
I'll get back to you with some follow-up questions later.
Hi again Julian,

The one problem I notice so far is that in Chrome, the options in the bound control (unlike the standard unbound one) don't go blue when you click on them.  You have to click on the edge of the control (e.g. the scroll bar) to get them to go blue.  I've seen this problem with some solutions before, but it was not a problem with the solution I'm using in my original post.  That's all I can see so far which would prevent me from using your solution, although I haven't done any speed tests yet.  Any idea how to fix this?  I assume it's some kind of focus issue?

Also, would it be easy, efficient and practical to change it so instead of targeting just the options which have the specified target id, it works for all <option multiple> elements?  I have several of these elements on a page, but I think I can only have one of each "id", right?  I assume using a class would be another way, (presumably using getElementsByClassName('targetSelect')?), like this, which seems to work for all the select's I've given the 'targetSelect' class to:
window.addEventListener('load',function() {
  //var ts = document.getElementById('targetSelect');
  var ts = document.getElementsByClassName('targetSelect');
  for(var j = 0; j < ts.length; j++) {
    var options = ts[j].getElementsByTagName('option');
    for(var i = 0; i < options.length; i++) {
      bindSelectHandler(options[i]);
    };
  };
});

Open in new window

But if it's efficient to simply target all <option multiple> elements, that would be ideal for me, so I don't need to put id's or class's on any of the selects.  Any suggestions?

Regarding other 'onload's wiping out my function, that is exactly what happened, and I ended up putting 2 functions inside the same "window.onload=function() {...}" to work around it, which was a right pain to code in my environment, so thanks for the alternative.

Regarding placement, what would be the pros & cons of putting the <script> code in the <head> rather than near the end of the <body>?

And regarding this code which I see in your post above, but not on the demo you linked to:
    // Iterate over options and bind event Handler
    var options = document.getElementsByTagName('option');
Should it be removed now since you also have this code?:
    // Get the options
    var options = ts.getElementsByTagName('option');

Thanks again!
tel2
Introduction to R

R is considered the predominant language for data scientist and statisticians. Learn how to use R for your own data science projects.

Most Valuable Expert 2017
Distinguished Expert 2018
You have to click on the edge of the control (e.g. the scroll bar) to get them to go blue.
I cannot replicate?

Also, would it be easy, efficient and practical to change it so instead of targeting just the options which have the specified target id, it works for all <option multiple> elements?
The code you have is as I would do it for binding to a class (which is the right way of doing it)
 
But if it's efficient to simply target all <option multiple> elements, that would be ideal for me, so I don't need to put id's or class's on any of the selects.  Any suggestions?
Never second guess your future requirements. Go with the class - don't assume everything as it will potentially lead to side effects down the line. Be explicit in what it is you want to do. Adding a class to a <select> is trivial.

Regarding placement, what would be the pros & cons of putting the <script> code in the <head> rather than near the end of the <body>?
SEO considerations aside the only real consideration is the order in which things run. The browser will implement as it finds so if it finds JS code referencing an id of an element it has not rendered yet then that code will not act as intended - it will run against a non existent element and terminate.

However, if you put your code inside a document ready (as we have done) it does not really make a difference where you put the code. Personally I prefer the bottom as it creates less clutter at the start of the document - but that is my personal preference.

And regarding this code which I see in your post above, but not on the demo you linked to:
The sample is more recent - and correct - the code should only target the options in the targeted <select>. I have updated the code above to reflect this.
Thanks again for your time Julian,

I cannot replicate?
What browser and version did you use to try to replicate it?  I previously said it was with Chrome, and I used version 79.  Attached is a screen shot of the result from your sample site, after clicking option "B" in the lower (unbound) control, then "B" in the upper control.  As you can see by the blue option and border in the lower control, it still has focus, even though the last option I clicked on is the upper control.
Can you reproduce that now?

Never second guess your future requirements. Go with the class - don't assume everything as it will potentially lead to side effects down the line. Be explicit in what it is you want to do. Adding a class to a <select> is trivial.
I hear your points, and I'd be happy to change it back to use classes in future if the need ever arises, but I highly doubt it in this case, and if it's easy to get this to automatically target <select multiple> elements only, I'd like to try that please.  Do you know of some easy change that would achieve that?

...I have updated the code above to reflect this.
I'm not sure you quite follow me.  I was trying to suggest you remove the first line.  Now both lines are identical so are duplicated, like this:
  // Get the options
  var options = ts.getElementsByTagName('option');

  // Iterate over options and bind event Handler
  var options = ts.getElementsByTagName('option'); // CORRECTED

Open in new window

Might pay to tidy that up for the benefit of others who may copy your code from this web page in future.

I get the impression that while your code has to iterate over all <option>s (of the specified id/class), doing various tasks for each, the code in my original post is applied only to clicked options (of all elements, including non-multiple select elements).  Am I right?

Thanks again.
tel2
EE-Chrome-79a.gif
Most Valuable Expert 2017
Distinguished Expert 2018
Chrome 79 - but I understand the problem.

Add parent.focus()
...
var parent = opt.parentElement;
parent.focus(); // Add this
...

Open in new window


On selecting the <select> - it goes to style but here is how you would do it

var ts = document.getElementById('targetSelect');

Open in new window

becomes
var ts = document.querySelector('select[multiple]');

Open in new window

Then iterate over results as for classes.

Duplication - corrected.

Your code bound to all options on the page irrespective of <select> type. The code I provided is mostly the same - the difference it targets options only in specific <select>s

the code in my original post is applied only to clicked options
Not quite. You have to bind to ANY element you want to handle an event on. The jQuery syntax may be misleading
$('option') => means bind to ALL <option>s on the page
$('option').mousedown() => means bind a mousedown() event handler to ALL <option>s on the page. We are doing the same thing just with a different syntax. You can't dynamically bind an event handler AFTER the event happens. If you look at your page (console) you should see events bound to each option (waiting in anticipation for a click)
Thanks again, Julian.

I couldn't get "var ts = document.querySelector('select[multiple]');" to work, but after a web search I found that it doesn't return an array, it returns the first matching element, so I changed it to "var ts = document.querySelectorAll('select[multiple]');" and that worked.  So you certainly pointed me in the right direction, thanks.

When you say "it goes to style", are you talking about the different approaches coming down to programming style, or something else?

After you've answered that last question, I think you can take the rest of the day off, and thanks very much for your patience and various areas of help with this.

tel2
Most Valuable Expert 2017
Distinguished Expert 2018
document.querySelectorAll('select[multiple]');

Open in new window

Typo on my part - but kudos on finding the solution.

When you say "it goes to style", are you talking about the different approaches coming down to programming style, or something else?
Programming style - when you program defensively and in a way that takes potential future changes into account it puts you in a different class.

Generally we code on the assumption that things will change - even if we know they won't. It is just a good habit to have.

You are welcome.

Do more with

Expert Office
Submit tech questions to Ask the Experts™ at any time to receive solutions, advice, and new ideas from leading industry professionals.

Start 7-Day Free Trial