Created
October 27, 2012 14:28
-
-
Save tim-peterson/3964869 to your computer and use it in GitHub Desktop.
jqueryui.autocomplete.js
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
/*! | |
* jQuery UI Autocomplete 1.8.20 | |
* | |
* 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 | |
*/ | |
(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, | |
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.isMultiLine = this.element.is( "textarea" ); | |
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.propAttr( "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._keyEvent( "previous", event ); | |
break; | |
case keyCode.DOWN: | |
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: | |
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.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 && | |
!$.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", 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 ); | |
} | |
} | |
}, | |
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; | |
// #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 ); 10-27-12 timpeterson commented out to prevent submitting form error when hitting return | |
} | |
// 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 ) | |
// 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(); | |
} | |
// 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() { | |
this.element | |
.removeClass( "ui-autocomplete-input" ) | |
.removeAttr( "autocomplete" ) | |
.removeAttr( "role" ) | |
.removeAttr( "aria-autocomplete" ) | |
.removeAttr( "aria-haspopup" ); | |
this.menu.element.remove(); | |
$( window ).unbind( "beforeunload", this.beforeunloadHandler ); | |
$.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] ) | |
} | |
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", | |
success: function( data, status ) { | |
response( data ); | |
}, | |
error: function() { | |
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() { | |
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(); | |
} | |
}, | |
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 )); | |
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._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; | |
}, | |
_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 )); | |
/* | |
* jQuery UI Menu (not officially released) | |
* | |
* This widget isn't yet finished and the API is subject to change. We plan to finish | |
* 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 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 | |
* | |
* Depends: | |
* jquery.ui.core.js | |
* jquery.ui.widget.js | |
*/ | |
(function($) { | |
$.widget("ui.menu", { | |
_create: function() { | |
var self = this; | |
this.element | |
.addClass("ui-menu ui-widget ui-widget-content ui-corner-all") | |
.attr({ | |
role: "listbox", | |
"aria-activedescendant": "ui-active-menuitem" | |
}) | |
.click(function( event ) { | |
if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) { | |
return; | |
} | |
// temporary | |
event.preventDefault(); | |
self.select( event ); | |
}); | |
this.refresh(); | |
}, | |
refresh: function() { | |
var self = this; | |
// don't refresh list items that are already adapted | |
var items = this.element.children("li:not(.ui-menu-item):has(a)") | |
.addClass("ui-menu-item") | |
.attr("role", "menuitem"); | |
items.children("a") | |
.addClass("ui-corner-all") | |
.attr("tabindex", -1) | |
// mouseenter doesn't work with event delegation | |
.mouseenter(function( event ) { | |
self.activate( event, $(this).parent() ); | |
}) | |
.mouseleave(function() { | |
self.deactivate(); | |
}); | |
}, | |
activate: function( event, item ) { | |
this.deactivate(); | |
if (this.hasScroll()) { | |
var offset = item.offset().top - this.element.offset().top, | |
scroll = this.element.scrollTop(), | |
elementHeight = this.element.height(); | |
if (offset < 0) { | |
this.element.scrollTop( scroll + offset); | |
} else if (offset >= elementHeight) { | |
this.element.scrollTop( scroll + offset - elementHeight + item.height()); | |
} | |
} | |
this.active = item.eq(0) | |
.children("a") | |
.addClass("ui-state-hover") | |
.attr("id", "ui-active-menuitem") | |
.end(); | |
this._trigger("focus", event, { item: item }); | |
}, | |
deactivate: function() { | |
if (!this.active) { return; } | |
this.active.children("a") | |
.removeClass("ui-state-hover") | |
.removeAttr("id"); | |
this._trigger("blur"); | |
this.active = null; | |
}, | |
next: function(event) { | |
this.move("next", ".ui-menu-item:first", event); | |
}, | |
previous: function(event) { | |
this.move("prev", ".ui-menu-item:last", event); | |
}, | |
first: function() { | |
return this.active && !this.active.prevAll(".ui-menu-item").length; | |
}, | |
last: function() { | |
return this.active && !this.active.nextAll(".ui-menu-item").length; | |
}, | |
move: function(direction, edge, event) { | |
if (!this.active) { | |
this.activate(event, this.element.children(edge)); | |
return; | |
} | |
var next = this.active[direction + "All"](".ui-menu-item").eq(0); | |
if (next.length) { | |
this.activate(event, next); | |
} else { | |
this.activate(event, this.element.children(edge)); | |
} | |
}, | |
// TODO merge with previousPage | |
nextPage: function(event) { | |
if (this.hasScroll()) { | |
// TODO merge with no-scroll-else | |
if (!this.active || this.last()) { | |
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(".ui-menu-item").filter(function() { | |
var close = $(this).offset().top - base - height + $(this).height(); | |
// TODO improve approximation | |
return close < 10 && close > -10; | |
}); | |
// TODO try to catch this earlier when scrollTop indicates the last page anyway | |
if (!result.length) { | |
result = this.element.children(".ui-menu-item:last"); | |
} | |
this.activate(event, result); | |
} else { | |
this.activate(event, this.element.children(".ui-menu-item") | |
.filter(!this.active || this.last() ? ":first" : ":last")); | |
} | |
}, | |
// TODO merge with nextPage | |
previousPage: function(event) { | |
if (this.hasScroll()) { | |
// TODO merge with no-scroll-else | |
if (!this.active || this.first()) { | |
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(".ui-menu-item").filter(function() { | |
var close = $(this).offset().top - base + height - $(this).height(); | |
// TODO improve approximation | |
return close < 10 && close > -10; | |
}); | |
// TODO try to catch this earlier when scrollTop indicates the last page anyway | |
if (!result.length) { | |
result = this.element.children(".ui-menu-item:first"); | |
} | |
this.activate(event, result); | |
} else { | |
this.activate(event, this.element.children(".ui-menu-item") | |
.filter(!this.active || this.first() ? ":last" : ":first")); | |
} | |
}, | |
hasScroll: function() { | |
return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight"); | |
}, | |
select: function( event ) { | |
this._trigger("selected", event, { item: this.active }); | |
} | |
}); | |
}(jQuery)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment