weekly release 2.7dev
[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
73747aea
AN
121 if (this.get('modal')) {
122 this.plug(Y.M.core.LockScroll);
123 }
124
78686995
AN
125 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
126 // and allow setting of z-index in theme.
d61c96b6 127 bb = this.get('boundingBox');
bf7c86cf
DW
128
129 if (config.extraClasses) {
130 Y.Array.each(config.extraClasses, bb.addClass, bb);
131 }
132 if (config.visible) {
133 this.applyZIndex();
134 }
1389bcd7
JF
135 // Recalculate the zIndex every time the modal is altered.
136 this.on('maskShow', this.applyZIndex);
ce5867a1
DW
137 // We must show - after the dialogue has been positioned,
138 // either by centerDialogue or makeResonsive. This is because the show() will trigger
139 // a focus on the dialogue, which will scroll the page. If the dialogue has not
140 // been positioned it will scroll back to the top of the page.
141 if (config.visible) {
142 this.show();
586d393f 143 this.keyDelegation();
ce5867a1 144 }
0860dd78
AN
145
146 // Remove the dialogue from the DOM when it is destroyed.
147 this.after('destroyedChange', function(){
148 this.get(BASE).remove(true);
149 }, this);
bf7c86cf
DW
150 },
151
152 /**
153 * Either set the zindex to the supplied value, or set it to one more than the highest existing
154 * dialog in the page.
155 *
156 * @method visibilityChanged
157 * @return void
158 */
159 applyZIndex : function() {
1389bcd7
JF
160 var highestzindex = 1,
161 zindexvalue = 1,
f2b235cb 162 bb = this.get('boundingBox'),
1389bcd7 163 ol = this.get('maskNode'),
f2b235cb 164 zindex = this.get('zIndex');
1389bcd7 165 if (zindex !== 0 && !this._calculatedzindex) {
bf7c86cf 166 // The zindex was specified so we should use that.
f2b235cb 167 bb.setStyle('zIndex', zindex);
bf7c86cf 168 } else {
f2b235cb 169 // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
1389bcd7 170 Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR+', '+HAS_ZINDEX).each(function (node) {
f2b235cb
SH
171 var zindex = this.findZIndex(node);
172 if (zindex > highestzindex) {
173 highestzindex = zindex;
bf7c86cf 174 }
f2b235cb 175 }, this);
bf7c86cf 176 // Only set the zindex if we found a wrapper.
1389bcd7
JF
177 zindexvalue = (highestzindex + 1).toString();
178 bb.setStyle('zIndex', zindexvalue);
179 ol.setStyle('zIndex', zindexvalue);
180 this.set('zIndex', zindexvalue);
181 this._calculatedzindex = true;
bf7c86cf
DW
182 }
183 },
184
f2b235cb
SH
185 /**
186 * Finds the zIndex of the given node or its parent.
187 *
188 * @method findZIndex
189 * @param Node node
190 * @returns int Return either the zIndex of 0 if one was not found.
191 */
192 findZIndex : function(node) {
193 // In most cases the zindex is set on the parent of the dialog.
194 var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
195 if (zindex) {
196 return parseInt(zindex, 10);
197 }
198 return 0;
199 },
200
bf7c86cf
DW
201 /**
202 * Event listener for the visibility changed event.
203 *
204 * @method visibilityChanged
205 * @return void
206 */
78686995 207 visibilityChanged : function(e) {
586d393f 208 var titlebar, bb;
78686995
AN
209 if (e.attrName === 'visible') {
210 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6 211 if (e.prevVal && !e.newVal) {
586d393f 212 bb = this.get('boundingBox');
d61c96b6
DW
213 if (this._resizeevent) {
214 this._resizeevent.detach();
215 this._resizeevent = null;
216 }
217 if (this._orientationevent) {
218 this._orientationevent.detach();
219 this._orientationevent = null;
220 }
586d393f 221 bb.detach('key', this.keyDelegation);
d61c96b6 222 }
bf7c86cf
DW
223 if (!e.prevVal && e.newVal) {
224 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
225 this.applyZIndex();
226 // This needs to be done each time the dialog is shown as the window may have been resized.
227 this.makeResponsive();
228 if (!this.shouldResizeFullscreen()) {
229 if (this.get('draggable')) {
230 titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
231 this.plug(Y.Plugin.Drag, {handles : [titlebar]});
232 Y.one(titlebar).setStyle('cursor', 'move');
233 }
234 }
586d393f 235 this.keyDelegation();
bf7c86cf 236 }
78686995
AN
237 if (this.get('center') && !e.prevVal && e.newVal) {
238 this.centerDialogue();
239 }
78686995
AN
240 }
241 },
bf7c86cf
DW
242 /**
243 * If the responsive attribute is set on the dialog, and the window size is
244 * smaller than the responsive width - make the dialog fullscreen.
245 *
246 * @method makeResponsive
247 * @return void
248 */
249 makeResponsive : function() {
78686995 250 var bb = this.get('boundingBox'),
bf7c86cf
DW
251 content;
252
253 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
254 // Make this dialogue fullscreen on a small screen.
255 // Disable the page scrollbars.
bf7c86cf 256
d61c96b6
DW
257 // Size and position the fullscreen dialog.
258
2a808cef
DW
259 bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
260 bb.setStyles({'left' : null,
261 'top' : null,
262 'width' : null,
263 'height' : null,
264 'right' : null,
265 'bottom' : null});
d61c96b6
DW
266
267 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6
DW
268 } else {
269 if (this.get('responsive')) {
270 // We must reset any of the fullscreen changes.
2a808cef
DW
271 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
272 .setStyles({'width' : this.get('width'),
bf7c86cf 273 'height' : this.get('height')});
d61c96b6 274 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6 275 }
d61c96b6 276 }
bf7c86cf
DW
277 },
278 /**
279 * Center the dialog on the screen.
280 *
281 * @method centerDialogue
282 * @return void
283 */
284 centerDialogue : function() {
285 var bb = this.get('boundingBox'),
286 hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
287 x,
288 y;
289
290 // Don't adjust the position if we are in full screen mode.
291 if (this.shouldResizeFullscreen()) {
292 return;
293 }
294 if (hidden) {
295 bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
296 }
297 x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
298 y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
299 bb.setStyles({ 'left' : x, 'top' : y});
78686995
AN
300
301 if (hidden) {
bf7c86cf 302 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 303 }
d61c96b6 304 },
bf7c86cf
DW
305 /**
306 * Return if this dialogue should be fullscreen or not.
307 * Responsive attribute must be true and we should not be in an iframe and the screen width should
308 * be less than the responsive width.
309 *
310 * @method shouldResizeFullscreen
311 * @return Boolean
312 */
313 shouldResizeFullscreen : function() {
314 return (window === window.parent) && this.get('responsive') &&
315 Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
2eaaae00
JF
316 },
317
318 /**
319 * Override the show method to set keyboard focus on the dialogue.
320 *
321 * @method show
322 * @return void
323 */
324 show : function() {
325 var result = null,
326 header = this.headerNode,
327 content = this.bodyNode;
328
73747aea
AN
329 // Lock scroll if the plugin is present.
330 if (this.lockScroll) {
331 this.lockScroll.enableScrollLock();
332 }
333
2eaaae00
JF
334 result = DIALOGUE.superclass.show.call(this);
335 if (header && header !== '') {
336 header.focus();
337 } else if (content && content !== '') {
338 content.focus();
339 }
340 return result;
586d393f 341 },
73747aea
AN
342
343 hide: function() {
344 // Unlock scroll if the plugin is present.
345 if (this.lockScroll) {
346 this.lockScroll.disableScrollLock();
347 }
348
349 return DIALOGUE.superclass.hide.call(this, arguments);
350 },
586d393f 351 /**
352 * Setup key delegation to keep tabbing within the open dialogue.
353 *
354 * @method keyDelegation
355 */
356 keyDelegation : function() {
357 var bb = this.get('boundingBox');
358 bb.delegate('key', function(e){
359 var target = e.target;
360 var direction = 'forward';
361 if (e.shiftKey) {
362 direction = 'backward';
363 }
364 if (this.trapFocus(target, direction)) {
365 e.preventDefault();
366 }
367 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
368 },
369 /**
370 * Trap the tab focus within the open modal.
371 *
372 * @param string target the element target
373 * @param string direction tab key for forward and tab+shift for backward
374 * @returns bool
375 */
376 trapFocus : function(target, direction) {
377 var bb = this.get('boundingBox'),
378 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
379 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
380
381 if (target === lastitem && direction === 'forward') { // Tab key.
382 return firstitem.focus();
383 } else if (target === firstitem && direction === 'backward') { // Tab+shift key.
384 return lastitem.focus();
385 }
78686995
AN
386 }
387}, {
388 NAME : DIALOGUE_NAME,
389 CSS_PREFIX : DIALOGUE_PREFIX,
390 ATTRS : {
391 notificationBase : {
392
393 },
394
395 /**
396 * Whether to display the dialogue modally and with a
397 * lightbox style.
398 *
399 * @attribute lightbox
400 * @type Boolean
401 * @default true
cff3b8fe 402 * @deprecated Since Moodle 2.7. Please use modal instead.
78686995 403 */
cff3b8fe
AN
404 lightbox: {
405 lazyAdd: false,
406 setter: function(value) {
407 Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, please use the modal attribute instead",
408 'warn', 'moodle-core-notification-dialogue');
409 this.set('modal', value);
410 }
78686995
AN
411 },
412
413 /**
414 * Whether to display a close button on the dialogue.
415 *
416 * Note, we do not recommend hiding the close button as this has
417 * potential accessibility concerns.
418 *
419 * @attribute closeButton
420 * @type Boolean
421 * @default true
422 */
423 closeButton : {
424 validator : Y.Lang.isBoolean,
425 value : true
426 },
427
428 /**
429 * The title for the close button if one is to be shown.
430 *
431 * @attribute closeButtonTitle
432 * @type String
433 * @default 'Close'
434 */
435 closeButtonTitle : {
436 validator : Y.Lang.isString,
437 value : 'Close'
438 },
439
440 /**
441 * Whether to display the dialogue centrally on the screen.
442 *
443 * @attribute center
444 * @type Boolean
445 * @default true
446 */
447 center : {
448 validator : Y.Lang.isBoolean,
449 value : true
450 },
451
452 /**
453 * Whether to make the dialogue movable around the page.
454 *
455 * @attribute draggable
456 * @type Boolean
457 * @default false
458 */
459 draggable : {
460 validator : Y.Lang.isBoolean,
461 value : false
462 },
bf7c86cf
DW
463
464 /**
465 * Used to generate a unique id for the dialogue.
466 *
467 * @attribute COUNT
468 * @type Integer
469 * @default 0
470 */
78686995
AN
471 COUNT: {
472 value: 0
d61c96b6 473 },
bf7c86cf
DW
474
475 /**
476 * Used to disable the fullscreen resizing behaviour if required.
477 *
478 * @attribute responsive
479 * @type Boolean
480 * @default true
481 */
d61c96b6
DW
482 responsive : {
483 validator : Y.Lang.isBoolean,
484 value : true
485 },
bf7c86cf
DW
486
487 /**
488 * The width that this dialogue should be resized to fullscreen.
489 *
490 * @attribute responsiveWidth
491 * @type Integer
492 * @default 768
493 */
d61c96b6
DW
494 responsiveWidth : {
495 value : 768
78686995
AN
496 }
497 }
498});
499
16d02434
AN
500Y.Base.modifyAttrs(DIALOGUE, {
501 /**
502 * String with units, or number, representing the width of the Widget.
503 * If a number is provided, the default unit, defined by the Widgets
504 * DEF_UNIT, property is used.
505 *
506 * If a value of 'auto' is used, then an empty String is instead
507 * returned.
508 *
509 * @attribute width
510 * @default '400px'
511 * @type {String|Number}
512 */
513 width: {
514 value: '400px',
515 setter: function(value) {
516 if (value === 'auto') {
517 return '';
518 }
519 return value;
520 }
c46cca4f
AN
521 },
522
523 /**
524 * Boolean indicating whether or not the Widget is visible.
525 *
526 * We override this from the default Widget attribute value.
527 *
528 * @attribute visible
529 * @default false
530 * @type Boolean
531 */
532 visible: {
533 value: false
a67233e7
AN
534 },
535
536 /**
537 * A convenience Attribute, which can be used as a shortcut for the
538 * `align` Attribute.
539 *
540 * Note: We override this in Moodle such that it sets a value for the
541 * `center` attribute if set. The `centered` will always return false.
542 *
543 * @attribute centered
544 * @type Boolean|Node
545 * @default false
546 */
547 centered: {
548 setter: function(value) {
549 if (value) {
550 this.set('center', true);
551 }
552 return false;
553 }
d9bf4be4
SH
554 },
555
556 /**
557 * Boolean determining whether to render the widget during initialisation.
558 *
559 * We override this to change the default from false to true for the dialogue.
560 * We then proceed to early render the dialogue during our initialisation rather than waiting
561 * for YUI to render it after that.
562 *
563 * @attribute render
564 * @type Boolean
565 * @default true
566 */
567 render : {
568 value : true,
569 writeOnce : true
16d02434
AN
570 }
571});
572
78686995
AN
573M.core.dialogue = DIALOGUE;
574
575
73747aea 576}, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin", "moodle-core-lockscroll"]});