/*! * jQuery UI Autocomplete @VERSION * * 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 * * Depends: * jquery.ui.core.js * jquery.ui.widget.js * jquery.ui.position.js * jquery.ui.menu.js */ (function( $, undefined ) { // used to prevent race conditions with remote data sources var requestIndex = 0; $.widget( "ui.autocomplete", { version: "@VERSION", defaultElement: "", options: { appendTo: "body", autoFocus: false, delay: 300, minLength: 1, position: { my: "left top", at: "left bottom", collision: "none" }, source: null, // callbacks change: null, close: null, focus: null, open: null, response: null, search: null, select: null }, pending: 0, _create: function() { var self = this, // Some browsers only repeat keydown events, not keypress events, // so we use the suppressKeyPress flag to determine if we've already // handled the keydown event. #7269 // Unfortunately the code for & in keypress is the same as the up arrow, // so we use the suppressKeyPressRepeat flag to avoid handling keypress // events when we know the keydown event was used to modify the // search term. #7799 suppressKeyPress, suppressKeyPressRepeat, suppressInput; this.isMultiLine = this.element.is( "textarea,[contenteditable]" ); this.valueMethod = this.element[ this.element.is( "input,textarea" ) ? "val" : "text" ]; this.element .addClass( "ui-autocomplete-input" ) .attr( "autocomplete", "off" ) // TODO verify these actually work as intended .attr({ role: "textbox", "aria-autocomplete": "list", "aria-haspopup": "true" }) .bind( "keydown.autocomplete", function( event ) { if ( self.options.disabled || self.element.prop( "readOnly" ) ) { suppressKeyPress = true; suppressInput = true; suppressKeyPressRepeat = true; return; } suppressKeyPress = false; suppressInput = false; suppressKeyPressRepeat = false; var keyCode = $.ui.keyCode; switch( event.keyCode ) { case keyCode.PAGE_UP: suppressKeyPress = true; self._move( "previousPage", event ); break; case keyCode.PAGE_DOWN: suppressKeyPress = true; self._move( "nextPage", event ); break; case keyCode.UP: suppressKeyPress = true; self._keyEvent( "previous", event ); break; case keyCode.DOWN: suppressKeyPress = true; self._keyEvent( "next", event ); break; case keyCode.ENTER: 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 case keyCode.TAB: if ( !self.menu.active ) { return; } self.menu.select( event ); break; case keyCode.ESCAPE: if ( self.menu.element.is(":visible") ) { self._value( self.term ); self.close( event ); } break; default: suppressKeyPressRepeat = true; // search timeout should be triggered before the input value is changed self._searchTimeout( event ); break; } }) .bind( "keypress.autocomplete", function( event ) { if ( suppressKeyPress ) { suppressKeyPress = false; event.preventDefault(); return; } if ( suppressKeyPressRepeat ) { return; } // replicate some key handlers to allow them to repeat in Firefox and Opera var keyCode = $.ui.keyCode; switch( event.keyCode ) { case keyCode.PAGE_UP: self._move( "previousPage", event ); break; case keyCode.PAGE_DOWN: self._move( "nextPage", event ); break; case keyCode.UP: self._keyEvent( "previous", event ); break; case keyCode.DOWN: self._keyEvent( "next", event ); break; } }) .bind( "input.autocomplete", function(event) { if ( suppressInput ) { suppressInput = false; event.preventDefault(); return; } self._searchTimeout( event ); }) .bind( "focus.autocomplete", function() { if ( self.options.disabled ) { return; } self.selectedItem = null; self.previous = self._value(); }) .bind( "blur.autocomplete", function( event ) { if ( self.options.disabled ) { return; } if ( self.cancelBlur ) { delete self.cancelBlur; return; } clearTimeout( self.searching ); self.close( event ); self._change( event ); }); this._initSource(); this.response = function() { return self._response.apply( self, arguments ); }; this.menu = $( "" ) .addClass( "ui-autocomplete" ) .appendTo( this.document.find( this.options.appendTo || "body" )[0] ) // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown) .mousedown(function( event ) { // prevent moving focus out of the text field event.preventDefault(); // IE doesn't prevent moving focus even with event.preventDefault() // so we set a flag to know when we should ignore the blur event self.cancelBlur = true; setTimeout(function() { delete self.cancelBlur; }, 1 ); // 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() { self.document.one( 'mousedown', function( event ) { if ( event.target !== self.element[ 0 ] && event.target !== menuElement && !$.contains( menuElement, event.target ) ) { self.close(); } }); }, 1 ); } }) .menu({ // custom key handling for now input: $(), focus: function( event, ui ) { // back compat for _renderItem using item.autocomplete, via #7810 // TODO remove the fallback, see #8156 var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" ); 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._value( item.value ); } } }, select: function( event, ui ) { // back compat for _renderItem using item.autocomplete, via #7810 // TODO remove the fallback, see #8156 var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" ); previous = self.previous; // only trigger when focus was lost (click on menu) if ( self.element[0] !== self.document[0].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._value( item.value ); } // reset the term after the select event // this allows custom select handling to work properly self.term = self._value(); self.close( event ); self.selectedItem = item; } }) .zIndex( this.element.zIndex() + 1 ) .hide() .data( "menu" ); 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 this._bind( this.window, { beforeunload: function() { this.element.removeAttr( "autocomplete" ); } }); }, _destroy: function() { clearTimeout( this.searching ); this.element .removeClass( "ui-autocomplete-input" ) .removeAttr( "autocomplete" ) .removeAttr( "role" ) .removeAttr( "aria-autocomplete" ) .removeAttr( "aria-haspopup" ); this.menu.element.remove(); }, _setOption: function( key, value ) { this._super( key, value ); if ( key === "source" ) { this._initSource(); } if ( key === "appendTo" ) { this.menu.element.appendTo( this.document.find( value || "body" )[0] ); } if ( key === "disabled" && value && this.xhr ) { this.xhr.abort(); } }, _initSource: function() { var self = this, array, url; if ( $.isArray(this.options.source) ) { array = this.options.source; this.source = function( request, response ) { response( $.ui.autocomplete.filter(array, request.term) ); }; } else if ( typeof this.options.source === "string" ) { url = this.options.source; this.source = function( request, response ) { if ( self.xhr ) { self.xhr.abort(); } self.xhr = $.ajax({ url: url, data: request, dataType: "json", context: { autocompleteRequest: ++requestIndex }, success: function( data, status ) { if ( this.autocompleteRequest === requestIndex ) { response( data ); } }, error: function() { if ( this.autocompleteRequest === requestIndex ) { response( [] ); } } }); }; } else { this.source = this.options.source; } }, _searchTimeout: function( event ) { var self = this; clearTimeout( self.searching ); self.searching = setTimeout(function() { // only search if the value has changed if ( self.term !== self._value() ) { self.selectedItem = null; self.search( null, event ); } }, self.options.delay ); }, search: function( value, event ) { value = value != null ? value : this._value(); // always save the actual value, not the one passed as an argument this.term = this._value(); if ( value.length < this.options.minLength ) { return this.close( event ); } if ( this._trigger( "search", event ) === false ) { return; } return this._search( value ); }, _search: function( value ) { this.pending++; this.element.addClass( "ui-autocomplete-loading" ); this.cancelSearch = false; this.source( { term: value }, this.response ); }, _response: function( content ) { if ( content ) { content = this._normalize( content ); } this._trigger( "response", null, { content: content } ); if ( !this.options.disabled && content && content.length && !this.cancelSearch ) { this._suggest( content ); this._trigger( "open" ); } else { // use ._close() instead of .close() so we don't cancel future searches this._close(); } this.pending--; if ( !this.pending ) { this.element.removeClass( "ui-autocomplete-loading" ); } }, close: function( event ) { this.cancelSearch = true; this._close( event ); }, _close: function( event ) { clearTimeout( this.closing ); if ( this.menu.element.is(":visible") ) { this.menu.element.hide(); this.menu.blur(); this._trigger( "close", event ); } }, _change: function( event ) { if ( this.previous !== this._value() ) { this._trigger( "change", event, { item: this.selectedItem } ); } }, _normalize: function( items ) { // assume all items have the right format when the first item is complete if ( items.length && items[0].label && items[0].value ) { return items; } return $.map( items, function(item) { if ( typeof item === "string" ) { return { label: item, value: item }; } return $.extend({ label: item.label || item.value, value: item.value || item.label }, item ); }); }, _suggest: function( items ) { var ul = this.menu.element .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 blur this.menu.blur(); this.menu.refresh(); // 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 ) { self._renderItemData( ul, item ); }); }, _renderItemData: function( ul, item ) { return this._renderItem( ul, item ).data( "ui-autocomplete-item", item ); }, _renderItem: function( ul, item ) { return $( "
  • " ) .append( $( "" ).text( item.label ) ) .appendTo( ul ); }, _move: function( direction, event ) { if ( !this.menu.element.is(":visible") ) { this.search( null, event ); return; } if ( this.menu.isFirstItem() && /^previous/.test(direction) || this.menu.isLastItem() && /^next/.test(direction) ) { this._value( this.term ); this.menu.blur(); return; } this.menu[ direction ]( event ); }, widget: function() { return this.menu.element; }, _value: function( value ) { return this.valueMethod.apply( this.element, arguments ); }, _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(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); }, filter: function(array, term) { var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" ); return $.grep( array, function(value) { return matcher.test( value.label || value.value || value ); }); } }); }( jQuery ));