15

I am using Jquery UI's autocomplete for multiple selectable values. All is good except the list of options closes after each selection. I would like the options to stay open until the user decides they are done making selections. I have looked through the documentation and I don't see any way to keep the options open. Any ideas?

<meta charset="utf-8">

<script>
$(function() {
    var availableTags = [
        "ActionScript",
        "AppleScript",
        "Asp",
        "BASIC",
        "C",
        "C++",
        "Clojure",
        "COBOL",
        "ColdFusion",
        "Erlang",
        "Fortran",
        "Groovy",
        "Haskell",
        "Java",
        "JavaScript",
        "Lisp",
        "Perl",
        "PHP",
        "Python",
        "Ruby",
        "Scala",
        "Scheme"
    ];
    function split( val ) {
        return val.split( /,\s*/ );
    }
    function extractLast( term ) {
        return split( term ).pop();
    }

    $( "#tags" ).autocomplete({
        minLength: 0,
        source: function( request, response ) {
            // delegate back to autocomplete, but extract the last term
            response( $.ui.autocomplete.filter(
                availableTags, extractLast( request.term ) ) );
        },
        focus: function() {
            // prevent value inserted on focus
            return false;
        },
        select: function( event, ui ) {
            var terms = split( this.value );
            // remove the current input
            terms.pop();
            // add the selected item
            terms.push( ui.item.value );
            // add placeholder to get the comma-and-space at the end
            terms.push( "" );
            this.value = terms.join( ", " );
            return false;
        }
    });
});
</script>
Tag programming languages:
Rusty
  • 213
  • 1
  • 3
  • 10

18 Answers18

11

You can do something like this: first define a variable named readyToClose and set it to be false at the begining. And when you want to close your menu on next selection, set this variable to the true. And also we should reimplement the close method of JQuery UI.

