mirror of
https://github.com/jquery/jquery-ui.git
synced 2024-11-21 11:04:24 +00:00
37dcc3e21d
For instance, this is useful for the jquery-ui-rails gem, which does not use jQuery UI's own minification, but relies on Rails to minify the files where necessary. Rails in turn uses UglifyJS for JS and YUI for CSS, both of which respect the /*! ... */ convention.
536 lines
14 KiB
JavaScript
536 lines
14 KiB
JavaScript
/*!
|
|
* 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: "<input>",
|
|
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 = $( "<ul></ul>" )
|
|
.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 $( "<li></li>" )
|
|
.append( $( "<a></a>" ).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 ));
|