开发者

How to implement "mustMatch" and "selectFirst" in jQuery UI Autocomplete?

I recently migrated a few of my Autocomplete plugins from th开发者_StackOverflow中文版e one produced by bassistance to the jQuery UI autocomplete.

How can the "mustMatch" and "selectFirst" be implemented with just callbacks and other options without modifying the core autocomplete code itself?


I think I solved both features...

To make things easier, I used a common custom selector:

$.expr[':'].textEquals = function (a, i, m) {
    return $(a).text().match("^" + m[3] + "$");
};

The rest of the code:

$(function () {
    $("#tags").autocomplete({
        source: '/get_my_data/',
        change: function (event, ui) {
            //if the value of the textbox does not match a suggestion, clear its value
            if ($(".ui-autocomplete li:textEquals('" + $(this).val() + "')").size() == 0) {
                $(this).val('');
            }
        }
    }).live('keydown', function (e) {
        var keyCode = e.keyCode || e.which;
        //if TAB or RETURN is pressed and the text in the textbox does not match a suggestion, set the value of the textbox to the text of the first suggestion
        if((keyCode == 9 || keyCode == 13) && ($(".ui-autocomplete li:textEquals('" + $(this).val() + "')").size() == 0)) {
            $(this).val($(".ui-autocomplete li:visible:first").text());
        }
    });
});

If any of your autocomplete suggestions contain any 'special' characters used by regexp, you must escape those characters within m[3] in the custom selector:

