MDL-43646 "disapprove" changed to "undo approval"
[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
AN
72 config.srcNode = '#'+id;
73 config.width = config.width || '400px';
4fd8adab 74 config.center = config.centered && true;
78686995
AN
75 config.centered = false;
76 config.COUNT = COUNT;
77
b59f2e3b
SH
78 if (config.width === 'auto') {
79 delete config.width;
80 }
81
78686995
AN
82 // closeButton param to keep the stable versions API.
83 if (config.closeButton === false) {
84 config.buttons = null;
85 } else {
86 config.buttons = [
87 {
88 section: Y.WidgetStdMod.HEADER,
89 classNames: 'closebutton',
90 action: function () {
91 this.hide();
92 }
93 }
94 ];
95 }
96 DIALOGUE.superclass.constructor.apply(this, [config]);
97
98 if (config.closeButton !== false) {
99 // The buttons constructor does not allow custom attributes
100 this.get('buttons').header[0].setAttribute('title', this.get('closeButtonTitle'));
101 }
102};
103Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
104 // Window resize event listener.
105 _resizeevent : null,
106 // Orientation change event listener.
107 _orientationevent : null,
1389bcd7 108 _calculatedzindex : false,
bf7c86cf
DW
109 /**
110 * Initialise the dialogue.
111 *
112 * @method initializer
113 * @return void
114 */
115 initializer : function(config) {
116 var bb;
117
78686995 118 this.render();
ce5867a1 119 this.makeResponsive();
bf7c86cf 120 this.after('visibleChange', this.visibilityChanged, this);
dd66b6ab
DW
121 if (config.center) {
122 this.centerDialogue();
123 }
78686995
AN
124 this.set('COUNT', COUNT);
125
126 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
127 // and allow setting of z-index in theme.
d61c96b6 128 bb = this.get('boundingBox');
bf7c86cf
DW
129
130 if (config.extraClasses) {
131 Y.Array.each(config.extraClasses, bb.addClass, bb);
132 }
133 if (config.visible) {
134 this.applyZIndex();
135 }
1389bcd7
JF
136 // Recalculate the zIndex every time the modal is altered.
137 this.on('maskShow', this.applyZIndex);
ce5867a1
DW
138 // We must show - after the dialogue has been positioned,
139 // either by centerDialogue or makeResonsive. This is because the show() will trigger
140 // a focus on the dialogue, which will scroll the page. If the dialogue has not
141 // been positioned it will scroll back to the top of the page.
142 if (config.visible) {
143 this.show();
586d393f 144 this.keyDelegation();
ce5867a1 145 }
bf7c86cf
DW
146 },
147
148 /**
149 * Either set the zindex to the supplied value, or set it to one more than the highest existing
150 * dialog in the page.
151 *
152 * @method visibilityChanged
153 * @return void
154 */
155 applyZIndex : function() {
1389bcd7
JF
156 var highestzindex = 1,
157 zindexvalue = 1,
f2b235cb 158 bb = this.get('boundingBox'),
1389bcd7 159 ol = this.get('maskNode'),
f2b235cb 160 zindex = this.get('zIndex');
1389bcd7 161 if (zindex !== 0 && !this._calculatedzindex) {
bf7c86cf 162 // The zindex was specified so we should use that.
f2b235cb 163 bb.setStyle('zIndex', zindex);
bf7c86cf 164 } else {
f2b235cb 165 // Determine the correct zindex by looking at all existing dialogs and menubars in the page.
1389bcd7 166 Y.all(DIALOGUE_SELECTOR+', '+MENUBAR_SELECTOR+', '+HAS_ZINDEX).each(function (node) {
f2b235cb
SH
167 var zindex = this.findZIndex(node);
168 if (zindex > highestzindex) {
169 highestzindex = zindex;
bf7c86cf 170 }
f2b235cb 171 }, this);
bf7c86cf 172 // Only set the zindex if we found a wrapper.
1389bcd7
JF
173 zindexvalue = (highestzindex + 1).toString();
174 bb.setStyle('zIndex', zindexvalue);
175 ol.setStyle('zIndex', zindexvalue);
176 this.set('zIndex', zindexvalue);
177 this._calculatedzindex = true;
bf7c86cf
DW
178 }
179 },
180
f2b235cb
SH
181 /**
182 * Finds the zIndex of the given node or its parent.
183 *
184 * @method findZIndex
185 * @param Node node
186 * @returns int Return either the zIndex of 0 if one was not found.
187 */
188 findZIndex : function(node) {
189 // In most cases the zindex is set on the parent of the dialog.
190 var zindex = node.getStyle('zIndex') || node.ancestor().getStyle('zIndex');
191 if (zindex) {
192 return parseInt(zindex, 10);
193 }
194 return 0;
195 },
196
bf7c86cf
DW
197 /**
198 * Event listener for the visibility changed event.
199 *
200 * @method visibilityChanged
201 * @return void
202 */
78686995 203 visibilityChanged : function(e) {
586d393f 204 var titlebar, bb;
78686995
AN
205 if (e.attrName === 'visible') {
206 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6 207 if (e.prevVal && !e.newVal) {
586d393f 208 bb = this.get('boundingBox');
d61c96b6
DW
209 if (this._resizeevent) {
210 this._resizeevent.detach();
211 this._resizeevent = null;
212 }
213 if (this._orientationevent) {
214 this._orientationevent.detach();
215 this._orientationevent = null;
216 }
586d393f 217 bb.detach('key', this.keyDelegation);
d61c96b6 218 }
bf7c86cf
DW
219 if (!e.prevVal && e.newVal) {
220 // This needs to be done each time the dialog is shown as new dialogs may have been opened.
221 this.applyZIndex();
222 // This needs to be done each time the dialog is shown as the window may have been resized.
223 this.makeResponsive();
224 if (!this.shouldResizeFullscreen()) {
225 if (this.get('draggable')) {
226 titlebar = '#' + this.get('id') + ' .' + CSS.HEADER;
227 this.plug(Y.Plugin.Drag, {handles : [titlebar]});
228 Y.one(titlebar).setStyle('cursor', 'move');
229 }
230 }
586d393f 231 this.keyDelegation();
bf7c86cf 232 }
78686995
AN
233 if (this.get('center') && !e.prevVal && e.newVal) {
234 this.centerDialogue();
235 }
78686995
AN
236 }
237 },
bf7c86cf
DW
238 /**
239 * If the responsive attribute is set on the dialog, and the window size is
240 * smaller than the responsive width - make the dialog fullscreen.
241 *
242 * @method makeResponsive
243 * @return void
244 */
245 makeResponsive : function() {
78686995 246 var bb = this.get('boundingBox'),
bf7c86cf
DW
247 content;
248
249 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
250 // Make this dialogue fullscreen on a small screen.
251 // Disable the page scrollbars.
bf7c86cf 252
d61c96b6
DW
253 // Size and position the fullscreen dialog.
254
2a808cef
DW
255 bb.addClass(DIALOGUE_FULLSCREEN_CLASS);
256 bb.setStyles({'left' : null,
257 'top' : null,
258 'width' : null,
259 'height' : null,
260 'right' : null,
261 'bottom' : null});
d61c96b6
DW
262
263 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6
DW
264 } else {
265 if (this.get('responsive')) {
266 // We must reset any of the fullscreen changes.
2a808cef
DW
267 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
268 .setStyles({'width' : this.get('width'),
bf7c86cf 269 'height' : this.get('height')});
d61c96b6 270 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6 271 }
d61c96b6 272 }
bf7c86cf
DW
273 },
274 /**
275 * Center the dialog on the screen.
276 *
277 * @method centerDialogue
278 * @return void
279 */
280 centerDialogue : function() {
281 var bb = this.get('boundingBox'),
282 hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
283 x,
284 y;
285
286 // Don't adjust the position if we are in full screen mode.
287 if (this.shouldResizeFullscreen()) {
288 return;
289 }
290 if (hidden) {
291 bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
292 }
293 x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
294 y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
295 bb.setStyles({ 'left' : x, 'top' : y});
78686995
AN
296
297 if (hidden) {
bf7c86cf 298 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 299 }
d61c96b6 300 },
bf7c86cf
DW
301 /**
302 * Return if this dialogue should be fullscreen or not.
303 * Responsive attribute must be true and we should not be in an iframe and the screen width should
304 * be less than the responsive width.
305 *
306 * @method shouldResizeFullscreen
307 * @return Boolean
308 */
309 shouldResizeFullscreen : function() {
310 return (window === window.parent) && this.get('responsive') &&
311 Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
2eaaae00
JF
312 },
313
314 /**
315 * Override the show method to set keyboard focus on the dialogue.
316 *
317 * @method show
318 * @return void
319 */
320 show : function() {
321 var result = null,
322 header = this.headerNode,
323 content = this.bodyNode;
324
325 result = DIALOGUE.superclass.show.call(this);
326 if (header && header !== '') {
327 header.focus();
328 } else if (content && content !== '') {
329 content.focus();
330 }
331 return result;
586d393f 332 },
333 /**
334 * Setup key delegation to keep tabbing within the open dialogue.
335 *
336 * @method keyDelegation
337 */
338 keyDelegation : function() {
339 var bb = this.get('boundingBox');
340 bb.delegate('key', function(e){
341 var target = e.target;
342 var direction = 'forward';
343 if (e.shiftKey) {
344 direction = 'backward';
345 }
346 if (this.trapFocus(target, direction)) {
347 e.preventDefault();
348 }
349 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
350 },
351 /**
352 * Trap the tab focus within the open modal.
353 *
354 * @param string target the element target
355 * @param string direction tab key for forward and tab+shift for backward
356 * @returns bool
357 */
358 trapFocus : function(target, direction) {
359 var bb = this.get('boundingBox'),
360 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
361 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
362
363 if (target === lastitem && direction === 'forward') { // Tab key.
364 return firstitem.focus();
365 } else if (target === firstitem && direction === 'backward') { // Tab+shift key.
366 return lastitem.focus();
367 }
78686995
AN
368 }
369}, {
370 NAME : DIALOGUE_NAME,
371 CSS_PREFIX : DIALOGUE_PREFIX,
372 ATTRS : {
373 notificationBase : {
374
375 },
376
377 /**
378 * Whether to display the dialogue modally and with a
379 * lightbox style.
380 *
381 * @attribute lightbox
382 * @type Boolean
383 * @default true
cff3b8fe 384 * @deprecated Since Moodle 2.7. Please use modal instead.
78686995 385 */
cff3b8fe
AN
386 lightbox: {
387 lazyAdd: false,
388 setter: function(value) {
389 Y.log("The lightbox attribute of M.core.dialogue has been deprecated since Moodle 2.7, please use the modal attribute instead",
390 'warn', 'moodle-core-notification-dialogue');
391 this.set('modal', value);
392 }
78686995
AN
393 },
394
395 /**
396 * Whether to display a close button on the dialogue.
397 *
398 * Note, we do not recommend hiding the close button as this has
399 * potential accessibility concerns.
400 *
401 * @attribute closeButton
402 * @type Boolean
403 * @default true
404 */
405 closeButton : {
406 validator : Y.Lang.isBoolean,
407 value : true
408 },
409
410 /**
411 * The title for the close button if one is to be shown.
412 *
413 * @attribute closeButtonTitle
414 * @type String
415 * @default 'Close'
416 */
417 closeButtonTitle : {
418 validator : Y.Lang.isString,
419 value : 'Close'
420 },
421
422 /**
423 * Whether to display the dialogue centrally on the screen.
424 *
425 * @attribute center
426 * @type Boolean
427 * @default true
428 */
429 center : {
430 validator : Y.Lang.isBoolean,
431 value : true
432 },
433
434 /**
435 * Whether to make the dialogue movable around the page.
436 *
437 * @attribute draggable
438 * @type Boolean
439 * @default false
440 */
441 draggable : {
442 validator : Y.Lang.isBoolean,
443 value : false
444 },
bf7c86cf
DW
445
446 /**
447 * Used to generate a unique id for the dialogue.
448 *
449 * @attribute COUNT
450 * @type Integer
451 * @default 0
452 */
78686995
AN
453 COUNT: {
454 value: 0
d61c96b6 455 },
bf7c86cf
DW
456
457 /**
458 * Used to disable the fullscreen resizing behaviour if required.
459 *
460 * @attribute responsive
461 * @type Boolean
462 * @default true
463 */
d61c96b6
DW
464 responsive : {
465 validator : Y.Lang.isBoolean,
466 value : true
467 },
bf7c86cf
DW
468
469 /**
470 * The width that this dialogue should be resized to fullscreen.
471 *
472 * @attribute responsiveWidth
473 * @type Integer
474 * @default 768
475 */
d61c96b6
DW
476 responsiveWidth : {
477 value : 768
78686995
AN
478 }
479 }
480});
481
482M.core.dialogue = DIALOGUE;
483
484
485}, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});