2 * jQuery UI Autocomplete 1.8
\r
4 * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
\r
5 * Dual licensed under the MIT (MIT-LICENSE.txt)
\r
6 * and GPL (GPL-LICENSE.txt) licenses.
\r
8 * http://docs.jquery.com/UI/Autocomplete
\r
12 * jquery.ui.widget.js
\r
13 * jquery.ui.position.js
\r
17 $.widget( "ui.autocomplete", {
\r
22 _create: function() {
\r
24 doc = this.element[ 0 ].ownerDocument;
\r
26 .addClass( "ui-autocomplete-input" )
\r
27 .attr( "autocomplete", "off" )
\r
28 // TODO verify these actually work as intended
\r
31 "aria-autocomplete": "list",
\r
32 "aria-haspopup": "true"
\r
34 .bind( "keydown.autocomplete", function( event ) {
\r
35 var keyCode = $.ui.keyCode;
\r
36 switch( event.keyCode ) {
\r
37 case keyCode.PAGE_UP:
\r
38 self._move( "previousPage", event );
\r
40 case keyCode.PAGE_DOWN:
\r
41 self._move( "nextPage", event );
\r
44 self._move( "previous", event );
\r
45 // prevent moving cursor to beginning of text field in some browsers
\r
46 event.preventDefault();
\r
49 self._move( "next", event );
\r
50 // prevent moving cursor to end of text field in some browsers
\r
51 event.preventDefault();
\r
54 // when menu is open or has focus
\r
55 if ( self.menu.active ) {
\r
56 event.preventDefault();
\r
58 //passthrough - ENTER and TAB both select the current element
\r
60 if ( !self.menu.active ) {
\r
65 case keyCode.ESCAPE:
\r
66 self.element.val( self.term );
\r
67 self.close( event );
\r
70 case keyCode.CONTROL:
\r
72 // ignore metakeys (shift, ctrl, alt)
\r
75 // keypress is triggered before the input value is changed
\r
76 clearTimeout( self.searching );
\r
77 self.searching = setTimeout(function() {
\r
78 self.search( null, event );
\r
79 }, self.options.delay );
\r
83 .bind( "focus.autocomplete", function() {
\r
84 self.previous = self.element.val();
\r
86 .bind( "blur.autocomplete", function( event ) {
\r
87 clearTimeout( self.searching );
\r
88 // clicks on the menu (or a button to trigger a search) will cause a blur event
\r
89 // TODO try to implement this without a timeout, see clearTimeout in search()
\r
90 self.closing = setTimeout(function() {
\r
91 self.close( event );
\r
95 this.response = function() {
\r
96 return self._response.apply( self, arguments );
\r
98 this.menu = $( "<ul></ul>" )
\r
99 .addClass( "ui-autocomplete" )
\r
100 .appendTo( "body", doc )
\r
102 focus: function( event, ui ) {
\r
103 var item = ui.item.data( "item.autocomplete" );
\r
104 if ( false !== self._trigger( "focus", null, { item: item } ) ) {
\r
105 // use value to match what will end up in the input
\r
106 self.element.val( item.value );
\r
109 selected: function( event, ui ) {
\r
110 var item = ui.item.data( "item.autocomplete" );
\r
111 if ( false !== self._trigger( "select", event, { item: item } ) ) {
\r
112 self.element.val( item.value );
\r
114 self.close( event );
\r
115 self.previous = self.element.val();
\r
116 // only trigger when focus was lost (click on menu)
\r
117 if ( self.element[0] !== doc.activeElement ) {
\r
118 self.element.focus();
\r
121 blur: function( event, ui ) {
\r
122 if ( self.menu.element.is(":visible") ) {
\r
123 self.element.val( self.term );
\r
127 .zIndex( this.element.zIndex() + 1 )
\r
128 // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
\r
129 .css({ top: 0, left: 0 })
\r
132 if ( $.fn.bgiframe ) {
\r
133 this.menu.element.bgiframe();
\r
137 destroy: function() {
\r
139 .removeClass( "ui-autocomplete-input ui-widget ui-widget-content" )
\r
140 .removeAttr( "autocomplete" )
\r
141 .removeAttr( "role" )
\r
142 .removeAttr( "aria-autocomplete" )
\r
143 .removeAttr( "aria-haspopup" );
\r
144 this.menu.element.remove();
\r
145 $.Widget.prototype.destroy.call( this );
\r
148 _setOption: function( key ) {
\r
149 $.Widget.prototype._setOption.apply( this, arguments );
\r
150 if ( key === "source" ) {
\r
151 this._initSource();
\r
155 _initSource: function() {
\r
158 if ( $.isArray(this.options.source) ) {
\r
159 array = this.options.source;
\r
160 this.source = function( request, response ) {
\r
161 // escape regex characters
\r
162 var matcher = new RegExp( $.ui.autocomplete.escapeRegex(request.term), "i" );
\r
163 response( $.grep( array, function(value) {
\r
164 return matcher.test( value.label || value.value || value );
\r
167 } else if ( typeof this.options.source === "string" ) {
\r
168 url = this.options.source;
\r
169 this.source = function( request, response ) {
\r
170 $.getJSON( url, request, response );
\r
173 this.source = this.options.source;
\r
177 search: function( value, event ) {
\r
178 value = value != null ? value : this.element.val();
\r
179 if ( value.length < this.options.minLength ) {
\r
180 return this.close( event );
\r
183 clearTimeout( this.closing );
\r
184 if ( this._trigger("search") === false ) {
\r
188 return this._search( value );
\r
191 _search: function( value ) {
\r
192 this.term = this.element
\r
193 .addClass( "ui-autocomplete-loading" )
\r
194 // always save the actual value, not the one passed as an argument
\r
197 this.source( { term: value }, this.response );
\r
200 _response: function( content ) {
\r
201 if ( content.length ) {
\r
202 content = this._normalize( content );
\r
203 this._suggest( content );
\r
204 this._trigger( "open" );
\r
208 this.element.removeClass( "ui-autocomplete-loading" );
\r
211 close: function( event ) {
\r
212 clearTimeout( this.closing );
\r
213 if ( this.menu.element.is(":visible") ) {
\r
214 this._trigger( "close", event );
\r
215 this.menu.element.hide();
\r
216 this.menu.deactivate();
\r
218 if ( this.previous !== this.element.val() ) {
\r
219 this._trigger( "change", event );
\r
223 _normalize: function( items ) {
\r
224 // assume all items have the right format when the first item is complete
\r
225 if ( items.length && items[0].label && items[0].value ) {
\r
228 return $.map( items, function(item) {
\r
229 if ( typeof item === "string" ) {
\r
236 label: item.label || item.value,
\r
237 value: item.value || item.label
\r
242 _suggest: function( items ) {
\r
243 var ul = this.menu.element
\r
245 .zIndex( this.element.zIndex() + 1 ),
\r
248 this._renderMenu( ul, items );
\r
249 // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
\r
250 this.menu.deactivate();
\r
251 this.menu.refresh();
\r
252 this.menu.element.show().position({
\r
259 menuWidth = ul.width( "" ).width();
\r
260 textWidth = this.element.width();
\r
261 ul.width( Math.max( menuWidth, textWidth ) );
\r
264 _renderMenu: function( ul, items ) {
\r
266 $.each( items, function( index, item ) {
\r
267 self._renderItem( ul, item );
\r
271 _renderItem: function( ul, item) {
\r
272 return $( "<li></li>" )
\r
273 .data( "item.autocomplete", item )
\r
274 .append( "<a>" + item.label + "</a>" )
\r
278 _move: function( direction, event ) {
\r
279 if ( !this.menu.element.is(":visible") ) {
\r
280 this.search( null, event );
\r
283 if ( this.menu.first() && /^previous/.test(direction) ||
\r
284 this.menu.last() && /^next/.test(direction) ) {
\r
285 this.element.val( this.term );
\r
286 this.menu.deactivate();
\r
289 this.menu[ direction ]();
\r
292 widget: function() {
\r
293 return this.menu.element;
\r
297 $.extend( $.ui.autocomplete, {
\r
298 escapeRegex: function( value ) {
\r
299 return value.replace( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1" );
\r
306 * jQuery UI Menu (not officially released)
\r
308 * This widget isn't yet finished and the API is subject to change. We plan to finish
\r
309 * it for the next release. You're welcome to give it a try anyway and give us feedback,
\r
310 * as long as you're okay with migrating your code later on. We can help with that, too.
\r
312 * Copyright (c) 2010 AUTHORS.txt (http://jqueryui.com/about)
\r
313 * Dual licensed under the MIT (MIT-LICENSE.txt)
\r
314 * and GPL (GPL-LICENSE.txt) licenses.
\r
316 * http://docs.jquery.com/UI/Menu
\r
319 * jquery.ui.core.js
\r
320 * jquery.ui.widget.js
\r
324 $.widget("ui.menu", {
\r
325 _create: function() {
\r
328 .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
\r
331 "aria-activedescendant": "ui-active-menuitem"
\r
333 .click(function(e) {
\r
335 e.preventDefault();
\r
341 refresh: function() {
\r
344 // don't refresh list items that are already adapted
\r
345 var items = this.element.children("li:not(.ui-menu-item):has(a)")
\r
346 .addClass("ui-menu-item")
\r
347 .attr("role", "menuitem");
\r
349 items.children("a")
\r
350 .addClass("ui-corner-all")
\r
351 .attr("tabindex", -1)
\r
352 // mouseenter doesn't work with event delegation
\r
353 .mouseenter(function() {
\r
354 self.activate($(this).parent());
\r
356 .mouseleave(function() {
\r
361 activate: function(item) {
\r
363 if (this.hasScroll()) {
\r
364 var offset = item.offset().top - this.element.offset().top,
\r
365 scroll = this.element.attr("scrollTop"),
\r
366 elementHeight = this.element.height();
\r
368 this.element.attr("scrollTop", scroll + offset);
\r
369 } else if (offset > elementHeight) {
\r
370 this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
\r
373 this.active = item.eq(0)
\r
375 .addClass("ui-state-hover")
\r
376 .attr("id", "ui-active-menuitem")
\r
378 this._trigger("focus", null, { item: item });
\r
381 deactivate: function() {
\r
382 if (!this.active) { return; }
\r
384 this.active.children("a")
\r
385 .removeClass("ui-state-hover")
\r
387 this._trigger("blur");
\r
388 this.active = null;
\r
392 this.move("next", "li:first");
\r
395 previous: function() {
\r
396 this.move("prev", "li:last");
\r
399 first: function() {
\r
400 return this.active && !this.active.prev().length;
\r
404 return this.active && !this.active.next().length;
\r
407 move: function(direction, edge) {
\r
408 if (!this.active) {
\r
409 this.activate(this.element.children(edge));
\r
412 var next = this.active[direction]();
\r
414 this.activate(next);
\r
416 this.activate(this.element.children(edge));
\r
420 // TODO merge with previousPage
\r
421 nextPage: function() {
\r
422 if (this.hasScroll()) {
\r
423 // TODO merge with no-scroll-else
\r
424 if (!this.active || this.last()) {
\r
425 this.activate(this.element.children(":first"));
\r
428 var base = this.active.offset().top,
\r
429 height = this.element.height(),
\r
430 result = this.element.children("li").filter(function() {
\r
431 var close = $(this).offset().top - base - height + $(this).height();
\r
432 // TODO improve approximation
\r
433 return close < 10 && close > -10;
\r
436 // TODO try to catch this earlier when scrollTop indicates the last page anyway
\r
437 if (!result.length) {
\r
438 result = this.element.children(":last");
\r
440 this.activate(result);
\r
442 this.activate(this.element.children(!this.active || this.last() ? ":first" : ":last"));
\r
446 // TODO merge with nextPage
\r
447 previousPage: function() {
\r
448 if (this.hasScroll()) {
\r
449 // TODO merge with no-scroll-else
\r
450 if (!this.active || this.first()) {
\r
451 this.activate(this.element.children(":last"));
\r
455 var base = this.active.offset().top,
\r
456 height = this.element.height();
\r
457 result = this.element.children("li").filter(function() {
\r
458 var close = $(this).offset().top - base + height - $(this).height();
\r
459 // TODO improve approximation
\r
460 return close < 10 && close > -10;
\r
463 // TODO try to catch this earlier when scrollTop indicates the last page anyway
\r
464 if (!result.length) {
\r
465 result = this.element.children(":first");
\r
467 this.activate(result);
\r
469 this.activate(this.element.children(!this.active || this.first() ? ":last" : ":first"));
\r
473 hasScroll: function() {
\r
474 return this.element.height() < this.element.attr("scrollHeight");
\r
477 select: function() {
\r
478 this._trigger("selected", null, { item: this.active });
\r