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

Last change on this file since 18630 was 18630, checked in by rvelices, 12 years ago

feature 2771: upgrade jquery from 1.7.2 to 1.8.2 and jquery.ui from 1.8.16 to 1.9.0
Attention plugins: jquery ui effect script ids change when using combine_script because file names changed ...

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