NOBUG: Fixed file access permissions
[moodle.git] / lib / yuilib / 3.13.0 / autocomplete-list / autocomplete-list.js
blob407901d79d0d42364e660463d5919de96aaee8b0
1 /*
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/
6 */
8 YUI.add('autocomplete-list', function (Y, NAME) {
10 /**
11 Traditional autocomplete dropdown list widget, just like Mom used to make.
13 @module autocomplete
14 @submodule autocomplete-list
15 **/
17 /**
18 Traditional autocomplete dropdown list widget, just like Mom used to make.
20 @class AutoCompleteList
21 @extends Widget
22 @uses AutoCompleteBase
23 @uses WidgetPosition
24 @uses WidgetPositionAlign
25 @constructor
26 @param {Object} config Configuration object.
27 **/
29 var Lang   = Y.Lang,
30     Node   = Y.Node,
31     YArray = Y.Array,
33     // Whether or not we need an iframe shim.
34     useShim = Y.UA.ie && Y.UA.ie < 7,
36     // keyCode constants.
37     KEY_TAB = 9,
39     // String shorthand.
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',
49     ID               = 'id',
50     ITEM             = 'item',
51     LIST             = 'list',
52     RESULT           = 'result',
53     RESULTS          = 'results',
54     VISIBLE          = 'visible',
55     WIDTH            = 'width',
57     // Event names.
58     EVT_SELECT = 'select',
60 List = Y.Base.create('autocompleteList', Y.Widget, [
61     Y.AutoCompleteBase,
62     Y.WidgetPosition,
63     Y.WidgetPositionAlign
64 ], {
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;
83         return uiEvents;
84     }()),
86     // -- Lifecycle Prototype Methods ------------------------------------------
87     initializer: function () {
88         var inputNode = this.get('inputNode');
90         if (!inputNode) {
91             Y.error('No inputNode specified.');
92             return;
93         }
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];
108         /**
109         Fires when an autocomplete suggestion is selected from the list,
110         typically via a keyboard action or mouse click.
112         @event select
113         @param {Node} itemNode List item node that was selected.
114         @param {Object} result AutoComplete result object.
115         @preventable _defSelectFn
116         **/
117         this.publish(EVT_SELECT, {
118             defaultFn: this._defSelectFn
119         });
120     },
122     destructor: function () {
123         while (this._listEvents.length) {
124             this._listEvents.pop().detach();
125         }
127         if (this._ariaNode) {
128             this._ariaNode.remove().destroy(true);
129         }
130     },
132     bindUI: function () {
133         this._bindInput();
134         this._bindList();
135     },
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')
149         });
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.
156         if (useShim) {
157             boundingBox.plug(Y.Plugin.Shim);
158         }
160         this._ariaNode    = ariaNode;
161         this._boundingBox = boundingBox;
162         this._contentBox  = contentBox;
163         this._listNode    = listNode;
164         this._parentNode  = parentNode;
165     },
167     syncUI: function () {
168         // No need to call _syncPosition() here; the other _sync methods will
169         // call it when necessary.
170         this._syncResults();
171         this._syncVisibility();
172     },
174     // -- Public Prototype Methods ---------------------------------------------
176     /**
177     Hides the list, unless the `alwaysShowList` attribute is `true`.
179     @method hide
180     @see show
181     @chainable
182     **/
183     hide: function () {
184         return this.get(ALWAYS_SHOW_LIST) ? this : this.set(VISIBLE, false);
185     },
187     /**
188     Selects the specified _itemNode_, or the current `activeItem` if _itemNode_
189     is not specified.
191     @method selectItem
192     @param {Node} [itemNode] Item node to select.
193     @param {EventFacade} [originEvent] Event that triggered the selection, if
194         any.
195     @chainable
196     **/
197     selectItem: function (itemNode, originEvent) {
198         if (itemNode) {
199             if (!itemNode.hasClass(this[_CLASS_ITEM])) {
200                 return this;
201             }
202         } else {
203             itemNode = this.get(ACTIVE_ITEM);
205             if (!itemNode) {
206                 return this;
207             }
208         }
210         this.fire(EVT_SELECT, {
211             itemNode   : itemNode,
212             originEvent: originEvent || null,
213             result     : itemNode.getData(RESULT)
214         });
216         return this;
217     },
219     // -- Protected Prototype Methods ------------------------------------------
221     /**
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
224     input node.
226     @method _activateNextItem
227     @chainable
228     @protected
229     **/
230     _activateNextItem: function () {
231         var item = this.get(ACTIVE_ITEM),
232             nextItem;
234         if (item) {
235             nextItem = item.next(this[_SELECTOR_ITEM]) ||
236                     (this.get(CIRCULAR) ? null : item);
237         } else {
238             nextItem = this._getFirstItemNode();
239         }
241         this.set(ACTIVE_ITEM, nextItem);
243         return this;
244     },
246     /**
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
249     to the input node.
251     @method _activatePrevItem
252     @chainable
253     @protected
254     **/
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);
262         return this;
263     },
265     /**
266     Appends the specified result _items_ to the list inside a new item node.
268     @method _add
269     @param {Array|Node|HTMLElement|String} items Result item or array of
270         result items.
271     @return {NodeList} Added nodes.
272     @protected
273     **/
274     _add: function (items) {
275         var itemNodes = [];
277         YArray.each(Lang.isArray(items) ? items : [items], function (item) {
278             itemNodes.push(this._createItemNode(item).setData(RESULT, item));
279         }, this);
281         itemNodes = Y.all(itemNodes);
282         this._listNode.append(itemNodes.toFrag());
284         return itemNodes;
285     },
287     /**
288     Updates the ARIA live region with the specified message.
290     @method _ariaSay
291     @param {String} stringId String id (from the `strings` attribute) of the
292         message to speak.
293     @param {Object} [subs] Substitutions for placeholders in the string.
294     @protected
295     **/
296     _ariaSay: function (stringId, subs) {
297         var message = this.get('strings.' + stringId);
298         this._ariaNode.set('text', subs ? Lang.sub(message, subs) : message);
299     },
301     /**
302     Binds `inputNode` events and behavior.
304     @method _bindInput
305     @protected
306     **/
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
313         // alignment.
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;
320             this.set('align', {
321                 node  : alignNode,
322                 points: ['tl', 'bl']
323             });
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);
330             }
331         }
333         // Attach inputNode events.
334         this._listEvents = this._listEvents.concat([
335             inputNode.after('blur',  this._afterListInputBlur, this),
336             inputNode.after('focus', this._afterListInputFocus, this)
337         ]);
338     },
340     /**
341     Binds list events.
343     @method _bindList
344     @protected
345     **/
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),
351             this.after({
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
360             }),
362             this._listNode.delegate('click', this._onItemClick,
363                     this[_SELECTOR_ITEM], this)
364         ]);
365     },
367     /**
368     Clears the contents of the tray.
370     @method _clear
371     @protected
372     **/
373     _clear: function () {
374         this.set(ACTIVE_ITEM, null);
375         this._set(HOVERED_ITEM, null);
377         this._listNode.get('children').remove(true);
378     },
380     /**
381     Creates and returns an ARIA live region node.
383     @method _createAriaNode
384     @return {Node} ARIA node.
385     @protected
386     **/
387     _createAriaNode: function () {
388         var ariaNode = Node.create(this.ARIA_TEMPLATE);
390         return ariaNode.addClass(this.getClassName('aria')).setAttrs({
391             'aria-live': 'polite',
392             role       : 'status'
393         });
394     },
396     /**
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.
402     @protected
403     **/
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),
409             role: 'option'
410         }).setAttribute('data-text', result.text).append(result.display);
411     },
413     /**
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.
419     @protected
420     **/
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),
426             role: 'listbox'
427         });
429         this._set('listNode', listNode);
430         this.get('contentBox').append(listNode);
432         return listNode;
433     },
435     /**
436     Gets the first item node in the list, or `null` if the list is empty.
438     @method _getFirstItemNode
439     @return {Node|null}
440     @protected
441     **/
442     _getFirstItemNode: function () {
443         return this._listNode.one(this[_SELECTOR_ITEM]);
444     },
446     /**
447     Gets the last item node in the list, or `null` if the list is empty.
449     @method _getLastItemNode
450     @return {Node|null}
451     @protected
452     **/
453     _getLastItemNode: function () {
454         return this._listNode.one(this[_SELECTOR_ITEM] + ':last-child');
455     },
457     /**
458     Synchronizes the result list's position and alignment.
460     @method _syncPosition
461     @protected
462     **/
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.
468         this._syncShim();
469     },
471     /**
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.
475     @method _syncResults
476     @param {Array} [results] Results.
477     @protected
478     **/
479     _syncResults: function (results) {
480         if (!results) {
481             results = this.get(RESULTS);
482         }
484         this._clear();
486         if (results.length) {
487             this._add(results);
488             this._ariaSay('items_available');
489         }
491         this._syncPosition();
493         if (this.get('activateFirstItem') && !this.get(ACTIVE_ITEM)) {
494             this.set(ACTIVE_ITEM, this._getFirstItemNode());
495         }
496     },
498     /**
499     Synchronizes the size of the iframe shim used for IE6 and lower. In other
500     browsers, this method is a noop.
502     @method _syncShim
503     @protected
504     **/
505     _syncShim: useShim ? function () {
506         var shim = this._boundingBox.shim;
508         if (shim) {
509             shim.sync();
510         }
511     } : function () {},
513     /**
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.
519     @protected
520     **/
521     _syncVisibility: function (visible) {
522         if (this.get(ALWAYS_SHOW_LIST)) {
523             visible = true;
524             this.set(VISIBLE, visible);
525         }
527         if (typeof visible === 'undefined') {
528             visible = this.get(VISIBLE);
529         }
531         this._inputNode.set('aria-expanded', visible);
532         this._boundingBox.set('aria-hidden', !visible);
534         if (visible) {
535             this._syncPosition();
536         } else {
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');
544         }
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.
549         if (Y.UA.ie === 7) {
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
553             // really matter.
554             Y.one('body')
555                 .addClass('yui3-ie7-sucks')
556                 .removeClass('yui3-ie7-sucks');
557         }
558     },
560     // -- Protected Event Handlers ---------------------------------------------
562     /**
563     Handles `activeItemChange` events.
565     @method _afterActiveItemChange
566     @param {EventFacade} e
567     @protected
568     **/
569     _afterActiveItemChange: function (e) {
570         var inputNode = this._inputNode,
571             newVal    = e.newVal,
572             prevVal   = e.prevVal,
573             node;
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]);
579         }
581         if (newVal) {
582             newVal.addClass(this[_CLASS_ITEM_ACTIVE]);
583             inputNode.set('aria-activedescendant', newVal.get(ID));
584         } else {
585             inputNode.removeAttribute('aria-activedescendant');
586         }
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();
595             }
596         }
597     },
599     /**
600     Handles `alwaysShowListChange` events.
602     @method _afterAlwaysShowListChange
603     @param {EventFacade} e
604     @protected
605     **/
606     _afterAlwaysShowListChange: function (e) {
607         this.set(VISIBLE, e.newVal || this.get(RESULTS).length > 0);
608     },
610     /**
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
616     @protected
617     @since 3.5.0
618     **/
619     _afterDocClick: function (e) {
620         var boundingBox = this._boundingBox,
621             target      = e.target;
623         if(target !== this._inputNode && target !== boundingBox &&
624                 target.ancestor('#' + boundingBox.get('id'), true)){
625             this.hide();
626         }
627     },
629     /**
630     Handles `hoveredItemChange` events.
632     @method _afterHoveredItemChange
633     @param {EventFacade} e
634     @protected
635     **/
636     _afterHoveredItemChange: function (e) {
637         var newVal  = e.newVal,
638             prevVal = e.prevVal;
640         if (prevVal) {
641             prevVal.removeClass(this[_CLASS_ITEM_HOVER]);
642         }
644         if (newVal) {
645             newVal.addClass(this[_CLASS_ITEM_HOVER]);
646         }
647     },
649     /**
650     Handles `inputNode` blur events.
652     @method _afterListInputBlur
653     @protected
654     **/
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))) {
663             this.hide();
664         }
665     },
667     /**
668     Handles `inputNode` focus events.
670     @method _afterListInputFocus
671     @protected
672     **/
673     _afterListInputFocus: function () {
674         this._listInputFocused = true;
675     },
677     /**
678     Handles `mouseover` events.
680     @method _afterMouseOver
681     @param {EventFacade} e
682     @protected
683     **/
684     _afterMouseOver: function (e) {
685         var itemNode = e.domEvent.target.ancestor(this[_SELECTOR_ITEM], true);
687         this._mouseOverList = true;
689         if (itemNode) {
690             this._set(HOVERED_ITEM, itemNode);
691         }
692     },
694     /**
695     Handles `mouseout` events.
697     @method _afterMouseOut
698     @param {EventFacade} e
699     @protected
700     **/
701     _afterMouseOut: function () {
702         this._mouseOverList = false;
703         this._set(HOVERED_ITEM, null);
704     },
706     /**
707     Handles `resultsChange` events.
709     @method _afterResultsChange
710     @param {EventFacade} e
711     @protected
712     **/
713     _afterResultsChange: function (e) {
714         this._syncResults(e.newVal);
716         if (!this.get(ALWAYS_SHOW_LIST)) {
717             this.set(VISIBLE, !!e.newVal.length);
718         }
719     },
721     /**
722     Handles `visibleChange` events.
724     @method _afterVisibleChange
725     @param {EventFacade} e
726     @protected
727     **/
728     _afterVisibleChange: function (e) {
729         this._syncVisibility(!!e.newVal);
730     },
732     /**
733     Delegated event handler for item `click` events.
735     @method _onItemClick
736     @param {EventFacade} e
737     @protected
738     **/
739     _onItemClick: function (e) {
740         var itemNode = e.currentTarget;
742         this.set(ACTIVE_ITEM, itemNode);
743         this.selectItem(itemNode, e);
744     },
746     // -- Protected Default Event Handlers -------------------------------------
748     /**
749     Default `select` event handler.
751     @method _defSelectFn
752     @param {EventFacade} e
753     @protected
754     **/
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});
762         this.hide();
763     }
764 }, {
765     ATTRS: {
766         /**
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
771         @type Boolean
772         @default false
773         **/
774         activateFirstItem: {
775             value: false
776         },
778         /**
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
783         @type Node
784         **/
785         activeItem: {
786             setter: Y.one,
787             value: null
788         },
790         /**
791         If `true`, the list will remain visible even when there are no results
792         to display.
794         @attribute alwaysShowList
795         @type Boolean
796         @default false
797         **/
798         alwaysShowList: {
799             value: false
800         },
802         /**
803         If `true`, keyboard navigation will wrap around to the opposite end of
804         the list when navigating past the first or last item.
806         @attribute circular
807         @type Boolean
808         @default true
809         **/
810         circular: {
811             value: true
812         },
814         /**
815         Item currently being hovered over by the mouse, if any.
817         @attribute hoveredItem
818         @type Node|null
819         @readOnly
820         **/
821         hoveredItem: {
822             readOnly: true,
823             value: null
824         },
826         /**
827         Node that will contain result items.
829         @attribute listNode
830         @type Node|null
831         @initOnly
832         **/
833         listNode: {
834             writeOnce: 'initOnly',
835             value: null
836         },
838         /**
839         If `true`, the viewport will be scrolled to ensure that the active list
840         item is visible when necessary.
842         @attribute scrollIntoView
843         @type Boolean
844         @default false
845         **/
846         scrollIntoView: {
847             value: false
848         },
850         /**
851         Translatable strings used by the AutoCompleteList widget.
853         @attribute strings
854         @type Object
855         **/
856         strings: {
857             valueFn: function () {
858                 return Y.Intl.get('autocomplete-list');
859             }
860         },
862         /**
863         If `true`, pressing the tab key while the list is visible will select
864         the active item, if any.
866         @attribute tabSelect
867         @type Boolean
868         @default true
869         **/
870         tabSelect: {
871             value: true
872         },
874         // The "visible" attribute is documented in Widget.
875         visible: {
876             value: false
877         }
878     },
880     CSS_PREFIX: Y.ClassNameManager.getClassName('aclist')
883 Y.AutoCompleteList = List;
886 Alias for <a href="AutoCompleteList.html">`AutoCompleteList`</a>. See that class
887 for API docs.
889 @class AutoComplete
892 Y.AutoComplete = List;
895 }, '3.13.0', {
896     "lang": [
897         "en",
898         "es",
899         "hu",
900         "it"
901     ],
902     "requires": [
903         "autocomplete-base",
904         "event-resize",
905         "node-screen",
906         "selector-css3",
907         "shim-plugin",
908         "widget",
909         "widget-position",
910         "widget-position-align"
911     ],
912     "skinnable": true