Here I reimplemented the close method of JQuery UI in your code, not in the source file! This is the same thing we do to render the list in custom way (e.g. http://jqueryui.com/demos/autocomplete/custom-data.html )

var readyToClose = false;
$( "#tags" ).autocomplete({
    minLength: 0,
    source: function( request, response ) {
        // delegate back to autocomplete, but extract the last term
        response( $.ui.autocomplete.filter(
            availableTags, extractLast( request.term ) ) );
    },
    focus: function() {
        // prevent value inserted on focus
        return false;
    },
    select: function( event, ui ) {
        var terms = split( this.value );
        // remove the current input
        terms.pop();
        // add the selected item
        terms.push( ui.item.value );
        // add placeholder to get the comma-and-space at the end
        terms.push( "" );
        this.value = terms.join( ", " );
        return false;
    }
}).data( "autocomplete" ).close = function(e){
    if(readyToClose)
        clearTimeout(this.closing), this.menu.element.is(":visible") && (this.menu.element.hide(), this.menu.deactivate(), this._trigger("close", e));
    else
        return false;    
};

Note: In newer versions of jQuery (i.e. 1.9.0), replace "autocomplete" with "uiAutocomplete", as in:

$("#tags")
    .autocomplete({...})
    .data("uiAutocomplete").close = ...
Nate Anderson
  • 18,334
  • 18
  • 100
  • 135
Mehran
  • 1,951
  • 4
  • 20
  • 26
  • Saving me dealing with a bug after 3 days searching - sometimes click on a list item didn't work for me.....thanks a lot! – Samih A Feb 08 '15 at 08:43
6

I know it's an old question that may no longer by relevant to the OP, but for the sake of completeness, a cleaner solution would be to extend the autocomplete widget and overriding _close, as well as extending the event object in the select event handler. This allows you to perform custom logic to determine whether the menu should be closed or not on a case by case (event by event) basis. See also http://learn.jquery.com/jquery-ui/widget-factory/extending-widgets/

//override the autocomplete widget
jQuery.widget( "ui.autocomplete", jQuery.ui.autocomplete, {
    _close: function( event ) {
        if(event!== undefined && event.keepOpen===true) {
            //trigger new search with current value
            this.search( null, event );
            return true;
        }
        //otherwise invoke the original
        return this._super( event );
    }
});

$('ac').autocomplete(
    {
        ...custom options...
        select: function( event, ui ) {
            ...custom logic...
            if(menu should remain open) {
                //extend original event with special flag to keep dropdown open
                //the o-event continues to be passed through the chain of listeners
                //and will end up being processed during _close()
                jQuery.extend(event.originalEvent,{keepOpen:true});
                //modify value as required
                jQuery(this).val(...);
                return false; //prevent selected value from being set,
                              //i.e. keeping modified value above
            }
        }
    }
);

In the above jQuery.extend(event.originalEvent,{keepOpen:true}); is used to add a special keepOpen property to event.originalEvent. Since extend modifies the original object (first argument), any subsequent use of the same event.originalEvent will have this property available. Having read and stepped through the code it ends up being the same object that is referenced by event in the _close method. This code will break should this change in future, however, it is a lot simpler to maintain than the equivalent of what ingredient_15939 suggested 18 months ago.

user3482702
  • 61
  • 1
  • 1
5

Just ran into this issue myself. Here is a simple one liner for you all, just throw it into autocomplete's options.

$ac.autocomplete({
  ...
  close: function () { $('.ui-autocomplete').show() }
});

If you want to close it or if you want it to fade out when closing then you can remove the close statement or simply add .fadeOut() to the end, respectively.

$ac.autocomplete({
  ...
  close: function () { $('.ui-autocomplete').show().fadeOut() }
});
Blake Bell
  • 376
  • 5
  • 16
3

I needed this same ability. IMO the authors could have easily given us the option. What I did was exclude autocomplete from the UI bundle, and include an edited copy of the separate file: jquery.ui.autocomplete.js (get it from the "development-bundle\ui" folder).

I added a couple of new options, closeOnSelect and updateElement (both boolean default true), as follows (at the top of file):

$.widget("ui.autocomplete", {
  options: {

  ...

  closeOnSelect: true, // add this line - controls list closing.
  updateElement: true  // add this line - controls updating input box.
},

Then search for the string "selected:" in the code (there will be only one occurrence). Within this function, replace these last few lines:

if ( false !== self._trigger( "select", event, { item: item } ) ) {
    self.element.val( item.value );
}
// reset the term after the select event
// this allows custom select handling to work properly
self.term = self.element.val();

self.close( event );
self.selectedItem = item;

With this:

if (false !== self._trigger("select", event, { item: item })) {

  if (self.options.updateElement) {
    self.element.val(item.value);
    self.term = self.element.val(); // Ensure term string is same.
  }
  if (self.options.removeOnSelect) { // Remove menu item if option is true.
    console.log(ui);
  } else {
    self.selectedItem = item;
  }
  if (self.options.closeOnSelect) self.close(event); // Close menu if option is true.

} else { // Returned false.
  self.term = self.element.val(); // Ensure term string is same, in case callback changed element value.
}

Then search for string "focus: function" (again, only one occurrence). Within that function, replace the line:

self.element.val(item.value);

With this:

if (self.options.updateElement) self.element.val(item.value);

Now, when you create the autocomplete, simply set those two options as you like:

$("#txtsearch").autocomplete({
  closeOnSelect: false, // Keep list open when item selected.
  updateElement: false, // Don't change the input box contents.
  // etc...

This works perfectly for me. Regardless of whether it's deemed "bad UI practice" by the original authors, everyone's needs are different and options should be there! :)

ingredient_15939
  • 3,022
  • 7
  • 35
  • 55
  • @ingredient_15939 Can you change these values inside of event functions? eg I want to change closeOnselect value based on the selected one inside of select event. – likeachamp Apr 03 '15 at 14:16
3

The jQuery UI team thinks this is bad UX practice: http://forum.jquery.com/topic/enhanced-autocomplete-interest-in-getting-this-into-jqueryui#14737000001125152

So no built-in way to cancel it. You can try re-triggering the autocomplete after the select & close events. And probably you should cache the results array so that no new requests are made (if it's in Ajax mode).

Didn't get into the details though - I have convinced my UI designer that we don't need this for this version :)

1

Not so good solution, but i managed it like this:

var $input = $('input').autocomplete({
    select: function(event, obj) {
        var ac_data = $(event.target).data("autocomplete");
        ac_data.instaSearch = true;

        // Your action

        return false;
    }
});
$input.data("autocomplete")._close = function( event ) {
    if ( this.menu.element.is( ":visible" )) {
        this.menu.element.hide();
        this.menu.blur();
        this.isNewMenu = true;
        this._trigger( "close", event );
        if (this.instaSearch) {
            this.search(this.term);
            this.instaSearch = false;
        }
    }
};
Lauri
  • 65
  • 4
  • 12
1

This solution worked for me. I was using Autocomplete to display a list of the first four character possibilities for names. Then if the user selects one of those, then I have Autocomplete display the names associated with the selection. The autocomplete drop down stays open after the first selection and you can see the list change to the other options. Hope this helps with this thread.

select: function( event, ui ) {
    // the number of chars was driving my decision for behavior, yours may be different
    if (chars.length <= 4) {
        jq.each(ui, function(key, value) {
            jq.each(value, function(key1, value1) {
                // put the selected value in the text area, 
                // since normal behavior will be prevented
                jq("#mod_autocomplete").val(value1);

                // trigger another search
                jq("#mod_autocomplete").autocomplete("search");

                // open the autocomplete dropdown
                jq("#mod_autocomplete").autocomplete("open");
            });

        });

        event.preventDefault();

    } else {
        // put whatever behavior you need here for next selection
    }

}
Dunn
  • 11
  • 1
1

I know there are so many answers already there but still I am putting a reference of an answer which is more clean and straight forward. Please have a look at this answer

Use the same code from the answer that will keep open the menu and now in order to hide the menu from the out side click(on body) then use the below piece of code.

 $('body').on('click', function(e){
            // click event will prevent for the input and for auto complete menu div
            if($(e.target).hasClass('ui-autocomplete-input') || $(e.target).hasClass('ui-menu-item-wrapper')){
                e.preventDefault();
                return;
            }
// in other case hide the menu
            $("ul.ui-autocomplete").hide();
        });
dom
  • 1,086
  • 11
  • 24
1

You'll need to edit your jquery-ui javascript file. In the autocomplete section replace

close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this._trigger("close",a);this.menu.element.hide();this.menu.deactivate()}}

with

close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){if(this._trigger("close",a)!==false){this.menu.element.hide();this.menu.deactivate()}}}

