]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - jssource/src_files/include/javascript/yui3/build/autocomplete/autocomplete-list.js
Release 6.5.0
[Github/sugarcrm.git] / jssource / src_files / include / javascript / yui3 / build / autocomplete / autocomplete-list.js
1 /*
2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
5 version: 3.3.0
6 build: 3167
7 */
8 YUI.add('autocomplete-list', function(Y) {
9
10 /**
11  * Traditional autocomplete dropdown list widget, just like Mom used to make.
12  *
13  * @module autocomplete
14  * @submodule autocomplete-list
15  * @class AutoCompleteList
16  * @extends Widget
17  * @uses AutoCompleteBase
18  * @uses WidgetPosition
19  * @uses WidgetPositionAlign
20  * @uses WidgetStack
21  * @constructor
22  * @param {Object} config Configuration object.
23  */
24
25 var Lang   = Y.Lang,
26     Node   = Y.Node,
27     YArray = Y.Array,
28
29     // keyCode constants.
30     KEY_TAB = 9,
31
32     // String shorthand.
33     _CLASS_ITEM        = '_CLASS_ITEM',
34     _CLASS_ITEM_ACTIVE = '_CLASS_ITEM_ACTIVE',
35     _CLASS_ITEM_HOVER  = '_CLASS_ITEM_HOVER',
36     _SELECTOR_ITEM     = '_SELECTOR_ITEM',
37
38     ACTIVE_ITEM      = 'activeItem',
39     ALWAYS_SHOW_LIST = 'alwaysShowList',
40     CIRCULAR         = 'circular',
41     HOVERED_ITEM     = 'hoveredItem',
42     ID               = 'id',
43     ITEM             = 'item',
44     LIST             = 'list',
45     RESULT           = 'result',
46     RESULTS          = 'results',
47     VISIBLE          = 'visible',
48     WIDTH            = 'width',
49
50     // Event names.
51     EVT_SELECT = 'select',
52
53 List = Y.Base.create('autocompleteList', Y.Widget, [
54     Y.AutoCompleteBase,
55     Y.WidgetPosition,
56     Y.WidgetPositionAlign,
57     Y.WidgetStack
58 ], {
59     // -- Prototype Properties -------------------------------------------------
60     ARIA_TEMPLATE: '<div/>',
61     ITEM_TEMPLATE: '<li/>',
62     LIST_TEMPLATE: '<ul/>',
63
64     // -- Lifecycle Prototype Methods ------------------------------------------
65     initializer: function () {
66         var inputNode = this.get('inputNode');
67
68         if (!inputNode) {
69             Y.error('No inputNode specified.');
70             return;
71         }
72
73         this._inputNode  = inputNode;
74         this._listEvents = [];
75
76         // This ensures that the list is rendered inside the same parent as the
77         // input node by default, which is necessary for proper ARIA support.
78         this.DEF_PARENT_NODE = inputNode.get('parentNode');
79
80         // Cache commonly used classnames and selectors for performance.
81         this[_CLASS_ITEM]        = this.getClassName(ITEM);
82         this[_CLASS_ITEM_ACTIVE] = this.getClassName(ITEM, 'active');
83         this[_CLASS_ITEM_HOVER]  = this.getClassName(ITEM, 'hover');
84         this[_SELECTOR_ITEM]     = '.' + this[_CLASS_ITEM];
85
86         /**
87          * Fires when an autocomplete suggestion is selected from the list,
88          * typically via a keyboard action or mouse click.
89          *
90          * @event select
91          * @param {EventFacade} e Event facade with the following additional
92          *   properties:
93          *
94          * <dl>
95          *   <dt>itemNode (Node)</dt>
96          *   <dd>
97          *     List item node that was selected.
98          *   </dd>
99          *
100          *   <dt>result (Object)</dt>
101          *   <dd>
102          *     AutoComplete result object.
103          *   </dd>
104          * </dl>
105          *
106          * @preventable _defSelectFn
107          */
108         this.publish(EVT_SELECT, {
109             defaultFn: this._defSelectFn
110         });
111     },
112
113     destructor: function () {
114         while (this._listEvents.length) {
115             this._listEvents.pop().detach();
116         }
117     },
118
119     bindUI: function () {
120         this._bindInput();
121         this._bindList();
122     },
123
124     renderUI: function () {
125         var ariaNode   = this._createAriaNode(),
126             contentBox = this.get('contentBox'),
127             inputNode  = this._inputNode,
128             listNode,
129             parentNode = inputNode.get('parentNode');
130
131         listNode = this._createListNode();
132         this._set('listNode', listNode);
133         contentBox.append(listNode);
134
135         inputNode.addClass(this.getClassName('input')).setAttrs({
136             'aria-autocomplete': LIST,
137             'aria-expanded'    : false,
138             'aria-owns'        : listNode.get('id'),
139             role               : 'combobox'
140         });
141
142         // ARIA node must be outside the widget or announcements won't be made
143         // when the widget is hidden.
144         parentNode.append(ariaNode);
145
146         this._ariaNode    = ariaNode;
147         this._boundingBox = this.get('boundingBox');
148         this._contentBox  = contentBox;
149         this._listNode    = listNode;
150         this._parentNode  = parentNode;
151     },
152
153     syncUI: function () {
154         this._syncResults();
155         this._syncVisibility();
156     },
157
158     // -- Public Prototype Methods ---------------------------------------------
159
160     /**
161      * Hides the list, unless the <code>alwaysShowList</code> attribute is
162      * <code>true</code>.
163      *
164      * @method hide
165      * @see show
166      * @chainable
167      */
168     hide: function () {
169         return this.get(ALWAYS_SHOW_LIST) ? this : this.set(VISIBLE, false);
170     },
171
172     /**
173      * Selects the specified <i>itemNode</i>, or the current
174      * <code>activeItem</code> if <i>itemNode</i> is not specified.
175      *
176      * @method selectItem
177      * @param {Node} itemNode (optional) Item node to select.
178      * @chainable
179      */
180     selectItem: function (itemNode) {
181         if (itemNode) {
182             if (!itemNode.hasClass(this[_CLASS_ITEM])) {
183                 return this;
184             }
185         } else {
186             itemNode = this.get(ACTIVE_ITEM);
187
188             if (!itemNode) {
189                 return this;
190             }
191         }
192
193         this.fire(EVT_SELECT, {
194             itemNode: itemNode,
195             result  : itemNode.getData(RESULT)
196         });
197
198         return this;
199     },
200
201     // -- Protected Prototype Methods ------------------------------------------
202
203     /**
204      * Activates the next item after the currently active item. If there is no
205      * next item and the <code>circular</code> attribute is <code>true</code>,
206      * focus will wrap back to the input node.
207      *
208      * @method _activateNextItem
209      * @chainable
210      * @protected
211      */
212     _activateNextItem: function () {
213         var item = this.get(ACTIVE_ITEM),
214             nextItem;
215
216         if (item) {
217             nextItem = item.next(this[_SELECTOR_ITEM]) ||
218                     (this.get(CIRCULAR) ? null : item);
219         } else {
220             nextItem = this._getFirstItemNode();
221         }
222
223         this.set(ACTIVE_ITEM, nextItem);
224
225         return this;
226     },
227
228     /**
229      * Activates the item previous to the currently active item. If there is no
230      * previous item and the <code>circular</code> attribute is
231      * <code>true</code>, focus will wrap back to the input node.
232      *
233      * @method _activatePrevItem
234      * @chainable
235      * @protected
236      */
237     _activatePrevItem: function () {
238         var item     = this.get(ACTIVE_ITEM),
239             prevItem = item ? item.previous(this[_SELECTOR_ITEM]) :
240                     this.get(CIRCULAR) && this._getLastItemNode();
241
242         this.set(ACTIVE_ITEM, prevItem || null);
243
244         return this;
245     },
246
247     /**
248      * Appends the specified result <i>items</i> to the list inside a new item
249      * node.
250      *
251      * @method _add
252      * @param {Array|Node|HTMLElement|String} items Result item or array of
253      *   result items.
254      * @return {NodeList} Added nodes.
255      * @protected
256      */
257     _add: function (items) {
258         var itemNodes = [];
259
260         YArray.each(Lang.isArray(items) ? items : [items], function (item) {
261             itemNodes.push(this._createItemNode(item).setData(RESULT, item));
262         }, this);
263
264         itemNodes = Y.all(itemNodes);
265         this._listNode.append(itemNodes.toFrag());
266
267         return itemNodes;
268     },
269
270     /**
271      * Updates the ARIA live region with the specified message.
272      *
273      * @method _ariaSay
274      * @param {String} stringId String id (from the <code>strings</code>
275      *   attribute) of the message to speak.
276      * @param {Object} subs (optional) Substitutions for placeholders in the
277      *   string.
278      * @protected
279      */
280     _ariaSay: function (stringId, subs) {
281         var message = this.get('strings.' + stringId);
282         this._ariaNode.setContent(subs ? Lang.sub(message, subs) : message);
283     },
284
285     /**
286      * Binds <code>inputNode</code> events and behavior.
287      *
288      * @method _bindInput
289      * @protected
290      */
291     _bindInput: function () {
292         var inputNode = this._inputNode,
293             alignNode, alignWidth, tokenInput;
294
295         // Null align means we can auto-align. Set align to false to prevent
296         // auto-alignment, or a valid alignment config to customize the
297         // alignment.
298         if (this.get('align') === null) {
299             // If this is a tokenInput, align with its bounding box.
300             // Otherwise, align with the inputNode. Bit of a cheat.
301             tokenInput = this.get('tokenInput');
302             alignNode  = (tokenInput && tokenInput.get('boundingBox')) || inputNode;
303
304             this.set('align', {
305                 node  : alignNode,
306                 points: ['tl', 'bl']
307             });
308
309             // If no width config is set, attempt to set the list's width to the
310             // width of the alignment node. If the alignment node's width is
311             // falsy, do nothing.
312             if (!this.get(WIDTH) && (alignWidth = alignNode.get('offsetWidth'))) {
313                 this.set(WIDTH, alignWidth);
314             }
315         }
316
317         // Attach inputNode events.
318         this._listEvents.push(inputNode.on('blur', this._onListInputBlur, this));
319     },
320
321     /**
322      * Binds list events.
323      *
324      * @method _bindList
325      * @protected
326      */
327     _bindList: function () {
328         this._listEvents.concat([
329             this.after({
330               mouseover: this._afterMouseOver,
331               mouseout : this._afterMouseOut,
332
333               activeItemChange    : this._afterActiveItemChange,
334               alwaysShowListChange: this._afterAlwaysShowListChange,
335               hoveredItemChange   : this._afterHoveredItemChange,
336               resultsChange       : this._afterResultsChange,
337               visibleChange       : this._afterVisibleChange
338             }),
339
340             this._listNode.delegate('click', this._onItemClick, this[_SELECTOR_ITEM], this)
341         ]);
342     },
343
344     /**
345      * Clears the contents of the tray.
346      *
347      * @method _clear
348      * @protected
349      */
350     _clear: function () {
351         this.set(ACTIVE_ITEM, null);
352         this._set(HOVERED_ITEM, null);
353
354         this._listNode.get('children').remove(true);
355     },
356
357     /**
358      * Creates and returns an ARIA live region node.
359      *
360      * @method _createAriaNode
361      * @return {Node} ARIA node.
362      * @protected
363      */
364     _createAriaNode: function () {
365         var ariaNode = Node.create(this.ARIA_TEMPLATE);
366
367         return ariaNode.addClass(this.getClassName('aria')).setAttrs({
368             'aria-live': 'polite',
369             role       : 'status'
370         });
371     },
372
373     /**
374      * Creates and returns an item node with the specified <i>content</i>.
375      *
376      * @method _createItemNode
377      * @param {Object} result Result object.
378      * @return {Node} Item node.
379      * @protected
380      */
381     _createItemNode: function (result) {
382         var itemNode = Node.create(this.ITEM_TEMPLATE);
383
384         return itemNode.addClass(this[_CLASS_ITEM]).setAttrs({
385             id  : Y.stamp(itemNode),
386             role: 'option'
387         }).setAttribute('data-text', result.text).append(result.display);
388     },
389
390     /**
391      * Creates and returns a list node.
392      *
393      * @method _createListNode
394      * @return {Node} List node.
395      * @protected
396      */
397     _createListNode: function () {
398         var listNode = Node.create(this.LIST_TEMPLATE);
399
400         return listNode.addClass(this.getClassName(LIST)).setAttrs({
401             id  : Y.stamp(listNode),
402             role: 'listbox'
403         });
404     },
405
406     /**
407      * Gets the first item node in the list, or <code>null</code> if the list is
408      * empty.
409      *
410      * @method _getFirstItemNode
411      * @return {Node|null}
412      * @protected
413      */
414     _getFirstItemNode: function () {
415         return this._listNode.one(this[_SELECTOR_ITEM]);
416     },
417
418     /**
419      * Gets the last item node in the list, or <code>null</code> if the list is
420      * empty.
421      *
422      * @method _getLastItemNode
423      * @return {Node|null}
424      * @protected
425      */
426     _getLastItemNode: function () {
427         return this._listNode.one(this[_SELECTOR_ITEM] + ':last-child');
428     },
429
430     /**
431      * Synchronizes the results displayed in the list with those in the
432      * <i>results</i> argument, or with the <code>results</code> attribute if an
433      * argument is not provided.
434      *
435      * @method _syncResults
436      * @param {Array} results (optional) Results.
437      * @protected
438      */
439     _syncResults: function (results) {
440         var items;
441
442         if (!results) {
443             results = this.get(RESULTS);
444         }
445
446         this._clear();
447
448         if (results.length) {
449             items = this._add(results);
450             this._ariaSay('items_available');
451         }
452
453         if (this.get('activateFirstItem') && !this.get(ACTIVE_ITEM)) {
454             this.set(ACTIVE_ITEM, this._getFirstItemNode());
455         }
456     },
457
458     /**
459      * Synchronizes the visibility of the tray with the <i>visible</i> argument,
460      * or with the <code>visible</code> attribute if an argument is not
461      * provided.
462      *
463      * @method _syncVisibility
464      * @param {Boolean} visible (optional) Visibility.
465      * @protected
466      */
467     _syncVisibility: function (visible) {
468         if (this.get(ALWAYS_SHOW_LIST)) {
469             visible = true;
470             this.set(VISIBLE, visible);
471         }
472
473         if (typeof visible === 'undefined') {
474             visible = this.get(VISIBLE);
475         }
476
477         this._inputNode.set('aria-expanded', visible);
478         this._boundingBox.set('aria-hidden', !visible);
479
480         if (visible) {
481             // Force WidgetPositionAlign to refresh its alignment.
482             this._syncUIPosAlign();
483         } else {
484             this.set(ACTIVE_ITEM, null);
485             this._set(HOVERED_ITEM, null);
486
487             // Force a reflow to work around a glitch in IE6 and 7 where some of
488             // the contents of the list will sometimes remain visible after the
489             // container is hidden.
490             this._boundingBox.get('offsetWidth');
491         }
492     },
493
494     // -- Protected Event Handlers ---------------------------------------------
495
496     /**
497      * Handles <code>activeItemChange</code> events.
498      *
499      * @method _afterActiveItemChange
500      * @param {EventTarget} e
501      * @protected
502      */
503     _afterActiveItemChange: function (e) {
504         var inputNode = this._inputNode,
505             newVal    = e.newVal,
506             prevVal   = e.prevVal;
507
508         // The previous item may have disappeared by the time this handler runs,
509         // so we need to be careful.
510         if (prevVal && prevVal._node) {
511             prevVal.removeClass(this[_CLASS_ITEM_ACTIVE]);
512         }
513
514         if (newVal) {
515             newVal.addClass(this[_CLASS_ITEM_ACTIVE]);
516             inputNode.set('aria-activedescendant', newVal.get(ID));
517         } else {
518             inputNode.removeAttribute('aria-activedescendant');
519         }
520
521         if (this.get('scrollIntoView')) {
522             (newVal || inputNode).scrollIntoView();
523         }
524     },
525
526     /**
527      * Handles <code>alwaysShowListChange</code> events.
528      *
529      * @method _afterAlwaysShowListChange
530      * @param {EventTarget} e
531      * @protected
532      */
533     _afterAlwaysShowListChange: function (e) {
534         this.set(VISIBLE, e.newVal || this.get(RESULTS).length > 0);
535     },
536
537     /**
538      * Handles <code>hoveredItemChange</code> events.
539      *
540      * @method _afterHoveredItemChange
541      * @param {EventTarget} e
542      * @protected
543      */
544     _afterHoveredItemChange: function (e) {
545         var newVal  = e.newVal,
546             prevVal = e.prevVal;
547
548         if (prevVal) {
549             prevVal.removeClass(this[_CLASS_ITEM_HOVER]);
550         }
551
552         if (newVal) {
553             newVal.addClass(this[_CLASS_ITEM_HOVER]);
554         }
555     },
556
557     /**
558      * Handles <code>mouseover</code> events.
559      *
560      * @method _afterMouseOver
561      * @param {EventTarget} e
562      * @protected
563      */
564     _afterMouseOver: function (e) {
565         var itemNode = e.domEvent.target.ancestor(this[_SELECTOR_ITEM], true);
566
567         this._mouseOverList = true;
568
569         if (itemNode) {
570             this._set(HOVERED_ITEM, itemNode);
571         }
572     },
573
574     /**
575      * Handles <code>mouseout</code> events.
576      *
577      * @method _afterMouseOut
578      * @param {EventTarget} e
579      * @protected
580      */
581     _afterMouseOut: function () {
582         this._mouseOverList = false;
583         this._set(HOVERED_ITEM, null);
584     },
585
586     /**
587      * Handles <code>resultsChange</code> events.
588      *
589      * @method _afterResultsChange
590      * @param {EventFacade} e
591      * @protected
592      */
593     _afterResultsChange: function (e) {
594         this._syncResults(e.newVal);
595
596         if (!this.get(ALWAYS_SHOW_LIST)) {
597             this.set(VISIBLE, !!e.newVal.length);
598         }
599     },
600
601     /**
602      * Handles <code>visibleChange</code> events.
603      *
604      * @method _afterVisibleChange
605      * @param {EventFacade} e
606      * @protected
607      */
608     _afterVisibleChange: function (e) {
609         this._syncVisibility(!!e.newVal);
610     },
611
612     /**
613      * Handles <code>inputNode</code> <code>blur</code> events.
614      *
615      * @method _onListInputBlur
616      * @param {EventTarget} e
617      * @protected
618      */
619     _onListInputBlur: function (e) {
620         // Hide the list on inputNode blur events, unless the mouse is currently
621         // over the list (which indicates that the user is probably interacting
622         // with it). The _lastInputKey property comes from the
623         // autocomplete-list-keys module.
624         if (!this._mouseOverList || this._lastInputKey === KEY_TAB) {
625             this.hide();
626         }
627     },
628
629     /**
630      * Delegated event handler for item <code>click</code> events.
631      *
632      * @method _onItemClick
633      * @param {EventTarget} e
634      * @protected
635      */
636     _onItemClick: function (e) {
637         var itemNode = e.currentTarget;
638
639         this.set(ACTIVE_ITEM, itemNode);
640         this.selectItem(itemNode);
641     },
642
643     // -- Protected Default Event Handlers -------------------------------------
644
645     /**
646      * Default <code>select</code> event handler.
647      *
648      * @method _defSelectFn
649      * @param {EventTarget} e
650      * @protected
651      */
652     _defSelectFn: function (e) {
653         var text = e.result.text;
654
655         // TODO: support typeahead completion, etc.
656         this._inputNode.focus();
657         this._updateValue(text);
658         this._ariaSay('item_selected', {item: text});
659         this.hide();
660     }
661 }, {
662     ATTRS: {
663         /**
664          * If <code>true</code>, the first item in the list will be activated by
665          * default when the list is initially displayed and when results change.
666          *
667          * @attribute activateFirstItem
668          * @type Boolean
669          * @default false
670          */
671         activateFirstItem: {
672             value: false
673         },
674
675         /**
676          * Item that's currently active, if any. When the user presses enter,
677          * this is the item that will be selected.
678          *
679          * @attribute activeItem
680          * @type Node
681          */
682         activeItem: {
683             setter: Y.one,
684             value: null
685         },
686
687         /**
688          * If <code>true</code>, the list will remain visible even when there
689          * are no results to display.
690          *
691          * @attribute alwaysShowList
692          * @type Boolean
693          * @default false
694          */
695         alwaysShowList: {
696             value: false
697         },
698
699         /**
700          * If <code>true</code>, keyboard navigation will wrap around to the
701          * opposite end of the list when navigating past the first or last item.
702          *
703          * @attribute circular
704          * @type Boolean
705          * @default true
706          */
707         circular: {
708             value: true
709         },
710
711         /**
712          * Item currently being hovered over by the mouse, if any.
713          *
714          * @attribute hoveredItem
715          * @type Node|null
716          * @readonly
717          */
718         hoveredItem: {
719             readOnly: true,
720             value: null
721         },
722
723         /**
724          * Node that will contain result items.
725          *
726          * @attribute listNode
727          * @type Node|null
728          * @readonly
729          */
730         listNode: {
731             readOnly: true,
732             value: null
733         },
734
735         /**
736          * If <code>true</code>, the viewport will be scrolled to ensure that
737          * the active list item is visible when necessary.
738          *
739          * @attribute scrollIntoView
740          * @type Boolean
741          * @default false
742          */
743         scrollIntoView: {
744             value: false
745         },
746
747         /**
748          * Translatable strings used by the AutoCompleteList widget.
749          *
750          * @attribute strings
751          * @type Object
752          */
753         strings: {
754             valueFn: function () {
755                 return Y.Intl.get('autocomplete-list');
756             }
757         },
758
759         /**
760          * If <code>true</code>, pressing the tab key while the list is visible
761          * will select the active item, if any.
762          *
763          * @attribute tabSelect
764          * @type Boolean
765          * @default true
766          */
767         tabSelect: {
768             value: true
769         },
770
771         // The "visible" attribute is documented in Widget.
772         visible: {
773             value: false
774         }
775     },
776
777     CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
778 });
779
780 Y.AutoCompleteList = List;
781
782 /**
783  * Alias for <a href="AutoCompleteList.html"><code>AutoCompleteList</code></a>.
784  * See that class for API docs.
785  *
786  * @class AutoComplete
787  */
788
789 Y.AutoComplete = List;
790
791
792 }, '3.3.0' ,{lang:['en'], requires:['autocomplete-base', 'selector-css3', 'widget', 'widget-position', 'widget-position-align', 'widget-stack'], after:['autocomplete-sources'], skinnable:true});