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

Last change on this file since 28780 was 28780, checked in by rvelices, 10 years ago

upgrade jquery ui from 1.10.1 to 1.10.4

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