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