javascriptjqueryjquery-select2-4jquery-select2

Select2 Custom Matcher to keep options open if group title matches


I hope my question makes sense - wasn't sure on the best way to describe this. I have a grouped Select2 select form input something like this:

So you start typing App and of course you get Apples from the Select2 dropdown. If you type veg you get Vegemite and the Vegetables group heading but all the options are hidden. I would like to keep all the group options visible if a search term matches the group heading.

I did some digging in the select2 source code and I think it's actually easy but I could be wrong and if I am right I am stuck on how to make it work. Here is the source code: https://github.com/select2/select2/blob/81a4a68b113e0d3e0fb1d0f8b1c33ae1b48ba04f/src/js/select2/defaults.js:

and a Gist I created vs. trying to paste it in here:

https://gist.github.com/jasper502/40b810e55b2195476342

I switched the order of the code and made some slight variable name changes to reflect this. I think this would keep the option group open. I tried to make a custom matcher based on this (see my Gist) but I was stuck at the point where it calls DIACRITICS:

https://github.com/select2/select2/blob/8ad8f200ba8c9429d636453b8ee3bcf593e8c87a/src/js/select2/diacritics.js

After some Googling I realized that this is replacing accented characters which I know I don't have so I removed that portion.

Now my matcher fails with TypeError: data.indexOf is not a function. (In 'data.indexOf(term)', 'data.indexOf' is undefined) errors in my browser.

I am sure I am very close here but I am a bit over my head and beyond my experience and/or skill level to finish this off. Any pointers or ideas would be appreciated.

UPDATE

Here is a JSfiddle with what I am working with:

https://jsfiddle.net/jasper502/xfw4tmbx/9/


Solution

  • What I gather from your question is you want to be able to show options for selection when there's a match in either the option text OR the option's parent optgroup value attribute.

    This is relatively straightforward: Mainly, look at both of the values and if either matches, return true using Select2's matcher option:

    (Note: Using Select2 v3.5.4.)

    (function() {
        function matcher(term, text, opt) {
            var $option = $(opt),
                $optgroup = $option.parent('optgroup'),
                label = $optgroup.attr('label');
    
            term = term.toUpperCase();
            text = text.toUpperCase();
    
            if (text.indexOf(term) > -1
                 || (label !== undefined 
                     && label.toUpperCase().indexOf(term) > -1)) {
                return true;
            }
    
            return false;
        }
    
        $(".select2").select2({
            matcher: matcher
        });
    })();
    

    https://jsfiddle.net/xfw4tmbx/2/

    v4.* and above changed the term and text to a more complex object, so it'll be slightly different, but the main concept is the same. As you can see, all I'm doing is using jQuery to select up to the option's parent if it's an optgroup element and including that in the matcher check.

    Also, an optgroup will display if any of it's children are shown, so you only have to worry about displaying one or more of the option's, and not actually "show" the optgroup by manually showing it.

    If you have a different requirement, please provide a (working/non-working?) demonstration fiddle showing what you have where we can actually run it.

    EDIT

    Select2 custom matching changed significantly with the 4.0 release. Here is a custom matcher that was posted to this GitHub issue. It is reproduced as-is below for completeness.

    Notice that it's calling itself for child elements (the option elements within the optgroup elements), so modelMatcher() is running against both the optgroup and the option elements, but the combined set is returned after removing the optgroup and option elements that don't match. In the version above, you got every option element and simply returned true/false if you wanted it (and the parent) displayed. Not that much more complicated, but you do have to think about it a little bit more.

    (function() {
        function modelMatcher(params, data) {
            data.parentText = data.parentText || "";
    
            // Always return the object if there is nothing to compare
            if ($.trim(params.term) === '') {
                return data;
            }
    
            // Do a recursive check for options with children
            if (data.children && data.children.length > 0) {
                // Clone the data object if there are children
                // This is required as we modify the object to remove any non-matches
                var match = $.extend(true, {}, data);
    
                // Check each child of the option
                for (var c = data.children.length - 1; c >= 0; c--) {
                    var child = data.children[c];
                    child.parentText += data.parentText + " " + data.text;
    
                    var matches = modelMatcher(params, child);
    
                    // If there wasn't a match, remove the object in the array
                    if (matches == null) {
                        match.children.splice(c, 1);
                    }
                }
    
                // If any children matched, return the new object
                if (match.children.length > 0) {
                    return match;
                }
    
                // If there were no matching children, check just the plain object
                return modelMatcher(params, match);
            }
    
            // If the typed-in term matches the text of this term, or the text from any
            // parent term, then it's a match.
            var original = (data.parentText + ' ' + data.text).toUpperCase();
            var term = params.term.toUpperCase();
    
            // Check if the text contains the term
            if (original.indexOf(term) > -1) {
                return data;
            }
    
            // If it doesn't contain the term, don't return anything
            return null;
        }
    
    
        $(".select2").select2({
            matcher: modelMatcher
        });
    })();
    

    https://jsfiddle.net/xfw4tmbx/16/