12

I'm using the jquery ui autocomplete combobox, and it's working great but now i'm getting a bit greedy. I would like to be able to add categories to it. The combobox is generated off of a menu so if I added categories see example below the tag would show up like the categories are in the jquery ui autocomplete categories version

<select>
<optgroup name="Cat 1">    
<option value="1a">One A</option>
<option value="1b">One B</option>
<option value="1c">One C</option>
</optgroup>
<optgroup name="Cat 2">    
<option value="2a">Two A</option>
<option value="2b">Two B</option>
<option value="2c">Two C</option>
</optgroup>
</select>

I created a http://jsfiddle.net/nH3b6/11/.

Thanks for any help or direction.

Mark K
  • 581
  • 1
  • 3
  • 13

4 Answers4

11

Expanding on @Jarry's suggestion, I would update your code to determine what optgroup the option belongs to. From there, you can use similar code as found on the jQueryUI website:

(function($) {
    $.widget("ui.combobox", {
        _create: function() {
            var input, self = this,
                select = this.element.hide(),
                selected = select.children(":selected"),
                value = selected.val() ? selected.text() : "",
                wrapper = this.wrapper = $("<span>").addClass("ui-combobox").insertAfter(select);

            input = $("<input>").appendTo(wrapper).val(value).addClass("ui-state-default ui-combobox-input").autocomplete({
                delay: 0,
                minLength: 0,
                source: function(request, response) {
                    var matcher = new RegExp($.ui.autocomplete.escapeRegex(request.term), "i");

                    response(select.find("option").map(function() {
                        var text = $(this).text();
                        if (this.value && (!request.term || matcher.test(text))) return {
                            label: text.replace(
                            new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + $.ui.autocomplete.escapeRegex(request.term) + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>"),
                            value: text,
                            option: this,
                            category: $(this).closest("optgroup").attr("label")
                        };
                        //MK
                        $('#test').attr('style', 'display: none;');
                    }).get());
                },
                select: function(event, ui) {
                    ui.item.option.selected = true;
                    self._trigger("selected", event, {
                        item: ui.item.option
                    });
                },
                change: function(event, ui) {
                    if (!ui.item) {
                        var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex($(this).val()) + "$", "i"),
                            valid = false;
                        select.children("option").each(function() {
                            if ($(this).text().match(matcher)) {
                                this.selected = valid = true;
                                return false;
                            }
                        });
                        if (!valid) {
                            $('#test').attr('style', 'display: block;');
                            // remove invalid value, as it didn't match anything
                            //$( this ).val( "" );
                            //select.val( "" );
                            //input.data( "autocomplete" ).term = "";
                            //return false;                    
                        }
                    }
                }
            }).addClass("ui-widget ui-widget-content ui-corner-left");

            input.data("autocomplete")._renderItem = function(ul, item) {
                return $("<li></li>").data("item.autocomplete", item).append("<a>" + item.label + "</a>").appendTo(ul);
            };

            input.data("autocomplete")._renderMenu = function(ul, items) {
                var self = this,
                    currentCategory = "";
                $.each(items, function(index, item) {
                    if (item.category != currentCategory) {
                        if (item.category) {
                            ul.append("<li class='ui-autocomplete-category'>" + item.category + "</li>");
                        }
                        currentCategory = item.category;
                    }
                    self._renderItem(ul, item);
                });
            };

            $("<a>").attr("tabIndex", -1).attr("title", "Show All Items").appendTo(wrapper).button({
                icons: {
                    primary: "ui-icon-triangle-1-s"
                },
                text: false
            }).removeClass("ui-corner-all").addClass("ui-corner-right ui-combobox-toggle").click(function() {
                // close if already visible
                if (input.autocomplete("widget").is(":visible")) {
                    input.autocomplete("close");
                    return;
                }

                // work around a bug (likely same cause as #5265)
                $(this).blur();

                // pass empty string as value to search for, displaying all results
                input.autocomplete("search", "");
                input.focus();
            });
        },

        destroy: function() {
            this.wrapper.remove();
            this.element.show();
            $.Widget.prototype.destroy.call(this);
        }
    });
})(jQuery);

$(function() {
    $("#combobox").combobox();
    $("#toggle").click(function() {
        $("#combobox").toggle();
    });
});

Example: http://jsfiddle.net/gB32r/

