|
/* |
|
* jQuery UI Autocomplete @VERSION |
|
* |
|
* Copyright 2011, 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", { |
|
defaultElement: "<input>", |
|
options: { |
|
appendTo: "body", |
|
delay: 300, |
|
minLength: 1, |
|
position: { |
|
my: "left top", |
|
at: "left bottom", |
|
collision: "none" |
|
}, |
|
source: null |
|
}, |
|
|
|
pending: 0, |
|
|
|
_create: function() { |
|
var self = this, |
|
doc = this.element[ 0 ].ownerDocument, |
|
suppressKeyPress; |
|
|
|
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.attr( "readonly" ) ) { |
|
return; |
|
} |
|
|
|
suppressKeyPress = false; |
|
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._move( "previous", event ); |
|
// prevent moving cursor to beginning of text field in some browsers |
|
event.preventDefault(); |
|
break; |
|
case keyCode.DOWN: |
|
self._move( "next", event ); |
|
// prevent moving cursor to end of text field in some browsers |
|
event.preventDefault(); |
|
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: |
|
self.element.val( self.term ); |
|
self.close( event ); |
|
break; |
|
default: |
|
// keypress is triggered before the input value is changed |
|
clearTimeout( self.searching ); |
|
self.searching = setTimeout(function() { |
|
// 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() { |
|
self.close( event ); |
|
self._change( event ); |
|
}, 150 ); |
|
}); |
|
this._initSource(); |
|
this.response = function() { |
|
return self._response.apply( self, arguments ); |
|
}; |
|
this.menu = $( "<ul></ul>" ) |
|
.addClass( "ui-autocomplete" ) |
|
.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", 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 ); |
|
} |
|
} |
|
}, |
|
select: function( event, ui ) { |
|
var item = ui.item.data( "item.autocomplete" ), |
|
previous = self.previous; |
|
|
|
// only trigger when focus was lost (click on menu) |
|
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 ) { |
|
// 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 ) |
|
.hide() |
|
.data( "menu" ); |
|
if ( $.fn.bgiframe ) { |
|
this.menu.element.bgiframe(); |
|
} |
|
}, |
|
|
|
_destroy: function() { |
|
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( "_setOption", key, value ); |
|
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 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", |
|
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; |
|
} |
|
}, |
|
|
|
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", event ) === false ) { |
|
return; |
|
} |
|
|
|
return this._search( value ); |
|
}, |
|
|
|
_search: function( value ) { |
|
this.pending++; |
|
this.element.addClass( "ui-autocomplete-loading" ); |
|
|
|
this.source( { term: value }, this.response ); |
|
}, |
|
|
|
_response: function( content ) { |
|
if ( !this.options.disabled && content && content.length ) { |
|
content = this._normalize( content ); |
|
this._suggest( content ); |
|
this._trigger( "open" ); |
|
} else { |
|
this.close(); |
|
} |
|
this.pending--; |
|
if ( !this.pending ) { |
|
this.element.removeClass( "ui-autocomplete-loading" ); |
|
} |
|
}, |
|
|
|
close: function( event ) { |
|
clearTimeout( this.closing ); |
|
if ( this.menu.element.is(":visible") ) { |
|
this.menu.element.hide(); |
|
this.menu.deactivate(); |
|
this._trigger( "close", event ); |
|
} |
|
}, |
|
|
|
_change: function( event ) { |
|
if ( this.previous !== this.element.val() ) { |
|
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 deactivate |
|
this.menu.deactivate(); |
|
this.menu.refresh(); |
|
|
|
// 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 ) { |
|
self._renderItem( ul, item ); |
|
}); |
|
}, |
|
|
|
_renderItem: function( ul, item) { |
|
return $( "<li></li>" ) |
|
.data( "item.autocomplete", item ) |
|
.append( $( "<a></a>" ).text( item.label ) ) |
|
.appendTo( ul ); |
|
}, |
|
|
|
_move: function( direction, event ) { |
&n |