diff options
Diffstat (limited to 'ui/jquery.ui.autocomplete.js')
-rw-r--r-- | ui/jquery.ui.autocomplete.js | 211 |
1 files changed, 157 insertions, 54 deletions
diff --git a/ui/jquery.ui.autocomplete.js b/ui/jquery.ui.autocomplete.js index bd55c1ed4..f48dc032a 100644 --- a/ui/jquery.ui.autocomplete.js +++ b/ui/jquery.ui.autocomplete.js @@ -1,9 +1,9 @@ /* * jQuery UI Autocomplete @VERSION * - * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license * * http://docs.jquery.com/UI/Autocomplete * @@ -13,16 +13,28 @@ * jquery.ui.position.js * jquery.ui.menu.js */ -(function( $ ) { +(function( $, undefined ) { $.widget( "ui.autocomplete", { options: { + appendTo: "body", + delay: 300, minLength: 1, - delay: 300 + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + source: null }, + + pending: 0, + _create: function() { var self = this, - doc = this.element[ 0 ].ownerDocument; + doc = this.element[ 0 ].ownerDocument, + suppressKeyPress; + this.element .addClass( "ui-autocomplete-input" ) .attr( "autocomplete", "off" ) @@ -33,6 +45,11 @@ $.widget( "ui.autocomplete", { "aria-haspopup": "true" }) .bind( "keydown.autocomplete", function( event ) { + if ( self.options.disabled || self.element.attr( "readonly" ) ) { + return; + } + + suppressKeyPress = false; var keyCode = $.ui.keyCode; switch( event.keyCode ) { case keyCode.PAGE_UP: @@ -52,8 +69,12 @@ $.widget( "ui.autocomplete", { event.preventDefault(); break; case keyCode.ENTER: - // when menu is open or has focus + case keyCode.NUMPAD_ENTER: + // when menu is open and has focus if ( self.menu.active ) { + // #6055 - Opera still allows the keypress to occur + // which causes forms to submit + suppressKeyPress = true; event.preventDefault(); } //passthrough - ENTER and TAB both select the current element @@ -67,30 +88,40 @@ $.widget( "ui.autocomplete", { self.element.val( self.term ); self.close( event ); break; - case keyCode.LEFT: - case keyCode.RIGHT: - case keyCode.SHIFT: - case keyCode.CONTROL: - case keyCode.ALT: - // ignore metakeys (shift, ctrl, alt) - break; default: // keypress is triggered before the input value is changed clearTimeout( self.searching ); self.searching = setTimeout(function() { - self.search( null, event ); + // only search if the value has changed + if ( self.term != self.element.val() ) { + self.selectedItem = null; + self.search( null, event ); + } }, self.options.delay ); break; } }) + .bind( "keypress.autocomplete", function( event ) { + if ( suppressKeyPress ) { + suppressKeyPress = false; + event.preventDefault(); + } + }) .bind( "focus.autocomplete", function() { + if ( self.options.disabled ) { + return; + } + self.selectedItem = null; self.previous = self.element.val(); }) .bind( "blur.autocomplete", function( event ) { + if ( self.options.disabled ) { + return; + } + clearTimeout( self.searching ); // clicks on the menu (or a button to trigger a search) will cause a blur event - // TODO try to implement this without a timeout, see clearTimeout in search() self.closing = setTimeout(function() { self.close( event ); self._change( event ); @@ -102,13 +133,37 @@ $.widget( "ui.autocomplete", { }; this.menu = $( "<ul></ul>" ) .addClass( "ui-autocomplete" ) - .appendTo( "body", doc ) + .appendTo( $( this.options.appendTo || "body", doc )[0] ) + // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown) + .mousedown(function( event ) { + // clicking on the scrollbar causes focus to shift to the body + // but we can't detect a mouseup or a click immediately afterward + // so we have to track the next mousedown and close the menu if + // the user clicks somewhere outside of the autocomplete + var menuElement = self.menu.element[ 0 ]; + if ( !$( event.target ).closest( ".ui-menu-item" ).length ) { + setTimeout(function() { + $( document ).one( 'mousedown', function( event ) { + if ( event.target !== self.element[ 0 ] && + event.target !== menuElement && + !$.contains( menuElement, event.target ) ) { + self.close(); + } + }); + }, 1 ); + } + + // use another timeout to make sure the blur-event-handler on the input was already triggered + setTimeout(function() { + clearTimeout( self.closing ); + }, 13); + }) .menu({ // custom key handling for now input: $(), focus: function( event, ui ) { var item = ui.item.data( "item.autocomplete" ); - if ( false !== self._trigger( "focus", null, { item: item } ) ) { + if ( false !== self._trigger( "focus", event, { item: item } ) ) { // use value to match what will end up in the input, if it was a key event if ( /^key/.test(event.originalEvent.type) ) { self.element.val( item.value ); @@ -116,28 +171,42 @@ $.widget( "ui.autocomplete", { } }, select: function( event, ui ) { - var item = ui.item.data( "item.autocomplete" ); - if ( false !== self._trigger( "select", event, { item: item } ) ) { - self.element.val( item.value ); - } - self.close( event ); + var item = ui.item.data( "item.autocomplete" ), + previous = self.previous; + // only trigger when focus was lost (click on menu) - var previous = self.previous; if ( self.element[0] !== doc.activeElement ) { self.element.focus(); self.previous = previous; + // #6109 - IE triggers two focus events and the second + // is asynchronous, so we need to reset the previous + // term synchronously and asynchronously :-( + setTimeout(function() { + self.previous = previous; + self.selectedItem = item; + }, 1); } + + 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; }, blur: function( event, ui ) { - if ( self.menu.element.is(":visible") ) { + // don't set the value of the text field if it's already correct + // this prevents moving the cursor unnecessarily + if ( self.menu.element.is(":visible") && + ( self.element.val() !== self.term ) ) { self.element.val( self.term ); } } }) .zIndex( this.element.zIndex() + 1 ) - // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781 - .css({ top: 0, left: 0 }) .hide() .data( "menu" ); if ( $.fn.bgiframe ) { @@ -156,15 +225,22 @@ $.widget( "ui.autocomplete", { $.Widget.prototype.destroy.call( this ); }, - _setOption: function( key ) { + _setOption: function( key, value ) { $.Widget.prototype._setOption.apply( this, arguments ); if ( key === "source" ) { this._initSource(); } + if ( key === "appendTo" ) { + this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] ) + } + if ( key === "disabled" && value && this.xhr ) { + this.xhr.abort(); + } }, _initSource: function() { - var array, + var self = this, + array, url; if ( $.isArray(this.options.source) ) { array = this.options.source; @@ -174,7 +250,26 @@ $.widget( "ui.autocomplete", { } else if ( typeof this.options.source === "string" ) { url = this.options.source; this.source = function( request, response ) { - $.getJSON( url, request, response ); + if ( self.xhr ) { + self.xhr.abort(); + } + self.xhr = $.ajax({ + url: url, + data: request, + dataType: "json", + success: function( data, status, xhr ) { + if ( xhr === self.xhr ) { + response( data ); + } + self.xhr = null; + }, + error: function( xhr ) { + if ( xhr === self.xhr ) { + response( [] ); + } + self.xhr = null; + } + }); }; } else { this.source = this.options.source; @@ -183,12 +278,16 @@ $.widget( "ui.autocomplete", { search: function( value, event ) { value = value != null ? value : this.element.val(); + + // always save the actual value, not the one passed as an argument + this.term = this.element.val(); + if ( value.length < this.options.minLength ) { return this.close( event ); } clearTimeout( this.closing ); - if ( this._trigger("search") === false ) { + if ( this._trigger( "search", event ) === false ) { return; } @@ -196,31 +295,32 @@ $.widget( "ui.autocomplete", { }, _search: function( value ) { - this.term = this.element - .addClass( "ui-autocomplete-loading" ) - // always save the actual value, not the one passed as an argument - .val(); + this.pending++; + this.element.addClass( "ui-autocomplete-loading" ); this.source( { term: value }, this.response ); }, _response: function( content ) { - if ( content.length ) { + if ( !this.options.disabled && content && content.length ) { content = this._normalize( content ); this._suggest( content ); this._trigger( "open" ); } else { this.close(); } - this.element.removeClass( "ui-autocomplete-loading" ); + this.pending--; + if ( !this.pending ) { + this.element.removeClass( "ui-autocomplete-loading" ); + } }, close: function( event ) { clearTimeout( this.closing ); if ( this.menu.element.is(":visible") ) { - this._trigger( "close", event ); this.menu.element.hide(); this.menu.deactivate(); + this._trigger( "close", event ); } }, @@ -251,26 +351,29 @@ $.widget( "ui.autocomplete", { _suggest: function( items ) { var ul = this.menu.element - .empty() - .zIndex( this.element.zIndex() + 1 ), - menuWidth, - textWidth; + .empty() + .zIndex( this.element.zIndex() + 1 ); this._renderMenu( ul, items ); // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate this.menu.deactivate(); this.menu.refresh(); - this.menu.element.show().position({ - my: "left top", - at: "left bottom", - of: this.element, - collision: "none" - }); - menuWidth = ul.width( "" ).width(); - textWidth = this.element.width(); - ul.width( Math.max( menuWidth, textWidth ) ); + // size and position menu + ul.show(); + this._resizeMenu(); + ul.position( $.extend({ + of: this.element + }, this.options.position )); }, - + + _resizeMenu: function() { + var ul = this.menu.element; + ul.outerWidth( Math.max( + ul.width( "" ).outerWidth(), + this.element.outerWidth() + ) ); + }, + _renderMenu: function( ul, items ) { var self = this; $.each( items, function( index, item ) { @@ -281,7 +384,7 @@ $.widget( "ui.autocomplete", { _renderItem: function( ul, item) { return $( "<li></li>" ) .data( "item.autocomplete", item ) - .append( "<a>" + item.label + "</a>" ) + .append( $( "<a></a>" ).text( item.label ) ) .appendTo( ul ); }, @@ -306,7 +409,7 @@ $.widget( "ui.autocomplete", { $.extend( $.ui.autocomplete, { escapeRegex: function( value ) { - return value.replace( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1" ); + return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }, filter: function(array, term) { var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); |