then you should be able to cancel the close event as described in Shaans' answer.

Simon
  • 25,468
  • 44
  • 152
  • 266
  • 1
    editing the source code is not a good idea! we should try to reach our goal in our code! – Mehran Jul 22 '12 at 11:47
  • 2
    Thanks for that suggestion, it helped me get this option working. But what I did was just exclude autocomplete from the UI bundle, and include the separate, custom-edited file `jquery.ui.autocomplete.js`. I added my own config option `closeOnSelect` (boolean, default true) which IMO is what the authors should have done in the first place, to give us the option. :) – ingredient_15939 Sep 07 '12 at 12:08
0

I tweaked the solution from JQuery UI to respond to the close event and re-open the autocomplete menu if the input still has focus, the escape wasn't pressed to close the popup, and the value of the input ends in either a ',' or a ' ' (space):

close: function(e) {
  if ($el.is(':focus') && e.keyCode !== $.ui.keyCode.ESCAPE && (this.value != null) && /[,\s]+$/.test(this.value)) {
    return $el.autocomplete('search', '');
  }
}

full solution is below:

var $el = $('#autocomplete-field');
$el.bind('keydown', function(e) {
  if (e.keyCode === $.ui.keyCode.TAB && $(this).data('autocomplete').menu.active) {
    e.preventDefault();
  }
}).autocomplete({
  delay: 0,
  autoFocus: true,
  minLength: 0,
  source: function(req, resp) {
    resp($.ui.autocomplete.filter(source, extractLast(req.term)));
  },
  focus: function() {
    return false;
  },
  select: function(e, ui) {
    var terms;
    terms = regexSplit(this.value);
    terms.pop();
    terms.push(ui.item.value);
    terms.push('');
    this.value = terms.join(', ');
    return false;
  },
  close: function(e) {
    if ($el.is(':focus') && e.keyCode !== $.ui.keyCode.ESCAPE && (this.value != null) && /[,\s]+$/.test(this.value)) {
      return $el.autocomplete('search', '');
    }
  }
}).focus(function() {
  $el.autocomplete('search', '');
});
ralfthewise
  • 543
  • 7
  • 10
