Commit | Line | Data |
---|---|---|
78686995 AN |
1 | YUI.add('moodle-core-notification-dialogue', function (Y, NAME) { |
2 | ||
3 | var DIALOGUE_PREFIX, | |
4 | BASE, | |
5 | COUNT, | |
6 | CONFIRMYES, | |
7 | CONFIRMNO, | |
8 | TITLE, | |
9 | QUESTION, | |
10 | CSS; | |
11 | ||
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 | }; | |
29 | ||
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 | */ | |
38 | ||
39 | var 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 |
56 | DIALOGUE = 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 | }; | |
96 | Y.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 |
500 | Y.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 |
573 | M.core.dialogue = DIALOGUE; |
574 | ||
575 | ||
73747aea | 576 | }, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin", "moodle-core-lockscroll"]}); |