178f2fbba1d319092f8ef86260c961fbb69a581f
[moodle.git] / lib / yui / src / notification / js / dialogue.js
1 /**
2  * The generic dialogue class for use in Moodle.
3  *
4  * @module moodle-core-notification
5  * @submodule moodle-core-notification-dialogue
6  */
8 var DIALOGUE_NAME = 'Moodle dialogue',
9     DIALOGUE,
10     DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen',
11     DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden',
12     DIALOGUE_SELECTOR =' [role=dialog]',
13     MENUBAR_SELECTOR = '[role=menubar]';
15 /**
16  * A re-usable dialogue box with Moodle classes applied.
17  *
18  * @param {Object} config Object literal specifying the dialogue configuration properties.
19  * @constructor
20  * @class M.core.dialogue
21  * @extends Y.Panel
22  */
23 DIALOGUE = function(config) {
24     COUNT++;
25     var id = 'moodle-dialogue-'+COUNT;
26     config.notificationBase =
27         Y.Node.create('<div class="'+CSS.BASE+'">')
28               .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
29               .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
30               .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
31               .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
32     Y.one(document.body).append(config.notificationBase);
34     if (config.additionalBaseClass) {
35         config.notificationBase.addClass(config.additionalBaseClass);
36     }
38     config.srcNode =    '#'+id;
39     config.width =      config.width || '400px';
40     config.visible =    config.visible || false;
41     config.center =     config.centered && true;
42     config.centered =   false;
43     config.COUNT = COUNT;
45     if (config.width === 'auto') {
46         delete config.width;
47     }
49     // lightbox param to keep the stable versions API.
50     if (config.lightbox !== false) {
51         config.modal = true;
52     }
53     delete config.lightbox;
55     // closeButton param to keep the stable versions API.
56     if (config.closeButton === false) {
57         config.buttons = null;
58     } else {
59         config.buttons = [
60             {
61                 section: Y.WidgetStdMod.HEADER,
62                 classNames: 'closebutton',
63                 action: function () {
64                     this.hide();
65                 }
66             }
67         ];
68     }
69     DIALOGUE.superclass.constructor.apply(this, [config]);
71     if (config.closeButton !== false) {
72         // The buttons constructor does not allow custom attributes
73         this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
74     }
75 };
76 Y.extend(DIALOGUE, Y.Panel, {
77     // Window resize event listener.
78     _resizeevent : null,
79     // Orientation change event listener.
80     _orientationevent : null,
82     /**
83      * Initialise the dialogue.
84      *
85      * @method initializer
86      * @return void
87      */
88     initializer : function(config) {
89         var bb;
91         this.render();
92         this.makeResponsive();
93         this.after('visibleChange', this.visibilityChanged, this);
94         if (config.center) {
95             this.centerDialogue();
96         }
97         this.set('COUNT', COUNT);
99         // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
100         // and allow setting of z-index in theme.
101         bb = this.get('boundingBox');
103         if (config.extraClasses) {
104             Y.Array.each(config.extraClasses, bb.addClass, bb);
105         }
106         if (config.visible) {
107             this.applyZIndex();
108         }
109         // We must show - after the dialogue has been positioned,
110         // either by centerDialogue or makeResonsive. This is because the show() will trigger
111         // a focus on the dialogue, which will scroll the page. If the dialogue has not
112         // been positioned it will scroll back to the top of the page.
113         if (config.visible) {
114             this.show();
115         }
116     },
118     /**
119      * Either set the zindex to the supplied value, or set it to one more than the highest existing
120      * dialog in the page.
121      *
122      * @method visibilityChanged
123      * @return void
124      */
125     applyZIndex : function() {
126         var highestzindex = 0,
127             bb = this.get('boundingBox'),
128             zindex = this.get('zIndex');
129         if (zindex) {
130             // The zindex was specified so we should use that.
131             bb.setStyle('zIndex', zindex);
132         } else {
133             // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
134             Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR).each(function (node) {
135                 var zindex = this.findZIndex(node);
136                 if (zindex > highestzindex) {
137                     highestzindex = zindex;
138                 }
139             }, this);
140             // Only set the zindex if we found a wrapper.
141             if (highestzindex > 0) {
142                 bb.setStyle('zIndex', (highestzindex + 1).toString());
143             }
144         }
145     },
147     /**
148      * Finds the zIndex of the given node or its parent.
149      *
150      * @method findZIndex
151      * @param Node node
152      * @returns int Return either the zIndex of 0 if one was not found.
153      */
154     findZIndex : function(node) {
155         // In most cases the zindex is set on the parent of the dialog.
156         var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
157         if (zindex) {
158             return parseInt(zindex, 10);
159         }
160         return 0;
161     },
163     /**
164      * Event listener for the visibility changed event.
165      *
166      * @method visibilityChanged
167      * @return void
168      */
169     visibilityChanged : function(e) {
170         var titlebar;
171         if (e.attrName === 'visible') {
172             this.get('maskNode').addClass(CSS.LIGHTBOX);
173             if (e.prevVal && !e.newVal) {
174                 if (this._resizeevent) {
175                     this._resizeevent.detach();
176                     this._resizeevent = null;
177                 }
178                 if (this._orientationevent) {
179                     this._orientationevent.detach();
180                     this._orientationevent = null;
181                 }
182             }
183             if (!e.prevVal && e.newVal) {
184                 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
185                 this.applyZIndex();
186                 // This needs to be done each time the dialog is shown as the window may have been resized.
187                 this.makeResponsive();
188                 if (!this.shouldResizeFullscreen()) {
189                     if (this.get('draggable')) {
190                         titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
191                         this.plug(Y.Plugin.Drag, {handles : [titlebar]});
192                         Y.one(titlebar).setStyle('cursor', 'move');
193                     }
194                 }
195             }
196             if (this.get('center') && !e.prevVal && e.newVal) {
197                 this.centerDialogue();
198             }
199         }
200     },
201     /**
202      * If the responsive attribute is set on the dialog, and the window size is
203      * smaller than the responsive width - make the dialog fullscreen.
204      *
205      * @method makeResponsive
206      * @return void
207      */
208     makeResponsive : function() {
209         var bb = this.get('boundingBox'),
210             content;
212         if (this.shouldResizeFullscreen()) {
213             // Make this dialogue fullscreen on a small screen.
214             // Disable the page scrollbars.
216             // Size and position the fullscreen dialog.
218             bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
219             bb.setStyles({'left' : null,
220                           'top' : null,
221                           'width' : null,
222                           'height' : null,
223                           'right' : null,
224                           'bottom' : null});
226             content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
227             content.setStyle('overflow', 'auto');
228         } else {
229             if (this.get('responsive')) {
230                 // We must reset any of the fullscreen changes.
231                 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
232                     .setStyles({'width' : this.get('width'),
233                                 'height' : this.get('height')});
234                 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
235                 content.setStyle('overflow', 'inherit');
237             }
238         }
239     },
240     /**
241      * Center the dialog on the screen.
242      *
243      * @method centerDialogue
244      * @return void
245      */
246     centerDialogue : function() {
247         var bb = this.get('boundingBox'),
248             hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
249             x,
250             y;
252         // Don't adjust the position if we are in full screen mode.
253         if (this.shouldResizeFullscreen()) {
254             return;
255         }
256         if (hidden) {
257             bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
258         }
259         x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
260         y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
261         bb.setStyles({ 'left' : x, 'top' : y});
263         if (hidden) {
264             bb.addClass(DIALOGUE_HIDDEN_CLASS);
265         }
266     },
267     /**
268      * Return if this dialogue should be fullscreen or not.
269      * Responsive attribute must be true and we should not be in an iframe and the screen width should
270      * be less than the responsive width.
271      *
272      * @method shouldResizeFullscreen
273      * @return Boolean
274      */
275     shouldResizeFullscreen : function() {
276         return (window === window.parent) && this.get('responsive') &&
277                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
278     },
280     /**
281      * Override the show method to set keyboard focus on the dialogue.
282      *
283      * @method show
284      * @return void
285      */
286     show : function() {
287         var result = null,
288             header = this.headerNode,
289             content = this.bodyNode;
291         result = DIALOGUE.superclass.show.call(this);
292         if (header && header !== '') {
293             header.focus();
294         } else if (content && content !== '') {
295             content.focus();
296         }
297         return result;
298     }
299 }, {
300     NAME : DIALOGUE_NAME,
301     CSS_PREFIX : DIALOGUE_PREFIX,
302     ATTRS : {
303         notificationBase : {
305         },
307         /**
308          * Whether to display the dialogue modally and with a
309          * lightbox style.
310          *
311          * @attribute lightbox
312          * @type Boolean
313          * @default true
314          */
315         lightbox : {
316             validator : Y.Lang.isBoolean,
317             value : true
318         },
320         /**
321          * Whether to display a close button on the dialogue.
322          *
323          * Note, we do not recommend hiding the close button as this has
324          * potential accessibility concerns.
325          *
326          * @attribute closeButton
327          * @type Boolean
328          * @default true
329          */
330         closeButton : {
331             validator : Y.Lang.isBoolean,
332             value : true
333         },
335         /**
336          * The title for the close button if one is to be shown.
337          *
338          * @attribute closeButtonTitle
339          * @type String
340          * @default 'Close'
341          */
342         closeButtonTitle : {
343             validator : Y.Lang.isString,
344             value : 'Close'
345         },
347         /**
348          * Whether to display the dialogue centrally on the screen.
349          *
350          * @attribute center
351          * @type Boolean
352          * @default true
353          */
354         center : {
355             validator : Y.Lang.isBoolean,
356             value : true
357         },
359         /**
360          * Whether to make the dialogue movable around the page.
361          *
362          * @attribute draggable
363          * @type Boolean
364          * @default false
365          */
366         draggable : {
367             validator : Y.Lang.isBoolean,
368             value : false
369         },
371         /**
372          * Used to generate a unique id for the dialogue.
373          *
374          * @attribute COUNT
375          * @type Integer
376          * @default 0
377          */
378         COUNT: {
379             value: 0
380         },
382         /**
383          * Used to disable the fullscreen resizing behaviour if required.
384          *
385          * @attribute responsive
386          * @type Boolean
387          * @default true
388          */
389         responsive : {
390             validator : Y.Lang.isBoolean,
391             value : true
392         },
394         /**
395          * The width that this dialogue should be resized to fullscreen.
396          *
397          * @attribute responsiveWidth
398          * @type Integer
399          * @default 768
400          */
401         responsiveWidth : {
402             value : 768
403         }
404     }
405 });
407 M.core.dialogue = DIALOGUE;