on-demand release 2.6beta+
[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]',
45 CAN_RECEIVE_FOCUS_SELECTOR = 'input:not([type="hidden"]), a[href], button, textarea, select, [tabindex]';
78686995
AN
46
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 */
55DIALOGUE = function(config) {
56 COUNT++;
bf7c86cf 57 var id = 'moodle-dialogue-'+COUNT;
78686995
AN
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>')
baffb422 61 .append(Y.Node.create('<div id="'+id+'-header-text" class="'+CSS.HEADER+' yui3-widget-hd"></div>'))
78686995
AN
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);
b59f2e3b
SH
65
66 if (config.additionalBaseClass) {
67 config.notificationBase.addClass(config.additionalBaseClass);
68 }
69
78686995
AN
70 config.srcNode = '#'+id;
71 config.width = config.width || '400px';
72 config.visible = config.visible || false;
4fd8adab 73 config.center = config.centered && true;
78686995
AN
74 config.centered = false;
75 config.COUNT = COUNT;
76
b59f2e3b
SH
77 if (config.width === 'auto') {
78 delete config.width;
79 }
80
78686995
AN
81 // lightbox param to keep the stable versions API.
82 if (config.lightbox !== false) {
83 config.modal = true;
84 }
85 delete config.lightbox;
86
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]);
102
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};
108Y.extend(DIALOGUE, Y.Panel, {
d61c96b6
DW
109 // Window resize event listener.
110 _resizeevent : null,
111 // Orientation change event listener.
112 _orientationevent : null,
113
bf7c86cf
DW
114 /**
115 * Initialise the dialogue.
116 *
117 * @method initializer
118 * @return void
119 */
120 initializer : function(config) {
121 var bb;
122
78686995 123 this.render();
ce5867a1 124 this.makeResponsive();
bf7c86cf 125 this.after('visibleChange', this.visibilityChanged, this);
dd66b6ab
DW
126 if (config.center) {
127 this.centerDialogue();
128 }
78686995
AN
129 this.set('COUNT', COUNT);
130
131 // Workaround upstream YUI bug http://yuilibrary.com/projects/yui3/ticket/2532507
132 // and allow setting of z-index in theme.
d61c96b6 133 bb = this.get('boundingBox');
bf7c86cf
DW
134
135 if (config.extraClasses) {
136 Y.Array.each(config.extraClasses, bb.addClass, bb);
137 }
138 if (config.visible) {
139 this.applyZIndex();
140 }
ce5867a1
DW
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();
586d393f 147 this.keyDelegation();
ce5867a1 148 }
bf7c86cf
DW
149 },
150
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,
f2b235cb
SH
160 bb = this.get('boundingBox'),
161 zindex = this.get('zIndex');
162 if (zindex) {
bf7c86cf 163 // The zindex was specified so we should use that.
f2b235cb 164 bb.setStyle('zIndex', zindex);
bf7c86cf 165 } else {
f2b235cb
SH
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;
bf7c86cf 171 }
f2b235cb 172 }, this);
bf7c86cf
DW
173 // Only set the zindex if we found a wrapper.
174 if (highestzindex > 0) {
f2b235cb 175 bb.setStyle('zIndex', (highestzindex + 1).toString());
bf7c86cf
DW
176 }
177 }
178 },
179
f2b235cb
SH
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 },
195
bf7c86cf
DW
196 /**
197 * Event listener for the visibility changed event.
198 *
199 * @method visibilityChanged
200 * @return void
201 */
78686995 202 visibilityChanged : function(e) {
586d393f 203 var titlebar, bb;
78686995
AN
204 if (e.attrName === 'visible') {
205 this.get('maskNode').addClass(CSS.LIGHTBOX);
d61c96b6 206 if (e.prevVal && !e.newVal) {
586d393f 207 bb = this.get('boundingBox');
d61c96b6
DW
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 }
586d393f 216 bb.detach('key', this.keyDelegation);
d61c96b6 217 }
bf7c86cf
DW
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 }
586d393f 230 this.keyDelegation();
bf7c86cf 231 }
78686995
AN
232 if (this.get('center') && !e.prevVal && e.newVal) {
233 this.centerDialogue();
234 }
78686995
AN
235 }
236 },
bf7c86cf
DW
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() {
78686995 245 var bb = this.get('boundingBox'),
bf7c86cf
DW
246 content;
247
248 if (this.shouldResizeFullscreen()) {
d61c96b6
DW
249 // Make this dialogue fullscreen on a small screen.
250 // Disable the page scrollbars.
bf7c86cf 251
d61c96b6
DW
252 // Size and position the fullscreen dialog.
253
2a808cef
DW
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});
d61c96b6
DW
261
262 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6
DW
263 } else {
264 if (this.get('responsive')) {
265 // We must reset any of the fullscreen changes.
2a808cef
DW
266 bb.removeClass(DIALOGUE_FULLSCREEN_CLASS)
267 .setStyles({'width' : this.get('width'),
bf7c86cf 268 'height' : this.get('height')});
d61c96b6 269 content = Y.one('#' + this.get('id') + ' .' + CSS.BODY);
d61c96b6 270 }
d61c96b6 271 }
bf7c86cf
DW
272 },
273 /**
274 * Center the dialog on the screen.
275 *
276 * @method centerDialogue
277 * @return void
278 */
279 centerDialogue : function() {
280 var bb = this.get('boundingBox'),
281 hidden = bb.hasClass(DIALOGUE_HIDDEN_CLASS),
282 x,
283 y;
284
285 // Don't adjust the position if we are in full screen mode.
286 if (this.shouldResizeFullscreen()) {
287 return;
288 }
289 if (hidden) {
290 bb.setStyle('top', '-1000px').removeClass(DIALOGUE_HIDDEN_CLASS);
291 }
292 x = Math.max(Math.round((bb.get('winWidth') - bb.get('offsetWidth'))/2), 15);
293 y = Math.max(Math.round((bb.get('winHeight') - bb.get('offsetHeight'))/2), 15) + Y.one(window).get('scrollTop');
294 bb.setStyles({ 'left' : x, 'top' : y});
78686995
AN
295
296 if (hidden) {
bf7c86cf 297 bb.addClass(DIALOGUE_HIDDEN_CLASS);
78686995 298 }
d61c96b6 299 },
bf7c86cf
DW
300 /**
301 * Return if this dialogue should be fullscreen or not.
302 * Responsive attribute must be true and we should not be in an iframe and the screen width should
303 * be less than the responsive width.
304 *
305 * @method shouldResizeFullscreen
306 * @return Boolean
307 */
308 shouldResizeFullscreen : function() {
309 return (window === window.parent) && this.get('responsive') &&
310 Math.floor(Y.one(document.body).get('winWidth')) < this.get('responsiveWidth');
2eaaae00
JF
311 },
312
313 /**
314 * Override the show method to set keyboard focus on the dialogue.
315 *
316 * @method show
317 * @return void
318 */
319 show : function() {
320 var result = null,
321 header = this.headerNode,
322 content = this.bodyNode;
323
324 result = DIALOGUE.superclass.show.call(this);
325 if (header && header !== '') {
326 header.focus();
327 } else if (content && content !== '') {
328 content.focus();
329 }
330 return result;
586d393f 331 },
332 /**
333 * Setup key delegation to keep tabbing within the open dialogue.
334 *
335 * @method keyDelegation
336 */
337 keyDelegation : function() {
338 var bb = this.get('boundingBox');
339 bb.delegate('key', function(e){
340 var target = e.target;
341 var direction = 'forward';
342 if (e.shiftKey) {
343 direction = 'backward';
344 }
345 if (this.trapFocus(target, direction)) {
346 e.preventDefault();
347 }
348 }, 'down:9', CAN_RECEIVE_FOCUS_SELECTOR, this);
349 },
350 /**
351 * Trap the tab focus within the open modal.
352 *
353 * @param string target the element target
354 * @param string direction tab key for forward and tab+shift for backward
355 * @returns bool
356 */
357 trapFocus : function(target, direction) {
358 var bb = this.get('boundingBox'),
359 firstitem = bb.one(CAN_RECEIVE_FOCUS_SELECTOR),
360 lastitem = bb.all(CAN_RECEIVE_FOCUS_SELECTOR).pop();
361
362 if (target === lastitem && direction === 'forward') { // Tab key.
363 return firstitem.focus();
364 } else if (target === firstitem && direction === 'backward') { // Tab+shift key.
365 return lastitem.focus();
366 }
78686995
AN
367 }
368}, {
369 NAME : DIALOGUE_NAME,
370 CSS_PREFIX : DIALOGUE_PREFIX,
371 ATTRS : {
372 notificationBase : {
373
374 },
375
376 /**
377 * Whether to display the dialogue modally and with a
378 * lightbox style.
379 *
380 * @attribute lightbox
381 * @type Boolean
382 * @default true
383 */
384 lightbox : {
385 validator : Y.Lang.isBoolean,
386 value : true
387 },
388
389 /**
390 * Whether to display a close button on the dialogue.
391 *
392 * Note, we do not recommend hiding the close button as this has
393 * potential accessibility concerns.
394 *
395 * @attribute closeButton
396 * @type Boolean
397 * @default true
398 */
399 closeButton : {
400 validator : Y.Lang.isBoolean,
401 value : true
402 },
403
404 /**
405 * The title for the close button if one is to be shown.
406 *
407 * @attribute closeButtonTitle
408 * @type String
409 * @default 'Close'
410 */
411 closeButtonTitle : {
412 validator : Y.Lang.isString,
413 value : 'Close'
414 },
415
416 /**
417 * Whether to display the dialogue centrally on the screen.
418 *
419 * @attribute center
420 * @type Boolean
421 * @default true
422 */
423 center : {
424 validator : Y.Lang.isBoolean,
425 value : true
426 },
427
428 /**
429 * Whether to make the dialogue movable around the page.
430 *
431 * @attribute draggable
432 * @type Boolean
433 * @default false
434 */
435 draggable : {
436 validator : Y.Lang.isBoolean,
437 value : false
438 },
bf7c86cf
DW
439
440 /**
441 * Used to generate a unique id for the dialogue.
442 *
443 * @attribute COUNT
444 * @type Integer
445 * @default 0
446 */
78686995
AN
447 COUNT: {
448 value: 0
d61c96b6 449 },
bf7c86cf
DW
450
451 /**
452 * Used to disable the fullscreen resizing behaviour if required.
453 *
454 * @attribute responsive
455 * @type Boolean
456 * @default true
457 */
d61c96b6
DW
458 responsive : {
459 validator : Y.Lang.isBoolean,
460 value : true
461 },
bf7c86cf
DW
462
463 /**
464 * The width that this dialogue should be resized to fullscreen.
465 *
466 * @attribute responsiveWidth
467 * @type Integer
468 * @default 768
469 */
d61c96b6
DW
470 responsiveWidth : {
471 value : 768
78686995
AN
472 }
473 }
474});
475
476M.core.dialogue = DIALOGUE;
477
478
479}, '@VERSION@', {"requires": ["base", "node", "panel", "event-key", "dd-plugin"]});