#

API Docs for: 3.10.3
Show: Inherited Protected Private Deprecated

# File: autocomplete/js/autocomplete-list.js

    /**
    Traditional autocomplete dropdown list widget, just like Mom used to make.

    @module autocomplete
    @submodule autocomplete-list
    **/

    /**
    Traditional autocomplete dropdown list widget, just like Mom used to make.

    @class AutoCompleteList
    @extends Widget
    @uses AutoCompleteBase
    @uses WidgetPosition
    @uses WidgetPositionAlign
    @constructor
    @param {Object} config Configuration object.
    **/

    var Lang   = Y.Lang,
        Node   = Y.Node,
        YArray = Y.Array,

        // Whether or not we need an iframe shim.
        useShim = Y.UA.ie && Y.UA.ie < 7,

        // keyCode constants.
        KEY_TAB = 9,

        // String shorthand.
        _CLASS_ITEM        = '_CLASS_ITEM',
        _CLASS_ITEM_ACTIVE = '_CLASS_ITEM_ACTIVE',
        _CLASS_ITEM_HOVER  = '_CLASS_ITEM_HOVER',
        _SELECTOR_ITEM     = '_SELECTOR_ITEM',

        ACTIVE_ITEM      = 'activeItem',
        ALWAYS_SHOW_LIST = 'alwaysShowList',
        CIRCULAR         = 'circular',
        HOVERED_ITEM     = 'hoveredItem',
        ID               = 'id',
        ITEM             = 'item',
        LIST             = 'list',
        RESULT           = 'result',
        RESULTS          = 'results',
        VISIBLE          = 'visible',
        WIDTH            = 'width',

        // Event names.
        EVT_SELECT = 'select',

    List = Y.Base.create('autocompleteList', Y.Widget, [
        Y.AutoCompleteBase,
        Y.WidgetPosition,
        Y.WidgetPositionAlign
    ], {
        // -- Prototype Properties -------------------------------------------------
        ARIA_TEMPLATE: '<div/>',
        ITEM_TEMPLATE: '<li/>',
        LIST_TEMPLATE: '<ul/>',

        // Widget automatically attaches delegated event handlers to everything in
        // Y.Node.DOM_EVENTS, including synthetic events. Since Widget's event
        // delegation won't work for the synthetic valuechange event, and since
        // it creates a name collision between the backcompat "valueChange" synth
        // event alias and AutoCompleteList's "valueChange" event for the "value"
        // attr, this hack is necessary in order to prevent Widget from attaching
        // valuechange handlers.
        UI_EVENTS: (function () {
            var uiEvents = Y.merge(Y.Node.DOM_EVENTS);

            delete uiEvents.valuechange;
            delete uiEvents.valueChange;

            return uiEvents;
        }()),

        // -- Lifecycle Prototype Methods ------------------------------------------
        initializer: function () {
            var inputNode = this.get('inputNode');

            if (!inputNode) {
                Y.error('No inputNode specified.');
                return;
            }

            this._inputNode  = inputNode;
            this._listEvents = [];

            // This ensures that the list is rendered inside the same parent as the
            // input node by default, which is necessary for proper ARIA support.
            this.DEF_PARENT_NODE = inputNode.get('parentNode');

            // Cache commonly used classnames and selectors for performance.
            this[_CLASS_ITEM]        = this.getClassName(ITEM);
            this[_CLASS_ITEM_ACTIVE] = this.getClassName(ITEM, 'active');
            this[_CLASS_ITEM_HOVER]  = this.getClassName(ITEM, 'hover');
            this[_SELECTOR_ITEM]     = '.' + this[_CLASS_ITEM];

            /**
            Fires when an autocomplete suggestion is selected from the list,
            typically via a keyboard action or mouse click.

            @event select
            @param {Node} itemNode List item node that was selected.
            @param {Object} result AutoComplete result object.
            @preventable _defSelectFn
            **/
            this.publish(EVT_SELECT, {
                defaultFn: this._defSelectFn
            });
        },

        destructor: function () {
            while (this._listEvents.length) {
                this._listEvents.pop().detach();
            }

            if (this._ariaNode) {
                this._ariaNode.remove().destroy(true);
            }
        },

        bindUI: function () {
            this._bindInput();
            this._bindList();
        },

        renderUI: function () {
            var ariaNode    = this._createAriaNode(),
                boundingBox = this.get('boundingBox'),
                contentBox  = this.get('contentBox'),
                inputNode   = this._inputNode,
                listNode    = this._createListNode(),
                parentNode  = inputNode.get('parentNode');

            inputNode.addClass(this.getClassName('input')).setAttrs({
                'aria-autocomplete': LIST,
                'aria-expanded'    : false,
                'aria-owns'        : listNode.get('id')
            });

            // ARIA node must be outside the widget or announcements won't be made
            // when the widget is hidden.
            parentNode.append(ariaNode);

            // Add an iframe shim for IE6.
            if (useShim) {
                boundingBox.plug(Y.Plugin.Shim);
            }

            this._ariaNode    = ariaNode;
            this._boundingBox = boundingBox;
            this._contentBox  = contentBox;
            this._listNode    = listNode;
            this._parentNode  = parentNode;
        },

        syncUI: function () {
            // No need to call _syncPosition() here; the other _sync methods will
            // call it when necessary.
            this._syncResults();
            this._syncVisibility();
        },

        // -- Public Prototype Methods ---------------------------------------------

        /**
        Hides the list, unless the `alwaysShowList` attribute is `true`.

        @method hide
        @see show
        @chainable
        **/
        hide: function () {
            return this.get(ALWAYS_SHOW_LIST) ? this : this.set(VISIBLE, false);
        },

        /**
        Selects the specified _itemNode_, or the current `activeItem` if _itemNode_
        is not specified.

        @method selectItem
        @param {Node} [itemNode] Item node to select.
        @param {EventFacade} [originEvent] Event that triggered the selection, if
            any.
        @chainable
        **/
        selectItem: function (itemNode, originEvent) {
            if (itemNode) {
                if (!itemNode.hasClass(this[_CLASS_ITEM])) {
                    return this;
                }
            } else {
                itemNode = this.get(ACTIVE_ITEM);

                if (!itemNode) {
                    return this;
                }
            }

            this.fire(EVT_SELECT, {
                itemNode   : itemNode,
                originEvent: originEvent || null,
                result     : itemNode.getData(RESULT)
            });

            return this;
        },

        // -- Protected Prototype Methods ------------------------------------------

        /**
        Activates the next item after the currently active item. If there is no next
        item and the `circular` attribute is `true`, focus will wrap back to the
        input node.

        @method _activateNextItem
        @chainable
        @protected
        **/
        _activateNextItem: function () {
            var item = this.get(ACTIVE_ITEM),
                nextItem;

            if (item) {
                nextItem = item.next(this[_SELECTOR_ITEM]) ||
                        (this.get(CIRCULAR) ? null : item);
            } else {
                nextItem = this._getFirstItemNode();
            }

            this.set(ACTIVE_ITEM, nextItem);

            return this;
        },

        /**
        Activates the item previous to the currently active item. If there is no
        previous item and the `circular` attribute is `true`, focus will wrap back
        to the input node.

        @method _activatePrevItem
        @chainable
        @protected
        **/
        _activatePrevItem: function () {
            var item     = this.get(ACTIVE_ITEM),
                prevItem = item ? item.previous(this[_SELECTOR_ITEM]) :
                        this.get(CIRCULAR) && this._getLastItemNode();

            this.set(ACTIVE_ITEM, prevItem || null);

            return this;
        },

        /**
        Appends the specified result _items_ to the list inside a new item node.

        @method _add
        @param {Array|Node|HTMLElement|String} items Result item or array of
            result items.
        @return {NodeList} Added nodes.
        @protected
        **/
        _add: function (items) {
            var itemNodes = [];

            YArray.each(Lang.isArray(items) ? items : [items], function (item) {
                itemNodes.push(this._createItemNode(item).setData(RESULT, item));
            }, this);

            itemNodes = Y.all(itemNodes);
            this._listNode.append(itemNodes.toFrag());

            return itemNodes;
        },

        /**
        Updates the ARIA live region with the specified message.

        @method _ariaSay
        @param {String} stringId String id (from the `strings` attribute) of the
            message to speak.
        @param {Object} [subs] Substitutions for placeholders in the string.
        @protected
        **/
        _ariaSay: function (stringId, subs) {
            var message = this.get('strings.' + stringId);
            this._ariaNode.set('text', subs ? Lang.sub(message, subs) : message);
        },

        /**
        Binds `inputNode` events and behavior.

        @method _bindInput
        @protected
        **/
        _bindInput: function () {
            var inputNode = this._inputNode,
                alignNode, alignWidth, tokenInput;

            // Null align means we can auto-align. Set align to false to prevent
            // auto-alignment, or a valid alignment config to customize the
            // alignment.
            if (this.get('align') === null) {
                // If this is a tokenInput, align with its bounding box.
                // Otherwise, align with the inputNode. Bit of a cheat.
                tokenInput = this.get('tokenInput');
                alignNode  = (tokenInput && tokenInput.get('boundingBox')) || inputNode;

                this.set('align', {
                    node  : alignNode,
                    points: ['tl', 'bl']
                });

                // If no width config is set, attempt to set the list's width to the
                // width of the alignment node. If the alignment node's width is
                // falsy, do nothing.
                if (!this.get(WIDTH) && (alignWidth = alignNode.get('offsetWidth'))) {
                    this.set(WIDTH, alignWidth);
                }
            }

            // Attach inputNode events.
            this._listEvents = this._listEvents.concat([
                inputNode.after('blur',  this._afterListInputBlur, this),
                inputNode.after('focus', this._afterListInputFocus, this)
            ]);
        },

        /**
        Binds list events.

        @method _bindList
        @protected
        **/
        _bindList: function () {
            this._listEvents = this._listEvents.concat([
                Y.one('doc').after('click', this._afterDocClick, this),
                Y.one('win').after('windowresize', this._syncPosition, this),

                this.after({
                    mouseover: this._afterMouseOver,
                    mouseout : this._afterMouseOut,

                    activeItemChange    : this._afterActiveItemChange,
                    alwaysShowListChange: this._afterAlwaysShowListChange,
                    hoveredItemChange   : this._afterHoveredItemChange,
                    resultsChange       : this._afterResultsChange,
                    visibleChange       : this._afterVisibleChange
                }),

                this._listNode.delegate('click', this._onItemClick,
                        this[_SELECTOR_ITEM], this)
            ]);
        },

        /**
        Clears the contents of the tray.

        @method _clear
        @protected
        **/
        _clear: function () {
            this.set(ACTIVE_ITEM, null);
            this._set(HOVERED_ITEM, null);

            this._listNode.get('children').remove(true);
        },

        /**
        Creates and returns an ARIA live region node.

        @method _createAriaNode
        @return {Node} ARIA node.
        @protected
        **/
        _createAriaNode: function () {
            var ariaNode = Node.create(this.ARIA_TEMPLATE);

            return ariaNode.addClass(this.getClassName('aria')).setAttrs({
                'aria-live': 'polite',
                role       : 'status'
            });
        },

        /**
        Creates and returns an item node with the specified _content_.

        @method _createItemNode
        @param {Object} result Result object.
        @return {Node} Item node.
        @protected
        **/
        _createItemNode: function (result) {
            var itemNode = Node.create(this.ITEM_TEMPLATE);

            return itemNode.addClass(this[_CLASS_ITEM]).setAttrs({
                id  : Y.stamp(itemNode),
                role: 'option'
            }).setAttribute('data-text', result.text).append(result.display);
        },

        /**
        Creates and returns a list node. If the `listNode` attribute is already set
        to an existing node, that node will be used.

        @method _createListNode
        @return {Node} List node.
        @protected
        **/
        _createListNode: function () {
            var listNode = this.get('listNode') || Node.create(this.LIST_TEMPLATE);

            listNode.addClass(this.getClassName(LIST)).setAttrs({
                id  : Y.stamp(listNode),
                role: 'listbox'
            });

            this._set('listNode', listNode);
            this.get('contentBox').append(listNode);

            return listNode;
        },

        /**
        Gets the first item node in the list, or `null` if the list is empty.

        @method _getFirstItemNode
        @return {Node|null}
        @protected
        **/
        _getFirstItemNode: function () {
            return this._listNode.one(this[_SELECTOR_ITEM]);
        },

        /**
        Gets the last item node in the list, or `null` if the list is empty.

        @method _getLastItemNode
        @return {Node|null}
        @protected
        **/
        _getLastItemNode: function () {
            return this._listNode.one(this[_SELECTOR_ITEM] + ':last-child');
        },

        /**
        Synchronizes the result list's position and alignment.

        @method _syncPosition
        @protected
        **/
        _syncPosition: function () {
            // Force WidgetPositionAlign to refresh its alignment.
            this._syncUIPosAlign();

            // Resize the IE6 iframe shim to match the list's dimensions.
            this._syncShim();
        },

        /**
        Synchronizes the results displayed in the list with those in the _results_
        argument, or with the `results` attribute if an argument is not provided.

        @method _syncResults
        @param {Array} [results] Results.
        @protected
        **/
        _syncResults: function (results) {
            if (!results) {
                results = this.get(RESULTS);
            }

            this._clear();

            if (results.length) {
                this._add(results);
                this._ariaSay('items_available');
            }

            this._syncPosition();

            if (this.get('activateFirstItem') && !this.get(ACTIVE_ITEM)) {
                this.set(ACTIVE_ITEM, this._getFirstItemNode());
            }
        },

        /**
        Synchronizes the size of the iframe shim used for IE6 and lower. In other
        browsers, this method is a noop.

        @method _syncShim
        @protected
        **/
        _syncShim: useShim ? function () {
            var shim = this._boundingBox.shim;

            if (shim) {
                shim.sync();
            }
        } : function () {},

        /**
        Synchronizes the visibility of the tray with the _visible_ argument, or with
        the `visible` attribute if an argument is not provided.

        @method _syncVisibility
        @param {Boolean} [visible] Visibility.
        @protected
        **/
        _syncVisibility: function (visible) {
            if (this.get(ALWAYS_SHOW_LIST)) {
                visible = true;
                this.set(VISIBLE, visible);
            }

            if (typeof visible === 'undefined') {
                visible = this.get(VISIBLE);
            }

            this._inputNode.set('aria-expanded', visible);
            this._boundingBox.set('aria-hidden', !visible);

            if (visible) {
                this._syncPosition();
            } else {
                this.set(ACTIVE_ITEM, null);
                this._set(HOVERED_ITEM, null);

                // Force a reflow to work around a glitch in IE6 and 7 where some of
                // the contents of the list will sometimes remain visible after the
                // container is hidden.
                this._boundingBox.get('offsetWidth');
            }

            // In some pages, IE7 fails to repaint the contents of the list after it
            // becomes visible. Toggling a bogus class on the body forces a repaint
            // that fixes the issue.
            if (Y.UA.ie === 7) {
                // Note: We don't actually need to use ClassNameManager here. This
                // class isn't applying any actual styles; it's just frobbing the
                // body element to force a repaint. The actual class name doesn't
                // really matter.
                Y.one('body')
                    .addClass('yui3-ie7-sucks')
                    .removeClass('yui3-ie7-sucks');
            }
        },

        // -- Protected Event Handlers ---------------------------------------------

        /**
        Handles `activeItemChange` events.

        @method _afterActiveItemChange
        @param {EventFacade} e
        @protected
        **/
        _afterActiveItemChange: function (e) {
            var inputNode = this._inputNode,
                newVal    = e.newVal,
                prevVal   = e.prevVal,
                node;

            // The previous item may have disappeared by the time this handler runs,
            // so we need to be careful.
            if (prevVal && prevVal._node) {
                prevVal.removeClass(this[_CLASS_ITEM_ACTIVE]);
            }

            if (newVal) {
                newVal.addClass(this[_CLASS_ITEM_ACTIVE]);
                inputNode.set('aria-activedescendant', newVal.get(ID));
            } else {
                inputNode.removeAttribute('aria-activedescendant');
            }

            if (this.get('scrollIntoView')) {
                node = newVal || inputNode;

                if (!node.inRegion(Y.DOM.viewportRegion(), true)
                        || !node.inRegion(this._contentBox, true)) {

                    node.scrollIntoView();
                }
            }
        },

        /**
        Handles `alwaysShowListChange` events.

        @method _afterAlwaysShowListChange
        @param {EventFacade} e
        @protected
        **/
        _afterAlwaysShowListChange: function (e) {
            this.set(VISIBLE, e.newVal || this.get(RESULTS).length > 0);
        },

        /**
        Handles click events on the document. If the click is outside both the
        input node and the bounding box, the list will be hidden.

        @method _afterDocClick
        @param {EventFacade} e
        @protected
        @since 3.5.0
        **/
        _afterDocClick: function (e) {
            var boundingBox = this._boundingBox,
                target      = e.target;

            if(target !== this._inputNode && target !== boundingBox &&
                    target.ancestor('#' + boundingBox.get('id'), true)){
                this.hide();
            }
        },

        /**
        Handles `hoveredItemChange` events.

        @method _afterHoveredItemChange
        @param {EventFacade} e
        @protected
        **/
        _afterHoveredItemChange: function (e) {
            var newVal  = e.newVal,
                prevVal = e.prevVal;

            if (prevVal) {
                prevVal.removeClass(this[_CLASS_ITEM_HOVER]);
            }

            if (newVal) {
                newVal.addClass(this[_CLASS_ITEM_HOVER]);
            }
        },

        /**
        Handles `inputNode` blur events.

        @method _afterListInputBlur
        @protected
        **/
        _afterListInputBlur: function () {
            this._listInputFocused = false;

            if (this.get(VISIBLE) &&
                    !this._mouseOverList &&
                    (this._lastInputKey !== KEY_TAB ||
                        !this.get('tabSelect') ||
                        !this.get(ACTIVE_ITEM))) {
                this.hide();
            }
        },

        /**
        Handles `inputNode` focus events.

        @method _afterListInputFocus
        @protected
        **/
        _afterListInputFocus: function () {
            this._listInputFocused = true;
        },

        /**
        Handles `mouseover` events.

        @method _afterMouseOver
        @param {EventFacade} e
        @protected
        **/
        _afterMouseOver: function (e) {
            var itemNode = e.domEvent.target.ancestor(this[_SELECTOR_ITEM], true);

            this._mouseOverList = true;

            if (itemNode) {
                this._set(HOVERED_ITEM, itemNode);
            }
        },

        /**
        Handles `mouseout` events.

        @method _afterMouseOut
        @param {EventFacade} e
        @protected
        **/
        _afterMouseOut: function () {
            this._mouseOverList = false;
            this._set(HOVERED_ITEM, null);
        },

        /**
        Handles `resultsChange` events.

        @method _afterResultsChange
        @param {EventFacade} e
        @protected
        **/
        _afterResultsChange: function (e) {
            this._syncResults(e.newVal);

            if (!this.get(ALWAYS_SHOW_LIST)) {
                this.set(VISIBLE, !!e.newVal.length);
            }
        },

        /**
        Handles `visibleChange` events.

        @method _afterVisibleChange
        @param {EventFacade} e
        @protected
        **/
        _afterVisibleChange: function (e) {
            this._syncVisibility(!!e.newVal);
        },

        /**
        Delegated event handler for item `click` events.

        @method _onItemClick
        @param {EventFacade} e
        @protected
        **/
        _onItemClick: function (e) {
            var itemNode = e.currentTarget;

            this.set(ACTIVE_ITEM, itemNode);
            this.selectItem(itemNode, e);
        },

        // -- Protected Default Event Handlers -------------------------------------

        /**
        Default `select` event handler.

        @method _defSelectFn
        @param {EventFacade} e
        @protected
        **/
        _defSelectFn: function (e) {
            var text = e.result.text;

            // TODO: support typeahead completion, etc.
            this._inputNode.focus();
            this._updateValue(text);
            this._ariaSay('item_selected', {item: text});
            this.hide();
        }
    }, {
        ATTRS: {
            /**
            If `true`, the first item in the list will be activated by default when
            the list is initially displayed and when results change.

            @attribute activateFirstItem
            @type Boolean
            @default false
            **/
            activateFirstItem: {
                value: false
            },

            /**
            Item that's currently active, if any. When the user presses enter, this
            is the item that will be selected.

            @attribute activeItem
            @type Node
            **/
            activeItem: {
                setter: Y.one,
                value: null
            },

            /**
            If `true`, the list will remain visible even when there are no results
            to display.

            @attribute alwaysShowList
            @type Boolean
            @default false
            **/
            alwaysShowList: {
                value: false
            },

            /**
            If `true`, keyboard navigation will wrap around to the opposite end of
            the list when navigating past the first or last item.

            @attribute circular
            @type Boolean
            @default true
            **/
            circular: {
                value: true
            },

            /**
            Item currently being hovered over by the mouse, if any.

            @attribute hoveredItem
            @type Node|null
            @readOnly
            **/
            hoveredItem: {
                readOnly: true,
                value: null
            },

            /**
            Node that will contain result items.

            @attribute listNode
            @type Node|null
            @initOnly
            **/
            listNode: {
                writeOnce: 'initOnly',
                value: null
            },

            /**
            If `true`, the viewport will be scrolled to ensure that the active list
            item is visible when necessary.

            @attribute scrollIntoView
            @type Boolean
            @default false
            **/
            scrollIntoView: {
                value: false
            },

            /**
            Translatable strings used by the AutoCompleteList widget.

            @attribute strings
            @type Object
            **/
            strings: {
                valueFn: function () {
                    return Y.Intl.get('autocomplete-list');
                }
            },

            /**
            If `true`, pressing the tab key while the list is visible will select
            the active item, if any.

            @attribute tabSelect
            @type Boolean
            @default true
            **/
            tabSelect: {
                value: true
            },

            // The "visible" attribute is documented in Widget.
            visible: {
                value: false
            }
        },

        CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
    });

    Y.AutoCompleteList = List;

    /**
    Alias for <a href="AutoCompleteList.html">`AutoCompleteList`</a>. See that class
    for API docs.

    @class AutoComplete
    **/

    Y.AutoComplete = List;