adobe/brackets

View on GitHub
src/extensions/default/JavaScriptQuickEdit/unittest-files/jquery-ui/ui/jquery.ui.autocomplete.js

Summary

Maintainability
D
2 days
Test Coverage
/*!
 * jQuery UI Autocomplete @VERSION
 *
 * Copyright 2012, AUTHORS.txt (http://jqueryui.com/about)
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
 * http://docs.jquery.com/UI/Autocomplete
 *
 * Depends:
 *    jquery.ui.core.js
 *    jquery.ui.widget.js
 *    jquery.ui.position.js
 *    jquery.ui.menu.js
 */
(function( $, undefined ) {

// used to prevent race conditions with remote data sources
var requestIndex = 0;

$.widget( "ui.autocomplete", {
    version: "@VERSION",
    defaultElement: "<input>",
    options: {
        appendTo: "body",
        autoFocus: false,
        delay: 300,
        minLength: 1,
        position: {
            my: "left top",
            at: "left bottom",
            collision: "none"
        },
        source: null,

        // callbacks
        change: null,
        close: null,
        focus: null,
        open: null,
        response: null,
        search: null,
        select: null
    },

    pending: 0,

    _create: function() {
        // Some browsers only repeat keydown events, not keypress events,
        // so we use the suppressKeyPress flag to determine if we've already
        // handled the keydown event. #7269
        // Unfortunately the code for & in keypress is the same as the up arrow,
        // so we use the suppressKeyPressRepeat flag to avoid handling keypress
        // events when we know the keydown event was used to modify the
        // search term. #7799
        var suppressKeyPress, suppressKeyPressRepeat, suppressInput;

        this.isMultiLine = this.element.is( "textarea,[contenteditable]" );
        this.valueMethod = this.element[ this.element.is( "input,textarea" ) ? "val" : "text" ];
        this.isNewMenu = true;

        this.element
            .addClass( "ui-autocomplete-input" )
            .attr( "autocomplete", "off" );

        this._bind({
            keydown: function( event ) {
                if ( this.element.prop( "readOnly" ) ) {
                    suppressKeyPress = true;
                    suppressInput = true;
                    suppressKeyPressRepeat = true;
                    return;
                }

                suppressKeyPress = false;
                suppressInput = false;
                suppressKeyPressRepeat = false;
                var keyCode = $.ui.keyCode;
                switch( event.keyCode ) {
                case keyCode.PAGE_UP:
                    suppressKeyPress = true;
                    this._move( "previousPage", event );
                    break;
                case keyCode.PAGE_DOWN:
                    suppressKeyPress = true;
                    this._move( "nextPage", event );
                    break;
                case keyCode.UP:
                    suppressKeyPress = true;
                    this._keyEvent( "previous", event );
                    break;
                case keyCode.DOWN:
                    suppressKeyPress = true;
                    this._keyEvent( "next", event );
                    break;
                case keyCode.ENTER:
                case keyCode.NUMPAD_ENTER:
                    // when menu is open and has focus
                    if ( this.menu.active ) {
                        // #6055 - Opera still allows the keypress to occur
                        // which causes forms to submit
                        suppressKeyPress = true;
                        event.preventDefault();
                        this.menu.select( event );
                    }
                    break;
                case keyCode.TAB:
                    if ( this.menu.active ) {
                        this.menu.select( event );
                    }
                    break;
                case keyCode.ESCAPE:
                    if ( this.menu.element.is( ":visible" ) ) {
                        this._value( this.term );
                        this.close( event );
                        // Different browsers have different default behavior for escape
                        // Single press can mean undo or clear
                        // Double press in IE means clear the whole form
                        event.preventDefault();
                    }
                    break;
                default:
                    suppressKeyPressRepeat = true;
                    // search timeout should be triggered before the input value is changed
                    this._searchTimeout( event );
                    break;
                }
            },
            keypress: function( event ) {
                if ( suppressKeyPress ) {
                    suppressKeyPress = false;
                    event.preventDefault();
                    return;
                }
                if ( suppressKeyPressRepeat ) {
                    return;
                }

                // replicate some key handlers to allow them to repeat in Firefox and Opera
                var keyCode = $.ui.keyCode;
                switch( event.keyCode ) {
                case keyCode.PAGE_UP:
                    this._move( "previousPage", event );
                    break;
                case keyCode.PAGE_DOWN:
                    this._move( "nextPage", event );
                    break;
                case keyCode.UP:
                    this._keyEvent( "previous", event );
                    break;
                case keyCode.DOWN:
                    this._keyEvent( "next", event );
                    break;
                }
            },
            input: function( eventĀ ) {
                if ( suppressInput ) {
                    suppressInput = false;
                    event.preventDefault();
                    return;
                }
                this._searchTimeout( event );
            },
            focus: function() {
                this.selectedItem = null;
                this.previous = this._value();
            },
            blur: function( event ) {
                if ( this.cancelBlur ) {
                    delete this.cancelBlur;
                    return;
                }

                clearTimeout( this.searching );
                this.close( event );
                this._change( event );
            }
        });

        this._initSource();
        this.menu = $( "<ul>" )
            .addClass( "ui-autocomplete" )
            .appendTo( this.document.find( this.options.appendTo || "body" )[ 0 ] )
            .menu({
                // custom key handling for now
                input: $(),
                // disable ARIA support, the live region takes care of that
                role: null
            })
            .zIndex( this.element.zIndex() + 1 )
            .hide()
            .data( "menu" );
        this._bind( this.menu.element, {
            mousedown: function( event ) {
                // prevent moving focus out of the text field
                event.preventDefault();

                // IE doesn't prevent moving focus even with event.preventDefault()
                // so we set a flag to know when we should ignore the blur event
                this.cancelBlur = true;
                this._delay(function() {
                    delete this.cancelBlur;
                });

                // clicking on the scrollbar causes focus to shift to the body
                // but we can't detect a mouseup or a click immediately afterward
                // so we have to track the next mousedown and close the menu if
                // the user clicks somewhere outside of the autocomplete
                var menuElement = this.menu.element[ 0 ];
                if ( !$( event.target ).closest( ".ui-menu-item" ).length ) {
                    this._delay(function() {
                        var that = this;
                        this.document.one( "mousedown", function( event ) {
                            if ( event.target !== that.element[ 0 ] &&
                                    event.target !== menuElement &&
                                    !$.contains( menuElement, event.target ) ) {
                                that.close();
                            }
                        });
                    });
                }
            },
            menufocus: function( event, ui ) {
                // #7024 - Prevent accidental activation of menu items in Firefox
                if ( this.isNewMenu ) {
                    this.isNewMenu = false;
                    if ( event.originalEvent && /^mouse/.test( event.originalEvent.type ) ) {
                        this.menu.blur();

                        this.document.one( "mousemove", function() {
                            $( event.target ).trigger( event.originalEvent );
                        });

                        return;
                    }
                }

                // back compat for _renderItem using item.autocomplete, via #7810
                // TODO remove the fallback, see #8156
                var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" );
                if ( false !== this._trigger( "focus", event, { item: item } ) ) {
                    // use value to match what will end up in the input, if it was a key event
                    if ( event.originalEvent && /^key/.test( event.originalEvent.type ) ) {
                        this._value( item.value );
                    }
                } else {
                    // Normally the input is populated with the item's value as the
                    // menu is navigated, causing screen readers to notice a change and
                    // announce the item. Since the focus event was canceled, this doesn't
                    // happen, so we update the live region so that screen readers can
                    // still notice the change and announce it.
                    this.liveRegion.text( item.value );
                }
            },
            menuselect: function( event, ui ) {
                // back compat for _renderItem using item.autocomplete, via #7810
                // TODO remove the fallback, see #8156
                var item = ui.item.data( "ui-autocomplete-item" ) || ui.item.data( "item.autocomplete" ),
                    previous = this.previous;

                // only trigger when focus was lost (click on menu)
                if ( this.element[0] !== this.document[0].activeElement ) {
                    this.element.focus();
                    this.previous = previous;
                    // #6109 - IE triggers two focus events and the second
                    // is asynchronous, so we need to reset the previous
                    // term synchronously and asynchronously :-(
                    this._delay(function() {
                        this.previous = previous;
                        this.selectedItem = item;
                    });
                }

                if ( false !== this._trigger( "select", event, { item: item } ) ) {
                    this._value( item.value );
                }
                // reset the term after the select event
                // this allows custom select handling to work properly
                this.term = this._value();

                this.close( event );
                this.selectedItem = item;
            }
        });

        this.liveRegion = $( "<span>", {
                role: "status",
                "aria-live": "polite"
            })
            .addClass( "ui-helper-hidden-accessible" )
            .insertAfter( this.element );

        if ( $.fn.bgiframe ) {
             this.menu.element.bgiframe();
        }

        // turning off autocomplete prevents the browser from remembering the
        // value when navigating through history, so we re-enable autocomplete
        // if the page is unloaded before the widget is destroyed. #7790
        this._bind( this.window, {
            beforeunload: function() {
                this.element.removeAttr( "autocomplete" );
            }
        });
    },

    _destroy: function() {
        clearTimeout( this.searching );
        this.element
            .removeClass( "ui-autocomplete-input" )
            .removeAttr( "autocomplete" );
        this.menu.element.remove();
        this.liveRegion.remove();
    },

    _setOption: function( key, value ) {
        this._super( key, value );
        if ( key === "source" ) {
            this._initSource();
        }
        if ( key === "appendTo" ) {
            this.menu.element.appendTo( this.document.find( value || "body" )[0] );
        }
        if ( key === "disabled" && value && this.xhr ) {
            this.xhr.abort();
        }
    },

    _initSource: function() {
        var array, url,
            that = this;
        if ( $.isArray(this.options.source) ) {
            array = this.options.source;
            this.source = function( request, response ) {
                response( $.ui.autocomplete.filter( array, request.term ) );
            };
        } else if ( typeof this.options.source === "string" ) {
            url = this.options.source;
            this.source = function( request, response ) {
                if ( that.xhr ) {
                    that.xhr.abort();
                }
                that.xhr = $.ajax({
                    url: url,
                    data: request,
                    dataType: "json",
                    success: function( data, status ) {
                        response( data );
                    },
                    error: function() {
                        response( [] );
                    }
                });
            };
        } else {
            this.source = this.options.source;
        }
    },

    _searchTimeout: function( event ) {
        clearTimeout( this.searching );
        this.searching = this._delay(function() {
            // only search if the value has changed
            if ( this.term !== this._value() ) {
                this.selectedItem = null;
                this.search( null, event );
            }
        }, this.options.delay );
    },

    search: function( value, event ) {
        value = value != null ? value : this._value();

        // always save the actual value, not the one passed as an argument
        this.term = this._value();

        if ( value.length < this.options.minLength ) {
            return this.close( event );
        }

        if ( this._trigger( "search", event ) === false ) {
            return;
        }

        return this._search( value );
    },

    _search: function( value ) {
        this.pending++;
        this.element.addClass( "ui-autocomplete-loading" );
        this.cancelSearch = false;

        this.source( { term: value }, this._response() );
    },

    _response: function() {
        var that = this,
            index = ++requestIndex;

        return function( content ) {
            if ( index === requestIndex ) {
                that.__response( content );
            }

            that.pending--;
            if ( !that.pending ) {
                that.element.removeClass( "ui-autocomplete-loading" );
            }
        };
    },

    __response: function( content ) {
        if ( content ) {
            content = this._normalize( content );
        }
        this._trigger( "response", null, { content: content } );
        if ( !this.options.disabled && content && content.length && !this.cancelSearch ) {
            this._suggest( content );
            this._trigger( "open" );
        } else {
            // use ._close() instead of .close() so we don't cancel future searches
            this._close();
        }
    },

    close: function( event ) {
        this.cancelSearch = true;
        this._close( event );
    },

    _close: function( event ) {
        if ( this.menu.element.is( ":visible" ) ) {
            this.menu.element.hide();
            this.menu.blur();
            this.isNewMenu = true;
            this._trigger( "close", event );
        }
    },

    _change: function( event ) {
        if ( this.previous !== this._value() ) {
            this._trigger( "change", event, { item: this.selectedItem } );
        }
    },

    _normalize: function( items ) {
        // assume all items have the right format when the first item is complete
        if ( items.length && items[0].label && items[0].value ) {
            return items;
        }
        return $.map( items, function( item ) {
            if ( typeof item === "string" ) {
                return {
                    label: item,
                    value: item
                };
            }
            return $.extend({
                label: item.label || item.value,
                value: item.value || item.label
            }, item );
        });
    },

    _suggest: function( items ) {
        var ul = this.menu.element
            .empty()
            .zIndex( this.element.zIndex() + 1 );
        this._renderMenu( ul, items );
        // TODO refresh should check if the active item is still in the dom, removing the need for a manual blur
        this.menu.blur();
        this.menu.refresh();

        // size and position menu
        ul.show();
        this._resizeMenu();
        ul.position( $.extend({
            of: this.element
        }, this.options.position ));

        if ( this.options.autoFocus ) {
            this.menu.next();
        }
    },

    _resizeMenu: function() {
        var ul = this.menu.element;
        ul.outerWidth( Math.max(
            // Firefox wraps long text (possibly a rounding bug)
            // so we add 1px to avoid the wrapping (#7513)
            ul.width( "" ).outerWidth() + 1,
            this.element.outerWidth()
        ) );
    },

    _renderMenu: function( ul, items ) {
        var that = this;
        $.each( items, function( index, item ) {
            that._renderItemData( ul, item );
        });
    },

    _renderItemData: function( ul, item ) {
        return this._renderItem( ul, item ).data( "ui-autocomplete-item", item );
    },

    _renderItem: function( ul, item ) {
        return $( "<li>" )
            .append( $( "<a>" ).text( item.label ) )
            .appendTo( ul );
    },

    _move: function( direction, event ) {
        if ( !this.menu.element.is( ":visible" ) ) {
            this.search( null, event );
            return;
        }
        if ( this.menu.isFirstItem() && /^previous/.test( direction ) ||
                this.menu.isLastItem() && /^next/.test( direction ) ) {
            this._value( this.term );
            this.menu.blur();
            return;
        }
        this.menu[ direction ]( event );
    },

    widget: function() {
        return this.menu.element;
    },

    _value: function( value ) {
        return this.valueMethod.apply( this.element, arguments );
    },

    _keyEvent: function( keyEvent, event ) {
        if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) {
            this._move( keyEvent, event );

            // prevents moving cursor to beginning/end of the text field in some browsers
            event.preventDefault();
        }
    }
});

$.extend( $.ui.autocomplete, {
    escapeRegex: function( value ) {
        return value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
    },
    filter: function(array, term) {
        var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
        return $.grep( array, function(value) {
            return matcher.test( value.label || value.value || value );
        });
    }
});


// live region extension, adding a `messages` option
// NOTE: This is an experimental API. We are still investigating
// a full solution for string manipulation and internationalization.
$.widget( "ui.autocomplete", $.ui.autocomplete, {
    options: {
        messages: {
            noResults: "No search results.",
            results: function(amount) {
                return amount + ( amount > 1 ? " results are" : " result is" ) +
                    " available, use up and down arrow keys to navigate.";
            }
        }
    },

    __response: function( content ) {
        var message;
        this._superApply( arguments );
        if ( this.options.disabled || this.cancelSearch) {
            return;
        }
        if ( content && content.length ) {
            message = this.options.messages.results( content.length );
        } else {
            message = this.options.messages.noResults;
        }
        this.liveRegion.text( message );
    }
});


}( jQuery ));