make mixed-xrefs "Related views" collapsible on feature page
[sgn.git] / js / jqueryui / jquery.ui.autocomplete.js
blob01b013545349ffafebffa8c6a2aefd80537320a9
1 /*
2  * jquery UI Autocomplete 1.8.4
3  *
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
7  *
8  * http://docs.jquery.com/UI/Autocomplete
9  *
10  * Depends:
11  *      jquery.ui.core.js
12  *      jquery.ui.widget.js
13  *      jquery.ui.position.js
14  */
15 (function( $, undefined ) {
17 $.widget( "ui.autocomplete", {
18         options: {
19                 appendTo: "body",
20                 delay: 300,
21                 minLength: 1,
22                 position: {
23                         my: "left top",
24                         at: "left bottom",
25                         collision: "none"
26                 },
27                 source: null
28         },
29         _create: function() {
30                 var self = this,
31                         doc = this.element[ 0 ].ownerDocument;
32                 this.element
33                         .addClass( "ui-autocomplete-input" )
34                         .attr( "autocomplete", "off" )
35                         // TODO verify these actually work as intended
36                         .attr({
37                                 role: "textbox",
38                                 "aria-autocomplete": "list",
39                                 "aria-haspopup": "true"
40                         })
41                         .bind( "keydown.autocomplete", function( event ) {
42                                 if ( self.options.disabled ) {
43                                         return;
44                                 }
46                                 var keyCode = $.ui.keyCode;
47                                 switch( event.keyCode ) {
48                                 case keyCode.PAGE_UP:
49                                         self._move( "previousPage", event );
50                                         break;
51                                 case keyCode.PAGE_DOWN:
52                                         self._move( "nextPage", event );
53                                         break;
54                                 case keyCode.UP:
55                                         self._move( "previous", event );
56                                         // prevent moving cursor to beginning of text field in some browsers
57                                         event.preventDefault();
58                                         break;
59                                 case keyCode.DOWN:
60                                         self._move( "next", event );
61                                         // prevent moving cursor to end of text field in some browsers
62                                         event.preventDefault();
63                                         break;
64                                 case keyCode.ENTER:
65                                 case keyCode.NUMPAD_ENTER:
66                                         // when menu is open or has focus
67                                         if ( self.menu.element.is( ":visible" ) ) {
68                                                 event.preventDefault();
69                                         }
70                                         //passthrough - ENTER and TAB both select the current element
71                                 case keyCode.TAB:
72                                         if ( !self.menu.active ) {
73                                                 return;
74                                         }
75                                         self.menu.select( event );
76                                         break;
77                                 case keyCode.ESCAPE:
78                                         self.element.val( self.term );
79                                         self.close( event );
80                                         break;
81                                 default:
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 );
89                                                 }
90                                         }, self.options.delay );
91                                         break;
92                                 }
93                         })
94                         .bind( "focus.autocomplete", function() {
95                                 if ( self.options.disabled ) {
96                                         return;
97                                 }
99                                 self.selectedItem = null;
100                                 self.previous = self.element.val();
101                         })
102                         .bind( "blur.autocomplete", function( event ) {
103                                 if ( self.options.disabled ) {
104                                         return;
105                                 }
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() {
110                                         self.close( event );
111                                         self._change( event );
112                                 }, 150 );
113                         });
114                 this._initSource();
115                 this.response = function() {
116                         return self._response.apply( self, arguments );
117                 };
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 ) ) {
134                                                                 self.close();
135                                                         }
136                                                 });
137                                         }, 1 );
138                                 }
140                                 // use another timeout to make sure the blur-event-handler on the input was already triggered
141                                 setTimeout(function() {
142                                         clearTimeout( self.closing );
143                                 }, 13);
144                         })
145                         .menu({
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 );
152                                                 }
153                                         }
154                                 },
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;
163                                         }
165                                         if ( false !== self._trigger( "select", event, { item: item } ) ) {
166                                                 self.element.val( item.value );
167                                         }
169                                         self.close( event );
170                                         self.selectedItem = item;
171                                 },
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 );
178                                         }
179                                 }
180                         })
181                         .zIndex( this.element.zIndex() + 1 )
182                         // workaround for jquery bug #5781 http://dev.jquery.com/ticket/5781
183                         .css({ top: 0, left: 0 })
184                         .hide()
185                         .data( "menu" );
186                 if ( $.fn.bgiframe ) {
187                          this.menu.element.bgiframe();
188                 }
189         },
191         destroy: function() {
192                 this.element
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 );
200         },
202         _setOption: function( key, value ) {
203                 $.Widget.prototype._setOption.apply( this, arguments );
204                 if ( key === "source" ) {
205                         this._initSource();
206                 }
207                 if ( key === "appendTo" ) {
208                         this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
209                 }
210         },
212         _initSource: function() {
213                 var array,
214                         url;
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) );
219                         };
220                 } else if ( typeof this.options.source === "string" ) {
221                         url = this.options.source;
222                         this.source = function( request, response ) {
223                                 $.getJSON( url, request, response );
224                         };
225                 } else {
226                         this.source = this.options.source;
227                 }
228         },
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 );
234                 }
236                 clearTimeout( this.closing );
237                 if ( this._trigger("search") === false ) {
238                         return;
239                 }
241                 return this._search( value );
242         },
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
248                         .val();
250                 this.source( { term: value }, this.response );
251         },
253         _response: function( content ) {
254                 if ( content.length ) {
255                         content = this._normalize( content );
256                         this._suggest( content );
257                         this._trigger( "open" );
258                 } else {
259                         this.close();
260                 }
261                 this.element.removeClass( "ui-autocomplete-loading" );
262         },
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();
270                 }
271         },
272         
273         _change: function( event ) {
274                 if ( this.previous !== this.element.val() ) {
275                         this._trigger( "change", event, { item: this.selectedItem } );
276                 }
277         },
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 ) {
282                         return items;
283                 }
284                 return $.map( items, function(item) {
285                         if ( typeof item === "string" ) {
286                                 return {
287                                         label: item,
288                                         value: item
289                                 };
290                         }
291                         return $.extend({
292                                 label: item.label || item.value,
293                                 value: item.value || item.label
294                         }, item );
295                 });
296         },
298         _suggest: function( items ) {
299                 var ul = this.menu.element
300                                 .empty()
301                                 .zIndex( this.element.zIndex() + 1 ),
302                         menuWidth,
303                         textWidth;
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();
307                 this.menu.refresh();
308                 this.menu.element.show().position( $.extend({
309                         of: this.element
310                 }, this.options.position ));
312                 menuWidth = ul.width( "" ).outerWidth();
313                 textWidth = this.element.outerWidth();
314                 ul.outerWidth( Math.max( menuWidth, textWidth ) );
315         },
317         _renderMenu: function( ul, items ) {
318                 var self = this;
319                 $.each( items, function( index, item ) {
320                         self._renderItem( ul, item );
321                 });
322         },
324         _renderItem: function( ul, item) {
325                 return $( "<li></li>" )
326                         .data( "item.autocomplete", item )
327                         .append( $( "<a></a>" ).text( item.label ) )
328                         .appendTo( ul );
329         },
331         _move: function( direction, event ) {
332                 if ( !this.menu.element.is(":visible") ) {
333                         this.search( null, event );
334                         return;
335                 }
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();
340                         return;
341                 }
342                 this.menu[ direction ]( event );
343         },
345         widget: function() {
346                 return this.menu.element;
347         }
350 $.extend( $.ui.autocomplete, {
351         escapeRegex: function( value ) {
352                 return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
353         },
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 );
358                 });
359         }
362 }( jquery ));
365  * jquery UI Menu (not officially released)
366  * 
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
377  * Depends:
378  *      jquery.ui.core.js
379  *  jquery.ui.widget.js
380  */
381 (function($) {
383 $.widget("ui.menu", {
384         _create: function() {
385                 var self = this;
386                 this.element
387                         .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
388                         .attr({
389                                 role: "listbox",
390                                 "aria-activedescendant": "ui-active-menuitem"
391                         })
392                         .click(function( event ) {
393                                 if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
394                                         return;
395                                 }
396                                 // temporary
397                                 event.preventDefault();
398                                 self.select( event );
399                         });
400                 this.refresh();
401         },
402         
403         refresh: function() {
404                 var self = this;
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");
410                 
411                 items.children("a")
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() );
417                         })
418                         .mouseleave(function() {
419                                 self.deactivate();
420                         });
421         },
423         activate: function( event, item ) {
424                 this.deactivate();
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();
429                         if (offset < 0) {
430                                 this.element.attr("scrollTop", scroll + offset);
431                         } else if (offset > elementHeight) {
432                                 this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
433                         }
434                 }
435                 this.active = item.eq(0)
436                         .children("a")
437                                 .addClass("ui-state-hover")
438                                 .attr("id", "ui-active-menuitem")
439                         .end();
440                 this._trigger("focus", event, { item: item });
441         },
443         deactivate: function() {
444                 if (!this.active) { return; }
446                 this.active.children("a")
447                         .removeClass("ui-state-hover")
448                         .removeAttr("id");
449                 this._trigger("blur");
450                 this.active = null;
451         },
453         next: function(event) {
454                 this.move("next", ".ui-menu-item:first", event);
455         },
457         previous: function(event) {
458                 this.move("prev", ".ui-menu-item:last", event);
459         },
461         first: function() {
462                 return this.active && !this.active.prevAll(".ui-menu-item").length;
463         },
465         last: function() {
466                 return this.active && !this.active.nextAll(".ui-menu-item").length;
467         },
469         move: function(direction, edge, event) {
470                 if (!this.active) {
471                         this.activate(event, this.element.children(edge));
472                         return;
473                 }
474                 var next = this.active[direction + "All"](".ui-menu-item").eq(0);
475                 if (next.length) {
476                         this.activate(event, next);
477                 } else {
478                         this.activate(event, this.element.children(edge));
479                 }
480         },
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"));
488                                 return;
489                         }
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;
496                                 });
498                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
499                         if (!result.length) {
500                                 result = this.element.children(":last");
501                         }
502                         this.activate(event, result);
503                 } else {
504                         this.activate(event, this.element.children(!this.active || this.last() ? ":first" : ":last"));
505                 }
506         },
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"));
514                                 return;
515                         }
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;
523                                 });
525                         // TODO try to catch this earlier when scrollTop indicates the last page anyway
526                         if (!result.length) {
527                                 result = this.element.children(":first");
528                         }
529                         this.activate(event, result);
530                 } else {
531                         this.activate(event, this.element.children(!this.active || this.first() ? ":last" : ":first"));
532                 }
533         },
535         hasScroll: function() {
536                 return this.element.height() < this.element.attr("scrollHeight");
537         },
539         select: function( event ) {
540                 this._trigger("selected", event, { item: this.active });
541         }
544 }(jquery));