Created
September 22, 2010 14:25
-
-
Save juanramon/591761 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$.widget( "ui.autocomplete", { | |
options: { | |
appendTo: "body", | |
delay: 300, | |
minLength: 1, | |
position: { | |
my: "left top", | |
at: "left bottom", | |
collision: "none" | |
}, | |
source: null | |
}, | |
_create: function() { | |
var self = this, | |
doc = this.element[ 0 ].ownerDocument; | |
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 ) { | |
return; | |
} | |
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 or has focus | |
if ( self.menu.element.is( ":visible" ) ) { | |
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( "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 cant 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 === menuElement ) { | |
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 ); | |
}, 13); | |
}) | |
.menu({ | |
focus: function( event, ui ) { | |
var item = ui.item.data( "item.autocomplete" ); | |
if ( false !== self._trigger( "focus", null, { 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 ); | |
} | |
} | |
}, | |
selected: 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; | |
} | |
if ( false !== self._trigger( "select", event, { item: item } ) ) { | |
self.term = item.value; | |
self.element.val( item.value ); | |
} | |
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 ) | |
// workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781 | |
.css({ top: 0, left: 0 }) | |
.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(); | |
$.Widget.prototype.destroy.call( this ); | |
}, | |
_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] ) | |
} | |
}, | |
_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 = $.getJSON( url, request, function( data, status, xhr ) { | |
if ( xhr === self.xhr ) { | |
response( data ); | |
} | |
self.xhr = null; | |
}); | |
}; | |
} 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") === false ) { | |
return; | |
} | |
return this._search( value ); | |
}, | |
_search: function( value ) { | |
this.element.addClass( "ui-autocomplete-loading" ); | |
this.source( { term: value }, this.response ); | |
}, | |
_response: function( content ) { | |
if ( 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(); | |
} | |
}, | |
_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 ), | |
menuWidth, | |
textWidth; | |
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( $.extend({ | |
of: this.element | |
}, this.options.position )); | |
menuWidth = ul.width( "" ).outerWidth(); | |
textWidth = this.element.outerWidth(); | |
ul.outerWidth( Math.max( menuWidth, textWidth ) ); | |
}, | |
_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 ) { | |
if ( !this.menu.element.is(":visible") ) { | |
this.search( null, event ); | |
return; | |
} | |
if ( this.menu.first() && /^previous/.test(direction) || | |
this.menu.last() && /^next/.test(direction) ) { | |
this.element.val( this.term ); | |
this.menu.deactivate(); | |
return; | |
} | |
this.menu[ direction ]( event ); | |
}, | |
widget: function() { | |
return this.menu.element; | |
} | |
}); | |
$.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 ); | |
}); | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment