2 YUI 3.13.0 (build 508226d)
3 Copyright 2013 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
8 YUI.add('autocomplete-list', function (Y, NAME) {
11 Traditional autocomplete dropdown list widget, just like Mom used to make.
14 @submodule autocomplete-list
18 Traditional autocomplete dropdown list widget, just like Mom used to make.
20 @class AutoCompleteList
22 @uses AutoCompleteBase
24 @uses WidgetPositionAlign
26 @param {Object} config Configuration object.
33 // Whether or not we need an iframe shim.
34 useShim = Y.UA.ie && Y.UA.ie < 7,
40 _CLASS_ITEM = '_CLASS_ITEM',
41 _CLASS_ITEM_ACTIVE = '_CLASS_ITEM_ACTIVE',
42 _CLASS_ITEM_HOVER = '_CLASS_ITEM_HOVER',
43 _SELECTOR_ITEM = '_SELECTOR_ITEM',
45 ACTIVE_ITEM = 'activeItem',
46 ALWAYS_SHOW_LIST = 'alwaysShowList',
47 CIRCULAR = 'circular',
48 HOVERED_ITEM = 'hoveredItem',
58 EVT_SELECT = 'select',
60 List = Y.Base.create('autocompleteList', Y.Widget, [
65 // -- Prototype Properties -------------------------------------------------
66 ARIA_TEMPLATE: '<div/>',
67 ITEM_TEMPLATE: '<li/>',
68 LIST_TEMPLATE: '<ul/>',
70 // Widget automatically attaches delegated event handlers to everything in
71 // Y.Node.DOM_EVENTS, including synthetic events. Since Widget's event
72 // delegation won't work for the synthetic valuechange event, and since
73 // it creates a name collision between the backcompat "valueChange" synth
74 // event alias and AutoCompleteList's "valueChange" event for the "value"
75 // attr, this hack is necessary in order to prevent Widget from attaching
76 // valuechange handlers.
77 UI_EVENTS: (function () {
78 var uiEvents = Y.merge(Y.Node.DOM_EVENTS);
80 delete uiEvents.valuechange;
81 delete uiEvents.valueChange;
86 // -- Lifecycle Prototype Methods ------------------------------------------
87 initializer: function () {
88 var inputNode = this.get('inputNode');
91 Y.error('No inputNode specified.');
95 this._inputNode = inputNode;
96 this._listEvents = [];
98 // This ensures that the list is rendered inside the same parent as the
99 // input node by default, which is necessary for proper ARIA support.
100 this.DEF_PARENT_NODE = inputNode.get('parentNode');
102 // Cache commonly used classnames and selectors for performance.
103 this[_CLASS_ITEM] = this.getClassName(ITEM);
104 this[_CLASS_ITEM_ACTIVE] = this.getClassName(ITEM, 'active');
105 this[_CLASS_ITEM_HOVER] = this.getClassName(ITEM, 'hover');
106 this[_SELECTOR_ITEM] = '.' + this[_CLASS_ITEM];
109 Fires when an autocomplete suggestion is selected from the list,
110 typically via a keyboard action or mouse click.
113 @param {Node} itemNode List item node that was selected.
114 @param {Object} result AutoComplete result object.
115 @preventable _defSelectFn
117 this.publish(EVT_SELECT, {
118 defaultFn: this._defSelectFn
122 destructor: function () {
123 while (this._listEvents.length) {
124 this._listEvents.pop().detach();
127 if (this._ariaNode) {
128 this._ariaNode.remove().destroy(true);
132 bindUI: function () {
137 renderUI: function () {
138 var ariaNode = this._createAriaNode(),
139 boundingBox = this.get('boundingBox'),
140 contentBox = this.get('contentBox'),
141 inputNode = this._inputNode,
142 listNode = this._createListNode(),
143 parentNode = inputNode.get('parentNode');
145 inputNode.addClass(this.getClassName('input')).setAttrs({
146 'aria-autocomplete': LIST,
147 'aria-expanded' : false,
148 'aria-owns' : listNode.get('id')
151 // ARIA node must be outside the widget or announcements won't be made
152 // when the widget is hidden.
153 parentNode.append(ariaNode);
155 // Add an iframe shim for IE6.
157 boundingBox.plug(Y.Plugin.Shim);
160 this._ariaNode = ariaNode;
161 this._boundingBox = boundingBox;
162 this._contentBox = contentBox;
163 this._listNode = listNode;
164 this._parentNode = parentNode;
167 syncUI: function () {
168 // No need to call _syncPosition() here; the other _sync methods will
169 // call it when necessary.
171 this._syncVisibility();
174 // -- Public Prototype Methods ---------------------------------------------
177 Hides the list, unless the `alwaysShowList` attribute is `true`.
184 return this.get(ALWAYS_SHOW_LIST) ? this : this.set(VISIBLE, false);
188 Selects the specified _itemNode_, or the current `activeItem` if _itemNode_
192 @param {Node} [itemNode] Item node to select.
193 @param {EventFacade} [originEvent] Event that triggered the selection, if
197 selectItem: function (itemNode, originEvent) {
199 if (!itemNode.hasClass(this[_CLASS_ITEM])) {
203 itemNode = this.get(ACTIVE_ITEM);
210 this.fire(EVT_SELECT, {
212 originEvent: originEvent || null,
213 result : itemNode.getData(RESULT)
219 // -- Protected Prototype Methods ------------------------------------------
222 Activates the next item after the currently active item. If there is no next
223 item and the `circular` attribute is `true`, focus will wrap back to the
226 @method _activateNextItem
230 _activateNextItem: function () {
231 var item = this.get(ACTIVE_ITEM),
235 nextItem = item.next(this[_SELECTOR_ITEM]) ||
236 (this.get(CIRCULAR) ? null : item);
238 nextItem = this._getFirstItemNode();
241 this.set(ACTIVE_ITEM, nextItem);
247 Activates the item previous to the currently active item. If there is no
248 previous item and the `circular` attribute is `true`, focus will wrap back
251 @method _activatePrevItem
255 _activatePrevItem: function () {
256 var item = this.get(ACTIVE_ITEM),
257 prevItem = item ? item.previous(this[_SELECTOR_ITEM]) :
258 this.get(CIRCULAR) && this._getLastItemNode();
260 this.set(ACTIVE_ITEM, prevItem || null);
266 Appends the specified result _items_ to the list inside a new item node.
269 @param {Array|Node|HTMLElement|String} items Result item or array of
271 @return {NodeList} Added nodes.
274 _add: function (items) {
277 YArray.each(Lang.isArray(items) ? items : [items], function (item) {
278 itemNodes.push(this._createItemNode(item).setData(RESULT, item));
281 itemNodes = Y.all(itemNodes);
282 this._listNode.append(itemNodes.toFrag());
288 Updates the ARIA live region with the specified message.
291 @param {String} stringId String id (from the `strings` attribute) of the
293 @param {Object} [subs] Substitutions for placeholders in the string.
296 _ariaSay: function (stringId, subs) {
297 var message = this.get('strings.' + stringId);
298 this._ariaNode.set('text', subs ? Lang.sub(message, subs) : message);
302 Binds `inputNode` events and behavior.
307 _bindInput: function () {
308 var inputNode = this._inputNode,
309 alignNode, alignWidth, tokenInput;
311 // Null align means we can auto-align. Set align to false to prevent
312 // auto-alignment, or a valid alignment config to customize the
314 if (this.get('align') === null) {
315 // If this is a tokenInput, align with its bounding box.
316 // Otherwise, align with the inputNode. Bit of a cheat.
317 tokenInput = this.get('tokenInput');
318 alignNode = (tokenInput && tokenInput.get('boundingBox')) || inputNode;
325 // If no width config is set, attempt to set the list's width to the
326 // width of the alignment node. If the alignment node's width is
327 // falsy, do nothing.
328 if (!this.get(WIDTH) && (alignWidth = alignNode.get('offsetWidth'))) {
329 this.set(WIDTH, alignWidth);
333 // Attach inputNode events.
334 this._listEvents = this._listEvents.concat([
335 inputNode.after('blur', this._afterListInputBlur, this),
336 inputNode.after('focus', this._afterListInputFocus, this)
346 _bindList: function () {
347 this._listEvents = this._listEvents.concat([
348 Y.one('doc').after('click', this._afterDocClick, this),
349 Y.one('win').after('windowresize', this._syncPosition, this),
352 mouseover: this._afterMouseOver,
353 mouseout : this._afterMouseOut,
355 activeItemChange : this._afterActiveItemChange,
356 alwaysShowListChange: this._afterAlwaysShowListChange,
357 hoveredItemChange : this._afterHoveredItemChange,
358 resultsChange : this._afterResultsChange,
359 visibleChange : this._afterVisibleChange
362 this._listNode.delegate('click', this._onItemClick,
363 this[_SELECTOR_ITEM], this)
368 Clears the contents of the tray.
373 _clear: function () {
374 this.set(ACTIVE_ITEM, null);
375 this._set(HOVERED_ITEM, null);
377 this._listNode.get('children').remove(true);
381 Creates and returns an ARIA live region node.
383 @method _createAriaNode
384 @return {Node} ARIA node.
387 _createAriaNode: function () {
388 var ariaNode = Node.create(this.ARIA_TEMPLATE);
390 return ariaNode.addClass(this.getClassName('aria')).setAttrs({
391 'aria-live': 'polite',
397 Creates and returns an item node with the specified _content_.
399 @method _createItemNode
400 @param {Object} result Result object.
401 @return {Node} Item node.
404 _createItemNode: function (result) {
405 var itemNode = Node.create(this.ITEM_TEMPLATE);
407 return itemNode.addClass(this[_CLASS_ITEM]).setAttrs({
408 id : Y.stamp(itemNode),
410 }).setAttribute('data-text', result.text).append(result.display);
414 Creates and returns a list node. If the `listNode` attribute is already set
415 to an existing node, that node will be used.
417 @method _createListNode
418 @return {Node} List node.
421 _createListNode: function () {
422 var listNode = this.get('listNode') || Node.create(this.LIST_TEMPLATE);
424 listNode.addClass(this.getClassName(LIST)).setAttrs({
425 id : Y.stamp(listNode),
429 this._set('listNode', listNode);
430 this.get('contentBox').append(listNode);
436 Gets the first item node in the list, or `null` if the list is empty.
438 @method _getFirstItemNode
442 _getFirstItemNode: function () {
443 return this._listNode.one(this[_SELECTOR_ITEM]);
447 Gets the last item node in the list, or `null` if the list is empty.
449 @method _getLastItemNode
453 _getLastItemNode: function () {
454 return this._listNode.one(this[_SELECTOR_ITEM] + ':last-child');
458 Synchronizes the result list's position and alignment.
460 @method _syncPosition
463 _syncPosition: function () {
464 // Force WidgetPositionAlign to refresh its alignment.
465 this._syncUIPosAlign();
467 // Resize the IE6 iframe shim to match the list's dimensions.
472 Synchronizes the results displayed in the list with those in the _results_
473 argument, or with the `results` attribute if an argument is not provided.
476 @param {Array} [results] Results.
479 _syncResults: function (results) {
481 results = this.get(RESULTS);
486 if (results.length) {
488 this._ariaSay('items_available');
491 this._syncPosition();
493 if (this.get('activateFirstItem') && !this.get(ACTIVE_ITEM)) {
494 this.set(ACTIVE_ITEM, this._getFirstItemNode());
499 Synchronizes the size of the iframe shim used for IE6 and lower. In other
500 browsers, this method is a noop.
505 _syncShim: useShim ? function () {
506 var shim = this._boundingBox.shim;
514 Synchronizes the visibility of the tray with the _visible_ argument, or with
515 the `visible` attribute if an argument is not provided.
517 @method _syncVisibility
518 @param {Boolean} [visible] Visibility.
521 _syncVisibility: function (visible) {
522 if (this.get(ALWAYS_SHOW_LIST)) {
524 this.set(VISIBLE, visible);
527 if (typeof visible === 'undefined') {
528 visible = this.get(VISIBLE);
531 this._inputNode.set('aria-expanded', visible);
532 this._boundingBox.set('aria-hidden', !visible);
535 this._syncPosition();
537 this.set(ACTIVE_ITEM, null);
538 this._set(HOVERED_ITEM, null);
540 // Force a reflow to work around a glitch in IE6 and 7 where some of
541 // the contents of the list will sometimes remain visible after the
542 // container is hidden.
543 this._boundingBox.get('offsetWidth');
546 // In some pages, IE7 fails to repaint the contents of the list after it
547 // becomes visible. Toggling a bogus class on the body forces a repaint
548 // that fixes the issue.
550 // Note: We don't actually need to use ClassNameManager here. This
551 // class isn't applying any actual styles; it's just frobbing the
552 // body element to force a repaint. The actual class name doesn't
555 .addClass('yui3-ie7-sucks')
556 .removeClass('yui3-ie7-sucks');
560 // -- Protected Event Handlers ---------------------------------------------
563 Handles `activeItemChange` events.
565 @method _afterActiveItemChange
566 @param {EventFacade} e
569 _afterActiveItemChange: function (e) {
570 var inputNode = this._inputNode,
575 // The previous item may have disappeared by the time this handler runs,
576 // so we need to be careful.
577 if (prevVal && prevVal._node) {
578 prevVal.removeClass(this[_CLASS_ITEM_ACTIVE]);
582 newVal.addClass(this[_CLASS_ITEM_ACTIVE]);
583 inputNode.set('aria-activedescendant', newVal.get(ID));
585 inputNode.removeAttribute('aria-activedescendant');
588 if (this.get('scrollIntoView')) {
589 node = newVal || inputNode;
591 if (!node.inRegion(Y.DOM.viewportRegion(), true)
592 || !node.inRegion(this._contentBox, true)) {
594 node.scrollIntoView();
600 Handles `alwaysShowListChange` events.
602 @method _afterAlwaysShowListChange
603 @param {EventFacade} e
606 _afterAlwaysShowListChange: function (e) {
607 this.set(VISIBLE, e.newVal || this.get(RESULTS).length > 0);
611 Handles click events on the document. If the click is outside both the
612 input node and the bounding box, the list will be hidden.
614 @method _afterDocClick
615 @param {EventFacade} e
619 _afterDocClick: function (e) {
620 var boundingBox = this._boundingBox,
623 if(target !== this._inputNode && target !== boundingBox &&
624 target.ancestor('#' + boundingBox.get('id'), true)){
630 Handles `hoveredItemChange` events.
632 @method _afterHoveredItemChange
633 @param {EventFacade} e
636 _afterHoveredItemChange: function (e) {
637 var newVal = e.newVal,
641 prevVal.removeClass(this[_CLASS_ITEM_HOVER]);
645 newVal.addClass(this[_CLASS_ITEM_HOVER]);
650 Handles `inputNode` blur events.
652 @method _afterListInputBlur
655 _afterListInputBlur: function () {
656 this._listInputFocused = false;
658 if (this.get(VISIBLE) &&
659 !this._mouseOverList &&
660 (this._lastInputKey !== KEY_TAB ||
661 !this.get('tabSelect') ||
662 !this.get(ACTIVE_ITEM))) {
668 Handles `inputNode` focus events.
670 @method _afterListInputFocus
673 _afterListInputFocus: function () {
674 this._listInputFocused = true;
678 Handles `mouseover` events.
680 @method _afterMouseOver
681 @param {EventFacade} e
684 _afterMouseOver: function (e) {
685 var itemNode = e.domEvent.target.ancestor(this[_SELECTOR_ITEM], true);
687 this._mouseOverList = true;
690 this._set(HOVERED_ITEM, itemNode);
695 Handles `mouseout` events.
697 @method _afterMouseOut
698 @param {EventFacade} e
701 _afterMouseOut: function () {
702 this._mouseOverList = false;
703 this._set(HOVERED_ITEM, null);
707 Handles `resultsChange` events.
709 @method _afterResultsChange
710 @param {EventFacade} e
713 _afterResultsChange: function (e) {
714 this._syncResults(e.newVal);
716 if (!this.get(ALWAYS_SHOW_LIST)) {
717 this.set(VISIBLE, !!e.newVal.length);
722 Handles `visibleChange` events.
724 @method _afterVisibleChange
725 @param {EventFacade} e
728 _afterVisibleChange: function (e) {
729 this._syncVisibility(!!e.newVal);
733 Delegated event handler for item `click` events.
736 @param {EventFacade} e
739 _onItemClick: function (e) {
740 var itemNode = e.currentTarget;
742 this.set(ACTIVE_ITEM, itemNode);
743 this.selectItem(itemNode, e);
746 // -- Protected Default Event Handlers -------------------------------------
749 Default `select` event handler.
752 @param {EventFacade} e
755 _defSelectFn: function (e) {
756 var text = e.result.text;
758 // TODO: support typeahead completion, etc.
759 this._inputNode.focus();
760 this._updateValue(text);
761 this._ariaSay('item_selected', {item: text});
767 If `true`, the first item in the list will be activated by default when
768 the list is initially displayed and when results change.
770 @attribute activateFirstItem
779 Item that's currently active, if any. When the user presses enter, this
780 is the item that will be selected.
782 @attribute activeItem
791 If `true`, the list will remain visible even when there are no results
794 @attribute alwaysShowList
803 If `true`, keyboard navigation will wrap around to the opposite end of
804 the list when navigating past the first or last item.
815 Item currently being hovered over by the mouse, if any.
817 @attribute hoveredItem
827 Node that will contain result items.
834 writeOnce: 'initOnly',
839 If `true`, the viewport will be scrolled to ensure that the active list
840 item is visible when necessary.
842 @attribute scrollIntoView
851 Translatable strings used by the AutoCompleteList widget.
857 valueFn: function () {
858 return Y.Intl.get('autocomplete-list');
863 If `true`, pressing the tab key while the list is visible will select
864 the active item, if any.
874 // The "visible" attribute is documented in Widget.
880 CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
883 Y.AutoCompleteList = List;
886 Alias for <a href="AutoCompleteList.html">`AutoCompleteList`</a>. See that class
892 Y.AutoComplete = List;
910 "widget-position-align"