Merge "Fix regression in API log events (bug 35635)"
[lhc/web/wiklou.git] / resources / jquery.ui / jquery.ui.autocomplete.js
index 9a12a6b..f8009c6 100644 (file)
@@ -1,9 +1,9 @@
-/*
- * jQuery UI Autocomplete 1.8.2
+/*!
+ * jQuery UI Autocomplete 1.8.21
  *
- * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * and GPL (GPL-LICENSE.txt) licenses.
+ * Copyright 2012, 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
  *
  *     jquery.ui.widget.js
  *     jquery.ui.position.js
  */
-(function( $ ) {
+(function( $, undefined ) {
+
+// used to prevent race conditions with remote data sources
+var requestIndex = 0;
 
 $.widget( "ui.autocomplete", {
        options: {
+               appendTo: "body",
+               autoFocus: false,
+               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.isMultiLine = this.element.is( "textarea" );
+
                this.element
                        .addClass( "ui-autocomplete-input" )
                        .attr( "autocomplete", "off" )
@@ -32,6 +49,11 @@ $.widget( "ui.autocomplete", {
                                "aria-haspopup": "true"
                        })
                        .bind( "keydown.autocomplete", function( event ) {
+                               if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
+                                       return;
+                               }
+
+                               suppressKeyPress = false;
                                var keyCode = $.ui.keyCode;
                                switch( event.keyCode ) {
                                case keyCode.PAGE_UP:
@@ -41,19 +63,18 @@ $.widget( "ui.autocomplete", {
                                        self._move( "nextPage", event );
                                        break;
                                case keyCode.UP:
-                                       self._move( "previous", event );
-                                       // prevent moving cursor to beginning of text field in some browsers
-                                       event.preventDefault();
+                                       self._keyEvent( "previous", event );
                                        break;
                                case keyCode.DOWN:
-                                       self._move( "next", event );
-                                       // prevent moving cursor to end of text field in some browsers
-                                       event.preventDefault();
+                                       self._keyEvent( "next", event );
                                        break;
                                case keyCode.ENTER:
                                case keyCode.NUMPAD_ENTER:
-                                       // when menu is open or has focus
+                                       // 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,33 +88,38 @@ $.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:
-                               case keyCode.COMMAND:
-                               case keyCode.COMMAND_RIGHT:
-                               case keyCode.INSERT:
-                               case keyCode.CAPS_LOCK:
-                               case keyCode.END:
-                               case keyCode.HOME:
-                                       // 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
                                self.closing = setTimeout(function() {
@@ -102,14 +128,28 @@ $.widget( "ui.autocomplete", {
                                }, 150 );
                        });
                this._initSource();
-               this.response = function() {
-                       return self._response.apply( self, arguments );
-               };
                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() {
+                       .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 &&
+                                                               !$.ui.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 );
@@ -118,7 +158,7 @@ $.widget( "ui.autocomplete", {
                        .menu({
                                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 );
@@ -126,21 +166,37 @@ $.widget( "ui.autocomplete", {
                                        }
                                },
                                selected: 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 );
                                        }
                                }
@@ -153,6 +209,13 @@ $.widget( "ui.autocomplete", {
                if ( $.fn.bgiframe ) {
                         this.menu.element.bgiframe();
                }
+               // turning off autocomplete prevents the browser from remembering the
+               // value when navigating through history, so we re-enable autocomplete
+               // if the page is unloaded before the widget is destroyed. #7790
+               self.beforeunloadHandler = function() {
+                       self.element.removeAttr( "autocomplete" );
+               };
+               $( window ).bind( "beforeunload", self.beforeunloadHandler );
        },
 
        destroy: function() {
@@ -163,18 +226,26 @@ $.widget( "ui.autocomplete", {
                        .removeAttr( "aria-autocomplete" )
                        .removeAttr( "aria-haspopup" );
                this.menu.element.remove();
+               $( window ).unbind( "beforeunload", this.beforeunloadHandler );
                $.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;
@@ -184,7 +255,20 @@ $.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 ) {
+                                               response( data );
+                                       },
+                                       error: function() {
+                                               response( [] );
+                                       }
+                               });
                        };
                } else {
                        this.source = this.options.source;
@@ -193,12 +277,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;
                }
 
@@ -206,31 +294,44 @@ $.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 );
+               this.source( { term: value }, this._response() );
        },
 
-       _response: function( content ) {
-               if ( content.length ) {
+       _response: function() {
+               var that = this,
+                       index = ++requestIndex;
+
+               return function( content ) {
+                       if ( index === requestIndex ) {
+                               that.__response( content );
+                       }
+
+                       that.pending--;
+                       if ( !that.pending ) {
+                               that.element.removeClass( "ui-autocomplete-loading" );
+                       }
+               };
+       },
+
+       __response: function( content ) {
+               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" );
        },
 
        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 );
                }
        },
        
@@ -261,26 +362,35 @@ $.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 ));
+
+               if ( this.options.autoFocus ) {
+                       this.menu.next( new $.Event("mouseover") );
+               }
        },
-       
+
+       _resizeMenu: function() {
+               var ul = this.menu.element;
+               ul.outerWidth( Math.max(
+                       // Firefox wraps long text (possibly a rounding bug)
+                       // so we add 1px to avoid the wrapping (#7513)
+                       ul.width( "" ).outerWidth() + 1,
+                       this.element.outerWidth()
+               ) );
+       },
+
        _renderMenu: function( ul, items ) {
                var self = this;
                $.each( items, function( index, item ) {
@@ -291,7 +401,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 );
        },
 
@@ -311,12 +421,20 @@ $.widget( "ui.autocomplete", {
 
        widget: function() {
                return this.menu.element;
+       },
+       _keyEvent: function( keyEvent, event ) {
+               if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
+                       this._move( keyEvent, event );
+
+                       // prevents moving cursor to beginning/end of the text field in some browsers
+                       event.preventDefault();
+               }
        }
 });
 
 $.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" );
@@ -335,9 +453,9 @@ $.extend( $.ui.autocomplete, {
  * it for the next release. You're welcome to give it a try anyway and give us feedback,
  * as long as you're okay with migrating your code later on. We can help with that, too.
  *
- * 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/Menu
  *
@@ -391,12 +509,12 @@ $.widget("ui.menu", {
                this.deactivate();
                if (this.hasScroll()) {
                        var offset = item.offset().top - this.element.offset().top,
-                               scroll = this.element.attr("scrollTop"),
+                               scroll = this.element.scrollTop(),
                                elementHeight = this.element.height();
                        if (offset < 0) {
-                               this.element.attr("scrollTop", scroll + offset);
-                       } else if (offset > elementHeight) {
-                               this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
+                               this.element.scrollTop( scroll + offset);
+                       } else if (offset >= elementHeight) {
+                               this.element.scrollTop( scroll + offset - elementHeight + item.height());
                        }
                }
                this.active = item.eq(0)
@@ -426,11 +544,11 @@ $.widget("ui.menu", {
        },
 
        first: function() {
-               return this.active && !this.active.prev().length;
+               return this.active && !this.active.prevAll(".ui-menu-item").length;
        },
 
        last: function() {
-               return this.active && !this.active.next().length;
+               return this.active && !this.active.nextAll(".ui-menu-item").length;
        },
 
        move: function(direction, edge, event) {
@@ -451,12 +569,12 @@ $.widget("ui.menu", {
                if (this.hasScroll()) {
                        // TODO merge with no-scroll-else
                        if (!this.active || this.last()) {
-                               this.activate(event, this.element.children(":first"));
+                               this.activate(event, this.element.children(".ui-menu-item:first"));
                                return;
                        }
                        var base = this.active.offset().top,
                                height = this.element.height(),
-                               result = this.element.children("li").filter(function() {
+                               result = this.element.children(".ui-menu-item").filter(function() {
                                        var close = $(this).offset().top - base - height + $(this).height();
                                        // TODO improve approximation
                                        return close < 10 && close > -10;
@@ -464,11 +582,12 @@ $.widget("ui.menu", {
 
                        // TODO try to catch this earlier when scrollTop indicates the last page anyway
                        if (!result.length) {
-                               result = this.element.children(":last");
+                               result = this.element.children(".ui-menu-item:last");
                        }
                        this.activate(event, result);
                } else {
-                       this.activate(event, this.element.children(!this.active || this.last() ? ":first" : ":last"));
+                       this.activate(event, this.element.children(".ui-menu-item")
+                               .filter(!this.active || this.last() ? ":first" : ":last"));
                }
        },
 
@@ -477,13 +596,13 @@ $.widget("ui.menu", {
                if (this.hasScroll()) {
                        // TODO merge with no-scroll-else
                        if (!this.active || this.first()) {
-                               this.activate(event, this.element.children(":last"));
+                               this.activate(event, this.element.children(".ui-menu-item:last"));
                                return;
                        }
 
                        var base = this.active.offset().top,
-                               height = this.element.height();
-                               result = this.element.children("li").filter(function() {
+                               height = this.element.height(),
+                               result = this.element.children(".ui-menu-item").filter(function() {
                                        var close = $(this).offset().top - base + height - $(this).height();
                                        // TODO improve approximation
                                        return close < 10 && close > -10;
@@ -491,16 +610,17 @@ $.widget("ui.menu", {
 
                        // TODO try to catch this earlier when scrollTop indicates the last page anyway
                        if (!result.length) {
-                               result = this.element.children(":first");
+                               result = this.element.children(".ui-menu-item:first");
                        }
                        this.activate(event, result);
                } else {
-                       this.activate(event, this.element.children(!this.active || this.first() ? ":last" : ":first"));
+                       this.activate(event, this.element.children(".ui-menu-item")
+                               .filter(!this.active || this.first() ? ":last" : ":first"));
                }
        },
 
        hasScroll: function() {
-               return this.element.height() < this.element.attr("scrollHeight");
+               return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
        },
 
        select: function( event ) {