2 * jquery UI Autocomplete 1.8.4
4 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
5 * Dual licensed under the MIT or GPL Version 2 licenses.
6 * http://jquery.org/license
8 * http://docs.jquery.com/UI/Autocomplete
13 * jquery.ui.position.js
15 (function( $, undefined ) {
17 $.widget( "ui.autocomplete", {
31 doc = this.element[ 0 ].ownerDocument;
33 .addClass( "ui-autocomplete-input" )
34 .attr( "autocomplete", "off" )
35 // TODO verify these actually work as intended
38 "aria-autocomplete": "list",
39 "aria-haspopup": "true"
41 .bind( "keydown.autocomplete", function( event ) {
42 if ( self.options.disabled ) {
46 var keyCode = $.ui.keyCode;
47 switch( event.keyCode ) {
49 self._move( "previousPage", event );
51 case keyCode.PAGE_DOWN:
52 self._move( "nextPage", event );
55 self._move( "previous", event );
56 // prevent moving cursor to beginning of text field in some browsers
57 event.preventDefault();
60 self._move( "next", event );
61 // prevent moving cursor to end of text field in some browsers
62 event.preventDefault();
65 case keyCode.NUMPAD_ENTER:
66 // when menu is open or has focus
67 if ( self.menu.element.is( ":visible" ) ) {
68 event.preventDefault();
70 //passthrough - ENTER and TAB both select the current element
72 if ( !self.menu.active ) {
75 self.menu.select( event );
78 self.element.val( self.term );
82 // keypress is triggered before the input value is changed
83 clearTimeout( self.searching );
84 self.searching = setTimeout(function() {
85 // only search if the value has changed
86 if ( self.term != self.element.val() ) {
87 self.selectedItem = null;
88 self.search( null, event );
90 }, self.options.delay );
94 .bind( "focus.autocomplete", function() {
95 if ( self.options.disabled ) {
99 self.selectedItem = null;
100 self.previous = self.element.val();
102 .bind( "blur.autocomplete", function( event ) {
103 if ( self.options.disabled ) {
107 clearTimeout( self.searching );
108 // clicks on the menu (or a button to trigger a search) will cause a blur event
109 self.closing = setTimeout(function() {
111 self._change( event );
115 this.response = function() {
116 return self._response.apply( self, arguments );
118 this.menu = $( "<ul></ul>" )
119 .addClass( "ui-autocomplete" )
120 .appendTo( $( this.options.appendTo || "body", doc )[0] )
121 // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
122 .mousedown(function( event ) {
123 // clicking on the scrollbar causes focus to shift to the body
124 // but we can't detect a mouseup or a click immediately afterward
125 // so we have to track the next mousedown and close the menu if
126 // the user clicks somewhere outside of the autocomplete
127 var menuElement = self.menu.element[ 0 ];
128 if ( event.target === menuElement ) {
129 setTimeout(function() {
130 $( document ).one( 'mousedown', function( event ) {
131 if ( event.target !== self.element[ 0 ] &&
132 event.target !== menuElement &&
133 !$.ui.contains( menuElement, event.target ) ) {
140 // use another timeout to make sure the blur-event-handler on the input was already triggered
141 setTimeout(function() {
142 clearTimeout( self.closing );
146 focus: function( event, ui ) {
147 var item = ui.item.data( "item.autocomplete" );
148 if ( false !== self._trigger( "focus", null, { item: item } ) ) {
149 // use value to match what will end up in the input, if it was a key event
150 if ( /^key/.test(event.originalEvent.type) ) {
151 self.element.val( item.value );
155 selected: function( event, ui ) {
156 var item = ui.item.data( "item.autocomplete" ),
157 previous = self.previous;
159 // only trigger when focus was lost (click on menu)
160 if ( self.element[0] !== doc.activeElement ) {
161 self.element.focus();
162 self.previous = previous;
165 if ( false !== self._trigger( "select", event, { item: item } ) ) {
166 self.element.val( item.value );
170 self.selectedItem = item;
172 blur: function( event, ui ) {
173 // don't set the value of the text field if it's already correct
174 // this prevents moving the cursor unnecessarily
175 if ( self.menu.element.is(":visible") &&
176 ( self.element.val() !== self.term ) ) {
177 self.element.val( self.term );
181 .zIndex( this.element.zIndex() + 1 )
182 // workaround for jquery bug #5781 http://dev.jquery.com/ticket/5781
183 .css({ top: 0, left: 0 })
186 if ( $.fn.bgiframe ) {
187 this.menu.element.bgiframe();
191 destroy: function() {
193 .removeClass( "ui-autocomplete-input" )
194 .removeAttr( "autocomplete" )
195 .removeAttr( "role" )
196 .removeAttr( "aria-autocomplete" )
197 .removeAttr( "aria-haspopup" );
198 this.menu.element.remove();
199 $.Widget.prototype.destroy.call( this );
202 _setOption: function( key, value ) {
203 $.Widget.prototype._setOption.apply( this, arguments );
204 if ( key === "source" ) {
207 if ( key === "appendTo" ) {
208 this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
212 _initSource: function() {
215 if ( $.isArray(this.options.source) ) {
216 array = this.options.source;
217 this.source = function( request, response ) {
218 response( $.ui.autocomplete.filter(array, request.term) );
220 } else if ( typeof this.options.source === "string" ) {
221 url = this.options.source;
222 this.source = function( request, response ) {
223 $.getJSON( url, request, response );
226 this.source = this.options.source;
230 search: function( value, event ) {
231 value = value != null ? value : this.element.val();
232 if ( value.length < this.options.minLength ) {
233 return this.close( event );
236 clearTimeout( this.closing );
237 if ( this._trigger("search") === false ) {
241 return this._search( value );
244 _search: function( value ) {
245 this.term = this.element
246 .addClass( "ui-autocomplete-loading" )
247 // always save the actual value, not the one passed as an argument
250 this.source( { term: value }, this.response );
253 _response: function( content ) {
254 if ( content.length ) {
255 content = this._normalize( content );
256 this._suggest( content );
257 this._trigger( "open" );
261 this.element.removeClass( "ui-autocomplete-loading" );
264 close: function( event ) {
265 clearTimeout( this.closing );
266 if ( this.menu.element.is(":visible") ) {
267 this._trigger( "close", event );
268 this.menu.element.hide();
269 this.menu.deactivate();
273 _change: function( event ) {
274 if ( this.previous !== this.element.val() ) {
275 this._trigger( "change", event, { item: this.selectedItem } );
279 _normalize: function( items ) {
280 // assume all items have the right format when the first item is complete
281 if ( items.length && items[0].label && items[0].value ) {
284 return $.map( items, function(item) {
285 if ( typeof item === "string" ) {
292 label: item.label || item.value,
293 value: item.value || item.label
298 _suggest: function( items ) {
299 var ul = this.menu.element
301 .zIndex( this.element.zIndex() + 1 ),
304 this._renderMenu( ul, items );
305 // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
306 this.menu.deactivate();
308 this.menu.element.show().position( $.extend({
310 }, this.options.position ));
312 menuWidth = ul.width( "" ).outerWidth();
313 textWidth = this.element.outerWidth();
314 ul.outerWidth( Math.max( menuWidth, textWidth ) );
317 _renderMenu: function( ul, items ) {
319 $.each( items, function( index, item ) {
320 self._renderItem( ul, item );
324 _renderItem: function( ul, item) {
325 return $( "<li></li>" )
326 .data( "item.autocomplete", item )
327 .append( $( "<a></a>" ).text( item.label ) )
331 _move: function( direction, event ) {
332 if ( !this.menu.element.is(":visible") ) {
333 this.search( null, event );
336 if ( this.menu.first() && /^previous/.test(direction) ||
337 this.menu.last() && /^next/.test(direction) ) {
338 this.element.val( this.term );
339 this.menu.deactivate();
342 this.menu[ direction ]( event );
346 return this.menu.element;
350 $.extend( $.ui.autocomplete, {
351 escapeRegex: function( value ) {
352 return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
354 filter: function(array, term) {
355 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
356 return $.grep( array, function(value) {
357 return matcher.test( value.label || value.value || value );
365 * jquery UI Menu (not officially released)
367 * This widget isn't yet finished and the API is subject to change. We plan to finish
368 * it for the next release. You're welcome to give it a try anyway and give us feedback,
369 * as long as you're okay with migrating your code later on. We can help with that, too.
371 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
372 * Dual licensed under the MIT or GPL Version 2 licenses.
373 * http://jquery.org/license
375 * http://docs.jquery.com/UI/Menu
379 * jquery.ui.widget.js
383 $.widget("ui.menu", {
384 _create: function() {
387 .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
390 "aria-activedescendant": "ui-active-menuitem"
392 .click(function( event ) {
393 if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
397 event.preventDefault();
398 self.select( event );
403 refresh: function() {
406 // don't refresh list items that are already adapted
407 var items = this.element.children("li:not(.ui-menu-item):has(a)")
408 .addClass("ui-menu-item")
409 .attr("role", "menuitem");
412 .addClass("ui-corner-all")
413 .attr("tabindex", -1)
414 // mouseenter doesn't work with event delegation
415 .mouseenter(function( event ) {
416 self.activate( event, $(this).parent() );
418 .mouseleave(function() {
423 activate: function( event, item ) {
425 if (this.hasScroll()) {
426 var offset = item.offset().top - this.element.offset().top,
427 scroll = this.element.attr("scrollTop"),
428 elementHeight = this.element.height();
430 this.element.attr("scrollTop", scroll + offset);
431 } else if (offset > elementHeight) {
432 this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
435 this.active = item.eq(0)
437 .addClass("ui-state-hover")
438 .attr("id", "ui-active-menuitem")
440 this._trigger("focus", event, { item: item });
443 deactivate: function() {
444 if (!this.active) { return; }
446 this.active.children("a")
447 .removeClass("ui-state-hover")
449 this._trigger("blur");
453 next: function(event) {
454 this.move("next", ".ui-menu-item:first", event);
457 previous: function(event) {
458 this.move("prev", ".ui-menu-item:last", event);
462 return this.active && !this.active.prevAll(".ui-menu-item").length;
466 return this.active && !this.active.nextAll(".ui-menu-item").length;
469 move: function(direction, edge, event) {
471 this.activate(event, this.element.children(edge));
474 var next = this.active[direction + "All"](".ui-menu-item").eq(0);
476 this.activate(event, next);
478 this.activate(event, this.element.children(edge));
482 // TODO merge with previousPage
483 nextPage: function(event) {
484 if (this.hasScroll()) {
485 // TODO merge with no-scroll-else
486 if (!this.active || this.last()) {
487 this.activate(event, this.element.children(":first"));
490 var base = this.active.offset().top,
491 height = this.element.height(),
492 result = this.element.children("li").filter(function() {
493 var close = $(this).offset().top - base - height + $(this).height();
494 // TODO improve approximation
495 return close < 10 && close > -10;
498 // TODO try to catch this earlier when scrollTop indicates the last page anyway
499 if (!result.length) {
500 result = this.element.children(":last");
502 this.activate(event, result);
504 this.activate(event, this.element.children(!this.active || this.last() ? ":first" : ":last"));
508 // TODO merge with nextPage
509 previousPage: function(event) {
510 if (this.hasScroll()) {
511 // TODO merge with no-scroll-else
512 if (!this.active || this.first()) {
513 this.activate(event, this.element.children(":last"));
517 var base = this.active.offset().top,
518 height = this.element.height();
519 result = this.element.children("li").filter(function() {
520 var close = $(this).offset().top - base + height - $(this).height();
521 // TODO improve approximation
522 return close < 10 && close > -10;
525 // TODO try to catch this earlier when scrollTop indicates the last page anyway
526 if (!result.length) {
527 result = this.element.children(":first");
529 this.activate(event, result);
531 this.activate(event, this.element.children(!this.active || this.first() ? ":last" : ":first"));
535 hasScroll: function() {
536 return this.element.height() < this.element.attr("scrollHeight");
539 select: function( event ) {
540 this._trigger("selected", event, { item: this.active });