Multiple select list results in order of last selected on top?

Hi.

The code at the following fiddle works fine in Firefox and Chrome, however, I have now discovered that IE9 and IE11 are the browser versions that the code needs to work on. Unfortunately, the code is not working on either version.

The filtering seems to work fine, however, nothing happens when I click an option in the list (no div appears). I have run the code through an HTML validator and have now removed all the spaces in the IDs. The validator also states “Attribute match not allowed on element div at this point”, however, I can’t see why the ‘match’ attributes are not correct. I then ran the code through a javascript validator and got 51 errors, which I am finding difficult to interpret, let alone resolve!

I was wondering if someone with the relevant knowledge can advise me on what is stopping the code from working on IE9/IE11 and what may be done to remedy this.

Thanks so much!

[quote=“TechnoKid, post:21, topic:259656, full:true”]
I have run the code through an HTML validator[/quote]

Which validator was that?

[quote=“TechnoKid, post:21, topic:259656, full:true”]
I then ran the code through a javascript validator and got 51 errors, which I am finding difficult to interpret, let alone resolve![/quote]

I’ll help to give some insight with the javascript validator. I don’t know which one you used, so I’m going to use jslint.com which is one of the more strict ones out there.

Be warned - this reply gets quite long.

Formatting

Most of the code formatting issues can be dealt with by running the code through the Online JavaScript Beautifier.

Needs a “use strict” pragma

Putting the beautified code in to jslint.com, the first issue is with the init function, saying This function needs a "use strict" pragma.

Using strict mode makes it easier to find errors, because it turns some silent errors in to more visible errors, and prevents some inconsistent behavior from occurring.

Typically we place "use strict" as the first line of the wrapper iife function:

(function iife() {
    "use strict";
    // rest of code in here
}());

