jQuery UI Autocomplete Category How to Skip Category Headers
Asked Answered
A

3

9

I've got a working autocomplete field in my web application and I'm looking for a way to increase the usability of the field by somehow automatically skipping the category fields when an arrow key is used to scroll down the available choices (after typing in a partial search term).

For example, if a user starts typing "an", the autocomplete will show two categories with items in each. The user wants to select one of the items in the list under "People". They use the arrow key to move down the list. Currently, this code inserts the categories in the results as a list item. When using the arrow keys, you must move passed them in order to highlight and select a result. Any way the application could automatically skip those category headers?

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

    var data = [
        { label: "annk K12", category: "Products" },
        { label: "annttop C13", category: "Products" },
        { label: "anders andersson", category: "People" },
        { label: "andreas andersson", category: "People" },
        { label: "andreas johnson", category: "People" }
    ];

    $( "#textfield" ).catcomplete({
        source: data,
        select: function(event, ui) {
            window.location.hash = "id_"+escape(ui.item.id);
        }
    });
Adapa answered 31/5, 2011 at 16:29 Comment(0)
G
6

This line:

ul.append( "<li class='ui-menu-item ui-category'>" + item.category + "</li>" );

is causing the problem.

Internally, the widget uses list items with a class ui-menu-item to distinguish whether or not an li is an actual menu item that can be selected. When you press the 'down' key, the widget finds the next item with a class ui-menu-item and moves to it.

Remove the class and your code works like you want it to:

ul.append( "<li class='ui-category'>" + item.category + "</li>" );

Here it is working:

http://jsfiddle.net/andrewwhitaker/pkFCF/

Gotthard answered 31/5, 2011 at 16:44 Comment(2)
Perfect, that was much easier than I thought it would be. Thank you for your help.Adapa
Since 1.10.4 this no longer works, due to this - Menu: Remove requirement for anchors in menu items. This is because the anchors have been removed so there is no longer any distinction between li elements, all get given ui-menu-item class regardless of whether you specify it or not.Garett
R
2

Since the accepted answer doesn't work in latest versions of jQueryUI (>1.10.4) I'll post my hack, maybe someone will find it useful.

I'm using jQueryUI 1.12.0

While appending category I added new class, i called it "categoryItem":

ul.append( "<li class='ui-autocomplete-category categoryItem'>" + "Category" + "</li>" );

Some of jQueryUI functions also need to be overridden to force jquery to ignore items with "categoryItem" class (two lines are changed).

$.widget("ui.menu", $.extend({}, $.ui.menu.prototype, {
  refresh: function() {
    var menus, items, newSubmenus, newItems, newWrappers,
        that = this,
        icon = this.options.icons.submenu,
        submenus = this.element.find( this.options.menus );

    this._toggleClass( "ui-menu-icons", null, !!this.element.find( ".ui-icon" ).length );
    // Initialize nested menus
    newSubmenus = submenus.filter( ":not(.ui-menu)" )
        .hide()
        .attr( {
            role: this.options.role,
            "aria-hidden": "true",
            "aria-expanded": "false"
        } )
        .each( function() {
            var menu = $( this ),
                item = menu.prev(),
                submenuCaret = $( "<span>" ).data( "ui-menu-submenu-caret", true );

            that._addClass( submenuCaret, "ui-menu-icon", "ui-icon " + icon );
            item
                .attr( "aria-haspopup", "true" )
                .prepend( submenuCaret );
            menu.attr( "aria-labelledby", item.attr( "id" ) );
        } );

    this._addClass( newSubmenus, "ui-menu", "ui-widget ui-widget-content ui-front" );

    menus = submenus.add( this.element );
    items = menus.find( this.options.items );

    // Initialize menu-items containing spaces and/or dashes only as dividers
    items.not( ".ui-menu-item" ).each( function() {
        var item = $( this );
        if ( that._isDivider( item ) ) {
            that._addClass( item, "ui-menu-divider", "ui-widget-content" );
        }
    } );

    // Don't refresh list items that are already adapted
    newItems = items.not( ".ui-menu-item, .ui-menu-divider" ).not(".categoryItem");
    newWrappers = newItems.children()
        .not( ".ui-menu" )
            .uniqueId()
            .attr( {
                tabIndex: -1,
                role: this._itemRole()
            } );
    this._addClass( newItems, "ui-menu-item" )
        ._addClass( newWrappers, "ui-menu-item-wrapper" );

    // Add aria-disabled attribute to any disabled menu item
    items.filter( ".ui-state-disabled" ).attr( "aria-disabled", "true" );

    // If the active item has been removed, blur the menu
    if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) {
        this.blur();
    }

},
    _move: function( direction, filter, event ) {
    var next;
    if ( this.active ) {
        if ( direction === "first" || direction === "last" ) {
            next = this.active
                [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" )
                .eq( -1 );
        } else {
            next = this.active
                [ direction + "All" ]( ".ui-menu-item" )
                .eq( 0 );
        }
    }
    if ( !next || !next.length || !this.active ) {
        next = this.activeMenu.find( this.options.items ).not(".categoryItem")[ filter ]();
    }

    this.focus( event, next );
}
}));
Riordan answered 14/7, 2016 at 8:28 Comment(0)
A
1

For anyone else who ended up here trying to get categories to work nicely in jQueryUI 1.12...

bakus33's answer works but I didn't like having to extend the widget (maybe I'm just too picky) and found a more local way.

Add the following inside your created handler in the autocomplete options:

$(this).data('uiAutocomplete').menu.options.items = '> *:not(.class-of-items-to-exclude)';

e.g.

$("#searchBox").autocomplete({
      create: function () {
            //access to jQuery Autocomplete widget differs depending
            //on jQuery UI version - you can also try .data('autocomplete')
            $(this).data('uiAutocomplete')._renderMenu = function (ul, items) {
                  var self = this;
                  var categoryArr = [];

                  function contain(item, array) {
                        var contains = false;
                        $.each(array, function (index, value) {
                              if (item === value) {
                                    contains = true;
                                    return false;
                              }
                        });
                        return contains;
                  }

                  $.each(items, function (index, item) {
                        if (!contain(item.category, categoryArr)) {
                              categoryArr.push(item.category);
                        }
                  });

                  $.each(categoryArr, function (index, category) {
                        ul.append("<li class='ui-autocomplete-group category-item'>" + category + "</li>");
                        $.each(items, function (index, item) {
                              if (item.category === category) {
                                    self._renderItemData(ul, item);
                              }
                        });
                  });
            };
            
            // Tell the menu to ignore '.category-item' items.
            $(this).data('uiAutocomplete').menu.options.items = '> *:not(.category-item)';
            
      },
});

Fair warning, this might have other side effects but seems to work for me. You also might want to check what the value of menu.options.items is for you and apply the :not() to that

Anti answered 6/10, 2021 at 15:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.