external library initial commit
[dwaf.git] / externals / jquery-ui-1.8 / ui / jquery.ui.autocomplete.js
blob8ee454694cf4625982f8e249ff90cf54000ba613
1 /*\r
2  * jQuery UI Autocomplete 1.8\r
3  *\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
7  *\r
8  * http://docs.jquery.com/UI/Autocomplete\r
9  *\r
10  * Depends:\r
11  *      jquery.ui.core.js\r
12  *      jquery.ui.widget.js\r
13  *      jquery.ui.position.js\r
14  */\r
15 (function( $ ) {\r
17 $.widget( "ui.autocomplete", {\r
18         options: {\r
19                 minLength: 1,\r
20                 delay: 300\r
21         },\r
22         _create: function() {\r
23                 var self = this,\r
24                         doc = this.element[ 0 ].ownerDocument;\r
25                 this.element\r
26                         .addClass( "ui-autocomplete-input" )\r
27                         .attr( "autocomplete", "off" )\r
28                         // TODO verify these actually work as intended\r
29                         .attr({\r
30                                 role: "textbox",\r
31                                 "aria-autocomplete": "list",\r
32                                 "aria-haspopup": "true"\r
33                         })\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
39                                         break;\r
40                                 case keyCode.PAGE_DOWN:\r
41                                         self._move( "nextPage", event );\r
42                                         break;\r
43                                 case keyCode.UP:\r
44                                         self._move( "previous", event );\r
45                                         // prevent moving cursor to beginning of text field in some browsers\r
46                                         event.preventDefault();\r
47                                         break;\r
48                                 case keyCode.DOWN:\r
49                                         self._move( "next", event );\r
50                                         // prevent moving cursor to end of text field in some browsers\r
51                                         event.preventDefault();\r
52                                         break;\r
53                                 case keyCode.ENTER:\r
54                                         // when menu is open or has focus\r
55                                         if ( self.menu.active ) {\r
56                                                 event.preventDefault();\r
57                                         }\r
58                                         //passthrough - ENTER and TAB both select the current element\r
59                                 case keyCode.TAB:\r
60                                         if ( !self.menu.active ) {\r
61                                                 return;\r
62                                         }\r
63                                         self.menu.select();\r
64                                         break;\r
65                                 case keyCode.ESCAPE:\r
66                                         self.element.val( self.term );\r
67                                         self.close( event );\r
68                                         break;\r
69                                 case keyCode.SHIFT:\r
70                                 case keyCode.CONTROL:\r
71                                 case 18:\r
72                                         // ignore metakeys (shift, ctrl, alt)\r
73                                         break;\r
74                                 default:\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
80                                         break;\r
81                                 }\r
82                         })\r
83                         .bind( "focus.autocomplete", function() {\r
84                                 self.previous = self.element.val();\r
85                         })\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
92                                 }, 150 );\r
93                         });\r
94                 this._initSource();\r
95                 this.response = function() {\r
96                         return self._response.apply( self, arguments );\r
97                 };\r
98                 this.menu = $( "<ul></ul>" )\r
99                         .addClass( "ui-autocomplete" )\r
100                         .appendTo( "body", doc )\r
101                         .menu({\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
107                                         }\r
108                                 },\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
113                                         }\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
119                                         }\r
120                                 },\r
121                                 blur: function( event, ui ) {\r
122                                         if ( self.menu.element.is(":visible") ) {\r
123                                                 self.element.val( self.term );\r
124                                         }\r
125                                 }\r
126                         })\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
130                         .hide()\r
131                         .data( "menu" );\r
132                 if ( $.fn.bgiframe ) {\r
133                          this.menu.element.bgiframe();\r
134                 }\r
135         },\r
137         destroy: function() {\r
138                 this.element\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
146         },\r
148         _setOption: function( key ) {\r
149                 $.Widget.prototype._setOption.apply( this, arguments );\r
150                 if ( key === "source" ) {\r
151                         this._initSource();\r
152                 }\r
153         },\r
155         _initSource: function() {\r
156                 var array,\r
157                         url;\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
165                                 }) );\r
166                         };\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
171                         };\r
172                 } else {\r
173                         this.source = this.options.source;\r
174                 }\r
175         },\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
181                 }\r
183                 clearTimeout( this.closing );\r
184                 if ( this._trigger("search") === false ) {\r
185                         return;\r
186                 }\r
188                 return this._search( value );\r
189         },\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
195                         .val();\r
197                 this.source( { term: value }, this.response );\r
198         },\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
205                 } else {\r
206                         this.close();\r
207                 }\r
208                 this.element.removeClass( "ui-autocomplete-loading" );\r
209         },\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
217                 }\r
218                 if ( this.previous !== this.element.val() ) {\r
219                         this._trigger( "change", event );\r
220                 }\r
221         },\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
226                         return items;\r
227                 }\r
228                 return $.map( items, function(item) {\r
229                         if ( typeof item === "string" ) {\r
230                                 return {\r
231                                         label: item,\r
232                                         value: item\r
233                                 };\r
234                         }\r
235                         return $.extend({\r
236                                 label: item.label || item.value,\r
237                                 value: item.value || item.label\r
238                         }, item );\r
239                 });\r
240         },\r
242         _suggest: function( items ) {\r
243                 var ul = this.menu.element\r
244                                 .empty()\r
245                                 .zIndex( this.element.zIndex() + 1 ),\r
246                         menuWidth,\r
247                         textWidth;\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
253                         my: "left top",\r
254                         at: "left bottom",\r
255                         of: this.element,\r
256                         collision: "none"\r
257                 });\r
259                 menuWidth = ul.width( "" ).width();\r
260                 textWidth = this.element.width();\r
261                 ul.width( Math.max( menuWidth, textWidth ) );\r
262         },\r
263         \r
264         _renderMenu: function( ul, items ) {\r
265                 var self = this;\r
266                 $.each( items, function( index, item ) {\r
267                         self._renderItem( ul, item );\r
268                 });\r
269         },\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
275                         .appendTo( ul );\r
276         },\r
278         _move: function( direction, event ) {\r
279                 if ( !this.menu.element.is(":visible") ) {\r
280                         this.search( null, event );\r
281                         return;\r
282                 }\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
287                         return;\r
288                 }\r
289                 this.menu[ direction ]();\r
290         },\r
292         widget: function() {\r
293                 return this.menu.element;\r
294         }\r
295 });\r
297 $.extend( $.ui.autocomplete, {\r
298         escapeRegex: function( value ) {\r
299                 return value.replace( /([\^\$\(\)\[\]\{\}\*\.\+\?\|\\])/gi, "\\$1" );\r
300         }\r
301 });\r
303 }( jQuery ));\r
305 /*\r
306  * jQuery UI Menu (not officially released)\r
307  * \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
311  *\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
315  *\r
316  * http://docs.jquery.com/UI/Menu\r
317  *\r
318  * Depends:\r
319  *      jquery.ui.core.js\r
320  *  jquery.ui.widget.js\r
321  */\r
322 (function($) {\r
324 $.widget("ui.menu", {\r
325         _create: function() {\r
326                 var self = this;\r
327                 this.element\r
328                         .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")\r
329                         .attr({\r
330                                 role: "listbox",\r
331                                 "aria-activedescendant": "ui-active-menuitem"\r
332                         })\r
333                         .click(function(e) {\r
334                                 // temporary\r
335                                 e.preventDefault();\r
336                                 self.select();\r
337                         });\r
338                 this.refresh();\r
339         },\r
340         \r
341         refresh: function() {\r
342                 var self = this;\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
348                 \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
355                         })\r
356                         .mouseleave(function() {\r
357                                 self.deactivate();\r
358                         });\r
359         },\r
361         activate: function(item) {\r
362                 this.deactivate();\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
367                         if (offset < 0) {\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
371                         }\r
372                 }\r
373                 this.active = item.eq(0)\r
374                         .children("a")\r
375                                 .addClass("ui-state-hover")\r
376                                 .attr("id", "ui-active-menuitem")\r
377                         .end();\r
378                 this._trigger("focus", null, { item: item });\r
379         },\r
381         deactivate: function() {\r
382                 if (!this.active) { return; }\r
384                 this.active.children("a")\r
385                         .removeClass("ui-state-hover")\r
386                         .removeAttr("id");\r
387                 this._trigger("blur");\r
388                 this.active = null;\r
389         },\r
391         next: function() {\r
392                 this.move("next", "li:first");\r
393         },\r
395         previous: function() {\r
396                 this.move("prev", "li:last");\r
397         },\r
399         first: function() {\r
400                 return this.active && !this.active.prev().length;\r
401         },\r
403         last: function() {\r
404                 return this.active && !this.active.next().length;\r
405         },\r
407         move: function(direction, edge) {\r
408                 if (!this.active) {\r
409                         this.activate(this.element.children(edge));\r
410                         return;\r
411                 }\r
412                 var next = this.active[direction]();\r
413                 if (next.length) {\r
414                         this.activate(next);\r
415                 } else {\r
416                         this.activate(this.element.children(edge));\r
417                 }\r
418         },\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
426                                 return;\r
427                         }\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
434                                 });\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
439                         }\r
440                         this.activate(result);\r
441                 } else {\r
442                         this.activate(this.element.children(!this.active || this.last() ? ":first" : ":last"));\r
443                 }\r
444         },\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
452                                 return;\r
453                         }\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
461                                 });\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
466                         }\r
467                         this.activate(result);\r
468                 } else {\r
469                         this.activate(this.element.children(!this.active || this.first() ? ":last" : ":first"));\r
470                 }\r
471         },\r
473         hasScroll: function() {\r
474                 return this.element.height() < this.element.attr("scrollHeight");\r
475         },\r
477         select: function() {\r
478                 this._trigger("selected", null, { item: this.active });\r
479         }\r
480 });\r
482 }(jQuery));\r