MDL-42928 JavaScript: Create a module to handle page scroll locking
[moodle.git] / lib / yui / build / moodle-core-notification-dialogue / moodle-core-notification-dialogue-debug.js
CommitLineData
78686995
AN
1YUI.add('moodle-core-notification-dialogue', function (Y, NAME) {
2
3var DIALOGUE_PREFIX,
4 BASE,
5 COUNT,
6 CONFIRMYES,
7 CONFIRMNO,
8 TITLE,
9 QUESTION,
10 CSS;
11
12DIALOGUE_PREFIX = 'moodle-dialogue',
13BASE = 'notificationBase',
14COUNT = 0,
15CONFIRMYES = 'yesLabel',
16CONFIRMNO = 'noLabel',
17TITLE = 'title',
18QUESTION = 'question',
19CSS = {
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};
29
30// Set up the namespace once.
31M.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 */
38
39var DIALOGUE_NAME = 'Moodle dialogue',
bf7c86cf 40 DIALOGUE,
f2b235cb
SH
41 DIALOGUE_FULLSCREEN_CLASS = DIALOGUE_PREFIX + '-fullscreen',
42 DIALOGUE_HIDDEN_CLASS = DIALOGUE_PREFIX + '-hidden',
f2b235cb 43 DIALOGUE_SELECTOR =' [role=dialog]',
586d393f 44 MENUBAR_SELECTOR = '[role=menubar]',
1389bcd7 45 HAS_ZINDEX = '.moodle-has-zindex',
586d393f 46 CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]';
78686995
AN
47
48/**
49 * A re-usable dialogue box with Moodle classes applied.
50 *
4246e5c7 51 * @param {Object} c Object literal specifying the dialogue configuration properties.
78686995
AN
52 * @constructor
53 * @class M.core.dialogue
54 * @extends Y.Panel
55 */
4246e5c7
AN
56DIALOGUE = function(c) {
57 var config = Y.clone(c);
78686995 58 COUNT++;
bf7c86cf 59 var id = 'moodle-dialogue-'+COUNT;
78686995
AN
60 config.notificationBase =
61 Y.Node.create('<div class="'+CSS.BASE+'">')
62 .append(Y.Node.create('<div id="'+id+'" role="dialog" aria-labelledby="'+id+'-header-text" class="'+CSS.WRAP+'"></div>')
baffb422 63 .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
78686995
AN
64 .append(Y.Node.create('<div class="'+CSS.BODY+' yui3-widget-bd"></div>'))
65 .append(Y.Node.create('<div class="'+CSS.FOOTER+' yui3-widget-ft"></div>')));
66 Y.one(document.body).append(config.notificationBase);
b59f2e3b
SH
67
68 if (config.additionalBaseClass) {
69 config.notificationBase.addClass(config.additionalBaseClass);
70 }
71
78686995 72 config.srcNode = '#'+id;
78686995
AN
73 config.COUNT = COUNT;
74
78686995
AN
75 // closeButton param to keep the stable versions API.
76 if (config.closeButton === false) {
77 config.buttons = null;
78 } else {
79 config.buttons = [
80 {
81 section: Y.WidgetStdMod.HEADER,
82 classNames: 'closebutton',
83 action: function () {
84 this.hide();
85 }
86 }
87 ];
88 }
89 DIALOGUE.superclass.constructor.apply(this, [config]);
90
91 if (config.closeButton !== false) {
92 // The buttons constructor does not allow custom attributes
93 this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
94 }
95};
96Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
97 // Window resize event listener.
98 _resizeevent : null,
99 // Orientation change event listener.
100 _orientationevent : null,
1389bcd7 101 _calculatedzindex : false,
bf7c86cf
DW
102 /**
103 * Initialise the dialogue.
104 *
105 * @method initializer
106 * @return void
107 */
108 initializer : function(config) {
109 var bb;
110
d9bf4be4
SH
111 if (this.get('render')) {
112 this.render();
113 }
ce5867a1 114 this.makeResponsive();
bf7c86cf 115 this.after('visibleChange', this.visibilityChanged, this);
dd66b6ab
DW
116 if (config.center) {
117 this.centerDialogue();
118 }
78686995
AN
119 this.set('COUNT', COUNT);
120
121 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
122 // and allow setting of z-index in theme.
d61c96b6 123 bb = this.get('boundingBox');
bf7c86cf
DW
124
125 if (config.extraClasses) {
126 Y.Array.each(config.extraClasses, bb.addClass, bb);
127 }
128 if (config.visible) {
129 this.applyZIndex();
130 }
1389bcd7
JF
131 // Recalculate the zIndex every time the modal is altered.
132 this.on('maskShow', this.applyZIndex);
ce5867a1
DW
133 // We must show - after the dialogue has been positioned,
134 // either by centerDialogue or makeResonsive. This is because the show() will trigger
135 // a focus on the dialogue, which will scroll the page. If the dialogue has not
136 // been positioned it will scroll back to the top of the page.
137 if (config.visible) {
138 this.show();
586d393f 139 this.keyDelegation();
ce5867a1 140 }
bf7c86cf
DW
141 },
142
143 /**
144 * Either set the zindex to the supplied value, or set it to one more than the highest existing
145 * dialog in the page.
146 *
147 * @method visibilityChanged
148 * @return void
149 */
150 applyZIndex : function() {
1389bcd7
JF
151 var highestzindex = 1,
152 zindexvalue = 1,
f2b235cb 153 bb = this.get('boundingBox'),
1389bcd7 154 ol = this.get('maskNode'),
f2b235cb 155 zindex = this.get('zIndex');
1389bcd7 156 if (zindex !== 0 && !this._calculatedzindex) {
bf7c86cf 157 // The zindex was specified so we should use that.
f2b235cb 158 bb.setStyle('zIndex', zindex);
bf7c86cf 159 } else {
f2b235cb 160 // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
1389bcd7 161 Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR+', '+HAS_ZINDEX).each(function (node) {
f2b235cb
SH
162 var zindex = this.findZIndex(node);
163 if (zindex > highestzindex) {
164 highestzindex = zindex;
bf7c86cf 165 }
f2b235cb 166 }, this);
bf7c86cf 167 // Only set the zindex if we found a wrapper.
1389bcd7
JF
168 zindexvalue = (highestzindex + 1).toString();
169 bb.setStyle('zIndex', zindexvalue);
170 ol.setStyle('zIndex', zindexvalue);
171 this.set('zIndex', zindexvalue);
172 this._calculatedzindex = true;
bf7c86cf
DW
173 }
174 },
175
f2b235cb
SH
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 },
191
bf7c86cf
DW
192 /**
193 * Event listener for the visibility changed event.
194 *
195 * @method visibilityChanged
196 * @return void
197 */
78686995 198 visibilityChanged : function(e) {
586d393f 199 var titlebar, bb;
78686995
AN
200 if (e.attrName === 'visible') {
201 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6 202 if (e.prevVal && !e.newVal) {
586d393f 203 bb = this.get('boundingBox');
d61c96b6
DW
204 if (this._resizeevent) {
205 this._resizeevent.detach();
206 this._resizeevent = null;
207 }
208 if (this._orientationevent) {
209 this._orientationevent.detach();
210 this._orientationevent = null;
211 }
586d393f 212 bb.detach('key', this.keyDelegation);
d61c96b6 213 }
bf7c86cf
DW
214 if (!e.prevVal && e.newVal) {
215 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
216 this.applyZIndex();
217 // This needs to be done each time the dialog is shown as the window may have been resized.
218 this.makeResponsive();
219 if (!this.shouldResizeFullscreen()) {
220 if (this.get('draggable')) {
221 titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
222 this.plug(Y.Plugin.Drag, {handles : [titlebar]});
223 Y.one(titlebar).setStyle('cursor', 'move');
224 }
225 }
586d393f 226 this.keyDelegation();
bf7c86cf 227 }
78686995
AN
228 if (this.get('center') && !e.prevVal && e.newVal) {
229 this.centerDialogue();
230 }
78686995
AN
231 }
232 },
bf7c86cf
DW
233 /**
234 * If the responsive attribute is set on the dialog, and the window size is
235 * smaller than the responsive width - make the dialog fullscreen.
236 *
237 * @method makeResponsive
238 * @return void
239 */
240 makeResponsive : function() {
78686995 241 var bb = this.get('boundingBox'),
bf7c86cf
DW
242 content;
243
244 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
245 // Make this dialogue fullscreen on a small screen.
246 // Disable the page scrollbars.
bf7c86cf 247
d61c96b6
DW
248 // Size and position the fullscreen dialog.
249
2a808cef
DW
250 bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
251 bb.setStyles({'left' : null,
252 'top' : null,
253 'width' : null,
254 'height' : null,
255 'right' : null,
256 'bottom' : null});
d61c96b6
DW
257
258 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6
DW
259 } else {
260 if (this.get('responsive')) {
261 // We must reset any of the fullscreen changes.
2a808cef
DW
262 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
263 .setStyles({'width' : this.get('width'),
bf7c86cf 264 'height' : this.get('height')});
d61c96b6 265 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6 266 }
d61c96b6 267 }
bf7c86cf
DW
268 },
269 /**
270 * Center the dialog on the screen.
271 *
272 * @method centerDialogue
273 * @return void
274 */
275 centerDialogue : function() {
276 var bb = this.get('boundingBox'),
277 hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
278 x,
279 y;
280
281 // Don't adjust the position if we are in full screen mode.
282 if (this.shouldResizeFullscreen()) {
283 return;
284 }
285 if (hidden) {
286 bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
287 }
288 x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
289 y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
290 bb.setStyles({ 'left' : x, 'top' : y});
78686995
AN
291
292 if (hidden) {
bf7c86cf 293 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 294 }
d61c96b6 295 },
bf7c86cf
DW
296 /**
297 * Return if this dialogue should be fullscreen or not.
298 * Responsive attribute must be true and we should not be in an iframe and the screen width should
299 * be less than the responsive width.
300 *
301 * @method shouldResizeFullscreen
302 * @return Boolean
303 */
304 shouldResizeFullscreen : function() {
305 return (window === window.parent) && this.get('responsive') &&
306 Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
2eaaae00
JF
307 },
308
309 /**
310 * Override the show method to set keyboard focus on the dialogue.
311 *
312 * @method show
313 * @return void
314 */
315 show : function() {
316 var result = null,
317 header = this.headerNode,
318 content = this.bodyNode;
319
320 result = DIALOGUE.superclass.show.call(this);
321 if (header && header !== '') {
322 header.focus();
323 } else if (content && content !== '') {
324 content.focus();
325 }
326 return result;
586d393f 327 },
328 /**
329 * Setup key delegation to keep tabbing within the open dialogue.
330 *
331 * @method keyDelegation
332 */
333 keyDelegation : function() {
334 var bb = this.get('boundingBox');
335 bb.delegate('key', function(e){
336 var target = e.target;
337 var direction = 'forward';
338 if (e.shiftKey) {
339 direction = 'backward';
340 }
341 if (this.trapFocus(target, direction)) {
342 e.preventDefault();
343 }
344 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
345 },
346 /**
347 * Trap the tab focus within the open modal.
348 *
349 * @param string target the element target
350 * @param string direction tab key for forward and tab+shift for backward
351 * @returns bool
352 */
353 trapFocus : function(target, direction) {
354 var bb = this.get('boundingBox'),
355 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
356 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
357
358 if (target === lastitem && direction === 'forward') { // Tab key.
359 return firstitem.focus();
360 } else if (target === firstitem && direction === 'backward') { // Tab+shift key.
361 return lastitem.focus();
362 }
78686995
AN
363 }
364}, {
365 NAME : DIALOGUE_NAME,
366 CSS_PREFIX : DIALOGUE_PREFIX,
367 ATTRS : {
368 notificationBase : {
369
370 },
371
372 /**
373 * Whether to display the dialogue modally and with a
374 * lightbox style.
375 *
376 * @attribute lightbox
377 * @type Boolean
378 * @default true
cff3b8fe 379 * @deprecated Since Moodle 2.7. Please use modal instead.
78686995 380 */
cff3b8fe
AN
381 lightbox: {
382 lazyAdd: false,
383 setter: function(value) {
384 Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, please use the modal attribute instead",
385 'warn', 'moodle-core-notification-dialogue');
386 this.set('modal', value);
387 }
78686995
AN
388 },
389
390 /**
391 * Whether to display a close button on the dialogue.
392 *
393 * Note, we do not recommend hiding the close button as this has
394 * potential accessibility concerns.
395 *
396 * @attribute closeButton
397 * @type Boolean
398 * @default true
399 */
400 closeButton : {
401 validator : Y.Lang.isBoolean,
402 value : true
403 },
404
405 /**
406 * The title for the close button if one is to be shown.
407 *
408 * @attribute closeButtonTitle
409 * @type String
410 * @default 'Close'
411 */
412 closeButtonTitle : {
413 validator : Y.Lang.isString,
414 value : 'Close'
415 },
416
417 /**
418 * Whether to display the dialogue centrally on the screen.
419 *
420 * @attribute center
421 * @type Boolean
422 * @default true
423 */
424 center : {
425 validator : Y.Lang.isBoolean,
426 value : true
427 },
428
429 /**
430 * Whether to make the dialogue movable around the page.
431 *
432 * @attribute draggable
433 * @type Boolean
434 * @default false
435 */
436 draggable : {
437 validator : Y.Lang.isBoolean,
438 value : false
439 },
bf7c86cf
DW
440
441 /**
442 * Used to generate a unique id for the dialogue.
443 *
444 * @attribute COUNT
445 * @type Integer
446 * @default 0
447 */
78686995
AN
448 COUNT: {
449 value: 0
d61c96b6 450 },
bf7c86cf
DW
451
452 /**
453 * Used to disable the fullscreen resizing behaviour if required.
454 *
455 * @attribute responsive
456 * @type Boolean
457 * @default true
458 */
d61c96b6
DW
459 responsive : {
460 validator : Y.Lang.isBoolean,
461 value : true
462 },
bf7c86cf
DW
463
464 /**
465 * The width that this dialogue should be resized to fullscreen.
466 *
467 * @attribute responsiveWidth
468 * @type Integer
469 * @default 768
470 */
d61c96b6
DW
471 responsiveWidth : {
472 value : 768
78686995
AN
473 }
474 }
475});
476
16d02434
AN
477Y.Base.modifyAttrs(DIALOGUE, {
478 /**
479 * String with units, or number, representing the width of the Widget.
480 * If a number is provided, the default unit, defined by the Widgets
481 * DEF_UNIT, property is used.
482 *
483 * If a value of 'auto' is used, then an empty String is instead
484 * returned.
485 *
486 * @attribute width
487 * @default '400px'
488 * @type {String|Number}
489 */
490 width: {
491 value: '400px',
492 setter: function(value) {
493 if (value === 'auto') {
494 return '';
495 }
496 return value;
497 }
c46cca4f
AN
498 },
499
500 /**
501 * Boolean indicating whether or not the Widget is visible.
502 *
503 * We override this from the default Widget attribute value.
504 *
505 * @attribute visible
506 * @default false
507 * @type Boolean
508 */
509 visible: {
510 value: false
a67233e7
AN
511 },
512
513 /**
514 * A convenience Attribute, which can be used as a shortcut for the
515 * `align` Attribute.
516 *
517 * Note: We override this in Moodle such that it sets a value for the
518 * `center` attribute if set. The `centered` will always return false.
519 *
520 * @attribute centered
521 * @type Boolean|Node
522 * @default false
523 */
524 centered: {
525 setter: function(value) {
526 if (value) {
527 this.set('center', true);
528 }
529 return false;
530 }
d9bf4be4
SH
531 },
532
533 /**
534 * Boolean determining whether to render the widget during initialisation.
535 *
536 * We override this to change the default from false to true for the dialogue.
537 * We then proceed to early render the dialogue during our initialisation rather than waiting
538 * for YUI to render it after that.
539 *
540 * @attribute render
541 * @type Boolean
542 * @default true
543 */
544 render : {
545 value : true,
546 writeOnce : true
16d02434
AN
547 }
548});
549
78686995
AN
550M.core.dialogue = DIALOGUE;
551
552
553}, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});