source: Dev/branches/jQueryUI/client/js/jquery/ui/jquery.ui.autocomplete.js @ 249

Last change on this file since 249 was 249, checked in by hendrikvanantwerpen, 13 years ago

This one's for Subversion, because it's so close...

First widget (stripped down sequencer).
Seperated client and server code in two direcotry trees.

File size: 16.7 KB
Line 
1/*
2 * jQuery UI Autocomplete 1.8.17
3 *
4 * Copyright 2011, 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 ) {
16
17// used to prevent race conditions with remote data sources
18var requestIndex = 0;
19
20$.widget( "ui.autocomplete", {
21        options: {
22                appendTo: "body",
23                autoFocus: false,
24                delay: 300,
25                minLength: 1,
26                position: {
27                        my: "left top",
28                        at: "left bottom",
29                        collision: "none"
30                },
31                source: null
32        },
33
34        pending: 0,
35
36        _create: function() {
37                var self = this,
38                        doc = this.element[ 0 ].ownerDocument,
39                        suppressKeyPress;
40
41                this.element
42                        .addClass( "ui-autocomplete-input" )
43                        .attr( "autocomplete", "off" )
44                        // TODO verify these actually work as intended
45                        .attr({
46                                role: "textbox",
47                                "aria-autocomplete": "list",
48                                "aria-haspopup": "true"
49                        })
50                        .bind( "keydown.autocomplete", function( event ) {
51                                if ( self.options.disabled || self.element.propAttr( "readOnly" ) ) {
52                                        return;
53                                }
54
55                                suppressKeyPress = false;
56                                var keyCode = $.ui.keyCode;
57                                switch( event.keyCode ) {
58                                case keyCode.PAGE_UP:
59                                        self._move( "previousPage", event );
60                                        break;
61                                case keyCode.PAGE_DOWN:
62                                        self._move( "nextPage", event );
63                                        break;
64                                case keyCode.UP:
65                                        self._move( "previous", event );
66                                        // prevent moving cursor to beginning of text field in some browsers
67                                        event.preventDefault();
68                                        break;
69                                case keyCode.DOWN:
70                                        self._move( "next", event );
71                                        // prevent moving cursor to end of text field in some browsers
72                                        event.preventDefault();
73                                        break;
74                                case keyCode.ENTER:
75                                case keyCode.NUMPAD_ENTER:
76                                        // when menu is open and has focus
77                                        if ( self.menu.active ) {
78                                                // #6055 - Opera still allows the keypress to occur
79                                                // which causes forms to submit
80                                                suppressKeyPress = true;
81                                                event.preventDefault();
82                                        }
83                                        //passthrough - ENTER and TAB both select the current element
84                                case keyCode.TAB:
85                                        if ( !self.menu.active ) {
86                                                return;
87                                        }
88                                        self.menu.select( event );
89                                        break;
90                                case keyCode.ESCAPE:
91                                        self.element.val( self.term );
92                                        self.close( event );
93                                        break;
94                                default:
95                                        // keypress is triggered before the input value is changed
96                                        clearTimeout( self.searching );
97                                        self.searching = setTimeout(function() {
98                                                // only search if the value has changed
99                                                if ( self.term != self.element.val() ) {
100                                                        self.selectedItem = null;
101                                                        self.search( null, event );
102                                                }
103                                        }, self.options.delay );
104                                        break;
105                                }
106                        })
107                        .bind( "keypress.autocomplete", function( event ) {
108                                if ( suppressKeyPress ) {
109                                        suppressKeyPress = false;
110                                        event.preventDefault();
111                                }
112                        })
113                        .bind( "focus.autocomplete", function() {
114                                if ( self.options.disabled ) {
115                                        return;
116                                }
117
118                                self.selectedItem = null;
119                                self.previous = self.element.val();
120                        })
121                        .bind( "blur.autocomplete", function( event ) {
122                                if ( self.options.disabled ) {
123                                        return;
124                                }
125
126                                clearTimeout( self.searching );
127                                // clicks on the menu (or a button to trigger a search) will cause a blur event
128                                self.closing = setTimeout(function() {
129                                        self.close( event );
130                                        self._change( event );
131                                }, 150 );
132                        });
133                this._initSource();
134                this.response = function() {
135                        return self._response.apply( self, arguments );
136                };
137                this.menu = $( "<ul></ul>" )
138                        .addClass( "ui-autocomplete" )
139                        .appendTo( $( this.options.appendTo || "body", doc )[0] )
140                        // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
141                        .mousedown(function( event ) {
142                                // clicking on the scrollbar causes focus to shift to the body
143                                // but we can't detect a mouseup or a click immediately afterward
144                                // so we have to track the next mousedown and close the menu if
145                                // the user clicks somewhere outside of the autocomplete
146                                var menuElement = self.menu.element[ 0 ];
147                                if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
148                                        setTimeout(function() {
149                                                $( document ).one( 'mousedown', function( event ) {
150                                                        if ( event.target !== self.element[ 0 ] &&
151                                                                event.target !== menuElement &&
152                                                                !$.ui.contains( menuElement, event.target ) ) {
153                                                                self.close();
154                                                        }
155                                                });
156                                        }, 1 );
157                                }
158
159                                // use another timeout to make sure the blur-event-handler on the input was already triggered
160                                setTimeout(function() {
161                                        clearTimeout( self.closing );
162                                }, 13);
163                        })
164                        .menu({
165                                focus: function( event, ui ) {
166                                        var item = ui.item.data( "item.autocomplete" );
167                                        if ( false !== self._trigger( "focus", event, { item: item } ) ) {
168                                                // use value to match what will end up in the input, if it was a key event
169                                                if ( /^key/.test(event.originalEvent.type) ) {
170                                                        self.element.val( item.value );
171                                                }
172                                        }
173                                },
174                                selected: function( event, ui ) {
175                                        var item = ui.item.data( "item.autocomplete" ),
176                                                previous = self.previous;
177
178                                        // only trigger when focus was lost (click on menu)
179                                        if ( self.element[0] !== doc.activeElement ) {
180                                                self.element.focus();
181                                                self.previous = previous;
182                                                // #6109 - IE triggers two focus events and the second
183                                                // is asynchronous, so we need to reset the previous
184                                                // term synchronously and asynchronously :-(
185                                                setTimeout(function() {
186                                                        self.previous = previous;
187                                                        self.selectedItem = item;
188                                                }, 1);
189                                        }
190
191                                        if ( false !== self._trigger( "select", event, { item: item } ) ) {
192                                                self.element.val( item.value );
193                                        }
194                                        // reset the term after the select event
195                                        // this allows custom select handling to work properly
196                                        self.term = self.element.val();
197
198                                        self.close( event );
199                                        self.selectedItem = item;
200                                },
201                                blur: function( event, ui ) {
202                                        // don't set the value of the text field if it's already correct
203                                        // this prevents moving the cursor unnecessarily
204                                        if ( self.menu.element.is(":visible") &&
205                                                ( self.element.val() !== self.term ) ) {
206                                                self.element.val( self.term );
207                                        }
208                                }
209                        })
210                        .zIndex( this.element.zIndex() + 1 )
211                        // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
212                        .css({ top: 0, left: 0 })
213                        .hide()
214                        .data( "menu" );
215                if ( $.fn.bgiframe ) {
216                         this.menu.element.bgiframe();
217                }
218                // turning off autocomplete prevents the browser from remembering the
219                // value when navigating through history, so we re-enable autocomplete
220                // if the page is unloaded before the widget is destroyed. #7790
221                self.beforeunloadHandler = function() {
222                        self.element.removeAttr( "autocomplete" );
223                };
224                $( window ).bind( "beforeunload", self.beforeunloadHandler );
225        },
226
227        destroy: function() {
228                this.element
229                        .removeClass( "ui-autocomplete-input" )
230                        .removeAttr( "autocomplete" )
231                        .removeAttr( "role" )
232                        .removeAttr( "aria-autocomplete" )
233                        .removeAttr( "aria-haspopup" );
234                this.menu.element.remove();
235                $( window ).unbind( "beforeunload", this.beforeunloadHandler );
236                $.Widget.prototype.destroy.call( this );
237        },
238
239        _setOption: function( key, value ) {
240                $.Widget.prototype._setOption.apply( this, arguments );
241                if ( key === "source" ) {
242                        this._initSource();
243                }
244                if ( key === "appendTo" ) {
245                        this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
246                }
247                if ( key === "disabled" && value && this.xhr ) {
248                        this.xhr.abort();
249                }
250        },
251
252        _initSource: function() {
253                var self = this,
254                        array,
255                        url;
256                if ( $.isArray(this.options.source) ) {
257                        array = this.options.source;
258                        this.source = function( request, response ) {
259                                response( $.ui.autocomplete.filter(array, request.term) );
260                        };
261                } else if ( typeof this.options.source === "string" ) {
262                        url = this.options.source;
263                        this.source = function( request, response ) {
264                                if ( self.xhr ) {
265                                        self.xhr.abort();
266                                }
267                                self.xhr = $.ajax({
268                                        url: url,
269                                        data: request,
270                                        dataType: "json",
271                                        autocompleteRequest: ++requestIndex,
272                                        success: function( data, status ) {
273                                                if ( this.autocompleteRequest === requestIndex ) {
274                                                        response( data );
275                                                }
276                                        },
277                                        error: function() {
278                                                if ( this.autocompleteRequest === requestIndex ) {
279                                                        response( [] );
280                                                }
281                                        }
282                                });
283                        };
284                } else {
285                        this.source = this.options.source;
286                }
287        },
288
289        search: function( value, event ) {
290                value = value != null ? value : this.element.val();
291
292                // always save the actual value, not the one passed as an argument
293                this.term = this.element.val();
294
295                if ( value.length < this.options.minLength ) {
296                        return this.close( event );
297                }
298
299                clearTimeout( this.closing );
300                if ( this._trigger( "search", event ) === false ) {
301                        return;
302                }
303
304                return this._search( value );
305        },
306
307        _search: function( value ) {
308                this.pending++;
309                this.element.addClass( "ui-autocomplete-loading" );
310
311                this.source( { term: value }, this.response );
312        },
313
314        _response: function( content ) {
315                if ( !this.options.disabled && content && content.length ) {
316                        content = this._normalize( content );
317                        this._suggest( content );
318                        this._trigger( "open" );
319                } else {
320                        this.close();
321                }
322                this.pending--;
323                if ( !this.pending ) {
324                        this.element.removeClass( "ui-autocomplete-loading" );
325                }
326        },
327
328        close: function( event ) {
329                clearTimeout( this.closing );
330                if ( this.menu.element.is(":visible") ) {
331                        this.menu.element.hide();
332                        this.menu.deactivate();
333                        this._trigger( "close", event );
334                }
335        },
336       
337        _change: function( event ) {
338                if ( this.previous !== this.element.val() ) {
339                        this._trigger( "change", event, { item: this.selectedItem } );
340                }
341        },
342
343        _normalize: function( items ) {
344                // assume all items have the right format when the first item is complete
345                if ( items.length && items[0].label && items[0].value ) {
346                        return items;
347                }
348                return $.map( items, function(item) {
349                        if ( typeof item === "string" ) {
350                                return {
351                                        label: item,
352                                        value: item
353                                };
354                        }
355                        return $.extend({
356                                label: item.label || item.value,
357                                value: item.value || item.label
358                        }, item );
359                });
360        },
361
362        _suggest: function( items ) {
363                var ul = this.menu.element
364                        .empty()
365                        .zIndex( this.element.zIndex() + 1 );
366                this._renderMenu( ul, items );
367                // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
368                this.menu.deactivate();
369                this.menu.refresh();
370
371                // size and position menu
372                ul.show();
373                this._resizeMenu();
374                ul.position( $.extend({
375                        of: this.element
376                }, this.options.position ));
377
378                if ( this.options.autoFocus ) {
379                        this.menu.next( new $.Event("mouseover") );
380                }
381        },
382
383        _resizeMenu: function() {
384                var ul = this.menu.element;
385                ul.outerWidth( Math.max(
386                        // Firefox wraps long text (possibly a rounding bug)
387                        // so we add 1px to avoid the wrapping (#7513)
388                        ul.width( "" ).outerWidth() + 1,
389                        this.element.outerWidth()
390                ) );
391        },
392
393        _renderMenu: function( ul, items ) {
394                var self = this;
395                $.each( items, function( index, item ) {
396                        self._renderItem( ul, item );
397                });
398        },
399
400        _renderItem: function( ul, item) {
401                return $( "<li></li>" )
402                        .data( "item.autocomplete", item )
403                        .append( $( "<a></a>" ).text( item.label ) )
404                        .appendTo( ul );
405        },
406
407        _move: function( direction, event ) {
408                if ( !this.menu.element.is(":visible") ) {
409                        this.search( null, event );
410                        return;
411                }
412                if ( this.menu.first() && /^previous/.test(direction) ||
413                                this.menu.last() && /^next/.test(direction) ) {
414                        this.element.val( this.term );
415                        this.menu.deactivate();
416                        return;
417                }
418                this.menu[ direction ]( event );
419        },
420
421        widget: function() {
422                return this.menu.element;
423        }
424});
425
426$.extend( $.ui.autocomplete, {
427        escapeRegex: function( value ) {
428                return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
429        },
430        filter: function(array, term) {
431                var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
432                return $.grep( array, function(value) {
433                        return matcher.test( value.label || value.value || value );
434                });
435        }
436});
437
438}( jQuery ));
439
440/*
441 * jQuery UI Menu (not officially released)
442 *
443 * This widget isn't yet finished and the API is subject to change. We plan to finish
444 * it for the next release. You're welcome to give it a try anyway and give us feedback,
445 * as long as you're okay with migrating your code later on. We can help with that, too.
446 *
447 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
448 * Dual licensed under the MIT or GPL Version 2 licenses.
449 * http://jquery.org/license
450 *
451 * http://docs.jquery.com/UI/Menu
452 *
453 * Depends:
454 *      jquery.ui.core.js
455 *  jquery.ui.widget.js
456 */
457(function($) {
458
459$.widget("ui.menu", {
460        _create: function() {
461                var self = this;
462                this.element
463                        .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
464                        .attr({
465                                role: "listbox",
466                                "aria-activedescendant": "ui-active-menuitem"
467                        })
468                        .click(function( event ) {
469                                if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
470                                        return;
471                                }
472                                // temporary
473                                event.preventDefault();
474                                self.select( event );
475                        });
476                this.refresh();
477        },
478       
479        refresh: function() {
480                var self = this;
481
482                // don't refresh list items that are already adapted
483                var items = this.element.children("li:not(.ui-menu-item):has(a)")
484                        .addClass("ui-menu-item")
485                        .attr("role", "menuitem");
486               
487                items.children("a")
488                        .addClass("ui-corner-all")
489                        .attr("tabindex", -1)
490                        // mouseenter doesn't work with event delegation
491                        .mouseenter(function( event ) {
492                                self.activate( event, $(this).parent() );
493                        })
494                        .mouseleave(function() {
495                                self.deactivate();
496                        });
497        },
498
499        activate: function( event, item ) {
500                this.deactivate();
501                if (this.hasScroll()) {
502                        var offset = item.offset().top - this.element.offset().top,
503                                scroll = this.element.scrollTop(),
504                                elementHeight = this.element.height();
505                        if (offset < 0) {
506                                this.element.scrollTop( scroll + offset);
507                        } else if (offset >= elementHeight) {
508                                this.element.scrollTop( scroll + offset - elementHeight + item.height());
509                        }
510                }
511                this.active = item.eq(0)
512                        .children("a")
513                                .addClass("ui-state-hover")
514                                .attr("id", "ui-active-menuitem")
515                        .end();
516                this._trigger("focus", event, { item: item });
517        },
518
519        deactivate: function() {
520                if (!this.active) { return; }
521
522                this.active.children("a")
523                        .removeClass("ui-state-hover")
524                        .removeAttr("id");
525                this._trigger("blur");
526                this.active = null;
527        },
528
529        next: function(event) {
530                this.move("next", ".ui-menu-item:first", event);
531        },
532
533        previous: function(event) {
534                this.move("prev", ".ui-menu-item:last", event);
535        },
536
537        first: function() {
538                return this.active && !this.active.prevAll(".ui-menu-item").length;
539        },
540
541        last: function() {
542                return this.active && !this.active.nextAll(".ui-menu-item").length;
543        },
544
545        move: function(direction, edge, event) {
546                if (!this.active) {
547                        this.activate(event, this.element.children(edge));
548                        return;
549                }
550                var next = this.active[direction + "All"](".ui-menu-item").eq(0);
551                if (next.length) {
552                        this.activate(event, next);
553                } else {
554                        this.activate(event, this.element.children(edge));
555                }
556        },
557
558        // TODO merge with previousPage
559        nextPage: function(event) {
560                if (this.hasScroll()) {
561                        // TODO merge with no-scroll-else
562                        if (!this.active || this.last()) {
563                                this.activate(event, this.element.children(".ui-menu-item:first"));
564                                return;
565                        }
566                        var base = this.active.offset().top,
567                                height = this.element.height(),
568                                result = this.element.children(".ui-menu-item").filter(function() {
569                                        var close = $(this).offset().top - base - height + $(this).height();
570                                        // TODO improve approximation
571                                        return close < 10 && close > -10;
572                                });
573
574                        // TODO try to catch this earlier when scrollTop indicates the last page anyway
575                        if (!result.length) {
576                                result = this.element.children(".ui-menu-item:last");
577                        }
578                        this.activate(event, result);
579                } else {
580                        this.activate(event, this.element.children(".ui-menu-item")
581                                .filter(!this.active || this.last() ? ":first" : ":last"));
582                }
583        },
584
585        // TODO merge with nextPage
586        previousPage: function(event) {
587                if (this.hasScroll()) {
588                        // TODO merge with no-scroll-else
589                        if (!this.active || this.first()) {
590                                this.activate(event, this.element.children(".ui-menu-item:last"));
591                                return;
592                        }
593
594                        var base = this.active.offset().top,
595                                height = this.element.height();
596                                result = this.element.children(".ui-menu-item").filter(function() {
597                                        var close = $(this).offset().top - base + height - $(this).height();
598                                        // TODO improve approximation
599                                        return close < 10 && close > -10;
600                                });
601
602                        // TODO try to catch this earlier when scrollTop indicates the last page anyway
603                        if (!result.length) {
604                                result = this.element.children(".ui-menu-item:first");
605                        }
606                        this.activate(event, result);
607                } else {
608                        this.activate(event, this.element.children(".ui-menu-item")
609                                .filter(!this.active || this.first() ? ":last" : ":first"));
610                }
611        },
612
613        hasScroll: function() {
614                return this.element.height() < this.element[ $.fn.prop ? "prop" : "attr" ]("scrollHeight");
615        },
616
617        select: function( event ) {
618                this._trigger("selected", event, { item: this.active });
619        }
620});
621
622}(jQuery));
Note: See TracBrowser for help on using the repository browser.