/* * jQuery UI Autocomplete @VERSION * * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about) * Dual licensed under the MIT (MIT-LICENSE.txt) * and GPL (GPL-LICENSE.txt) licenses. * * http://docs.jquery.com/UI/Autocomplete * * Depends: * jquery.ui.core.js * jquery.ui.widget.js * jquery.ui.position.js * jquery.ui.menu.js */ (function( $ ) { $.widget( "ui.autocomplete", { options: { minLength: 1, delay: 300 }, _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 ) { 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: // when menu is open or has focus if ( self.menu.active ) { 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; case keyCode.LEFT: case keyCode.RIGHT: case keyCode.SHIFT: case keyCode.CONTROL: case keyCode.ALT: // ignore metakeys (shift, ctrl, alt) break; default: // keypress is triggered before the input value is changed clearTimeout( self.searching ); self.searching = setTimeout(function() { self.search( null, event ); }, self.options.delay ); break; } }) .bind( "focus.autocomplete", function() { self.selectedItem = null; self.previous = self.element.val(); }) .bind( "blur.autocomplete", function( event ) { clearTimeout( self.searching ); // clicks on the menu (or a button to trigger a search) will cause a blur event // TODO try to implement this without a timeout, see clearTimeout in search() self.closing = setTimeout(function() { self.close( event ); self._change( event ); }, 150 ); }); this._initSource(); this.response = function() { return self._response.apply( self, arguments ); }; this.menu = $( "" ) .addClass( "ui-autocomplete" ) .appendTo( "body", doc ) .menu({ // custom key handling for now input: $(), 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 ); } } }, select: function( event, ui ) { var item = ui.item.data( "item.autocomplete" ); if ( false !== self._trigger( "select", event, { item: item } ) ) { self.element.val( item.value ); } self.close( event ); // only trigger when focus was lost (click on menu) var previous = self.previous; if ( self.element[0] !== doc.activeElement ) { self.element.focus(); self.previous = previous; } self.selectedItem = item; }, blur: function( event, ui ) { if ( self.menu.element.is(":visible") ) { 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 ) { $.Widget.prototype._setOption.apply( this, arguments ); if ( key === "source" ) { this._initSource(); } }, _initSource: function() { var 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 ) { $.getJSON( url, request, response ); }; } else { this.source = this.options.source; } }, search: function( value, event ) { value = value != null ? value : 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.term = this.element .addClass( "ui-autocomplete-loading" ) // always save the actual value, not the one passed as an argument .val(); 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({ my: "left top", at: "left bottom", of: this.element, collision: "none" }); menuWidth = ul.width( "" ).width(); textWidth = this.element.width(); ul.width( 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 $( "
  • " ) .data( "item.autocomplete", item ) .append( "" + 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( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1" ); }, 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 ));