Merge branch '40976-26' of git://github.com/samhemelryk/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             content.setStyle('overflow', 'auto');
264         } else {
265             if (this.get('responsive')) {
266                 // We must reset any of the fullscreen changes.
267                 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
268                     .setStyles({'width' : this.get('width'),
269                                 'height' : this.get('height')});
270                 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
271                 content.setStyle('overflow', 'inherit');
273             }
274         }
275     },
276     /**
277      * Center the dialog on the screen.
278      *
279      * @method centerDialogue
280      * @return void
281      */
282     centerDialogue : function() {
283         var bb = this.get('boundingBox'),
284             hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
285             x,
286             y;
288         // Don't adjust the position if we are in full screen mode.
289         if (this.shouldResizeFullscreen()) {
290             return;
291         }
292         if (hidden) {
293             bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
294         }
295         x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
296         y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
297         bb.setStyles({ 'left' : x, 'top' : y});
299         if (hidden) {
300             bb.addClass(DIALOGUE_HIDDEN_CLASS);
301         }
302     },
303     /**
304      * Return if this dialogue should be fullscreen or not.
305      * Responsive attribute must be true and we should not be in an iframe and the screen width should
306      * be less than the responsive width.
307      *
308      * @method shouldResizeFullscreen
309      * @return Boolean
310      */
311     shouldResizeFullscreen : function() {
312         return (window === window.parent) && this.get('responsive') &&
313                Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
314     },
316     /**
317      * Override the show method to set keyboard focus on the dialogue.
318      *
319      * @method show
320      * @return void
321      */
322     show : function() {
323         var result = null,
324             header = this.headerNode,
325             content = this.bodyNode;
327         result = DIALOGUE.superclass.show.call(this);
328         if (header && header !== '') {
329             header.focus();
330         } else if (content && content !== '') {
331             content.focus();
332         }
333         return result;
334     },
335     /**
336      * Setup key delegation to keep tabbing within the open dialogue.
337      *
338      * @method keyDelegation
339      */
340     keyDelegation : function() {
341         var bb = this.get('boundingBox');
342         bb.delegate('key', function(e){
343             var target = e.target;
344             var direction = 'forward';
345             if (e.shiftKey) {
346                 direction = 'backward';
347             }
348             if (this.trapFocus(target, direction)) {
349                 e.preventDefault();
350             }
351         }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
352     },
353     /**
354      * Trap the tab focus within the open modal.
355      *
356      * @param string target the element target
357      * @param string direction tab key for forward and tab+shift for backward
358      * @returns bool
359      */
360     trapFocus : function(target, direction) {
361         var bb = this.get('boundingBox'),
362             firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
363             lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
365         if (target === lastitem && direction === 'forward') { // Tab key.
366             return firstitem.focus();
367         } else if (target === firstitem && direction === 'backward') {  // Tab+shift key.
368             return lastitem.focus();
369         }
370     }
371 }, {
372     NAME : DIALOGUE_NAME,
373     CSS_PREFIX : DIALOGUE_PREFIX,
374     ATTRS : {
375         notificationBase : {
377         },
379         /**
380          * Whether to display the dialogue modally and with a
381          * lightbox style.
382          *
383          * @attribute lightbox
384          * @type Boolean
385          * @default true
386          */
387         lightbox : {
388             validator : Y.Lang.isBoolean,
389             value : true
390         },
392         /**
393          * Whether to display a close button on the dialogue.
394          *
395          * Note, we do not recommend hiding the close button as this has
396          * potential accessibility concerns.
397          *
398          * @attribute closeButton
399          * @type Boolean
400          * @default true
401          */
402         closeButton : {
403             validator : Y.Lang.isBoolean,
404             value : true
405         },
407         /**
408          * The title for the close button if one is to be shown.
409          *
410          * @attribute closeButtonTitle
411          * @type String
412          * @default 'Close'
413          */
414         closeButtonTitle : {
415             validator : Y.Lang.isString,
416             value : 'Close'
417         },
419         /**
420          * Whether to display the dialogue centrally on the screen.
421          *
422          * @attribute center
423          * @type Boolean
424          * @default true
425          */
426         center : {
427             validator : Y.Lang.isBoolean,
428             value : true
429         },
431         /**
432          * Whether to make the dialogue movable around the page.
433          *
434          * @attribute draggable
435          * @type Boolean
436          * @default false
437          */
438         draggable : {
439             validator : Y.Lang.isBoolean,
440             value : false
441         },
443         /**
444          * Used to generate a unique id for the dialogue.
445          *
446          * @attribute COUNT
447          * @type Integer
448          * @default 0
449          */
450         COUNT: {
451             value: 0
452         },
454         /**
455          * Used to disable the fullscreen resizing behaviour if required.
456          *
457          * @attribute responsive
458          * @type Boolean
459          * @default true
460          */
461         responsive : {
462             validator : Y.Lang.isBoolean,
463             value : true
464         },
466         /**
467          * The width that this dialogue should be resized to fullscreen.
468          *
469          * @attribute responsiveWidth
470          * @type Integer
471          * @default 768
472          */
473         responsiveWidth : {
474             value : 768
475         }
476     }
477 });
479 M.core.dialogue = DIALOGUE;
482 }, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});