function escape_regexp(text) {
  return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

and change the custom selector:

$.expr[':'].textEquals = function (a, i, m) {
  return $(a).text().match("^" + escape_regexp(m[3]) + "$");
};


I used something as simple as this for mustMatch and it works. I hope it helps someone.

        change: function (event, ui) {
            if (!ui.item) {
                 $(this).val('');
             }
        }


I think I got the mustMatch working with this code... It needs thorough test though:

<script type="text/javascript">
    $(function() {
        $("#my_input_id").autocomplete({
            source: '/get_my_data/',
            minChars: 3,
            change: function(event, ui) {
                // provide must match checking if what is in the input
                // is in the list of results. HACK!
                var source = $(this).val();
                var found = $('.ui-autocomplete li').text().search(source);
                console.debug('found:' + found);
                if(found < 0) {
                    $(this).val('');
                }
            }
        });
    });
</script>


I found this question to be useful.

I thought I'd post up the code I'm now using (adapted from Esteban Feldman's answer).

I've added my own mustMatch option, and a CSS class to highlight the issue before resetting the textbox value.

       change: function (event, ui) {
          if (options.mustMatch) {
            var found = $('.ui-autocomplete li').text().search($(this).val());

            if (found < 0) {
              $(this).addClass('ui-autocomplete-nomatch').val('');
              $(this).delay(1500).removeClass('ui-autocomplete-nomatch', 500);
            }
          }
        }

CSS

.ui-autocomplete-nomatch { background: white url('../Images/AutocompleteError.gif') right center no-repeat; }


The solution I've used to implement 'mustMatch':

<script type="text/javascript">
...

$('#recipient_name').autocomplete({
    source: friends,
    change: function (event, ui) {
        if ($('#message_recipient_id').attr('rel') != $(this).val()) {
            $(this).val('');
            $('#message_recipient_id').val('');
            $('#message_recipient_id').attr('rel', '');
        }
    },
    select: function(event, ui) {
        $('#message_recipient_id').val(ui.item.user_id);
        $('#message_recipient_id').attr('rel', ui.item.label);
    }
}); 

...
</script>


I discovered one issue. While the suggestion list is active you can submit your form even if the value doesn't match the suggestion. To dissallow this I added:

$('form').submit(function() {
        if ($(".ui-autocomplete li:textEquals('" + $(this).val() + "')").size() == 0) {
            $(this).val('');
            $("span").text("Select a valid city").show();
            return false;
        }
});

This prevents the form from being submitted and displays a message.


This JQuery-UI official demo has mustMatch, amongst other cool stuff: http://jqueryui.com/demos/autocomplete/#combobox

I've updated it to add autoFill, and a few other things.

Javascript:



/* stolen from http://jqueryui.com/demos/autocomplete/#combobox
 *
 * and these options added.
 *
 * - autoFill (default: true):  select first value rather than clearing if there's a match
 *
 * - clearButton (default: true): add a "clear" button
 *
 * - adjustWidth (default: true): if true, will set the autocomplete width the same as
 *    the old select.  (requires jQuery 1.4.4 to work on IE8)
 *
 * - uiStyle (default: false): if true, will add classes so that the autocomplete input
 *    takes a jQuery-UI style
 */
(function( $ ) {
    $.widget( "ui.combobox", {
        options: {
            autoFill: true,
            clearButton: true,
            adjustWidth: true,
            uiStyle: false,
            selected: null,
        },
    _create: function() {
        var self = this,
          select = this.element.hide(),
          selected = select.children( ":selected" ),
          value = selected.val() ? selected.text() : "",
              found = false;
        var input = this.input = $( "" )
                .attr('title', '' + select.attr("title") + '')
        .insertAfter( select )
        .val( value )
        .autocomplete({
            delay: 0,
            minLength: 0,
            source: function( request, response ) {
                var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
                        var resp = select.children( "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"
                        ), "$1" ),
                        value: text,
                        option: this
                    };
                });
                        found = resp.length > 0;
                response( resp );
            },
            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 || input.data("autocomplete").term=="" ) {
                    // set to first suggestion, unless blank or autoFill is turned off
                                var suggestion;
                                if(!self.options.autoFill || input.data("autocomplete").term=="") found=false;
                                if(found) {
                                    suggestion = jQuery(input.data("autocomplete").widget()).find("li:first")[0];
                                    var option = select.find("option[text="+suggestion.innerText+"]").attr('selected', true);
                                    $(this).val(suggestion.innerText);
                        input.data("autocomplete").term = suggestion.innerText;
                            self._trigger( "selected", event, { item: option[0] });
                                } else {
                                    suggestion={innerText: ''};
                                    select.find("option:selected").removeAttr("selected");
                                    $(this).val('');
                        input.data( "autocomplete" ).term = '';
                                    self._trigger( "selected", event, { item: null });
                                }
                    return found;
                    }
                }
            }
        });

            if( self.options.adjustWidth ) { input.width(select.width()); }

            if( self.options.uiStyle ) {
                input.addClass( "ui-widget ui-widget-content ui-corner-left" );
            }


        input.data( "autocomplete" )._renderItem = function( ul, item ) {
            return $( "
  • " ) .data( "item.autocomplete", item ) .append( "" + item.label + "" ) .appendTo( ul ); }; this.button = $( " " ) .attr( "tabIndex", -1 ) .attr( "title", "Show All Items" ) .insertAfter( input ) .button({ icons: { primary: "ui-icon-triangle-1-s" }, text: false }) .removeClass( "ui-corner-all" ) .addClass( "ui-corner-right ui-button-icon" ) .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(); }); if( self.options.clearButton ) { this.clear_button = $( " " ) .attr( "tabIndex", -1 ) .attr( "title", "Clear Entry" ) .insertAfter( input ) .button({ icons: { primary: "ui-icon-close" }, text: false }) .removeClass( "ui-corner-all" ) .click(function(event, ui) { select.find("option:selected").removeAttr("selected"); input.val( "" ); input.data( "autocomplete" ).term = ""; self._trigger( "selected", event, { item: null }); // work around a bug (likely same cause as #5265) $( this ).blur(); }); } }, destroy: function() { this.input.remove(); this.button.remove(); this.element.show(); $.Widget.prototype.destroy.call( this ); } }); })( jQuery );

    CSS (.hjq-combobox is a wrapping span)

    .hjq-combobox .ui-button { margin-left: -1px; }
    .hjq-combobox .ui-button-icon-only .ui-button-text { padding: 0; }
    .hjq-combobox button.ui-button-icon-only { width: 20px; }
    .hjq-combobox .ui-autocomplete-input { margin-right: 0; }
    .hjq-combobox {white-space: nowrap;}
    

    Note: this code is being updated and maintained here: https://github.com/tablatom/hobo/blob/master/hobo_jquery_ui/vendor/assets/javascripts/combobox.js


    Maybe it's just because this is an old issue, but I found that the easiest solution is already there in the plugin, you just need to use the proper functions to access it.

    This code will handle the cases when the autocomplete loses focus with an invalid value:

    change: function(e, ui) {
        if (!ui.item) {
            $(this).val("");
        }
    }
    

    And this code, much like the original functionality from bassistance, will handle the cases when there are no matches while typing in the autocomplete:

    response: function(e, ui) {
        if (ui.content.length == 0) {
            $(this).val("");
        }
    }
    

    This works well with either a static array source, or a JSON data source. Combined with the autoFocus: true option, it seems to do everything needed in an efficient manner.

    The last case that you may want to handle is what to do when the ESCAPE key is pressed with an invalid value in the textbox. What I do is use the value of the first matched result. And this is how I do that...

    First, declare a variable to hold the best match. Do this outside of your autocomplete plugin.

    var bestMatch = "";
    

    Then use the following option:

    open: function(e, ui) {
        bestMatch = "";
    
        var acData = $(this).data('uiAutocomplete');
        acData.menu.element.find("A").each(function () {
            var me = $(this);
    
            if (me.parent().index() == 0) {
                bestMatch = me.text();
            }
        });
    }
    

    Lastly, add the following event to your autocomplete:

    .on("keydown", function(e) {
        if (e.keyCode == 27)        // ESCAPE key
        {
            $(this).val(bestMatch);
        }
    })
    

    You can just as easily force the field to be empty when the escape key is pressed. All you have to do is set the value to an empty string when the key is pressed instead of the bestMatch variable (which isn't needed at all if you choose to empty the field).


    I'm doing it a little differently, caching the results and clearing the text field if the number of results for a certain term is zero:

    <script type='text/javascript'>
    function init_autocomplete( args )
    {
         var resultCache = {};
         var currentRequestTerm = null;
    
         var closeCallback = function()
         {
             // Clear text field if current request has no results
             if( resultCache[currentRequestTerm].length == 0 )
                 $(args.selector).val('');
         };
    
         var sourceCallback = function( request, responseCallback )
         {
             // Save request term
             currentRequestTerm = request.term;
    
             // Check for cache hit
             // ...
             // If no cache hit, fetch remote data
             $.post(
                 dataSourceUrl,
                 { ...  }, // post data
                 function( response )
                 {
                     // Store cache
                     resultCache[request.term] = response;
    
                     responseCallback( response );
                 }
         };
    
         $(args.selector).autocomplete({
             close:  closeCallback,
             source: sourceCallback
         });
    }
    </script>
    


    Scott Gonzalez has written a selectFirst extension (as well as several others) for jQueryUI AutoComplete.

    • Scott Gonzalez GitHub
    • selectFirst Demo


    Based on the accepted answer:

    My additional requirements: multiple autocompletes, unobtrusive error validation.

    change: function () {
        var target = $(this),
            widget = target.autocomplete('widget'),
            cond = widget.find('li:textEquals("' + target.val() + '")').length === 0;
    
        target.toggleClass('input-validation-error', cond);
    }
    


    Late reply but might help someone!

    Considering the two events in autocomplete widget

    1) change - triggered when field is blurred and value is changed.

    2) response - triggered when the search completes and the menu is shown.

    Modify the change and response events as follows:

    change : function(event,ui)
    {  
    if(!ui.item){
    $("selector").val("");
    }
    },
    
    response : function(event,ui){
    if(ui.content.length==0){
      $("selector").val("");
    }
    }
    

    Hope this helps!


    Here a simple definitive solution for "mustMatch" requirement:

    <script type="text/javascript">
        $(function() {
            $("#my_input_id").autocomplete({
                source: '/get_my_data/',
                minChars: 3,
                select: function(event, ui) {
                    // custom code
                    $(this).data("pre-ui-autocomplete-value", $(this).val());
                }
            }).on("focus", function () {
                $(this).data("pre-ui-autocomplete-value", $(this).val());
            }).on("blur", function () {
                $(this).val($(this).data("pre-ui-autocomplete-value"));
            });
        });
    </script>
    
    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新问答

    问答排行榜