MDL-32843 import YUI 3.5.1
[moodle.git] / lib / yui / 3.5.1 / build / widget-modality / widget-modality.js
blob6e5beae3c1244b62cc472256b29370ea0b7ca67c
1 /*
2 YUI 3.5.1 (build 22)
3 Copyright 2012 Yahoo! Inc. All rights reserved.
4 Licensed under the BSD License.
5 http://yuilibrary.com/license/
6 */
7 YUI.add('widget-modality', function(Y) {
9 /**
10  * Provides modality support for Widgets, though an extension
11  *
12  * @module widget-modality
13  */
15 var WIDGET       = 'widget',
16     RENDER_UI    = 'renderUI',
17     BIND_UI      = 'bindUI',
18     SYNC_UI      = 'syncUI',
19     BOUNDING_BOX = 'boundingBox',
20     CONTENT_BOX  = 'contentBox',
21     VISIBLE      = 'visible',
22     Z_INDEX      = 'zIndex',
23     CHANGE       = 'Change',
24     isBoolean    = Y.Lang.isBoolean,
25     getCN        = Y.ClassNameManager.getClassName,
26     MaskShow     = "maskShow",
27     MaskHide     = "maskHide",
28     ClickOutside = "clickoutside",
29     FocusOutside = "focusoutside",
31     supportsPosFixed = (function(){
33         /*! IS_POSITION_FIXED_SUPPORTED - Juriy Zaytsev (kangax) - http://yura.thinkweb2.com/cft/ */
35         var doc         = Y.config.doc,
36             isSupported = null,
37             el, root;
39         if (doc.createElement) {
40             el = doc.createElement('div');
41             if (el && el.style) {
42                 el.style.position = 'fixed';
43                 el.style.top = '10px';
44                 root = doc.body;
45                 if (root && root.appendChild && root.removeChild) {
46                     root.appendChild(el);
47                     isSupported = (el.offsetTop === 10);
48                     root.removeChild(el);
49                 }
50             }
51         }
53         return isSupported;
54     }());
56     /**
57      * Widget extension, which can be used to add modality support to the base Widget class,
58      * through the Base.create method.
59      *
60      * @class WidgetModality
61      * @param {Object} config User configuration object
62      */
63     function WidgetModal(config) {}
65     var MODAL           = 'modal',
66         MASK            = 'mask',
67         MODAL_CLASSES   = {
68             modal   : getCN(WIDGET, MODAL),
69             mask    : getCN(WIDGET, MASK)
70         };
72     /**
73     * Static property used to define the default attribute
74     * configuration introduced by WidgetModality.
75     *
76     * @property ATTRS
77     * @static
78     * @type Object
79     */
80     WidgetModal.ATTRS = {
81             /**
82              * @attribute maskNode
83              * @type Y.Node
84              *
85              * @description Returns a Y.Node instance of the node being used as the mask.
86              */
87             maskNode : {
88                 getter      : '_getMaskNode',
89                 readOnly    : true
90             },
93             /**
94              * @attribute modal
95              * @type boolean
96              *
97              * @description Whether the widget should be modal or not.
98              */
99             modal: {
100                 value:false,
101                 validator: isBoolean
102             },
104             /**
105              * @attribute focusOn
106              * @type array
107              *
108              * @description An array of objects corresponding to the nodes and events that will trigger a re-focus back on the widget.
109              * The implementer can supply an array of objects, with each object having the following properties:
110              * <p>eventName: (string, required): The eventName to listen to.</p>
111              * <p>node: (Y.Node, optional): The Y.Node that will fire the event (defaults to the boundingBox of the widget)</p>
112              * <p>By default, this attribute consists of two objects which will cause the widget to re-focus if anything
113              * outside the widget is clicked on or focussed upon.</p>
114              */
115             focusOn: {
116                 valueFn: function() {
117                     return [
118                         {
119                             // node: this.get(BOUNDING_BOX),
120                             eventName: ClickOutside
121                         },
122                         {
123                             //node: this.get(BOUNDING_BOX),
124                             eventName: FocusOutside
125                         }
126                     ];
127                 },
129                 validator: Y.Lang.isArray
130             }
132     };
135     WidgetModal.CLASSES = MODAL_CLASSES;
138     /**
139      * Returns the mask if it exists on the page - otherwise creates a mask. There's only
140      * one mask on a page at a given time.
141      * <p>
142      * This method in invoked internally by the getter of the maskNode ATTR.
143      * </p>
144      * @method _GET_MASK
145      * @static
146      */
147     WidgetModal._GET_MASK = function() {
149         var mask = Y.one(".yui3-widget-mask") || null,
150         win = Y.one('window');
152         if (mask) {
153             return mask;
154         }
155         else {
157             mask = Y.Node.create('<div></div>');
158             mask.addClass(MODAL_CLASSES.mask);
159             if (supportsPosFixed) {
160                 mask.setStyles({
161                     position    : 'fixed',
162                     width       : '100%',
163                     height      : '100%',
164                     top         : '0',
165                     left        : '0',
166                     display     : 'block'
167                 });
168             }
169             else {
170                 mask.setStyles({
171                     position    : 'absolute',
172                     width       : win.get('winWidth') +'px',
173                     height      : win.get('winHeight') + 'px',
174                     top         : '0',
175                     left        : '0',
176                     display     : 'block'
177                 });
178             }
182             return mask;
183         }
185     };
187     /**
188      * A stack of Y.Widget objects representing the current hierarchy of modal widgets presently displayed on the screen
189      * @property STACK
190      */
191     WidgetModal.STACK = [];
194     WidgetModal.prototype = {
196         initializer: function () {
197             Y.after(this._renderUIModal, this, RENDER_UI);
198             Y.after(this._syncUIModal, this, SYNC_UI);
199             Y.after(this._bindUIModal, this, BIND_UI);
200         },
202         destructor: function () {
203             // Hack to remove this thing from the STACK.
204             this._uiSetHostVisibleModal(false);
205         },
207         // *** Instance Members *** //
209         _uiHandlesModal: null,
212         /**
213          * Adds modal class to the bounding box of the widget
214          * <p>
215          * This method in invoked after renderUI is invoked for the Widget class
216          * using YUI's aop infrastructure.
217          * </p>
218          * @method _renderUIModal
219          * @protected
220          */
221         _renderUIModal : function () {
223             var bb = this.get(BOUNDING_BOX);
224                 //cb = this.get(CONTENT_BOX);
226             //this makes the content box content appear over the mask
227             // cb.setStyles({
228             //     position: ""
229             // });
231             this._repositionMask(this);
232             bb.addClass(MODAL_CLASSES.modal);
234         },
237         /**
238          * Hooks up methods to be executed when the widget's visibility or z-index changes
239          * <p>
240          * This method in invoked after bindUI is invoked for the Widget class
241          * using YUI's aop infrastructure.
242          * </p>
243          * @method _bindUIModal
244          * @protected
245          */
246         _bindUIModal : function () {
248             this.after(VISIBLE+CHANGE, this._afterHostVisibleChangeModal);
249             this.after(Z_INDEX+CHANGE, this._afterHostZIndexChangeModal);
250             this.after("focusOnChange", this._afterFocusOnChange);
252             // Re-align the mask in the viewport if `position: fixed;` is not
253             // supported. iOS < 5 and Android < 3 don't actually support it even
254             // though they both pass the feature test; the UA sniff is here to
255             // account for that. Ideally this should be replaced with a better
256             // feature test.
257             if (!supportsPosFixed ||
258                     (Y.UA.ios && Y.UA.ios < 5) ||
259                     (Y.UA.android && Y.UA.android < 3)) {
261                 Y.one('win').on('scroll', this._resyncMask, this);
262             }
263         },
265         /**
266          * Syncs the mask with the widget's current state, namely the visibility and z-index of the widget
267          * <p>
268          * This method in invoked after syncUI is invoked for the Widget class
269          * using YUI's aop infrastructure.
270          * </p>
271          * @method _syncUIModal
272          * @protected
273          */
274         _syncUIModal : function () {
276             //var host = this.get(HOST);
278             this._uiSetHostVisibleModal(this.get(VISIBLE));
279             this._uiSetHostZIndexModal(this.get(Z_INDEX));
281         },
283         /**
284          * Provides mouse and tab focus to the widget's bounding box.
285          *
286          * @method _focus
287          */
288         _focus : function (e) {
290             var bb = this.get(BOUNDING_BOX),
291             oldTI = bb.get('tabIndex');
293             bb.set('tabIndex', oldTI >= 0 ? oldTI : 0);
294             this.focus();
295         },
296         /**
297          * Blurs the widget.
298          *
299          * @method _blur
300          */
301         _blur : function () {
303             this.blur();
304         },
306         /**
307          * Returns the Y.Node instance of the maskNode
308          *
309          * @method _getMaskNode
310          * @return {Node} The Y.Node instance of the mask, as returned from WidgetModal._GET_MASK
311          */
312         _getMaskNode : function () {
314             return WidgetModal._GET_MASK();
315         },
317         /**
318          * Performs events attaching/detaching, stack shifting and mask repositioning based on the visibility of the widget
319          *
320          * @method _uiSetHostVisibleModal
321          * @param {boolean} Whether the widget is visible or not
322          */
323         _uiSetHostVisibleModal : function (visible) {
324             var stack    = WidgetModal.STACK,
325                 maskNode = this.get('maskNode'),
326                 isModal  = this.get('modal'),
327                 topModal, index;
329             if (visible) {
331                 Y.Array.each(stack, function(modal){
332                     modal._detachUIHandlesModal();
333                     modal._blur();
334                 });
336                 // push on top of stack
337                 stack.unshift(this);
339                 this._repositionMask(this);
340                 this._uiSetHostZIndexModal(this.get(Z_INDEX));
342                 if (isModal) {
343                     maskNode.show();
344                     Y.later(1, this, '_attachUIHandlesModal');
345                     this._focus();
346                 }
349             } else {
351                 index = Y.Array.indexOf(stack, this);
352                 if (index >= 0) {
353                     // Remove modal widget from global stack.
354                     stack.splice(index, 1);
355                 }
357                 this._detachUIHandlesModal();
358                 this._blur();
360                 if (stack.length) {
361                     topModal = stack[0];
362                     this._repositionMask(topModal);
363                     //topModal._attachUIHandlesModal();
364                     topModal._uiSetHostZIndexModal(topModal.get(Z_INDEX));
366                     if (topModal.get('modal')) {
367                         //topModal._attachUIHandlesModal();
368                         Y.later(1, topModal, '_attachUIHandlesModal');
369                         topModal._focus();
370                     }
372                 } else {
374                     if (maskNode.getStyle('display') === 'block') {
375                         maskNode.hide();
376                     }
378                 }
380             }
381         },
383         /**
384          * Sets the z-index of the mask node.
385          *
386          * @method _uiSetHostZIndexModal
387          * @param {Number} Z-Index of the widget
388          */
389         _uiSetHostZIndexModal : function (zIndex) {
391             if (this.get('modal')) {
392                 this.get('maskNode').setStyle(Z_INDEX, zIndex || 0);
393             }
395         },
397         /**
398          * Attaches UI Listeners for "clickoutside" and "focusoutside" on the
399          * widget. When these events occur, and the widget is modal, focus is
400          * shifted back onto the widget.
401          *
402          * @method _attachUIHandlesModal
403          */
404         _attachUIHandlesModal : function () {
406             if (this._uiHandlesModal || WidgetModal.STACK[0] !== this) {
407                 // Quit early if we have ui handles, or if we not at the top
408                 // of the global stack.
409                 return;
410             }
412             var bb          = this.get(BOUNDING_BOX),
413                 maskNode    = this.get('maskNode'),
414                 focusOn     = this.get('focusOn'),
415                 focus       = Y.bind(this._focus, this),
416                 uiHandles   = [],
417                 i, len, o;
419             for (i = 0, len = focusOn.length; i < len; i++) {
421                 o = {};
422                 o.node = focusOn[i].node;
423                 o.ev = focusOn[i].eventName;
424                 o.keyCode = focusOn[i].keyCode;
426                 //no keycode or node defined
427                 if (!o.node && !o.keyCode && o.ev) {
428                     uiHandles.push(bb.on(o.ev, focus));
429                 }
431                 //node defined, no keycode (not a keypress)
432                 else if (o.node && !o.keyCode && o.ev) {
433                     uiHandles.push(o.node.on(o.ev, focus));
434                 }
436                 //node defined, keycode defined, event defined (its a key press)
437                 else if (o.node && o.keyCode && o.ev) {
438                     uiHandles.push(o.node.on(o.ev, focus, o.keyCode));
439                 }
441                 else {
442                     Y.Log('focusOn ATTR Error: The event with name "'+o.ev+'" could not be attached.');
443                 }
445             }
447             if ( ! supportsPosFixed) {
448                 uiHandles.push(Y.one('win').on('scroll', Y.bind(function(e){
449                     maskNode.setStyle('top', maskNode.get('docScrollY'));
450                 }, this)));
451             }
453             this._uiHandlesModal = uiHandles;
454         },
456         /**
457          * Detaches all UI Listeners that were set in _attachUIHandlesModal from the widget.
458          *
459          * @method _detachUIHandlesModal
460          */
461         _detachUIHandlesModal : function () {
462             Y.each(this._uiHandlesModal, function(h){
463                 h.detach();
464             });
465             this._uiHandlesModal = null;
466         },
468         /**
469          * Default function that is called when visibility is changed on the widget.
470          *
471          * @method _afterHostVisibleChangeModal
472          * @param {EventFacade} e The event facade of the change
473          */
474         _afterHostVisibleChangeModal : function (e) {
476             this._uiSetHostVisibleModal(e.newVal);
477         },
479         /**
480          * Default function that is called when z-index is changed on the widget.
481          *
482          * @method _afterHostZIndexChangeModal
483          * @param {EventFacade} e The event facade of the change
484          */
485         _afterHostZIndexChangeModal : function (e) {
487             this._uiSetHostZIndexModal(e.newVal);
488         },
490         /**
491          * Returns a boolean representing whether the current widget is in a "nested modality" state.
492          * This is done by checking the number of widgets currently on the stack.
493          *
494          * @method isNested
495          * @public
496          */
497         isNested: function() {
498             var length = WidgetModal.STACK.length,
499             retval = (length > 1) ? true : false;
500             return retval;
501         },
503         /**
504          * Repositions the mask in the DOM for nested modality cases.
505          *
506          * @method _repositionMask
507          * @param {Widget} nextElem The Y.Widget instance that will be visible in the stack once the current widget is closed.
508          */
509         _repositionMask: function(nextElem) {
511             var currentModal = this.get('modal'),
512                 nextModal    = nextElem.get('modal'),
513                 maskNode     = this.get('maskNode'),
514                 bb, bbParent;
516             //if this is modal and host is not modal
517             if (currentModal && !nextModal) {
518                 //leave the mask where it is, since the host is not modal.
519                 maskNode.remove();
520                 this.fire(MaskHide);
521             }
523             //if the main widget is not modal but the host is modal, or both of them are modal
524             else if ((!currentModal && nextModal) || (currentModal && nextModal)) {
526                 //then remove the mask off DOM, reposition it, and reinsert it into the DOM
527                 maskNode.remove();
528                 this.fire(MaskHide);
529                 bb = nextElem.get(BOUNDING_BOX);
530                 bbParent = bb.get('parentNode') || Y.one('body');
531                 bbParent.insert(maskNode, bbParent.get('firstChild'));
532                 this.fire(MaskShow);
533             }
535         },
537         /**
538          * Resyncs the mask in the viewport for browsers that don't support fixed positioning
539          *
540          * @method _resyncMask
541          * @param {Y.Widget} nextElem The Y.Widget instance that will be visible in the stack once the current widget is closed.
542          * @private
543          */
544         _resyncMask: function (e) {
545             var o       = e.currentTarget,
546                 offsetX = o.get('docScrollX'),
547                 offsetY = o.get('docScrollY'),
548                 w       = o.get('innerWidth') || o.get('winWidth'),
549                 h       = o.get('innerHeight') || o.get('winHeight'),
550                 mask    = this.get('maskNode');
552             mask.setStyles({
553                 "top": offsetY + "px",
554                 "left": offsetX + "px",
555                 "width": w + 'px',
556                 "height": h + 'px'
557             });
558         },
560         /**
561          * Default function called when focusOn Attribute is changed. Remove existing listeners and create new listeners.
562          *
563          * @method _afterFocusOnChange
564          */
565         _afterFocusOnChange : function(e) {
566             this._detachUIHandlesModal();
568             if (this.get(VISIBLE)) {
569                 this._attachUIHandlesModal();
570             }
571         }
572     };
574     Y.WidgetModality = WidgetModal;
578 }, '3.5.1' ,{requires:['base-build', 'event-outside', 'widget'], skinnable:true});