Merge branch 'MDL-42340-master' of git://github.com/andrewnicols/moodle
[moodle.git] / lib / yui / build / moodle-core-notification-dialogue / moodle-core-notification-dialogue-debug.js
1 YUI.add('moodle-core-notification-dialogue', function (Y, NAME) {
3 var DIALOGUE_PREFIX,
4     BASE,
5     COUNT,
6     CONFIRMYES,
7     CONFIRMNO,
8     TITLE,
9     QUESTION,
10     CSS;
12 DIALOGUE_PREFIX = 'moodle-dialogue',
13 BASE = 'notificationBase',
14 COUNT = 0,
15 CONFIRMYES = 'yesLabel',
16 CONFIRMNO = 'noLabel',
17 TITLE = 'title',
18 QUESTION = 'question',
19 CSS = {
20     BASE : 'moodle-dialogue-base',
21     WRAP : 'moodle-dialogue-wrap',
22     HEADER : 'moodle-dialogue-hd',
23     BODY : 'moodle-dialogue-bd',
24     CONTENT : 'moodle-dialogue-content',
25     FOOTER : 'moodle-dialogue-ft',
26     HIDDEN : 'hidden',
27     LIGHTBOX : 'moodle-dialogue-lightbox'
28 };
30 // Set up the namespace once.
31 M.core = M.core || {};
32 /**
33  * The generic dialogue class for use in Moodle.
34  *
35  * @module moodle-core-notification
36  * @submodule moodle-core-notification-dialogue
37  */
39 var DIALOGUE_NAME = 'Moodle dialogue',
40     DIALOGUE,
41     DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen',
42     DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden',
43     DIALOGUE_SELECTOR =' [role=dialog]',
44     MENUBAR_SELECTOR = '[role=menubar]',
45     CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]';
47 /**
48  * A re-usable dialogue box with Moodle classes applied.
49  *
50  * @param {Object} config Object literal specifying the dialogue configuration properties.
51  * @constructor
52  * @class M.core.dialogue
53  * @extends Y.Panel
54  */
55 DIALOGUE = function(config) {
56     COUNT++;
57     var id = 'moodle-dialogue-'+COUNT;
58     config.notificationBase =
59         Y.Node.create('<div class="'+CSS.BASE+'">')
60               .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
61               .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
62               .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
63               .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
64     Y.one(document.body).append(config.notificationBase);
66     if (config.additionalBaseClass) {
67         config.notificationBase.addClass(config.additionalBaseClass);
68     }
70     config.srcNode =    '#'+id;
71     config.width =      config.width || '400px';
72     config.visible =    config.visible || false;
73     config.center =     config.centered && true;
74     config.centered =   false;
75     config.COUNT = COUNT;
77     if (config.width === 'auto') {
78         delete config.width;
79     }
81     // lightbox param to keep the stable versions API.
82     if (config.lightbox !== false) {
83         config.modal = true;
84     }
85     delete config.lightbox;
87     // closeButton param to keep the stable versions API.
88     if (config.closeButton === false) {
89         config.buttons = null;
90     } else {
91         config.buttons = [
92             {
93                 section: Y.WidgetStdMod.HEADER,
94                 classNames: 'closebutton',
95                 action: function () {
96                     this.hide();
97                 }
98             }
99         ];
100     }
101     DIALOGUE.superclass.constructor.apply(this, [config]);
103     if (config.closeButton !== false) {
104         // The buttons constructor does not allow custom attributes
105         this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
106     }
107 };
108 Y.extend(DIALOGUE, Y.Panel, {
109     // Window resize event listener.
110     _resizeevent : null,
111     // Orientation change event listener.
112     _orientationevent : null,
114     /**
115      * Initialise the dialogue.
116      *
117      * @method initializer
118      * @return void
119      */
120     initializer : function(config) {
121         var bb;
123         this.render();
124         this.makeResponsive();
125         this.after('visibleChange', this.visibilityChanged, this);
126         if (config.center) {
127             this.centerDialogue();
128         }
129         this.set('COUNT', COUNT);
131         // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
132         // and allow setting of z-index in theme.
133         bb = this.get('boundingBox');
135         if (config.extraClasses) {
136             Y.Array.each(config.extraClasses, bb.addClass, bb);
137         }
138         if (config.visible) {
139             this.applyZIndex();
140         }
141         // We must show - after the dialogue has been positioned,
142         // either by centerDialogue or makeResonsive. This is because the show() will trigger
143         // a focus on the dialogue, which will scroll the page. If the dialogue has not
144         // been positioned it will scroll back to the top of the page.
145         if (config.visible) {
146             this.show();
147             this.keyDelegation();
148         }
149     },
151     /**
152      * Either set the zindex to the supplied value, or set it to one more than the highest existing
153      * dialog in the page.
154      *
155      * @method visibilityChanged
156      * @return void
157      */
158     applyZIndex : function() {
159         var highestzindex = 0,
160             bb = this.get('boundingBox'),
161             zindex = this.get('zIndex');
162         if (zindex) {
163             // The zindex was specified so we should use that.
164             bb.setStyle('zIndex', zindex);
165         } else {
166             // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
167             Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR).each(function (node) {
168                 var zindex = this.findZIndex(node);
169                 if (zindex > highestzindex) {
170                     highestzindex = zindex;
171                 }
172             }, this);
173             // Only set the zindex if we found a wrapper.
174             if (highestzindex > 0) {
175                 bb.setStyle('zIndex', (highestzindex + 1).toString());
176             }
177         }
178     },
180     /**
181      * Finds the zIndex of the given node or its parent.
182      *
183      * @method findZIndex
184      * @param Node node
185      * @returns int Return either the zIndex of 0 if one was not found.
186      */
187     findZIndex : function(node) {
188         // In most cases the zindex is set on the parent of the dialog.
189         var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
190         if (zindex) {
191             return parseInt(zindex, 10);
192         }
193         return 0;
194     },
196     /**
197      * Event listener for the visibility changed event.
198      *
199      * @method visibilityChanged
200      * @return void
201      */
202     visibilityChanged : function(e) {
203         var titlebar, bb;
204         if (e.attrName === 'visible') {
205             this.get('maskNode').addClass(CSS.LIGHTBOX);
206             if (e.prevVal && !e.newVal) {
207                 bb = this.get('boundingBox');
208                 if (this._resizeevent) {
209                     this._resizeevent.detach();
210                     this._resizeevent = null;
211                 }
212                 if (this._orientationevent) {
213                     this._orientationevent.detach();
214                     this._orientationevent = null;
215                 }
216                 bb.detach('key', this.keyDelegation);
217             }
218             if (!e.prevVal && e.newVal) {
219                 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
220                 this.applyZIndex();
221                 // This needs to be done each time the dialog is shown as the window may have been resized.
222                 this.makeResponsive();
223                 if (!this.shouldResizeFullscreen()) {
224                     if (this.get('draggable')) {
225                         titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
226                         this.plug(Y.Plugin.Drag, {handles : [titlebar]});
227                         Y.one(titlebar).setStyle('cursor', 'move');
228                     }
229                 }
230                 this.keyDelegation();
231             }
232             if (this.get('center') && !e.prevVal && e.newVal) {
233                 this.centerDialogue();
234             }
235         }
236     },
237     /**
238      * If the responsive attribute is set on the dialog, and the window size is
239      * smaller than the responsive width - make the dialog fullscreen.
240      *
241      * @method makeResponsive
242      * @return void
243      */
244     makeResponsive : function() {
245         var bb = this.get('boundingBox'),
246             content;
248         if (this.shouldResizeFullscreen()) {
249             // Make this dialogue fullscreen on a small screen.
250             // Disable the page scrollbars.
252             // Size and position the fullscreen dialog.
254             bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
255             bb.setStyles({'left' : null,
256                           'top' : null,
257                           'width' : null,
258                           'height' : null,
259                           'right' : null,
260                           'bottom' : null});
262             content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
263         } else {
264             if (this.get('responsive')) {
265                 // We must reset any of the fullscreen changes.
266                 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
267                     .setStyles({'width' : this.get('width'),
268                                 'height' : this.get('height')});
269                 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
270             }
271         }
272     },
273     /**
274      * Center the dialog on the screen.
275      *
276      * @method centerDialogue
277      * @return void
278      */
279     centerDialogue : function() {
280         var bb = this.get('boundingBox'),
281             hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
282             x,
283             y;
285         // Don't adjust the position if we are in full screen mode.
286         if (this.shouldResizeFullscreen()) {
287             return;
288         }
289         if (hidden) {
290             bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
291         }
292         x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
293         y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
294         bb.setStyles({ 'left' : x, 'top' : y});
296         if (hidden) {
297             bb.addClass(DIALOGUE_HIDDEN_CLASS);
298         }
299     },
300     /**
301      * Return if this dialogue should be fullscreen or not.
302      * Responsive attribute must be true and we should not be in an iframe and the screen width should
303      * be less than the responsive width.
304      *
305      * @method shouldResizeFullscreen
306      * @return Boolean
307      */
308     shouldResizeFullscreen : function() {
309         return (window === window.parent) && this.get('responsive') &&
310                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
311     },
313     /**
314      * Override the show method to set keyboard focus on the dialogue.
315      *
316      * @method show
317      * @return void
318      */
319     show : function() {
320         var result = null,
321             header = this.headerNode,
322             content = this.bodyNode;
324         result = DIALOGUE.superclass.show.call(this);
325         if (header && header !== '') {
326             header.focus();
327         } else if (content && content !== '') {
328             content.focus();
329         }
330         return result;
331     },
332     /**
333      * Setup key delegation to keep tabbing within the open dialogue.
334      *
335      * @method keyDelegation
336      */
337     keyDelegation : function() {
338         var bb = this.get('boundingBox');
339         bb.delegate('key', function(e){
340             var target = e.target;
341             var direction = 'forward';
342             if (e.shiftKey) {
343                 direction = 'backward';
344             }
345             if (this.trapFocus(target, direction)) {
346                 e.preventDefault();
347             }
348         }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
349     },
350     /**
351      * Trap the tab focus within the open modal.
352      *
353      * @param string target the element target
354      * @param string direction tab key for forward and tab+shift for backward
355      * @returns bool
356      */
357     trapFocus : function(target, direction) {
358         var bb = this.get('boundingBox'),
359             firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
360             lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
362         if (target === lastitem && direction === 'forward') { // Tab key.
363             return firstitem.focus();
364         } else if (target === firstitem && direction === 'backward') {  // Tab+shift key.
365             return lastitem.focus();
366         }
367     }
368 }, {
369     NAME : DIALOGUE_NAME,
370     CSS_PREFIX : DIALOGUE_PREFIX,
371     ATTRS : {
372         notificationBase : {
374         },
376         /**
377          * Whether to display the dialogue modally and with a
378          * lightbox style.
379          *
380          * @attribute lightbox
381          * @type Boolean
382          * @default true
383          */
384         lightbox : {
385             validator : Y.Lang.isBoolean,
386             value : true
387         },
389         /**
390          * Whether to display a close button on the dialogue.
391          *
392          * Note, we do not recommend hiding the close button as this has
393          * potential accessibility concerns.
394          *
395          * @attribute closeButton
396          * @type Boolean
397          * @default true
398          */
399         closeButton : {
400             validator : Y.Lang.isBoolean,
401             value : true
402         },
404         /**
405          * The title for the close button if one is to be shown.
406          *
407          * @attribute closeButtonTitle
408          * @type String
409          * @default 'Close'
410          */
411         closeButtonTitle : {
412             validator : Y.Lang.isString,
413             value : 'Close'
414         },
416         /**
417          * Whether to display the dialogue centrally on the screen.
418          *
419          * @attribute center
420          * @type Boolean
421          * @default true
422          */
423         center : {
424             validator : Y.Lang.isBoolean,
425             value : true
426         },
428         /**
429          * Whether to make the dialogue movable around the page.
430          *
431          * @attribute draggable
432          * @type Boolean
433          * @default false
434          */
435         draggable : {
436             validator : Y.Lang.isBoolean,
437             value : false
438         },
440         /**
441          * Used to generate a unique id for the dialogue.
442          *
443          * @attribute COUNT
444          * @type Integer
445          * @default 0
446          */
447         COUNT: {
448             value: 0
449         },
451         /**
452          * Used to disable the fullscreen resizing behaviour if required.
453          *
454          * @attribute responsive
455          * @type Boolean
456          * @default true
457          */
458         responsive : {
459             validator : Y.Lang.isBoolean,
460             value : true
461         },
463         /**
464          * The width that this dialogue should be resized to fullscreen.
465          *
466          * @attribute responsiveWidth
467          * @type Integer
468          * @default 768
469          */
470         responsiveWidth : {
471             value : 768
472         }
473     }
474 });
476 M.core.dialogue = DIALOGUE;
479 }, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});