2
YUI 3.10.3 (build 2fb5187)
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;
909
"widget-position-align"