Merge branch '41214-26' of git://github.com/samhemelryk/moodle
[moodle.git] / lib / yui / build / moodle-core-notification-dialogue / moodle-core-notification-dialogue.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_MODAL_CLASS = 'yui3-widget-modal',
44     DIALOGUE_SELECTOR =' [role=dialog]',
45     MENUBAR_SELECTOR = '[role=menubar]',
46     NOSCROLLING_CLASS = 'no-scrolling';
48 /**
49  * A re-usable dialogue box with Moodle classes applied.
50  *
51  * @param {Object} config Object literal specifying the dialogue configuration properties.
52  * @constructor
53  * @class M.core.dialogue
54  * @extends Y.Panel
55  */
56 DIALOGUE = function(config) {
57     COUNT++;
58     var id = 'moodle-dialogue-'+COUNT;
59     config.notificationBase =
60         Y.Node.create('<div class="'+CSS.BASE+'">')
61               .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
62               .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
63               .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
64               .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
65     Y.one(document.body).append(config.notificationBase);
67     if (config.additionalBaseClass) {
68         config.notificationBase.addClass(config.additionalBaseClass);
69     }
71     config.srcNode =    '#'+id;
72     config.width =      config.width || '400px';
73     config.visible =    config.visible || false;
74     config.center =     config.centered && true;
75     config.centered =   false;
76     config.COUNT = COUNT;
78     if (config.width === 'auto') {
79         delete config.width;
80     }
82     // lightbox param to keep the stable versions API.
83     if (config.lightbox !== false) {
84         config.modal = true;
85     }
86     delete config.lightbox;
88     // closeButton param to keep the stable versions API.
89     if (config.closeButton === false) {
90         config.buttons = null;
91     } else {
92         config.buttons = [
93             {
94                 section: Y.WidgetStdMod.HEADER,
95                 classNames: 'closebutton',
96                 action: function () {
97                     this.hide();
98                 }
99             }
100         ];
101     }
102     DIALOGUE.superclass.constructor.apply(this, [config]);
104     if (config.closeButton !== false) {
105         // The buttons constructor does not allow custom attributes
106         this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
107     }
108 };
109 Y.extend(DIALOGUE, Y.Panel, {
110     // Window resize event listener.
111     _resizeevent : null,
112     // Orientation change event listener.
113     _orientationevent : null,
115     /**
116      * Initialise the dialogue.
117      *
118      * @method initializer
119      * @return void
120      */
121     initializer : function(config) {
122         var bb;
124         this.render();
125         this.show();
126         this.after('visibleChange', this.visibilityChanged, this);
127         if (config.center) {
128             this.centerDialogue();
129         }
130         if (!config.visible) {
131             this.hide();
132         }
133         this.set('COUNT', COUNT);
135         // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
136         // and allow setting of z-index in theme.
137         bb = this.get('boundingBox');
139         if (config.extraClasses) {
140             Y.Array.each(config.extraClasses, bb.addClass, bb);
141         }
142         if (config.visible) {
143             this.applyZIndex();
144         }
145     },
147     /**
148      * Either set the zindex to the supplied value, or set it to one more than the highest existing
149      * dialog in the page.
150      *
151      * @method visibilityChanged
152      * @return void
153      */
154     applyZIndex : function() {
155         var highestzindex = 0,
156             bb = this.get('boundingBox'),
157             zindex = this.get('zIndex');
158         if (zindex) {
159             // The zindex was specified so we should use that.
160             bb.setStyle('zIndex', zindex);
161         } else {
162             // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
163             Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR).each(function (node) {
164                 var zindex = this.findZIndex(node);
165                 if (zindex > highestzindex) {
166                     highestzindex = zindex;
167                 }
168             }, this);
169             // Only set the zindex if we found a wrapper.
170             if (highestzindex > 0) {
171                 bb.setStyle('zIndex', (highestzindex + 1).toString());
172             }
173         }
174     },
176     /**
177      * Finds the zIndex of the given node or its parent.
178      *
179      * @method findZIndex
180      * @param Node node
181      * @returns int Return either the zIndex of 0 if one was not found.
182      */
183     findZIndex : function(node) {
184         // In most cases the zindex is set on the parent of the dialog.
185         var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
186         if (zindex) {
187             return parseInt(zindex, 10);
188         }
189         return 0;
190     },
192     /**
193      * Enable or disable document scrolling (see if there are any modal or fullscreen popups).
194      *
195      * @method toggleDocumentScrolling
196      * @param Boolean scroll - If true, allow document scrolling.
197      * @return void
198      */
199     toggleDocumentScrolling : function() {
200         var windowroot = Y.one(Y.config.doc.body),
201             scroll = true,
202             search;
204         search = '.' + DIALOGUE_FULLSCREEN_CLASS + ', .' + DIALOGUE_MODAL_CLASS;
205         Y.all(search).each(function (node) {
206             if (!node.hasClass(DIALOGUE_HIDDEN_CLASS)) {
207                 scroll = false;
208             }
209         });
211         if (Y.UA.ie > 0) {
212             // Remember the previous value:
213             windowroot = Y.one('html');
214         }
215         if (scroll) {
216             if (windowroot.hasClass(NOSCROLLING_CLASS)) {
217                 windowroot.removeClass(NOSCROLLING_CLASS);
218             }
219         } else {
220             windowroot.addClass(NOSCROLLING_CLASS);
221         }
222     },
224     /**
225      * Event listener for the visibility changed event.
226      *
227      * @method visibilityChanged
228      * @return void
229      */
230     visibilityChanged : function(e) {
231         var titlebar;
232         if (e.attrName === 'visible') {
233             this.get('maskNode').addClass(CSS.LIGHTBOX);
234             if (e.prevVal && !e.newVal) {
235                 if (this._resizeevent) {
236                     this._resizeevent.detach();
237                     this._resizeevent = null;
238                 }
239                 if (this._orientationevent) {
240                     this._orientationevent.detach();
241                     this._orientationevent = null;
242                 }
243             }
244             if (!e.prevVal && e.newVal) {
245                 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
246                 this.applyZIndex();
247                 // This needs to be done each time the dialog is shown as the window may have been resized.
248                 this.makeResponsive();
249                 if (!this.shouldResizeFullscreen()) {
250                     if (this.get('draggable')) {
251                         titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
252                         this.plug(Y.Plugin.Drag, {handles : [titlebar]});
253                         Y.one(titlebar).setStyle('cursor', 'move');
254                     }
255                 }
256             }
257             if (this.get('center') && !e.prevVal && e.newVal) {
258                 this.centerDialogue();
259             }
260             this.toggleDocumentScrolling();
261         }
262     },
263     /**
264      * If the responsive attribute is set on the dialog, and the window size is
265      * smaller than the responsive width - make the dialog fullscreen.
266      *
267      * @method makeResponsive
268      * @return void
269      */
270     makeResponsive : function() {
271         var bb = this.get('boundingBox'),
272             content;
274         if (this.shouldResizeFullscreen()) {
275             // Make this dialogue fullscreen on a small screen.
276             // Disable the page scrollbars.
278             // Size and position the fullscreen dialog.
280             bb.addClass(DIALOGUE_PREFIX+'-fullscreen');
281             bb.setStyles({'left' : null, 'top' : null, 'width' : null, 'height' : null});
283             content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
284             content.setStyle('overflow', 'auto');
285         } else {
286             if (this.get('responsive')) {
287                 // We must reset any of the fullscreen changes.
288                 bb.removeClass(DIALOGUE_PREFIX+'-fullscreen')
289                     .setStyles({'overflow' : 'inherit',
290                                 'width' : this.get('width'),
291                                 'height' : this.get('height')});
292                 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
293                 content.setStyle('overflow', 'inherit');
295             }
296         }
297     },
298     /**
299      * Center the dialog on the screen.
300      *
301      * @method centerDialogue
302      * @return void
303      */
304     centerDialogue : function() {
305         var bb = this.get('boundingBox'),
306             hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
307             x,
308             y;
310         // Don't adjust the position if we are in full screen mode.
311         if (this.shouldResizeFullscreen()) {
312             return;
313         }
314         if (hidden) {
315             bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
316         }
317         x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
318         y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
319         bb.setStyles({ 'left' : x, 'top' : y});
321         if (hidden) {
322             bb.addClass(DIALOGUE_HIDDEN_CLASS);
323         }
324     },
325     /**
326      * Return if this dialogue should be fullscreen or not.
327      * Responsive attribute must be true and we should not be in an iframe and the screen width should
328      * be less than the responsive width.
329      *
330      * @method shouldResizeFullscreen
331      * @return Boolean
332      */
333     shouldResizeFullscreen : function() {
334         return (window === window.parent) && this.get('responsive') &&
335                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
336     },
338     /**
339      * Override the show method to set keyboard focus on the dialogue.
340      *
341      * @method show
342      * @return void
343      */
344     show : function() {
345         var result = null,
346             header = this.headerNode,
347             content = this.bodyNode;
349         result = DIALOGUE.superclass.show.call(this);
350         if (header && header !== '') {
351             header.focus();
352         } else if (content && content !== '') {
353             content.focus();
354         }
355         return result;
356     }
357 }, {
358     NAME : DIALOGUE_NAME,
359     CSS_PREFIX : DIALOGUE_PREFIX,
360     ATTRS : {
361         notificationBase : {
363         },
365         /**
366          * Whether to display the dialogue modally and with a
367          * lightbox style.
368          *
369          * @attribute lightbox
370          * @type Boolean
371          * @default true
372          */
373         lightbox : {
374             validator : Y.Lang.isBoolean,
375             value : true
376         },
378         /**
379          * Whether to display a close button on the dialogue.
380          *
381          * Note, we do not recommend hiding the close button as this has
382          * potential accessibility concerns.
383          *
384          * @attribute closeButton
385          * @type Boolean
386          * @default true
387          */
388         closeButton : {
389             validator : Y.Lang.isBoolean,
390             value : true
391         },
393         /**
394          * The title for the close button if one is to be shown.
395          *
396          * @attribute closeButtonTitle
397          * @type String
398          * @default 'Close'
399          */
400         closeButtonTitle : {
401             validator : Y.Lang.isString,
402             value : 'Close'
403         },
405         /**
406          * Whether to display the dialogue centrally on the screen.
407          *
408          * @attribute center
409          * @type Boolean
410          * @default true
411          */
412         center : {
413             validator : Y.Lang.isBoolean,
414             value : true
415         },
417         /**
418          * Whether to make the dialogue movable around the page.
419          *
420          * @attribute draggable
421          * @type Boolean
422          * @default false
423          */
424         draggable : {
425             validator : Y.Lang.isBoolean,
426             value : false
427         },
429         /**
430          * Used to generate a unique id for the dialogue.
431          *
432          * @attribute COUNT
433          * @type Integer
434          * @default 0
435          */
436         COUNT: {
437             value: 0
438         },
440         /**
441          * Used to disable the fullscreen resizing behaviour if required.
442          *
443          * @attribute responsive
444          * @type Boolean
445          * @default true
446          */
447         responsive : {
448             validator : Y.Lang.isBoolean,
449             value : true
450         },
452         /**
453          * The width that this dialogue should be resized to fullscreen.
454          *
455          * @attribute responsiveWidth
456          * @type Integer
457          * @default 768
458          */
459         responsiveWidth : {
460             value : 768
461         }
462     }
463 });
465 M.core.dialogue = DIALOGUE;
468 }, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});