Andrew Whitaker
  • 124,656
  • 32
  • 289
  • 307
  • Hey so I found a little bug as a by-product of the adjustment you made. If I drop down the list and select one of the options from a category it displays "No matches". If I select an option that is not under a category it works great. I tried to figure it out to no avail. – Mark K Jun 15 '12 at 13:36
  • Nevermind figured it out... select.children("optgroup").children("option").each(function() { – Mark K Jun 15 '12 at 13:46
  • @MarkK: Sorry about that, glad you figured it out `:)` – Andrew Whitaker Jun 15 '12 at 14:19
  • +1 I always find it funny when I'm looking into something and there's been a very recent question/answer on the same subject! – andyb Jun 21 '12 at 15:16
  • 1
    You might want to change all calls to `children` to `find`. This way, it works after postbacks (in case of ASP.NET) and other cases. I wanted to change the answer, but I think the company network is blocking ajax or something. Also, in my experience, you don't have to change the _renderItem method. – Peter Jul 09 '13 at 07:25
  • @AndrewWhitaker any suggestions on how to get the same functionality to work on the latest version of jQuery? http://jsfiddle.net/ZeNz0r/K5q3U/1/ Added the libraries to the resource panel instead of the HTML `head`. jQuery 1.10.2, jQueryUI 1.10.4 – Fernando Silva Jan 23 '14 at 19:14
  • 3
    @FernandoSilva: Hey Fernando -- the following update should work: http://jsfiddle.net/K5q3U/2/ The later versions of jQueryUI use different data names `uiAutocomplete` and `ui-autocomplete-item` – Andrew Whitaker Jan 23 '14 at 19:21
  • @AndrewWhitaker Perfect! How did I spend an entire afternoon looking at documentation and trying to rewrite this and missed that. Thanks, now I just need to adjust css and I'm ready to go. – Fernando Silva Jan 23 '14 at 19:28
  • @Andrew Whitaker this stopped working for me when trying to upgrade to the jQuery 1.11 and jQuery UI 1.11, any idea? has the menu changed? – NinaNa Aug 28 '14 at 05:54
  • See http://stackoverflow.com/a/18231527/1030049 to fix `input.data("autocomplete")` with `input.data("ui-autocomplete")` with jQuery 1.9+ – Jarno Argillander Sep 08 '16 at 05:57
3

There is a few features with jquery 10. I take autocomplete combobox from website of jquery ui: http://jqueryui.com/autocomplete/#combobox and join it with Andrew Whitaker answer.

(function( $ ) {
$.widget( "custom.combobox_with_optgroup", {
    _create: function() {
        this.wrapper = $( "<span>" )
            .addClass( "custom-combobox" )
            .insertAfter( this.element );
        this.element.hide();
        this._createAutocomplete();
        this._createShowAllButton();
    },
    _createAutocomplete: function() {
        var selected = this.element.find( ":selected" ),
            value = selected.val() ? selected.text() : "";
        this.input = $( "<input>" )
            .appendTo( this.wrapper )
            .val( value )
            .attr( "title", "" )
            .addClass( "custom-combobox-input ui-widget ui-widget-content ui-state-default ui-corner-left" )
            .autocomplete({
                delay: 0,
                minLength: 0,
                source: $.proxy( this, "_source" )
            })
            .tooltip({
                tooltipClass: "ui-state-highlight"
            });
        this._on( this.input, {
            autocompleteselect: function( event, ui ) {
                ui.item.option.selected = true;
                this._trigger( "select", event, {
                    item: ui.item.option
                });
            },
            autocompletechange: "_removeIfInvalid"
        });

        this.input.data("uiAutocomplete")._renderMenu = function(ul, items) {
            var self = this,
                currentCategory = "";
            $.each(items, function(index, item) {
                if (item.category != currentCategory) {
                    if (item.category) {
                        ul.append("<li class='custom-autocomplete-category'>" + item.category + "</li>");
                    }
                    currentCategory = item.category;
                }
                self._renderItemData(ul, item);
            });
        };
    },
    _createShowAllButton: function() {
        var input = this.input,
            wasOpen = false;
        $( "<a>" )
            .attr( "tabIndex", -1 )
            .attr( "title", "Показать все" )
            .tooltip()
            .appendTo( this.wrapper )
            .button({
                icons: {
                    primary: "ui-icon-triangle-1-s"
                },
                text: false
            })
            .removeClass( "ui-corner-all" )
            .addClass( "custom-combobox-toggle ui-corner-right" )
            .mousedown(function() {
                wasOpen = input.autocomplete( "widget" ).is( ":visible" );
            })
            .click(function() {
                input.focus();

                if ( wasOpen ) {
                    return;
                }

                input.autocomplete( "search", "" );
            });
    },
    _source: function( request, response ) {
        var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
        response( this.element.find( "option" ).map(function() {
            var text = $( this ).text();
            if ( this.value && ( !request.term || matcher.test(text) ) )
                return {
                    label: text,
                    value: text,
                    option: this,
                    category: $(this).closest("optgroup").attr("label")
                };
        }) );
    },
    _removeIfInvalid: function( event, ui ) {

        if ( ui.item ) {
            return;
        }

        var value = this.input.val(),
            valueLowerCase = value.toLowerCase(),
            valid = false;
        this.element.find( "option" ).each(function() {
            if ( $( this ).text().toLowerCase() === valueLowerCase ) {
                this.selected = valid = true;
                return false;
            }
        });

        if ( valid ) {
            return;
        }

        this.input
            .val( "" )
            .attr( "title", value + " не существует" )
            .tooltip( "open" );
        this.element.val( "" );
        this._delay(function() {
            this.input.tooltip( "close" ).attr( "title", "" );
        }, 2500 );
        this.input.data( "ui-autocomplete" ).term = "";
    },
    _destroy: function() {
        this.wrapper.remove();
        this.element.show();
    }
});
})( jQuery );
Jewel_Sam
  • 72
  • 4
2

as you can see in the jQueryUI docs, you have to customize the widget to do that

_renderMenu: function( ul, items ) {
            var self = this,
                currentCategory = "";
            $.each( items, function( index, item ) {
                if ( item.category != currentCategory ) {
                    ul.append( "<li class='ui-autocomplete-category'>" + item.category + "</li>" );
                    currentCategory = item.category;
                }
                self._renderItem( ul, item );
            });
        }

this its not tested, but should be a good start:

_renderMenu: function( ul, items ) {
            var self = this,
                currentCategory = "";
            $.each( items, function( index, item ) {
                if ( item.parent.attr('label') != currentCategory ) {
                    ul.append( "<li class='ui-autocomplete-category'>" + item.parent.attr('label') + "</li>" );
                    currentCategory = item.parent.attr('label');
                }
                self._renderItem( ul, item );
            });
        }

if this doesnt work, maybe you should debug to see whats into the items array that comes as a parameter of _renderMenu.

a side note: this is called MonkeyPatching, i wouldnt recommend doing this a lot, but since the docs show it, id say do it.

Jarry
  • 1,891
  • 3
  • 15
  • 27
  • Thanks for the reply. Correct me if i'm wrong, but you are starting with the code from the "category" version of the autocomplete I am starting from the "combobox" version. So the code above wouldn't work. – Mark K Jun 14 '12 at 19:59
  • you are not wrong. the code is from the category. im not sure if it would work, and i cant test it right now. i think it will work, you just have to override(AKA monkeypatch) the _renderMenu function. i bet that its being called despite from where the items come – Jarry Jun 14 '12 at 20:04
1

I'am using the jqueryui autocomplete widget on my webapp, with the combobox monkey patching, optgroup (categories) and ability to seach also in category names. The seach term is also emphasized inside compatible option and optgroup. I used several answers from stackoverflow and jqueryui website to get to this point, thanks !

I like to keep it working on the last version of jqueryui. Jqueryui 1.9 and 1.11 introduced breaking changes (in autocomplete and menu plugin, the lastest being used by the former) and I finally succeeded in making it work with the lastest version of jqueryui (1.11.0) and jquery ( 2.1.1 )

jsbin here

Important part : change menu widget options to not consider categories as normal menu link via new items option (so new that not inside the doc but in the jqueryui upgrade guide to 1.11

$.extend($.ui.menu.prototype.options, {
    items: "> :not(.aureltime-autocomplete-category)"
});
Aureltime
  • 373
  • 3
  • 10
  • This is perfect! I was hesitant at first to use this, but this actually worked and solved my issue, thanks a lot! – NinaNa Aug 28 '14 at 09:24