0

Fast, only css and a little hacky solution is:

...
close: function() {
   $('.ui-menu').css('display', 'block'); //or using #ui-id-x where x is number of autocomplete
}
...
Tukkan
  • 1,574
  • 2
  • 18
  • 33
0

You can handle the autocomplete close event http://jqueryui.com/demos/autocomplete/#multiple

Code examples

Supply a callback function to handle the close event as an init option.

$( ".selector" ).autocomplete({
   close: function(event, ui) { ... }
});

Bind to the close event by type: autocompleteclose.

$( ".selector" ).bind( "autocompleteclose", function(event, ui) {
  ...
});
shaans
  • 534
  • 2
  • 5
  • Thank you. I had found that in the documentation on jquery-ui's site. However, I don't know what to do inside of the close handler functions to stop the options from closing. I tried this: – Rusty Oct 13 '10 at 17:23
  • 3
    $( ".selector" ).bind( "autocompleteclose", function(event, ui) { event.preventDefault(); // DOES NOT WORK }); – Rusty Oct 13 '10 at 17:23
  • 1
    It appears as though this event is telling me the options have already closed. I need some way to interrupt and stop this from happening. – Rusty Oct 13 '10 at 17:24
  • I was just about to code this entire thing by scratch and you saved my life. thanks! – Trip Mar 09 '12 at 20:30
0

i know this is old Question , But it still helpful,

Below is the solution for lattest version of jquery UI.

$.ui.autocomplete.prototype._close = function(e){
        return false;    
};

OP can modify this code according to his need.

chhameed
  • 4,406
  • 4
  • 26
  • 44
0

select: function(event,ui)

if (event.keyCode == 13) {
    document.getElementById("#idofautocomplete").value = document.getElementById("#idofautocomplete").value;
 } else if (event.keyCode == 27) {
        $("#idofautocomplete").autocomplete("close");                                                                                   }

close: function(event,ui)

document.getElementById("idofautocomplete").value = "";

looks silly but it works for me. user searches for items, selects item with enter key, autocomplete is still open, user can select more options, at any time user can hit ESC button, close autocomplete, close function clears text from autocomplete. i get all my information from a database

B Nasty
  • 61
  • 1
  • 2
0

I am integrating a bootstrap jquery ui project, and I have done the opening and closing principles here.

$.widget('ui.' + internalComplete, $.ui.autocomplete, {
    options: {
        cancelClose: function(event, ui) {
            return false;
        }
    },
    close: function( event ) {
        if (!this._trigger('cancelClose', event, { ui: this.menu.element })) {
            this._super( event );
        }
    }
});

The second step is to choose to close according to the conditions

        this.uiEl = this.element[internalComplete](
            {
                minLength: 0,
                focus: opts.focus,
                select: opts.select,
                cancelClose: function(event, ui) {
                    var hasMenuItem = _applyOriginalEvent(event, function(e) {
                        return $(e.target).hasClass('ui-menu-item') || null;
                    });
                    return hasMenuItem === true;
                }
            }
        )[internalComplete]('instance');
0

/** * Created by GaoZuo on 2019/12/25. */

(function($) {

    var internalComplete = 'card-internalAutocomplete';
    var _uuid = 0;

    function _applyOriginalEvent(event, fn) {
        var ret = fn(event);
        if (ret == null) {
            var oe = event.originalEvent;
            if (oe != null) {
                return _applyOriginalEvent(oe, fn);
            }
        }
        return ret;
    }

    $.widget('ui.' + internalComplete, $.ui.autocomplete, {
        options: {
            cancelClose: function(event, ui) {
                return false;
            }
        },
        close: function( event ) {
            if (!this._trigger('cancelClose', event, { ui: this.menu.element })) {
                this._super( event );
            }
        }
    });

    $.widget('ui.card-autocomplete', {
        options: {
            source: null,
            link: ' a.card-link',
            /* source: [
                {
                    title: 'title',
                    subtitle: 'Card subtitle',
                    desc: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
                    'group-prefix': 'js',
                    groups: [
                        {
                            value: "sizzlejs",
                            label: "Sizzle JS",
                            desc: "a pure-JavaScript CSS selector engine",
                            icon: "sizzlejs_32x32.png"
                        }
                    ]
                }
            ],*/
            focus: $.noop,
            select: function(event, ui) {
                return false;
            }
        },
        _create: function() {
            var that = this,
                opts = this.options;

            this.uiEl = this.element[internalComplete](
                {
                    minLength: 0,
                    focus: opts.focus,
                    select: opts.select,
                    cancelClose: function(event, ui) {
                        var hasMenuItem = _applyOriginalEvent(event, function(e) {
                            return $(e.target).hasClass('ui-menu-item') || null;
                        });
                        return hasMenuItem === true;
                    }
                }
            )[internalComplete]('instance');

            this._on(this.element, {
                focus: function(event) {
                    this.uiEl.search(null, event);
                }
            });

            $.extend(this.uiEl, {
                _renderMenu: function(ui, items) {
                    $.each(opts.source, function(i, n) {
                        that._createCards($('<li>').appendTo(ui), n, items);
                    });
                }
            });

            this.uiEl.menu.option('items', this.options.link);
            this._initSource();
        },

        _initSource: function() {
            var data = this.options.source;
            if ($.isArray(data)) {
                this.setSource(this._dataToSource(data));
            }
        },

        _setOption: function(key, value) {
            this._super( key, value );
            if ( key = 'source') {
                this._initSource();
            }
        },

        setSource: function(array) {
            this.uiEl.option('source', array);
        },

        _dataToSource: function(data) {
            var array = [];
            var that = this;
            $.each(data, function(i, n) {
                that._initData(array, n);
            });
            return array;
        },

        _initData: function(array, group) {
            if ($.isArray(group.groups)) {
                var prefix = group.prefix =
                    ( group.prefix || $.ui['card-autocomplete'].getPrefix( true ) );
                $.each(group.groups, function(i, n) {
                    n._prefix = prefix;
                    array.push(n);
                });
                delete group.groups;
            }
        },

        _createCards: function(li, n, list) {
            var body = $('<div>').addClass('card-body');
            li.append( $('<div>').addClass('card').append(body) );
            if (n.title != null) {
                body.append( $('<h5>').addClass('card-title').text(n.title) );
            }
            if (n.subtitle != null) {
                body.append( $('<h6>').addClass('card-subtitle mb-2 text-muted').text(n.subtitle) );
            }
            if (n.desc != null) {
                body.append( $('<p>').addClass('card-text').text(n.desc) );
            }
            this._createList(n, list, body);
        },

        _createList: function(n, list, body) {
            var that = this;
            $.each(list, function(i, li) {
                if (li._prefix == n.prefix) {
                    that._renderLi(li, body);
                }
            });
        },
        _renderLi: function(li, body) {
            $.ui['card-autocomplete'].createLi(li)
                .appendTo( body )
                .data( "ui-autocomplete-item", li );
        }
    });

    $.extend($.ui['card-autocomplete'], {
        createLi: function(li) {
            var link = $('<a>').addClass('card-link').prop('href', '#').text(li.label);
            if (li.desc != null) {
                link.prop('title', li.desc)
            }
            if (li.value != null) {
                link.attr('data-id', li.value);
            }
            return link;
        },
        getPrefix: function(next) {
            if (next) {
                _uuid++;
            }
            return "-" + _uuid;
        }
    });
})(jQuery);

******************************html*********************************************

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>jQuery UI Autocomplete - Custom data and display</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <link rel="stylesheet" href="../../../themes/base/jquery.ui.all.css">
    <!--<link rel="stylesheet" href="../../demos.css">-->


    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script src="jquery-ui.js"></script>
    <script src="jquery.ui.cards.autocomplete.js"></script>
    <!--<script src="../../../ui/jquery.ui.core.js"></script>
    <script src="../../../ui/jquery.ui.widget.js"></script>
    <script src="../../../ui/jquery.ui.position.js"></script>
    <script src="../../../ui/jquery.ui.menu.js"></script>
    <script src="../../../ui/jquery.ui.autocomplete.js"></script>-->
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    <style>
        #project-label {
            display: block;
            font-weight: bold;
            margin-bottom: 1em;
        }
        #project-icon {
            float: left;
            height: 32px;
            width: 32px;
        }
        #project-description {
            margin: 0;
            padding: 0;
        }
    </style>
    <script>
        $(function() {

            var projects = [
                {
                    title: 'Card title',
                    subtitle: 'Card subtitle',
                    desc: 'Some quick example text to build on the card title and make up the bulk of the cards content.',
                    groups: [
                        {
                            value: "jquery",
                            label: "jQuery",
                            desc: "the write less, do more, JavaScript library",
                            icon: "jquery_32x32.png"
                        },
                        {
                            value: "jquery-ui",
                            label: "jQuery UI",
                            desc: "the official user interface library for jQuery",
                            icon: "jqueryui_32x32.png"
                        }
                    ]
                },
                {
                    title: '第二个分组',
                    subtitle: '第二个分组.....',
                    desc: '第二个分组的描述....',
                    groups: [
                        {
                            value: "sizzlejs",
                            label: "Sizzle JS",
                            desc: "a pure-JavaScript CSS selector engine",
                            icon: "sizzlejs_32x32.png"
                        }
                    ]
                }
            ];

            var ele = $( "#project" )['card-autocomplete']({
                minLength: 0,
                source: projects,
                focus: function( event, ui ) {
                    return false;
                },
                select: function( event, ui ) {
                    return false;
                }
            });


        });
    </script>
</head>
<body>

<div id="project-label">Select a project (type "j" for a start):</div>
<img id="project-icon" src="../images/transparent_1x1.png" class="ui-state-default" alt="">
<input id="project" class="w-50">
<input type="hidden" id="project-id">
<p id="project-description"></p>

<div class="demo-description">
    <p>You can use your own custom data formats and displays by simply overriding the default focus and select actions.</p>
    <p>Try typing "j" to get a list of projects or just press the down arrow.</p>
</div>










</body>
</html>
0

I think this is the best way:

$(yourselector).autocomplete({
        minLength: 0,
        //...
        close: function (e) {
            if('originalEvent' in e && e.originalEvent.type === 'menuselect') {
                $(this).autocomplete('search');
            }
        }
        //...
    })

You just need to check if the initial event was related to 'menuselect' event and if so, instead of closing menu, refresh it's results.

0

The following code seems to be working in my case:

$('#search_product').autocomplete({
    ...
    ...
    ...
    select: function(event, ui) {
        // Keep the menu opened after selection
        $(this).open();
    }
    ...
    ...
    ...
});