Instead of doing that, I have placed the “use strict” declaration at the top of the init function, to see if we need anything more after that. If so, another technique can be used instead.

    init: function init() {
        "use strict";
           ...

‘remove’ is out of scope

According to jslint.com, functions should appear first before they are used in the code. In this case, because the add() function calls the remove() function, that one needs to be moved up to appear before the add() function.

Undeclared ‘jQuery’

jslint.com needs to know that variables haven’t been misspelled, so we need to tell jslint that jQuery is properly expected. We can do that by placing the following comments at the top of the code:

/*jslint browser */
/*global jQuery */

This function needs a “use strict” pragma

The filterByText() function is outside of the scope of the first "use strict" that was used. This is a good time to decide if you want to separately add "use strict" to each needed function, or to use an iife wrapper for all of the code. I’ll go with the second option here, and wrap all of the code with an iife wrapper, allowing me remove all other "use strict" statements from the code.

/*jslint browser */
/*global jQuery */
(function iife() {
    "use strict";
    // rest of code here
}());

This does mean though indenting all of the rest of the code, so using the Online JavaScript Beautifier once again helps us to easily achieve that.

Unexpected ‘this’

The this keyword tends to result in far too much confusion, so jslint requires that you use easier to understand techniques than the this keyword.

In this case it is return this.each(... that is being complained about. Why do we need that? It’s used to handle multiple selectors being called with filterByText, which currently doesn’t exist. It’s also used to assign the this keyword to a variable called select, which isn’t used either.

We can remove them, and the code continues to work exactly as we require it to.

Undeclared ‘$’

We haven’t told jslint that we are going to use the $ symbol for jQuery.

$(function () {
    ...
});

The normal way to handle this is to use a jQuery callback, so that the code will be executed when the DOM has finished loading, and also to pass in the $ symbol instead of jQuery:

jQuery(function domReady($) {
    // use $ inside here
});

We can replace that iife wrapper with the jQuery one instead, which ends up looking like this:

/*jslint browser */
/*global jQuery */
jQuery(function domReady($) {
    "use strict";
    // rest of code here
});

Because we now have that domReady wrapper, we no longer need the other wrapper that was used inside of it:

// $(function () {
    $('#mydropdown').filterByText($('#mytextbox'), true);
// });

I have commented it here for the sake of example, but the commented lines can be deleted.

Undeclared ‘mydropdown’

The problem line is:

$(mydropdown).find('option').each(function () {

We can now see that the mydropdown variable hasn’t actually been assigned, and that the earlier variable called select was most likely supposed to have been mydropdown instead.

Why did this code work regardless? Because HTML elements with id attributes are given an automatic global variable of the same name, so it was just a coincidence that this worked.

How do we fix this? We can bring back the this keyword, but doing so requires us to tell jslint that we are using the this keyword.

We can also improve things by renaming mytextbox to searchField, and remove the unused true from the end of the last line of code.

/*jslint browser, this */
jQuery.fn.filterByText = function (searchField) {
    var mydropdown = this;
    ...
    $(searchField).bind('change keyup', function (evt) {
        ...
...
$('#mydropdown').($('#mytextbox'));

That jslint declaration at the top of the code about the this keyword is a reminder to us that we should investigate removing the this keyword if we can later on.

Use double quotes, not single quotes

For the sake of consistency, only one type of quotes should be used. Because we sometimes use apostrophes in text, and we shouldn’t use HTML code in JavaScript, the double quotes have been chosen to be the single type of quotes that need to be used.

The following is an example of the change that is made to achieve this:

// $(mydropdown).find('option').each(function () {
$(mydropdown).find("option").each(function () {

Redefinition of ‘options’ from line 47

The filterByText() function uses an options variable to store all of the available options, and then separately the keyup event retrieves that list of options. The problem here is that the options still exist in the parent scope.

Does this mean that we don’t even need to save aside the options as data at all? Can we do without the code that adds and retrieves the data values?

// $(mydropdown).data('options', options);
   $(mydropdown).empty();
   // var options = $(mydropdown).empty().data("options");

And everything continues to work as before. There was no need to save information to the data object, when it remains available from the options variable in the filterByText() function.

We can now clean up some of the code that used that data too, by removing the awkward $.each section and replacing it with a much easier to understand forEach method.

// $.each(options, function (i) {
// var option = options[i];
options.forEach(option) {
    ...
}

Unused ‘regex’

This code is not used, so it gets deleted.

var regex = new RegExp(search, "gi");

Unused ‘exists’

The exists function isn’t used by anything, and can be removed.

    $(document).ready(function () {
        // function exists(elemt, arr) {
        //      return (jQuery.inArray(elemt, arr) > -1);
        // }

That document ready line is itself inside of the jQuery wrapper that was placed around all of the code, so that document ready section can be removed too.

// $(document).ready(function () {
    functions are inside here, that need to remain
// });

If you are editing the code in a code editor, it is easy to outdent the functions to maintain proper indenting. Because I am doing this via jsfiddle and the jslint console, this is a good time for me to throw the code through the JavaScript beautifier once more.

Unused ‘select’

We can easily remove the unused select parameter from the function.

// function filterQueueForSelectedOptions(queue, select) {
function filterQueueForSelectedOptions(queue) {

That brings the linting to an end, and we’re left with better code than we started with.

There’s one more thing to fix, and that is the updateResults function near the end of the code. I don’t like how an undefined argument is being passed as the first parameter to the function.

updateResults(selectedOption, selectedOption.parentNode, queue);
...
updateResults(undefined, $("#mydropdown"), queue);

If something is to be undefined, it really should be the last argument to the function. We can instead place the queue first, followed by the dropdown, and have the selectedOption come last.

We can also assign a separate dropdown variable, to help make things easier to understand.

function updateResults(queue, dropdown, option) {
    ...
}
...
    var selectedOption = evt.target;
    var dropdown = selectedOption.parentNode;
    updateResults(queue, dropdown, selectedOption);
...
    updateResults(queue, $("#mydropdown"));

Looking at the above code now, while it is possible to get the dropdown from the option, it would instead be more consistant to move the dropdown variable up so that it can be used in both places.

var dropdown = $("#mydropdown");
$("#mydropdown").on("click", "option", function (evt) {
    var selectedOption = evt.target;
    updateResults(queue, dropdown, selectedOption);
});

$("#mytextbox").on("blur", function () {
    updateResults(queue, dropdown);
});

The resulting JavaScript code that we get after all of those changes is found at https://jsfiddle.net/kbq7ymbk/10/

When you let us know which HTML validator you were using before, we can then take a look at things there too.

3 Likes

Thanks so much for yet another amazing response, Paul_Wilkins!

I simply used online validators where I could just copy and paste the code as opposed to having to upload a file. It was whatever came up in a quick web search. I am pretty sure I used the following two.

Anyhow, unfortunately, the code still isn’t working in IE9 or IE11. The problem is the same as before; nothing happens when an item is clicked.

I can deal with unrelated errors gradually, however, my urgent priority at present is getting the code to work in the above two versions of IE.

Thanks again!

No of course it still isn’t working in IE9 or IE11, because nothing has been done yet to investigate the problem or to do anything about it.

That won’t happen until after the HTML validation side of things has been investigated as well.

1 Like

OK, thanks Paul_Wilkins.

I have just run the code in the HTML validator I linked to above butd the format the results appeared in does not seem familiar. I have tried to find the one I used but haven’t been able to, however, the following seems to generate results similar to the one I used.

I am not sure if the problem is ‘match’ because the original code I posted used ‘match’ and that worked in IE9.

Thanks.

My next long post has been in the works for a few hours - I will have something useful in it.

1 Like

No. It’s not done that way. The purpose of validation is to leave you with good clean code that has no problems, making it easier for any potential problems to be found much more easily.

Validate first, and then take care of everything else.

With the HTML validation, jsfiddle uses only the content inside of the body of the full HTML code.
That HTML code normally looks like this:

<!DOCTYPE html>
<html>
<head>
  <title>Page title</title>
</head>
<body>
  ...
</body>
</html>

When we validate the HTML code from jsfiddle, the above code isn’t included, so things like the encoding and doctype are not able to be determined by the HTML validator.

You can deal with that by validating the code, and then selecting utf-8 for the encoding, and HTML5 for the doctype, before validating once more.

When the above code is wrapped around the jsfiddle HTML code, we get sensible validation information.

Bad value idx Fruits for attribute id on element div: An ID must not contain whitespace.

This one is very self explanatory. The problem code is:

<div class="mydivs1" id="idx Fruits" match="optionx Fruits">

I don’t know why you want the unique identifier on them yet, so I’ve removed the idx prefix from all of the identifiers to get this properly validating.

<div class="mydivs1" id="Fruits" match="optionx Fruits">

After each correction that you make to the code, validate it once again to gain a fresh new insight in to the remaining problems.

Error: Attribute match not allowed on element div at this point.

An example of the problem code is:

<div class="mydivs1" id="Fruits" match="optionx Fruits">

The match attribute isn’t valid. Instead of using that, we already have the identifier that we can use for the match instead.

<div class="mydivs1" id="Fruits">

Now that we don’t have the match attribute, we should update the JavaScript code to show sections based on the identifier instead.

// $("[match='" + item + "']")
$("#" + item)
    .show()

And we also need to update all of the options, so that they don’t refer to the match, but instead to the identifier.

<!-- <option value="optionx Fruits">Fruits</option> -->
...
<option value="Fruits">Fruits</option>
<option value="Vegetables">Vegetables</option>
<option value="Nuts">Nuts</option>
<option value="Meats">Meats</option>

After making the above changes, the code continues to work in the same way that it had before.
The option text is what we are searching for, and the option value is the same as the identifier that gets to be shown.

It’s important to check that things still work after making changes to the code.

The HTML validation is now happy, after those above changes have been made, and leaves us with the code shown at https://jsfiddle.net/kbq7ymbk/12/

Testing on IE9 and IE11

Now that the validation and linting has been done, we can rest assured that the easy problems have been dealt with, and can move on to further testing.

Testing on IE11 I see that the search looks like it works and filters properly, but selecting one of the search results doesn’t show the info pane.

Investigating further, the following line is where IE seems to be ignoring things:

 $(document).on("click", "option", function (evt) {

Internet Explorer instead works with the dropdown select, so we will need some separate code for Internet Explorer.

$(document).on("click", "select", function (evt) {
    var select = evt.target;
    ...
});

The problem here is that we only get the select element. We cannot get the option that has just been clicked.
One way around that is to loop through all of the currently selected options, and if it’s not found in the current queue then add that option to the list.

The code is getting a little bit more complex, but here we get all of the selected options, and compare them with what is currently in the queue. If the option is missing from the queue then we add it to the results.

  $(document).on("click", "select", function (evt) {
    var select = evt.target;
    var queuedOptions = queue.getAll();
    var selectedOption = $(":selected", select).filter(function (ignore, option) {
      var isMissingFromQueue = $.inArray(option.value, queuedOptions) === -1;
      return isMissingFromQueue;
    }).get(0);
    updateResults(queue, dropdown, selectedOption);
  });

The above changes result in this jsfiddle code, which works on IE 11.

Now to check things out on IE 9. It seems to be looking good there too.

2 Likes

Paul_Wilkins, I would give you 10 million likes if I could!

OK, I didn’t know about that; I simply copied and pasted from the fiddle; thanks for this advice.

I think I used prefixes on all the IDs, Options and Values to be able to easily distinguish between them visually as my real options list contains hundreds of entries. I originally used ‘match’ for two reasons; one is that it came with the code I found and the other was that I originally had two possible results (divs) appearing from clicking on one item, so for example, clicking ‘Vegetables’ would show two different divs relating to vegetables, however, I don’t think I need that functionality any more. I did recreate the original code without using ‘match’ but opted for ‘match’ as I thought it offered more flexibility in case I did wish to have the above functionality again in future.

You have gone WAY above and beyond what I would ever had expected, so I’m quite reluctant to ask any more questions, however (HAHAH), I am trying to make the page as accessibility friendly as possible (the fonts, colours, sizes, etc, are different on the real page and I will try to change all sizes to em instead of px - hope all goes well!) and was wondering if it was possible to have a result/div appear by pressing the Enter key on the keyboard. I am able to use Tab to get to the dropdown and arrows to navigate, but pressing Enter does nothing. In any case would this be a help or hindrance in terms of accessibility?

EDIT: Or maybe it would be better using the space bar instead, so users can select/deselect with the space bar, instead of Enter to select and then something else (e.g. Esc) to deselect? I still have some reading to do regarding accessibility so am not sure of best practice as yet.

Thanks.

[quote=“TechnoKid, post:28, topic:259656, full:true”]
and was wondering if it was possible to have a result/div appear by pressing the Enter key on the keyboard[/quote]

Yes, I agree that when doing a search, pressing Enter should do something to show results. What should it do though? Should it select all of the options and show them, but then in what order are they selected, from top to bottom?
Or should pressing Enter instead select only the first of the items?

Before going there, a part of the previous code has been left unfinished. It started as:

$.inArray(option.value, queuedOptions) === -1;

To make it easier to understand what was happening there, it was assigned to well-named variable.

var isMissingFromQueue = $.inArray(option.value, queuedOptions) === -1;
return isMissingFromQueue;

But, now even though it is well named, nothing all that useful is being done with that variable other than returning it. That is a code smell, which indicates that we should be calling a function instead, such as:

return isMissingFromQueue(some, vars, here);

The most reliable way to achieve that is in a step-by-step process, where we first create the empty function, copy code over in to that function, before replacing the old code with the new code.

Moving code to a separate function

First, we’ll create the isMissingFromQueue() function and choose a good place to put it. Placing it below the filterQueueForSelectedOptions() looks to be a suitable location.

function filterQueueForSelectedOptions(queue) {
    ...
}
function isMissingFromQueue() {

}
function showResults(results, targetSelector) {
    ...

Normally it’s better for a function to return positive results. It’s bad for a function be called doesntHaveThis() or isMissingThat() so instead of calling this function isMissingFromQueue() we should instead rework it to say hasOptionInQueue()

With that in mind, suitable parameters for the function immediately spring to mind too.

// function isMissingFromQueue() {
function hasOptionInQueue(option, queue) {

}

Copy over code into the function

We can now copy code in to that function, keeping in mind that we want to return that an item was found:

function hasOptionInQueue(option, queue) {
    var queuedOptions = queue.getAll();
    return $.inArray(option.value, queuedOptions) === -1;
}

We aren’t checking yet that the the item was found. We should instead check if $.inArray() is greater than 1.

function hasOptionInQueue(option, queue) {
    var queuedItems = queue.getAll();
    return $.inArray(option.value, queuedOptions) > -1;
}

That function should now work well. Before trying to use it though we should run the code again, to make sure that no syntax errors occurred when creating the new function.

When things are confirmed as working, we can modify the rest of the code to use that function.

Replace old code with new code

While testing, it’s important that we can easily step backwards whenever we find a problem, which is why it’s useful to comment out old code when replacing it with new code. It also helps us to confirm that the the new code is doing the same job that the old code was doing.

var select = evt.target;
// var queuedOptions = queue.getAll();
var selectedOption = $(":selected", select).filter(function (index, option) {
    //var isMissingFromQueue = $.inArray(option.value, queuedOptions) === -1;
    //return isMissingFromQueue;
    return !hasOptionInQueue(option.value, queue);
}).get(0);

It’s important to note that we are negating the true/false coming from the hasOptionInQueue() function, as we want to know when the option needs to be added to the queue.

Testing the updated code, we find that things continue to work well. Here is the updated code.

var select = evt.target;
var selectedOption = $(":selected", select).filter(function (index, option) {
    return !hasOptionInQueue(option.value, queue);
}).get(0);

Improving the hasOptionInQueue() function

Our focus can now turn to improving the hasOptionInQueue function. While creating that function it came to mind that the queue really should have a queue.contains() method, that returns true/false. We can modify the hasOptionInQueue() function to achieve that, in a similar way to what was done above.

First, create the function that we want.

function getAll() {
    ...
}
function contains(item) {
    return list.indexOf(item) > -1;
}
return {
    ...
    getAll: getAll,
    contains: contains
};

Can I use Array.prototype.indexOf() in IE9? According to MDN’s indexOf browser compatibility section, yes I can. If we wanted IE8 compatibility then there is a polyfill there that we can use to provide further support too.

The hasOptionInQueue() function now becomes:

function hasOptionInQueue(option, queue) {
    return queue.contains(option.value);
}

That function is now so simple that we have no further use for it. It was beneficial in that it’s helped us to add the queue.contains() method, and now that we have it, there’s little benefit to be gained from the original function.

We can remove that function now and replace it with a more direct call to queue.contains() instead.

// function hasOptionInQueue(option, queue) {
//     return queue.contains(option.value);
// }
...
    var select = evt.target;
    var selectedOption = $(":selected", select).filter(function (index, option) {
        return !queue.contains(option.value);
    }).get(0);

Does that work? yes it does, so the commented code can now be removed.

We are left with an improved queue, and other code that that more clearly expresses what it does.

One more change, to get selected option

Now that the previous code is not getting in the way, it’s easier for us to see that a separate function to get the selected option will be of some benefit.

It’s the same process as above, and we end up with:

function getSelectedOption(select, queue) {
    // A work-around to find on Internet Explorer the option that was selected
    var selected = $(":selected", select);
    var filteredOptions = selected.filter(function (index, option) {
        return !queue.contains(option.value);
    });
    return filteredOptions.get(0);
}
...
$(document).on("click", "select", function (evt) {
    var select = evt.target;
    var selectedOption = getSelectedOption(select, queue);
    updateResults(queue, dropdown, selectedOption);
});

Normally I avoid comments in code. Using comments to explain what is happening tends to result in less useful code. Going without comments as a crutch, helps to force me to make the code easier to understand.
In this case though it’s important to mention why (instead of what) something is happening.

Further improvements can be made to code in a similar manner as above, but this has been a good exploration of how to spot the opportunity for such improvements, and how to make them.

Edit: The code that we end up with is shown in this jsfiddle, where the JavaScript code is:

/*jslint browser, this */
/*global jQuery */
jQuery(function domReady($) {
    "use strict";
    var Queue = {
        init: function init() {
            var list = [];

            function remove(item) {
                if (list.indexOf(item) > -1) {
                    list.splice(list.indexOf(item), 1);
                }
            }

            function add(item) {
                if (list.indexOf(item) === -1) {
                    list.unshift(item);
                } else {
                    remove(item);
                    add(item);
                }
            }

            function count() {
                return list.length;
            }

            function get(i) {
                return list[i];
            }

            function getAll() {
                return list.slice(0);
            }

            function contains(item) {
                return list.indexOf(item) > -1;
            }

            return {
                remove: remove,
                add: add,
                count: count,
                get: get,
                getAll: getAll,
                contains: contains
            };
        }
    };
    var queue = Queue.init();

    jQuery.fn.filterByText = function (searchField) {
        var mydropdown = this;
        var options = [];
        $(mydropdown).find("option").each(function () {
            options.push({
                value: $(this).val(),
                text: $(this).text()
            });
        });
        $(searchField).bind("change keyup", function (evt) {
            if (!evt.keyCode) {
                return;
            }
            $(mydropdown).empty();
            var search = $(this).val();

            options.forEach(function (option) {
                if (option.text.toLowerCase().indexOf(search.toLowerCase()) > -1) {
                    $(mydropdown).append(
                        $("<option>").text(option.text).val(option.value)
                    );
                }
            });
        });
    };

    function addOptionToQueue(option, queue) {
        queue.add(option.value);
    }

    function filterQueueForSelectedOptions(queue) {
        queue.getAll().forEach(function (optionValue) {
            if ($("option[value='" + optionValue + "']").prop("selected")) {
                return;
            }
            queue.remove(optionValue);
        });
    }

    function showResults(results, targetSelector) {
        results.reverse().forEach(function (item) {
            $("#" + item)
                .show()
                .prependTo(targetSelector);
        });
    }
    function getSelectedOption(select, queue) {
        var selected = $(":selected", select);
        var filteredOptions = selected.filter(function (ignore, option) {
            return !queue.contains(option.value);
        });
        return filteredOptions.get(0);
    }
    function updateResults(queue, dropdown, option) {
        $(".mydivs1").hide();
        if (option) {
            addOptionToQueue(option, queue);
        }
        filterQueueForSelectedOptions(queue, dropdown);
        showResults(queue.getAll(), "#results");
    }

    var dropdown = $("#mydropdown");

    function textboxBlurHandler() {
        updateResults(queue, dropdown);
    }
    function optionClickHandler(evt) {
        var selectedOption = evt.target;
        updateResults(queue, dropdown, selectedOption);
    }
    function selectClickHandler(evt) {
        var select = evt.target;
        var selectedOption = getSelectedOption(select, queue);
        optionClickHandler({
            target: selectedOption
        });
    }
    
    dropdown.filterByText($("#mytextbox"));
    $("#mytextbox").on("blur", textboxBlurHandler);

    $(document).on("click", "option", optionClickHandler);
    $(document).on("click", "select", selectClickHandler);    
});
2 Likes

[quote=“TechnoKid, post:28, topic:259656, full:true”]
and was wondering if it was possible to have a result/div appear by pressing the Enter key on the keyboard. I am able to use Tab to get to the dropdown and arrows to navigate, but pressing Enter does nothing. In any case would this be a help or hindrance in terms of accessibility?[/quote]

It seems that it would be a help. The manner in which to best achieve this, is to wrap the input field in a form so that the Enter key can trigger the submit event.

This also means that if you want to make things more accessible later on, the form can default to submitting to a php page somewhere which would return the required information.

But first, on to triggering things using the Enter key.

[quote=“TechnoKid, post:28, topic:259656, full:true”]
EDIT: Or maybe it would be better using the space bar instead, so users can select/deselect with the space bar, instead of Enter to select and then something else (e.g. Esc) to deselect? I still have some reading to do regarding accessibility so am not sure of best practice as yet.[/quote]

When it comes to keyboard-only controls of the multiple select area, you would hold down the control key to keep existing items selected, while you use the arrows and space (all with control held down) to add more items.

This has me thinking that the current multiple-select interface is going to result in a lot of pain.
There are much better interfaces for what it seems that you want to do. Using checkboxes allows multiple items to be easily selected and unselected, for example.

In the interest of usability, shall we explore using checkboxes instead of the multiple select?

2 Likes

To achieve that we need to place the input form inside of a form element, so that the enter key can be captured as the submit event from that form.

<form id="search">
    <p><input type="text" id="mytextbox" placeholder="Search Foods"></p>
</form>

We can place the form’s submit event close to where we manage the textbox blur event.

$("#mytextbox").on("blur", textboxBlurHandler);
$("#search").on("submit", formSubmitHandler);

When the form is submitted, we do not want the page to submit the entered text to a different page. Instead, we just want to prevent that default behaviour from occurring, so that with JavaScript we can do something else instead.

function formSubmitHandler(evt) {
    evt.preventDefault();
}

That something else that we want to do, is to move the focus to the dropdown element.

function formSubmitHandler(evt) {
    evt.preventDefault();
    dropdown.focus();
}

That results in the first option of the dropdown box being selected, but nothing yet appear displayed in the results area.

There is an optionClickHandler() function that gets called when you click on an option.

function optionClickHandler(evt) {
    var selectedOption = evt.target;
    updateResults(queue, dropdown, selectedOption);
}
...
$(document).on("click", "option", optionClickHandler);

What we can do is to trigger a click on that first option in the dropdown, which should result in the result are being populated.

This where we need to distinguish between a jQuery object that contains the dropdown element, and the dropdown element itself. Currently the dropdown variable contains the jQuery object. We can instead retrieve the first element in that jQuery object, and use that as a direct reference to the dropdown select element.

function formSubmitHandler(evt) {
    evt.preventDefault();
    var select = dropdown[0];
    select.focus();
}

This allows us to more easily gain direct access to the options in that select element.
What we want to do is to simulate a click on the first option, which should cause the results area to populate.

function formSubmitHandler(evt) {
    evt.preventDefault();
    var select = dropdown[0];
    select.focus();
    select.options[0].click();
}

One problem occurs here though. If the search ends up having no results shown, that code will give an error when we attempts to access item 0 from the empty options. We just need to check if anything is there before attempting to do so.

function formSubmitHandler(evt) {
    evt.preventDefault();
    var select = dropdown[0];
    select.focus();
    if (select.options[0].length > 0) {
        select.options[0].click();
    }
}

Notice though the duplication that occurs between the if statement and the next line?
It would be better to assign option zero to a variable. That way we can check if the variable contains anything instead.

function formSubmitHandler(evt) {
    evt.preventDefault();
    var select = dropdown[0];
    var firstOption = select.options[0];
    if (firstOption) {
        select.focus();
        firstOption.click();
    }
}

One more issue looks to remain. When a search has no results, such as for the letter Z, nothing shows in the dropdown but previous results remain onscreen. When pressing Enter, it would be informative for the users if the results area cleared, helping to give them feedback that something did happen when they pressed the Enter key.

To achieve this, the form submit handler should clear the results area first, before attempting to trigger the first option in the list. We can do that currently with the updateResults() function.

function formSubmitHandler(evt) {
    evt.preventDefault();
    updateResults(queue, dropdown);
    var select = dropdown[0];
    ...
}

Notice though that it doesn’t make much sense to do an update, then carry on with clicking one of the options? This is a bad code smell, that tells us that we need to separate out clearing the results, from updating them.

That way the updateResults() function can call a clearResults() function, and the formSubmitHandler() function can also call that same clearResults() function.

    function clearResults() {
        $(".mydivs1").hide();
    }
    function updateResults(queue, dropdown, option) {
        clearResults();
        if (option) {
        ...
}
function formSubmitHandler(evt) {
    evt.preventDefault();
    updateResults(queue, dropdown);
    var select = dropdown[0];
    ...

With those changes in place, pressing the Enter key works much more closely to how you expect is should work.

The update jsfiddle code can be found at https://jsfiddle.net/kbq7ymbk/19/ and looks like this:

/*jslint browser, this */
/*global jQuery */
jQuery(function domReady($) {
    "use strict";
    var Queue = {
        init: function init() {
            var list = [];

            function remove(item) {
                if (list.indexOf(item) > -1) {
                    list.splice(list.indexOf(item), 1);
                }
            }

            function add(item) {
                if (list.indexOf(item) === -1) {
                    list.unshift(item);
                } else {
                    remove(item);
                    add(item);
                }
            }

            function count() {
                return list.length;
            }

            function get(i) {
                return list[i];
            }

            function getAll() {
                return list.slice(0);
            }

            function contains(item) {
                return list.indexOf(item) > -1;
            }

            return {
                remove: remove,
                add: add,
                count: count,
                get: get,
                getAll: getAll,
                contains: contains
            };
        }
    };
    var queue = Queue.init();

    jQuery.fn.filterByText = function (searchField) {
        var mydropdown = this;
        var options = [];
        $(mydropdown).find("option").each(function () {
            options.push({
                value: $(this).val(),
                text: $(this).text()
            });
        });
        $(searchField).bind("change keyup", function (evt) {
            if (!evt.keyCode) {
                return;
            }
            $(mydropdown).empty();
            var search = $(this).val();

            options.forEach(function (option) {
                if (option.text.toLowerCase().indexOf(search.toLowerCase()) > -1) {
                    $(mydropdown).append(
                        $("<option>").text(option.text).val(option.value)
                    );
                }
            });
        });
    };

    function addOptionToQueue(option, queue) {
        queue.add(option.value);
    }

    function filterQueueForSelectedOptions(queue) {
        queue.getAll().forEach(function (optionValue) {
            if ($("option[value='" + optionValue + "']").prop("selected")) {
                return;
            }
            queue.remove(optionValue);
        });
    }

    function showResults(results, targetSelector) {
        results.reverse().forEach(function (item) {
            $("#" + item)
                .show()
                .prependTo(targetSelector);
        });
    }
    function getSelectedOption(select, queue) {
        var selected = $(":selected", select);
        var filteredOptions = selected.filter(function (ignore, option) {
            return !queue.contains(option.value);
        });
        return filteredOptions.get(0);
    }
    function clearResults() {
        $(".mydivs1").hide();
    }
    function updateResults(queue, dropdown, option) {
        clearResults();
        if (option) {
            addOptionToQueue(option, queue);
        }
        filterQueueForSelectedOptions(queue, dropdown);
        showResults(queue.getAll(), "#results");
    }

    var dropdown = $("#mydropdown");

    function textboxBlurHandler() {
        dropdown.prop("selectedIndex", 0);
        updateResults(queue, dropdown);
    }
    function formSubmitHandler(evt) {
        evt.preventDefault();
        clearResults();
        var select = dropdown[0];
        var firstOption = select.options[0];
        if (firstOption) {
            select.focus();
            firstOption.click();
        }
    }
    function optionClickHandler(evt) {
        var selectedOption = evt.target;
        updateResults(queue, dropdown, selectedOption);
    }
    function selectClickHandler(evt) {
        var select = evt.target;
        var selectedOption = getSelectedOption(select, queue);
        updateResults(queue, dropdown, selectedOption);
    }
    
    dropdown.filterByText($("#mytextbox"));
    $("#mytextbox").on("blur", textboxBlurHandler);
    $("#search").on("submit", formSubmitHandler);

    $(document).on("click", "option", optionClickHandler);
    $(document).on("click", "select", selectClickHandler); 

});
2 Likes

There is one more thing to add. While the above line works to get the select element from the jQuery object, it can be easily confused for getting an item from an array.

To remove that confusion we should instead use the jQuery .get() method to retrieve the element from the jQuery object.
Another technique that is sometimes uses is to suffix variables with a dollar symbol, to represent that it is a jQuery object, such as dropdown$, but that’s not something that I’ll be doing yet.

var select = dropdown.get(0);

That results in less confusion when attempting to understand the code.

The change has been made and updated to https://jsfiddle.net/kbq7ymbk/20/

2 Likes

In order to use checkboxes, a two-fold process is helpful to use, where first we duplicate and display a completely separate section as checkboxes, before removing unwanted section.

If the code is well-designed, this should be an easy process. Less well-designed code will result in some pain, which helps to prompt a better design of the code too.

Preparation

Before getting started, we should remove the terms dropdown and ‘option’ as much as possible from the code, replacing them with a more generic container and item terminology.

Sections of code that are specific to handling dropdowns and options, need to be separated out in to functions and be grouped together. Those separate functions will be used to provide similar behaviour for the checkbox list.

HTML checkboxes

We can start off by adding checkboxes to represent the ones that will be available. The good news about using checkboxes is that we don’t need to add/remove them, for later on when we are searching, we can just use CSS to hide them.

<div id="categories">
  <p><label><input type="checkbox" value="Fruits">Fruits</label></p>
  <p><label><input type="checkbox" value="Vegetables">Vegetables</label></p>
  <p><label><input type="checkbox" value="Nuts">Nuts</label></p>
  <p><label><input type="checkbox" value="Meats">Meats</label></p>
</div>

Checkboxes instead of options

Along with the optionClickHandler() we can also set up one for a checkboxClickHandler()

function checkboxClickHandler(evt) {
    var checkbox = evt.target;
    addItemToQueue(checkbox.value);
    updateCheckboxResults(queue, container);
}
// ...
$(document).on("click", "option", optionClickHandler);
$(document).on("click", "[type='checkbox']", checkboxClickHandler);

And the updateCheckboxResults() function along with the removeUnselectedCheckboxesFromQueue() and isNotCheckedCheckbox() functions are easy to create too, based on the pre-existing ones for options.

function isNotCheckedCheckbox(value) {
    return $("input[value='" + value + "']").prop("checked") === false;
}
// ...
function removeUncheckedCheckboxesFromQueue(queue) {
    queue.getAll()
        .filter(isNotCheckedCheckbox)
        .forEach(removeItemFromQueue);
}
// ...
function updateCheckboxResults(queue, container) {
    clearResults();
    removeUncheckedCheckboxesFromQueue(queue, container);
    showResults(queue.getAll(), "#results");
}

You can see the updated code at https://jsfiddle.net/pmw57/kbq7ymbk/25/ where a lot of other changes have occurred to move option-specific code out to separate functions.

The only other thing to work on is to get the search working with the checkboxes, after which IE9 and IE11 testing can occur, followed by removing the option-specific parts of the code. But that will have to wait until tomorrow.

2 Likes

From the three options mentioned, I think I would prefer the space bar method (if that was possible), so I will answer your questions above with reference to the space bar. Yes, I would like the Ctrl+click method of selecting multiple items to remain unchanged. I initially wanted to be able to click without using Ctrl to select multiple items as I thought that would help with accessibility, however, I then read that using Ctrl was the expected behaviour and shouldn’t be tampered with. Besides, it seemed to be a pain to implement!

Anyway, as I was saying, the way I had envisioned it was exactly as it is working now, i.e. pressing Tab to get to the list of items, then the downward arrow to navigate down the list, where each item is highlighted BUT then having the result/div appear when the space bar is pressed whilst the item is highlighted and removed when pressed again (and Ctrl + space bar for multiple items). So, the downward arrow would be doing what the mouse does on hover and pressing the space bar would do what the mouse does on clicking.

Sorry I didn’t respond sooner before you went to the trouble of exploring checkboxes. This was one of the options I was aware of before commencing this project, however, I opted not use them. What I did give serious consideration to are those dual transfer list boxes, however, do they support multiple lines of text anyway and can they only show what can be seen in the list and not additional hidden info?

Does the code at https://jsfiddle.net/kbq7ymbk/18/ work on IE9/IE10 when a search is typed in the textbox and then an item in the list clicked? It didn’t at my end, however, this may totally be my fault as I didn’t get to test it properly.

Thanks again for all your efforts; they are more than appreciated! Will reply more later.

[quote=“TechnoKid, post:35, topic:259656, full:true”]
Anyway, as I was saying, the way I had envisioned it was exactly as it is working now, i.e. pressing Tab to get to the list of items, then the downward arrow to navigate down the list, where each item is highlighted BUT then having the result/div appear when the space bar is pressed whilst the item is highlighted and removed when pressed again (and Ctrl + space bar for multiple items).[/quote]

It would have been good to know that from the start, for multiple selects do not work in that way.

Instead, checkboxes work a lot more closely in that way, and can be made via CSS to match the type of interface that you are using.

So, the downward arrow would be doing what the mouse does on hover and pressing the space bar would do what the mouse does on clicking.

[quote=“TechnoKid, post:35, topic:259656, full:true”]
Sorry I didn’t respond sooner before you went to the trouble of exploring checkboxes.[/quote]

I have completed exploring checkboxes, and have found them to be superior. My further assistance will only be given in regard to checkboxes now.

My testing of IE9 has been from using the IE11 debug panel to make IE11 work as if it were IE9 instead.
I’ll will check again how that works when I finish my post on converting over to using checkboxes.

1 Like

To make it easier to retrieve the checkbox text, we will use a common accessibility technique where the checkbox button and label are separate, with the label connected to the checkbox via an attribute called for. This provides very good accessibility, and the script can easily find the label by looking for the next label element after the checkbox.

<div id="categories">
  <p>
    <input id="chkFruits" type="checkbox" value="Fruits">
    <label for="chkFruits">Fruits</label>
  </p>
  ...

I’ve used a prefix of chk for checkbox, just because there are already sections below that already have an identifier of “Fruits”.
It doesn’t have to be chkFruits in their either. It can be anything at all to connect the label to the checkbox, so long as both the id and for attribute are the same as each other.

The filterCheckboxByText function has become a lot easier than when using options. As we are just hiding and showing elements, we don’t need to keep a separate list of what used to be there.

The main part of the code is now just the following:

$("p", container).hide();

$("[type='checkbox']", container)
    .filter(compareWithSearch)
    .each(showItem);

With the full showCheckboxesByText() function being:

    jQuery.fn.filterCheckboxesByText = function (searchField) {
        var container = this;

        $(searchField).bind("change keyup", function (evt) {
            if (!evt.keyCode) {
                return;
            }

            var search = $(this).val();

            function containsText(haystack, needle) {
                return needle.toLowerCase().indexOf(haystack.toLowerCase()) > -1;
            }
            function compareWithSearch() {
                var checkbox = this;
                var label = $(checkbox).next("label");
                return containsText(search, label.text());
            }
            function showItem() {
                var checkbox = this;
                $(checkbox).parent("p").show();
            }

            $("p", container).hide();

            $("[type='checkbox']", container)
                .filter(compareWithSearch)
                .each(showItem);
        });
    };

Removing options

I can now go through the rest of the code, and wherever there are doubled up functions, such as isNotSelectedOption() and isNotCheckedCheckbox() I can remove the one that relates to options.

Enter selects the first checkbox

When pressing Enter on the search box, we want the first checkbox to be automatically selected.

    function getFirstVisibleCheckbox(categories) {
        return $("[type='checkbox']:visible:eq(0)", categories);
    }
    function checkFirstCheckbox() {
        var firstItem = getFirstVisibleCheckbox(categories);
        if (firstItem) {
            console.log(firstItem);
            firstItem.prop("checked", true);
        }
    }
    function formSubmitHandler(evt) {
        evt.preventDefault();
        clearResults();
        setTimeout(checkFirstCheckbox, 10);
    }
    ...
    $("#search").on("submit", formSubmitHandler);

Arrow controls

The requested arrow controls are easy to deal with too.

    $("#categories").on("keyup", checkboxKeyHandler);

We just need to check for the up and down arrow keys:

    function checkboxKeyHandler(evt) {
        var checkbox = evt.target;
        if (evt.keyCode === 38) {
            moveToPreviousCheckbox(checkbox);
        }
        if (evt.keyCode === 40) {
            moveToNextCheckbox(checkbox);
        }
    }

Then move to the next appropriate checkbox, making sure that it’s only visible ones that we move to.

    function moveToPreviousCheckbox(checkbox) {
        var prev = $(checkbox).parents("p").prevAll("p:visible");
        $(":checkbox:visible", prev).focus();
    }
    function moveToNextCheckbox(checkbox) {
        var next = $(checkbox).parents("p").nextAll("p:visible");
        $(":checkbox", next).focus();
    }

You can now press the Enter key which selects the first item shown, then use the arrow keys to go up and down, selecting the desired categories that you want.

All that is needed now is to use CSS to make the checkboxes look better. The checkbox can be hidden, the labels can be given a different background, or the entire categories area (that contains the checkboxes) can be bordered in different ways.

Here’s the code in full as it now stands, with a link to the jsfiddle page at the bottom.

<form id="search">
    <p><input type="text" id="mytextbox" placeholder="Search Foods"></p>
</form>
<div id="categories">
  <p>
    <input id="chkFruits" type="checkbox" value="Fruits">
    <label for="chkFruits">Fruits</label>
  </p>
  <p>
    <input id="chkVegetables" type="checkbox" value="Vegetables">
    <label for="chkVegetables">Vegetables</label>
  <p>
    <input id="chkNuts" type="checkbox" value="Nuts">
    <label for="chkNuts">Nuts</label>
  <p>
    <input id="chkMeats" type="checkbox" value="Meats">
    <label for="chkMeats">Meats</label>
  </p>
</div>


<p></p>
Results:
<div id="results">
  <div class="mydivs1" id="Fruits">
    <div class="mydivs2">
      <button class="button1">X</button>
      <p style="clear:both">
        <button class="button2">C</button>
    </div>

    <div class="mydivs3">
      <span class="myspan1">Fruits</span>
      <p>
        <span class="myspan2">1:</span>
        <br>
        <span class="myspan3">Avocados</span>
        <p></p>
        <span class="myspan2">2:</span>
        <br />
        <span class="myspan3">Blackberries</span>
        <p></p>
        <span class="myspan2">3:</span>
        <BR />
        <span class="myspan3">Raspberries</span>
    </div>
    <p></p>

  </div>
  <p></p>

  <div class="mydivs1" id="Vegetables">
    <div class="mydivs2">
      <button class="button1">X</button>
      <p style="clear:both"></p>
      <button class="button2">C</button>
    </div>

    <div class="mydivs3">
      <span class="myspan1">Vegetables</span>
      <p></p>
      <span class="myspan2">1:</span>
      <br>
      <span class="myspan3">Beets</span>
      <p></p>
      <span class="myspan2">2:</span>
      <br />
      <span class="myspan3">Eggplants</span>
      <p></p>
      <span class="myspan2">3:</span>
      <BR />
      <span class="myspan3">Spinach</span>
    </div>
    <p></p>

  </div>
  <p></p>

  <div class="mydivs1" id="Nuts">
    <div class="mydivs2">
      <button class="button1">X</button>
      <p style="clear:both"></p>
      <button class="button2">C</button>
    </div>

    <div class="mydivs3">
      <span class="myspan1">Nuts</span>
      <p></p>
      <span class="myspan2">1:</span>
      <br />
      <span class="myspan3">Almonds</span>
      <p></p>
      <span class="myspan2">2:</span>
      <br />
      <span class="myspan3">Pecans</span>
      <p></p>
      <span class="myspan2">3:</span>
      <BR />
      <span class="myspan3">Walnuts</span>
    </div>
    <p></p>

  </div>
  <p></p>

  <div class="mydivs1" id="Meats">
    <div class="mydivs2">
      <button class="button1">X</button>
      <p style="clear:both"></p>
      <button class="button2">C</button>
    </div>

    <div class="mydivs3">
      <span class="myspan1">Meats</span>
      <p></p>
      <span class="myspan2">1:</span>
      <br />
      <span class="myspan3">Chicken</span>
      <p></p>
      <span class="myspan2">2:</span>
      <br />
      <span class="myspan3">Fish</span>
      <p></p>
      <span class="myspan2">3:</span>
      <BR />
      <span class="myspan3">Turkey</span>
    </div>
    <p></p>

  </div>
  <p></p>
</div>
#mydropdown option:hover {
  background: #c8c8c8;
}

#mydropdown option {
  padding: 5px;
}
#categories p {
  margin: 0;
}
.mydivs1 {
  display: none;
  margin-bottom: 1em;
}

br {
  display: block;
  content: "";
  margin-top: 5px;
}

.mydivs1 {
  border: 1px solid black;
  padding: 5px;
  height: 211px;
  width: 172px;
  margin-left: 5px;
  background-color: #ff8080;
}

.mydivs2 {
  border: 1px solid black;
  padding: 5px;
  height: 199px;
  width: 35px;
  float: left;
  background-color: #b3e6b3;
}

.mydivs3 {
  border: 1px solid black;
  height: 199px;
  width: 103px;
  margin-left: 52px;
  padding-left: 10px;
  padding-right: 5px;
  padding-top: 5px;
  padding-bottom: 5px;
  background-color: #cce6ff;
}
/*jslint browser, this */
/*global jQuery */
jQuery(function domReady($) {
    "use strict";
    var Queue = {
        init: function init() {
            var list = [];

            function remove(item) {
                if (list.indexOf(item) > -1) {
                    list.splice(list.indexOf(item), 1);
                }
            }

            function add(item) {
                if (list.indexOf(item) === -1) {
                    list.unshift(item);
                } else {
                    remove(item);
                    add(item);
                }
            }

            function count() {
                return list.length;
            }

            function get(i) {
                return list[i];
            }

            function getAll() {
                return list.slice(0);
            }

            function contains(item) {
                return list.indexOf(item) > -1;
            }

            return {
                remove: remove,
                add: add,
                count: count,
                get: get,
                getAll: getAll,
                contains: contains
            };
        }
    };
    var queue = Queue.init();

    $.fn.filterCheckboxesByText = function (searchField) {
        var container = this;

        $(searchField).bind("change keyup", function (evt) {
            if (!evt.keyCode || evt.keyCode === 13) {
                return;
            }
            var search = $(this).val();

            function containsText(haystack, needle) {
                return needle.toLowerCase().indexOf(haystack.toLowerCase()) > -1;
            }
            function compareWithSearch() {
                var checkbox = this;
                var label = $(checkbox).next("label");
                return containsText(search, label.text());
            }
            function showItem() {
                var checkbox = this;
                $(checkbox)
                    .prop("checked", false)
                    .parent("p").show();
            }

            $("p", container).hide();

            $(":checkbox", container)
                .filter(compareWithSearch)
                .each(showItem);
        });
    };

    function isNotCheckedCheckbox(value) {
        return $("input[value='" + value + "']").prop("checked") === false;
    }
    function removeItemFromQueue(item) {
        queue.remove(item);
    }
    function removeUncheckedCheckboxesFromQueue(queue) {
        queue.getAll()
            .filter(isNotCheckedCheckbox)
            .forEach(removeItemFromQueue);
    }

    function showResults(results, targetSelector) {
        results.reverse().forEach(function (item) {
            $("#" + item)
                .show()
                .prependTo(targetSelector);
        });
    }
    function clearResults() {
        $(".mydivs1").hide();
    }
    function addItemToQueue(item) {
        queue.add(item);
    }
    function updateCheckboxResults(queue, container) {
        clearResults();
        removeUncheckedCheckboxesFromQueue(queue, container);
        showResults(queue.getAll(), "#results");
    }

    var categories = $("#categories");

    function getFirstVisibleCheckbox(categories) {
        return $(":checkbox:visible:eq(0)", categories);
    }
    function checkFirstCheckbox() {
        var firstItem = getFirstVisibleCheckbox(categories);
        if (firstItem) {
            firstItem.focus().click();
        }
    }
    function formSubmitHandler(evt) {
        evt.preventDefault();
        clearResults();
        setTimeout(checkFirstCheckbox, 10);
    }
    function checkboxClickHandler(evt) {
        var checkbox = evt.target;
        addItemToQueue(checkbox.value);
        updateCheckboxResults(queue, categories);
    }
    function moveToPreviousCheckbox(checkbox) {
        var prev = $(checkbox).parents("p").prevAll("p:visible");
        $(":checkbox:visible", prev).focus();
    }
    function moveToNextCheckbox(checkbox) {
        var next = $(checkbox).parents("p").nextAll("p:visible");
        $(":checkbox", next).focus();
    }
    function checkboxKeyHandler(evt) {
        var checkbox = evt.target;
        if (evt.keyCode === 38) {
            moveToPreviousCheckbox(checkbox);
        }
        if (evt.keyCode === 40) {
            moveToNextCheckbox(checkbox);
        }
    }

    $("#categories").filterCheckboxesByText($("#mytextbox"));

    $("#search").on("submit", formSubmitHandler);
    $("#categories").on("keyup", checkboxKeyHandler);
    $(document).on("click", ":checkbox", checkboxClickHandler);
});

The above code can be found at the https://jsfiddle.net/pmw57/kbq7ymbk/26/ jsfiddle page.

2 Likes

The code at https://jsfiddle.net/kbq7ymbk/26/ definately works on IE9 and IE10 when put in to local HTML, CSS, and JavaScript files.

Having Microsoft Edge emuate IE11, or IE9, works on the #26 jsfiddle page too.

1 Like

I didn’t know myself until two days ago when I posted! Hahah.

I will consider checkboxes, but even if I decide to go with the other method, please be assured that I am extremely grateful for all the help you have provided and could not have implemented the requested feature(s) without it.

I will see how I get on this weekend.

Thanks.

Correction - the proper link is https://jsfiddle.net/pmw57/kbq7ymbk/26

1 Like

The checkboxes have now been restyled so that the checkbox doesn’t show, with a lightgrey and silver background unselected, and lightblue and skyblue when selected.

The arrow controls now work more properly, and it all looks to be a lot more robust now, working on IE9 without any troubles too.

1 Like