Select2 Custom Matcher to keep options open if group title matches
Asked Answered
J

1

10

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:

  • Vegetables
  • Lettuce
  • Tomatoes
  • Onions
  • Fruit
  • Apples
  • Oranges
  • Bananas
  • Spreads
  • Vegemite
  • Nutella
  • Peanut Butter

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/

Joey answered 13/3, 2016 at 4:50 Comment(3)
This would really help if you created a jsFiddle of this, instead of uploading it to GitHub. See: jsfiddle.netExtractive
The changes you made work just fine for me. Furthermore, I couldn't find any occurrence of data.indexOf in Select2. Are you sure it's Select2 that's erroring?Zoophyte
Is this what you're talking about? jsfiddle.net/xfw4tmbx/2Gopherwood
G
17

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/

Gopherwood answered 17/3, 2016 at 1:57 Comment(8)
That is what I am looking for but I am using v4 and the bootstrap theme so it's close. I should have specified what version I was using. I got your solution to work though but I loose the theme support.Joey
@DanTappin - Please create a jsfiddle.net with your current code and link to it in a comment here ([@my name] so I get a notification). And when you say you've lost theme support, what does that mean? You got it to work with v4 but it's not working with Bootstrap, or you switched to v3.5.4? Something else?Gopherwood
I am working on the jsfiddle right now. How do you get the CND links to the select2 is and css? Were those hosted by you? I downgraded my select2 rails gem to the version you had and your matcher works like a charm. I will post the errors that v4 throws.Joey
jsfiddle added to the main post. Figured out the CDN issue. As you see you will get TypeError: term.toUpperCase is not a function. (In 'term.toUpperCase()', 'term.toUpperCase' is undefined) errors. Same I seen in my code locally.Joey
Ok... progress. I stumbled upon this github.com/select2/select2/issues/3034 I am not sure how I didn't find this before. I updated my jsfiddle: jsfiddle.net/jasper502/xfw4tmbx/14 it works but your code seems way more concise and to the point. I will play with what you have here unless you beat me to it. @jaredfarrishJoey
@DanTappin - Unfortunately, the reason it seems simpler is because the matcher method was simpler. You can try loading the "full" Select2, which gives you access to oldMatcher() (see the Select2 docs), but I had issues showing sub-matches. Here it is working (it's semi-recursive, hence the inscrutability), but this is really what you're looking for: jsfiddle.net/xfw4tmbx/16 And by the way, it's not that easy to find. There should be example code in the 4.0 documentation demonstrating how to work with the (really different) new methodology, instead of showing only oldMatcher().Gopherwood
I agree - the docs are not that clear at all. I am going to accept this answer because you took the time to respond, it is does work (for the old version) and it prompted me to end up finding a working solution. Thanks.Joey
child.parentText += data.parentText + " " + data.text; should be child.parentText = data.parentText + " " + data.text; otherwise, child.parentText would keep growing with each search...Depilatory

© 2022 - 2024 — McMap. All rights reserved.