source: trunk/themes/default/js/ui/jquery.ui.autocomplete.js @ 20824

Last change on this file since 20824 was 20824, checked in by rvelices, 11 years ago

upgraded jquery ui from 1.9.0 to 1.10.1

File size: 15.5 KB
Line 
1/*!
2 * jQuery UI Autocomplete 1.10.1
3 * http://jqueryui.com
4 *
5 * Copyright 2013 jQuery Foundation and other contributors
6 * Released under the MIT license.
7 * http://jquery.org/license
8 *
9 * http://api.jqueryui.com/autocomplete/
10 *
11 * Depends:
12 *      jquery.ui.core.js
13 *      jquery.ui.widget.js
14 *      jquery.ui.position.js
15 *      jquery.ui.menu.js
16 */
17(function( $, undefined ) {
18
19// used to prevent race conditions with remote data sources
20var requestIndex = 0;
21
22$.widget( "ui.autocomplete", {
23        version: "1.10.1",
24        defaultElement: "<input>",
25        options: {
26                appendTo: null,
27                autoFocus: false,
28                delay: 300,
29                minLength: 1,
30                position: {
31                        my: "left top",
32                        at: "left bottom",
33                        collision: "none"
34                },
35                source: null,
36
37                // callbacks
38                change: null,
39                close: null,
40                focus: null,
41                open: null,
42                response: null,
43                search: null,
44                select: null
45        },
46
47        pending: 0,
48
49        _create: function() {
50                // Some browsers only repeat keydown events, not keypress events,
51                // so we use the suppressKeyPress flag to determine if we've already
52                // handled the keydown event. #7269
53                // Unfortunately the code for & in keypress is the same as the up arrow,
54                // so we use the suppressKeyPressRepeat flag to avoid handling keypress
55                // events when we know the keydown event was used to modify the
56                // search term. #7799
57                var suppressKeyPress, suppressKeyPressRepeat, suppressInput,
58                        nodeName = this.element[0].nodeName.toLowerCase(),
59                        isTextarea = nodeName === "textarea",
60                        isInput = nodeName === "input";
61
62                this.isMultiLine =
63                        // Textareas are always multi-line
64                        isTextarea ? true :
65                        // Inputs are always single-line, even if inside a contentEditable element
66                        // IE also treats inputs as contentEditable
67                        isInput ? false :
68                        // All other element types are determined by whether or not they're contentEditable
69                        this.element.prop( "isContentEditable" );
70
71                this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ];
72                this.isNewMenu = true;
73
74                this.element
75                        .addClass( "ui-autocomplete-input" )
76                        .attr( "autocomplete", "off" );
77
78                this._on( this.element, {
79                        keydown: function( event ) {
80                                /*jshint maxcomplexity:15*/
81                                if ( this.element.prop( "readOnly" ) ) {
82                                        suppressKeyPress = true;
83                                        suppressInput = true;
84                                        suppressKeyPressRepeat = true;
85                                        return;
86                                }
87
88                                suppressKeyPress = false;
89                                suppressInput = false;
90                                suppressKeyPressRepeat = false;
91                                var keyCode = $.ui.keyCode;
92                                switch( event.keyCode ) {
93                                case keyCode.PAGE_UP:
94                                        suppressKeyPress = true;
95                                        this._move( "previousPage", event );
96                                        break;
97                                case keyCode.PAGE_DOWN:
98                                        suppressKeyPress = true;
99                                        this._move( "nextPage", event );
100                                        break;
101                                case keyCode.UP:
102                                        suppressKeyPress = true;
103                                        this._keyEvent( "previous", event );
104                                        break;
105                                case keyCode.DOWN:
106                                        suppressKeyPress = true;
107                                        this._keyEvent( "next", event );
108                                        break;
109                                case keyCode.ENTER:
110                                case keyCode.NUMPAD_ENTER:
111                                        // when menu is open and has focus
112                                        if ( this.menu.active ) {
113                                                // #6055 - Opera still allows the keypress to occur
114                                                // which causes forms to submit
115                                                suppressKeyPress = true;
116                                                event.preventDefault();
117                                                this.menu.select( event );
118                                        }
119                                        break;
120                                case keyCode.TAB:
121                                        if ( this.menu.active ) {
122                                                this.menu.select( event );
123                                        }
124                                        break;
125                                case keyCode.ESCAPE:
126                                        if ( this.menu.element.is( ":visible" ) ) {
127                                                this._value( this.term );
128                                                this.close( event );
129                                                // Different browsers have different default behavior for escape
130                                                // Single press can mean undo or clear
131                                                // Double press in IE means clear the whole form
132                                                event.preventDefault();
133                                        }
134                                        break;
135                                default:
136                                        suppressKeyPressRepeat = true;
137                                        // search timeout should be triggered before the input value is changed
138                                        this._searchTimeout( event );
139                                        break;
140                                }
141                        },
142                        keypress: function( event ) {
143                                if ( suppressKeyPress ) {
144                                        suppressKeyPress = false;
145                                        event.preventDefault();
146                                        return;
147                                }
148                                if ( suppressKeyPressRepeat ) {
149                                        return;
150                                }
151
152                                // replicate some key handlers to allow them to repeat in Firefox and Opera
153                                var keyCode = $.ui.keyCode;
154                                switch( event.keyCode ) {
155                                case keyCode.PAGE_UP:
156                                        this._move( "previousPage", event );
157                                        break;
158                                case keyCode.PAGE_DOWN:
159                                        this._move( "nextPage", event );
160                                        break;
161                                case keyCode.UP:
162                                        this._keyEvent( "previous", event );
163                                        break;
164                                case keyCode.DOWN:
165                                        this._keyEvent( "next", event );
166                                        break;
167                                }
168                        },
169                        input: function( event ) {
170                                if ( suppressInput ) {
171                                        suppressInput = false;
172                                        event.preventDefault();
173                                        return;
174                                }
175                                this._searchTimeout( event );
176                        },
177                        focus: function() {
178                                this.selectedItem = null;
179                                this.previous = this._value();
180                        },
181                        blur: function( event ) {
182                                if ( this.cancelBlur ) {
183                                        delete this.cancelBlur;
184                                        return;
185                                }
186
187                                clearTimeout( this.searching );
188                                this.close( event );
189                                this._change( event );
190                        }
191                });
192
193                this._initSource();
194                this.menu = $( "<ul>" )
195                        .addClass( "ui-autocomplete ui-front" )
196                        .appendTo( this._appendTo() )
197                        .menu({
198                                // custom key handling for now
199                                input: $(),
200                                // disable ARIA support, the live region takes care of that
201                                role: null
202                        })
203                        .hide()
204                        .data( "ui-menu" );
205
206                this._on( this.menu.element, {
207                        mousedown: function( event ) {
208                                // prevent moving focus out of the text field
209                                event.preventDefault();
210
211                                // IE doesn't prevent moving focus even with event.preventDefault()
212                                // so we set a flag to know when we should ignore the blur event
213                                this.cancelBlur = true;
214                                this._delay(function() {
215                                        delete this.cancelBlur;
216                                });
217
218                                // clicking on the scrollbar causes focus to shift to the body
219                                // but we can't detect a mouseup or a click immediately afterward
220                                // so we have to track the next mousedown and close the menu if
221                                // the user clicks somewhere outside of the autocomplete
222                                var menuElement = this.menu.element[ 0 ];
223                                if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
224                                        this._delay(function() {
225                                                var that = this;
226                                                this.document.one( "mousedown", function( event ) {
227                                                        if ( event.target !== that.element[ 0 ] &&
228                                                                        event.target !== menuElement &&
229                                                                        !$.contains( menuElement, event.target ) ) {
230                                                                that.close();
231                                                        }
232                                                });
233                                        });
234                                }
235                        },
236                        menufocus: function( event, ui ) {
237                                // #7024 - Prevent accidental activation of menu items in Firefox
238                                if ( this.isNewMenu ) {
239                                        this.isNewMenu = false;
240                                        if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
241                                                this.menu.blur();
242
243                                                this.document.one( "mousemove", function() {
244                                                        $( event.target ).trigger( event.originalEvent );
245                                                });
246
247                                                return;
248                                        }
249                                }
250
251                                var item = ui.item.data( "ui-autocomplete-item" );
252                                if ( false !== this._trigger( "focus", event, { item: item } ) ) {
253                                        // use value to match what will end up in the input, if it was a key event
254                                        if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
255                                                this._value( item.value );
256                                        }
257                                } else {
258                                        // Normally the input is populated with the item's value as the
259                                        // menu is navigated, causing screen readers to notice a change and
260                                        // announce the item. Since the focus event was canceled, this doesn't
261                                        // happen, so we update the live region so that screen readers can
262                                        // still notice the change and announce it.
263                                        this.liveRegion.text( item.value );
264                                }
265                        },
266                        menuselect: function( event, ui ) {
267                                var item = ui.item.data( "ui-autocomplete-item" ),
268                                        previous = this.previous;
269
270                                // only trigger when focus was lost (click on menu)
271                                if ( this.element[0] !== this.document[0].activeElement ) {
272                                        this.element.focus();
273                                        this.previous = previous;
274                                        // #6109 - IE triggers two focus events and the second
275                                        // is asynchronous, so we need to reset the previous
276                                        // term synchronously and asynchronously :-(
277                                        this._delay(function() {
278                                                this.previous = previous;
279                                                this.selectedItem = item;
280                                        });
281                                }
282
283                                if ( false !== this._trigger( "select", event, { item: item } ) ) {
284                                        this._value( item.value );
285                                }
286                                // reset the term after the select event
287                                // this allows custom select handling to work properly
288                                this.term = this._value();
289
290                                this.close( event );
291                                this.selectedItem = item;
292                        }
293                });
294
295                this.liveRegion = $( "<span>", {
296                                role: "status",
297                                "aria-live": "polite"
298                        })
299                        .addClass( "ui-helper-hidden-accessible" )
300                        .insertAfter( this.element );
301
302                // turning off autocomplete prevents the browser from remembering the
303                // value when navigating through history, so we re-enable autocomplete
304                // if the page is unloaded before the widget is destroyed. #7790
305                this._on( this.window, {
306                        beforeunload: function() {
307                                this.element.removeAttr( "autocomplete" );
308                        }
309                });
310        },
311
312        _destroy: function() {
313                clearTimeout( this.searching );
314                this.element
315                        .removeClass( "ui-autocomplete-input" )
316                        .removeAttr( "autocomplete" );
317                this.menu.element.remove();
318                this.liveRegion.remove();
319        },
320
321        _setOption: function( key, value ) {
322                this._super( key, value );
323                if ( key === "source" ) {
324                        this._initSource();
325                }
326                if ( key === "appendTo" ) {
327                        this.menu.element.appendTo( this._appendTo() );
328                }
329                if ( key === "disabled" && value && this.xhr ) {
330                        this.xhr.abort();
331                }
332        },
333
334        _appendTo: function() {
335                var element = this.options.appendTo;
336
337                if ( element ) {
338                        element = element.jquery || element.nodeType ?
339                                $( element ) :
340                                this.document.find( element ).eq( 0 );
341                }
342
343                if ( !element ) {
344                        element = this.element.closest( ".ui-front" );
345                }
346
347                if ( !element.length ) {
348                        element = this.document[0].body;
349                }
350
351                return element;
352        },
353
354        _initSource: function() {
355                var array, url,
356                        that = this;
357                if ( $.isArray(this.options.source) ) {
358                        array = this.options.source;
359                        this.source = function( request, response ) {
360                                response( $.ui.autocomplete.filter( array, request.term ) );
361                        };
362                } else if ( typeof this.options.source === "string" ) {
363                        url = this.options.source;
364                        this.source = function( request, response ) {
365                                if ( that.xhr ) {
366                                        that.xhr.abort();
367                                }
368                                that.xhr = $.ajax({
369                                        url: url,
370                                        data: request,
371                                        dataType: "json",
372                                        success: function( data ) {
373                                                response( data );
374                                        },
375                                        error: function() {
376                                                response( [] );
377                                        }
378                                });
379                        };
380                } else {
381                        this.source = this.options.source;
382                }
383        },
384
385        _searchTimeout: function( event ) {
386                clearTimeout( this.searching );
387                this.searching = this._delay(function() {
388                        // only search if the value has changed
389                        if ( this.term !== this._value() ) {
390                                this.selectedItem = null;
391                                this.search( null, event );
392                        }
393                }, this.options.delay );
394        },
395
396        search: function( value, event ) {
397                value = value != null ? value : this._value();
398
399                // always save the actual value, not the one passed as an argument
400                this.term = this._value();
401
402                if ( value.length < this.options.minLength ) {
403                        return this.close( event );
404                }
405
406                if ( this._trigger( "search", event ) === false ) {
407                        return;
408                }
409
410                return this._search( value );
411        },
412
413        _search: function( value ) {
414                this.pending++;
415                this.element.addClass( "ui-autocomplete-loading" );
416                this.cancelSearch = false;
417
418                this.source( { term: value }, this._response() );
419        },
420
421        _response: function() {
422                var that = this,
423                        index = ++requestIndex;
424
425                return function( content ) {
426                        if ( index === requestIndex ) {
427                                that.__response( content );
428                        }
429
430                        that.pending--;
431                        if ( !that.pending ) {
432                                that.element.removeClass( "ui-autocomplete-loading" );
433                        }
434                };
435        },
436
437        __response: function( content ) {
438                if ( content ) {
439                        content = this._normalize( content );
440                }
441                this._trigger( "response", null, { content: content } );
442                if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
443                        this._suggest( content );
444                        this._trigger( "open" );
445                } else {
446                        // use ._close() instead of .close() so we don't cancel future searches
447                        this._close();
448                }
449        },
450
451        close: function( event ) {
452                this.cancelSearch = true;
453                this._close( event );
454        },
455
456        _close: function( event ) {
457                if ( this.menu.element.is( ":visible" ) ) {
458                        this.menu.element.hide();
459                        this.menu.blur();
460                        this.isNewMenu = true;
461                        this._trigger( "close", event );
462                }
463        },
464
465        _change: function( event ) {
466                if ( this.previous !== this._value() ) {
467                        this._trigger( "change", event, { item: this.selectedItem } );
468                }
469        },
470
471        _normalize: function( items ) {
472                // assume all items have the right format when the first item is complete
473                if ( items.length && items[0].label && items[0].value ) {
474                        return items;
475                }
476                return $.map( items, function( item ) {
477                        if ( typeof item === "string" ) {
478                                return {
479                                        label: item,
480                                        value: item
481                                };
482                        }
483                        return $.extend({
484                                label: item.label || item.value,
485                                value: item.value || item.label
486                        }, item );
487                });
488        },
489
490        _suggest: function( items ) {
491                var ul = this.menu.element.empty();
492                this._renderMenu( ul, items );
493                this.menu.refresh();
494
495                // size and position menu
496                ul.show();
497                this._resizeMenu();
498                ul.position( $.extend({
499                        of: this.element
500                }, this.options.position ));
501
502                if ( this.options.autoFocus ) {
503                        this.menu.next();
504                }
505        },
506
507        _resizeMenu: function() {
508                var ul = this.menu.element;
509                ul.outerWidth( Math.max(
510                        // Firefox wraps long text (possibly a rounding bug)
511                        // so we add 1px to avoid the wrapping (#7513)
512                        ul.width( "" ).outerWidth() + 1,
513                        this.element.outerWidth()
514                ) );
515        },
516
517        _renderMenu: function( ul, items ) {
518                var that = this;
519                $.each( items, function( index, item ) {
520                        that._renderItemData( ul, item );
521                });
522        },
523
524        _renderItemData: function( ul, item ) {
525                return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
526        },
527
528        _renderItem: function( ul, item ) {
529                return $( "<li>" )
530                        .append( $( "<a>" ).text( item.label ) )
531                        .appendTo( ul );
532        },
533
534        _move: function( direction, event ) {
535                if ( !this.menu.element.is( ":visible" ) ) {
536                        this.search( null, event );
537                        return;
538                }
539                if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
540                                this.menu.isLastItem() && /^next/.test( direction ) ) {
541                        this._value( this.term );
542                        this.menu.blur();
543                        return;
544                }
545                this.menu[ direction ]( event );
546        },
547
548        widget: function() {
549                return this.menu.element;
550        },
551
552        _value: function() {
553                return this.valueMethod.apply( this.element, arguments );
554        },
555
556        _keyEvent: function( keyEvent, event ) {
557                if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
558                        this._move( keyEvent, event );
559
560                        // prevents moving cursor to beginning/end of the text field in some browsers
561                        event.preventDefault();
562                }
563        }
564});
565
566$.extend( $.ui.autocomplete, {
567        escapeRegex: function( value ) {
568                return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
569        },
570        filter: function(array, term) {
571                var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
572                return $.grep( array, function(value) {
573                        return matcher.test( value.label || value.value || value );
574                });
575        }
576});
577
578
579// live region extension, adding a `messages` option
580// NOTE: This is an experimental API. We are still investigating
581// a full solution for string manipulation and internationalization.
582$.widget( "ui.autocomplete", $.ui.autocomplete, {
583        options: {
584                messages: {
585                        noResults: "No search results.",
586                        results: function( amount ) {
587                                return amount + ( amount > 1 ? " results are" : " result is" ) +
588                                        " available, use up and down arrow keys to navigate.";
589                        }
590                }
591        },
592
593        __response: function( content ) {
594                var message;
595                this._superApply( arguments );
596                if ( this.options.disabled || this.cancelSearch ) {
597                        return;
598                }
599                if ( content && content.length ) {
600                        message = this.options.messages.results( content.length );
601                } else {
602                        message = this.options.messages.noResults;
603                }
604                this.liveRegion.text( message );
605        }
606});
607
608}( jQuery ));
Note: See TracBrowser for help on using the repository browser.