MDL-30847 core Adding ARIA attribute to help windows
[moodle.git] / lib / javascript-static.js
CommitLineData
47aa42e2 1// Miscellaneous core Javascript functions for Moodle
e50b4c89 2// Global M object is initilised in inline javascript
34f76e9a 3
e50b4c89 4/**
2f422271 5 * Add module to list of available modules that can be loaded from YUI.
126590b7 6 * @param {Array} modules
e50b4c89
PS
7 */
8M.yui.add_module = function(modules) {
126590b7 9 for (var modname in modules) {
2f422271 10 YUI_config.modules[modname] = modules[modname];
fc367afb 11 }
e50b4c89 12};
2a102b90
SH
13/**
14 * The gallery version to use when loading YUI modules from the gallery.
15 * Will be changed every time when using local galleries.
16 */
17M.yui.galleryversion = '2010.04.21-21-51';
38224dcb 18
bca09754
PS
19/**
20 * Various utility functions
21 */
d29b8f0f 22M.util = M.util || {};
38224dcb 23
2b728cb5
PS
24/**
25 * Language strings - initialised from page footer.
26 */
d29b8f0f 27M.str = M.str || {};
2b728cb5 28
38224dcb
PS
29/**
30 * Returns url for images.
126590b7
SH
31 * @param {String} imagename
32 * @param {String} component
33 * @return {String}
38224dcb
PS
34 */
35M.util.image_url = function(imagename, component) {
38224dcb 36
b003b556 37 if (!component || component == '' || component == 'moodle' || component == 'core') {
9d473266 38 component = 'core';
38224dcb
PS
39 }
40
9d473266
PS
41 if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
42 var url = M.cfg.wwwroot + '/theme/image.php/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
43 } else {
44 var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
38224dcb
PS
45 }
46
47 return url;
48};
49
b4430cd6
PS
50M.util.in_array = function(item, array){
51 for( var i = 0; i<array.length; i++){
52 if(item==array[i]){
53 return true;
54 }
55 }
56 return false;
667c0ae1 57};
b4430cd6 58
38224dcb
PS
59/**
60 * Init a collapsible region, see print_collapsible_region in weblib.php
126590b7
SH
61 * @param {YUI} Y YUI3 instance with all libraries loaded
62 * @param {String} id the HTML id for the div.
63 * @param {String} userpref the user preference that records the state of this box. false if none.
64 * @param {String} strtooltip
38224dcb
PS
65 */
66M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
67 Y.use('anim', function(Y) {
3b044ba3 68 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
38224dcb
PS
69 });
70};
71
72/**
126590b7 73 * Object to handle a collapsible region : instantiate and forget styled object
3b044ba3 74 *
126590b7 75 * @class
38224dcb 76 * @constructor
126590b7
SH
77 * @param {YUI} Y YUI3 instance with all libraries loaded
78 * @param {String} id The HTML id for the div.
79 * @param {String} userpref The user preference that records the state of this box. false if none.
80 * @param {String} strtooltip
38224dcb
PS
81 */
82M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
38224dcb
PS
83 // Record the pref name
84 this.userpref = userpref;
38224dcb
PS
85
86 // Find the divs in the document.
126590b7
SH
87 this.div = Y.one('#'+id);
88
89 // Get the caption for the collapsible region
90 var caption = this.div.one('#'+id + '_caption');
91 caption.setAttribute('title', strtooltip);
92
93 // Create a link
94 var a = Y.Node.create('<a href="#"></a>');
95 // Create a local scoped lamba function to move nodes to a new link
96 var movenode = function(node){
97 node.remove();
98 a.append(node);
99 };
100 // Apply the lamba function on each of the captions child nodes
101 caption.get('children').each(movenode, this);
102 caption.append(a);
103
104 // Get the height of the div at this point before we shrink it if required
105 var height = this.div.get('offsetHeight');
106 if (this.div.hasClass('collapsed')) {
107 // Add the correct image and record the YUI node created in the process
108 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
109 // Shrink the div as it is collapsed by default
110 this.div.setStyle('height', caption.get('offsetHeight')+'px');
111 } else {
112 // Add the correct image and record the YUI node created in the process
113 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
38224dcb 114 }
126590b7 115 a.append(this.icon);
38224dcb
PS
116
117 // Create the animation.
126590b7 118 var animation = new Y.Anim({
38224dcb
PS
119 node: this.div,
120 duration: 0.3,
126590b7
SH
121 easing: Y.Easing.easeBoth,
122 to: {height:caption.get('offsetHeight')},
123 from: {height:height}
38224dcb 124 });
3b044ba3 125
38224dcb 126 // Handler for the animation finishing.
126590b7
SH
127 animation.on('end', function() {
128 this.div.toggleClass('collapsed');
129 if (this.div.hasClass('collapsed')) {
130 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
131 } else {
132 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
fc367afb 133 }
38224dcb 134 }, this);
126590b7
SH
135
136 // Hook up the event handler.
137 a.on('click', function(e, animation) {
138 e.preventDefault();
139 // Animate to the appropriate size.
140 if (animation.get('running')) {
141 animation.stop();
142 }
143 animation.set('reverse', this.div.hasClass('collapsed'));
144 // Update the user preference.
145 if (this.userpref) {
41912a26 146 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
126590b7
SH
147 }
148 animation.run();
149 }, this, animation);
38224dcb
PS
150};
151
152/**
153 * The user preference that stores the state of this box.
154 * @property userpref
155 * @type String
156 */
157M.util.CollapsibleRegion.prototype.userpref = null;
158
159/**
160 * The key divs that make up this
126590b7
SH
161 * @property div
162 * @type Y.Node
38224dcb
PS
163 */
164M.util.CollapsibleRegion.prototype.div = null;
38224dcb
PS
165
166/**
167 * The key divs that make up this
168 * @property icon
126590b7 169 * @type Y.Node
38224dcb
PS
170 */
171M.util.CollapsibleRegion.prototype.icon = null;
172
41912a26
PS
173/**
174 * Makes a best effort to connect back to Moodle to update a user preference,
175 * however, there is no mechanism for finding out if the update succeeded.
176 *
177 * Before you can use this function in your JavsScript, you must have called
178 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
179 * the udpate is allowed, and how to safely clean and submitted values.
180 *
181 * @param String name the name of the setting to udpate.
182 * @param String the value to set it to.
183 */
184M.util.set_user_preference = function(name, value) {
2f422271 185 YUI().use('io', function(Y) {
41912a26
PS
186 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
187 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
188
189 // If we are a developer, ensure that failures are reported.
190 var cfg = {
20fb563e 191 method: 'get',
8dd14645 192 on: {}
41912a26
PS
193 };
194 if (M.cfg.developerdebug) {
195 cfg.on.failure = function(id, o, args) {
20fb563e 196 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
41912a26
PS
197 }
198 }
199
200 // Make the request.
201 Y.io(url, cfg);
202 });
203};
204
20fb563e
PS
205/**
206 * Prints a confirmation dialog in the style of DOM.confirm().
207 * @param object event A YUI DOM event or null if launched manually
208 * @param string message The message to show in the dialog
209 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
210 * @param function fn A JS function to run if YES is clicked.
211 */
212M.util.show_confirm_dialog = function(e, args) {
213 var target = e.target;
214 if (e.preventDefault) {
215 e.preventDefault();
216 }
217
2f422271 218 YUI().use('yui2-container', 'yui2-event', function(Y) {
82e7d74e 219 var simpledialog = new Y.YUI2.widget.SimpleDialog('confirmdialog',
f855a5d2 220 {width: '300px',
20fb563e
PS
221 fixedcenter: true,
222 modal: true,
223 visible: false,
224 draggable: false
225 }
226 );
227
2b728cb5 228 simpledialog.setHeader(M.str.admin.confirmation);
20fb563e 229 simpledialog.setBody(args.message);
82e7d74e 230 simpledialog.cfg.setProperty('icon', Y.YUI2.widget.SimpleDialog.ICON_WARN);
20fb563e
PS
231
232 var handle_cancel = function() {
233 simpledialog.hide();
234 };
235
236 var handle_yes = function() {
237 simpledialog.hide();
238
239 if (args.callback) {
240 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
241 var callback = null;
242 if (Y.Lang.isFunction(args.callback)) {
243 callback = args.callback;
244 } else {
245 callback = eval('('+args.callback+')');
246 }
247
248 if (Y.Lang.isObject(args.scope)) {
249 var sc = args.scope;
250 } else {
251 var sc = e.target;
252 }
253
254 if (args.callbackargs) {
255 callback.apply(sc, args.callbackargs);
256 } else {
257 callback.apply(sc);
258 }
259 return;
260 }
261
712b55e1
SH
262 var targetancestor = null,
263 targetform = null;
264
265 if (target.test('a')) {
20fb563e 266 window.location = target.get('href');
e16c0b9d 267
712b55e1
SH
268 } else if ((targetancestor = target.ancestor('a')) !== null) {
269 window.location = targetancestor.get('href');
e16c0b9d 270
712b55e1 271 } else if (target.test('input')) {
511b4cf8
TH
272 targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
273 // We cannot use target.ancestor('form') on the previous line
274 // because of http://yuilibrary.com/projects/yui3/ticket/2531561
e16c0b9d
TH
275 if (!targetform) {
276 return;
277 }
278 if (target.get('name') && target.get('value')) {
279 targetform.append('<input type="hidden" name="' + target.get('name') +
280 '" value="' + target.get('value') + '">');
20fb563e 281 }
e16c0b9d
TH
282 targetform.submit();
283
284 } else if (target.get('tagName').toLowerCase() == 'form') {
285 // We cannot use target.test('form') on the previous line because of
286 // http://yuilibrary.com/projects/yui3/ticket/2531561
287 target.submit();
288
20fb563e 289 } else if (M.cfg.developerdebug) {
f8b2e545 290 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
20fb563e
PS
291 }
292 };
293
8f78cd5a
TH
294 if (!args.cancellabel) {
295 args.cancellabel = M.str.moodle.cancel;
296 }
297 if (!args.continuelabel) {
298 args.continuelabel = M.str.moodle.yes;
299 }
300
301 var buttons = [
302 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
303 {text: args.continuelabel, handler: handle_yes}
304 ];
20fb563e
PS
305
306 simpledialog.cfg.queueProperty('buttons', buttons);
307
308 simpledialog.render(document.body);
309 simpledialog.show();
310 });
667c0ae1 311};
20fb563e 312
dd9b1882
PS
313/** Useful for full embedding of various stuff */
314M.util.init_maximised_embed = function(Y, id) {
315 var obj = Y.one('#'+id);
316 if (!obj) {
317 return;
318 }
319
dd9b1882
PS
320 var get_htmlelement_size = function(el, prop) {
321 if (Y.Lang.isString(el)) {
322 el = Y.one('#' + el);
323 }
324 var val = el.getStyle(prop);
325 if (val == 'auto') {
326 val = el.getComputedStyle(prop);
327 }
328 return parseInt(val);
329 };
330
331 var resize_object = function() {
332 obj.setStyle('width', '0px');
333 obj.setStyle('height', '0px');
fcd2cbaf 334 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
dd9b1882 335
fcd2cbaf 336 if (newwidth > 500) {
dd9b1882
PS
337 obj.setStyle('width', newwidth + 'px');
338 } else {
fcd2cbaf 339 obj.setStyle('width', '500px');
dd9b1882 340 }
f6fa64f0
PS
341
342 var headerheight = get_htmlelement_size('page-header', 'height');
343 var footerheight = get_htmlelement_size('page-footer', 'height');
82e7d74e 344 var newheight = parseInt(Y.YUI2.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
f6fa64f0
PS
345 if (newheight < 400) {
346 newheight = 400;
dd9b1882 347 }
f6fa64f0 348 obj.setStyle('height', newheight+'px');
dd9b1882
PS
349 };
350
351 resize_object();
352 // fix layout if window resized too
353 window.onresize = function() {
354 resize_object();
355 };
356};
357
a9967cf5
PS
358/**
359 * Attach handler to single_select
360 */
edc28287 361M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
c7bbb86f
SH
362 Y.use('event-key', function() {
363 var select = Y.one('#'+selectid);
364 if (select) {
365 // Try to get the form by id
366 var form = Y.one('#'+formid) || (function(){
367 // Hmmm the form's id may have been overriden by an internal input
368 // with the name id which will KILL IE.
369 // We need to manually iterate at this point because if the case
370 // above is true YUI's ancestor method will also kill IE!
371 var form = select;
372 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
373 form = form.ancestor();
374 }
375 return form;
376 })();
377 // Make sure we have the form
378 if (form) {
379 // Create a function to handle our change event
8d0845d3
AD
380 var processchange = function(e, paramobject) {
381 if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
382 //prevent event bubbling and detach handlers to prevent multiple submissions caused by double clicking
383 e.halt();
2173e56a
EL
384 paramobject.eventkeypress.detach();
385 paramobject.eventblur.detach();
8d0845d3 386 paramobject.eventchangeorblur.detach();
2173e56a 387
c7bbb86f
SH
388 this.submit();
389 }
667c0ae1 390 };
2173e56a 391 // Attach the change event to the keypress, blur, and click actions.
c7bbb86f
SH
392 // We don't use the change event because IE fires it on every arrow up/down
393 // event.... usability
8d0845d3
AD
394 var paramobject = new Object();
395 paramobject.lastindex = select.get('selectedIndex');
2173e56a
EL
396 paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
397 paramobject.eventblur = select.on('blur', processchange, form, paramobject);
398 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
399 if (Y.UA.webkit) {
400 paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
401 } else {
402 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
403 }
c7bbb86f 404 }
4aea3cc7 405 }
4aea3cc7 406 });
a9967cf5 407};
dd9b1882 408
4d10e579
PS
409/**
410 * Attach handler to url_select
411 */
412M.util.init_url_select = function(Y, formid, selectid, nothing) {
2f422271 413 YUI().use('node', function(Y) {
4aea3cc7 414 Y.on('change', function() {
c7bbb86f
SH
415 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
416 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
417 }
4aea3cc7
PS
418 },
419 '#'+selectid);
420 });
421};
422
423/**
424 * Breaks out all links to the top frame - used in frametop page layout.
425 */
426M.util.init_frametop = function(Y) {
427 Y.all('a').each(function(node) {
428 node.set('target', '_top');
429 });
430 Y.all('form').each(function(node) {
431 node.set('target', '_top');
432 });
4d10e579
PS
433};
434
24e27ac0
SH
435/**
436 * Finds all nodes that match the given CSS selector and attaches events to them
437 * so that they toggle a given classname when clicked.
438 *
439 * @param {YUI} Y
440 * @param {string} id An id containing elements to target
441 * @param {string} cssselector A selector to use to find targets
442 * @param {string} toggleclassname A classname to toggle
443 */
c8ffba95
SH
444M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
445
446 if (togglecssselector == '') {
447 togglecssselector = cssselector;
448 }
449
24e27ac0 450 var node = Y.one('#'+id);
c8ffba95
SH
451 node.all(cssselector).each(function(n){
452 n.on('click', function(e){
24e27ac0 453 e.stopPropagation();
c8ffba95
SH
454 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
455 if (this.test(togglecssselector)) {
456 this.toggleClass(toggleclassname);
457 } else {
458 this.ancestor(togglecssselector).toggleClass(toggleclassname);
459 }
24e27ac0 460 }
c8ffba95 461 }, n);
24e27ac0
SH
462 });
463 // Attach this click event to the node rather than all selectors... will be much better
464 // for performance
465 node.on('click', function(e){
466 if (e.target.hasClass('addtoall')) {
c8ffba95 467 this.all(togglecssselector).addClass(toggleclassname);
24e27ac0 468 } else if (e.target.hasClass('removefromall')) {
c8ffba95 469 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
24e27ac0
SH
470 }
471 }, node);
667c0ae1 472};
24e27ac0 473
233b6ff9
SH
474/**
475 * Initialises a colour picker
476 *
477 * Designed to be used with admin_setting_configcolourpicker although could be used
478 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
479 * above or below the input (must have the same parent) and then call this with the
480 * id.
481 *
482 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
483 * contrib/blocks. For better docs refer to that.
484 *
485 * @param {YUI} Y
486 * @param {int} id
487 * @param {object} previewconf
488 */
489M.util.init_colour_picker = function(Y, id, previewconf) {
490 /**
491 * We need node and event-mouseenter
492 */
493 Y.use('node', 'event-mouseenter', function(){
494 /**
495 * The colour picker object
496 */
497 var colourpicker = {
498 box : null,
499 input : null,
500 image : null,
501 preview : null,
502 current : null,
503 eventClick : null,
504 eventMouseEnter : null,
505 eventMouseLeave : null,
506 eventMouseMove : null,
507 width : 300,
508 height : 100,
509 factor : 5,
510 /**
511 * Initalises the colour picker by putting everything together and wiring the events
512 */
513 init : function() {
514 this.input = Y.one('#'+id);
515 this.box = this.input.ancestor().one('.admin_colourpicker');
516 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
517 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
518 this.preview = Y.Node.create('<div class="previewcolour"></div>');
519 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
520 this.current = Y.Node.create('<div class="currentcolour"></div>');
521 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
522 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
523
524 if (typeof(previewconf) === 'object' && previewconf !== null) {
525 Y.one('#'+id+'_preview').on('click', function(e){
3c3a3c3c 526 if (Y.Lang.isString(previewconf.selector)) {
bb9e9362
SH
527 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
528 } else {
529 for (var i in previewconf.selector) {
530 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
531 }
532 }
233b6ff9
SH
533 }, this);
534 }
535
536 this.eventClick = this.image.on('click', this.pickColour, this);
537 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
538 },
539 /**
540 * Starts to follow the mouse once it enter the image
541 */
542 startFollow : function(e) {
543 this.eventMouseEnter.detach();
544 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
545 this.eventMouseMove = this.image.on('mousemove', function(e){
546 this.preview.setStyle('backgroundColor', this.determineColour(e));
547 }, this);
548 },
549 /**
550 * Stops following the mouse
551 */
552 endFollow : function(e) {
553 this.eventMouseMove.detach();
554 this.eventMouseLeave.detach();
555 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
556 },
557 /**
558 * Picks the colour the was clicked on
559 */
560 pickColour : function(e) {
561 var colour = this.determineColour(e);
562 this.input.set('value', colour);
563 this.current.setStyle('backgroundColor', colour);
564 },
565 /**
566 * Calculates the colour fromthe given co-ordinates
567 */
568 determineColour : function(e) {
569 var eventx = Math.floor(e.pageX-e.target.getX());
570 var eventy = Math.floor(e.pageY-e.target.getY());
571
572 var imagewidth = this.width;
573 var imageheight = this.height;
574 var factor = this.factor;
575 var colour = [255,0,0];
576
577 var matrices = [
578 [ 0, 1, 0],
579 [ -1, 0, 0],
580 [ 0, 0, 1],
581 [ 0, -1, 0],
582 [ 1, 0, 0],
583 [ 0, 0, -1]
584 ];
585
586 var matrixcount = matrices.length;
587 var limit = Math.round(imagewidth/matrixcount);
588 var heightbreak = Math.round(imageheight/2);
589
590 for (var x = 0; x < imagewidth; x++) {
591 var divisor = Math.floor(x / limit);
592 var matrix = matrices[divisor];
593
594 colour[0] += matrix[0]*factor;
595 colour[1] += matrix[1]*factor;
596 colour[2] += matrix[2]*factor;
597
598 if (eventx==x) {
599 break;
600 }
601 }
602
603 var pixel = [colour[0], colour[1], colour[2]];
604 if (eventy < heightbreak) {
605 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
606 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
607 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
608 } else if (eventy > heightbreak) {
609 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
610 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
611 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
612 }
613
614 return this.convert_rgb_to_hex(pixel);
615 },
616 /**
617 * Converts an RGB value to Hex
618 */
619 convert_rgb_to_hex : function(rgb) {
620 var hex = '#';
621 var hexchars = "0123456789ABCDEF";
622 for (var i=0; i<3; i++) {
623 var number = Math.abs(rgb[i]);
624 if (number == 0 || isNaN(number)) {
625 hex += '00';
626 } else {
627 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
628 }
629 }
630 return hex;
631 }
667c0ae1 632 };
233b6ff9
SH
633 /**
634 * Initialise the colour picker :) Hoorah
635 */
636 colourpicker.init();
637 });
667c0ae1 638};
233b6ff9 639
cbb54cce
SH
640M.util.init_block_hider = function(Y, config) {
641 Y.use('base', 'node', function(Y) {
642 M.util.block_hider = M.util.block_hider || (function(){
643 var blockhider = function() {
644 blockhider.superclass.constructor.apply(this, arguments);
667c0ae1 645 };
cbb54cce
SH
646 blockhider.prototype = {
647 initializer : function(config) {
667c0ae1 648 this.set('block', '#'+this.get('id'));
cbb54cce
SH
649 var b = this.get('block'),
650 t = b.one('.title'),
651 a = null;
652 if (t && (a = t.one('.block_action'))) {
7cf83f47 653 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
cbb54cce 654 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
7cf83f47
JB
655 hide.on('keypress', this.updateStateKey, this, true);
656 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
cbb54cce 657 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
7cf83f47 658 show.on('keypress', this.updateStateKey, this, false);
cbb54cce
SH
659 a.insert(show, 0).insert(hide, 0);
660 }
661 },
662 updateState : function(e, hide) {
663 M.util.set_user_preference(this.get('preference'), hide);
664 if (hide) {
665 this.get('block').addClass('hidden');
666 } else {
667 this.get('block').removeClass('hidden');
668 }
7cf83f47
JB
669 },
670 updateStateKey : function(e, hide) {
671 if (e.keyCode == 13) { //allow hide/show via enter key
672 this.updateState(this, hide);
673 }
cbb54cce 674 }
667c0ae1 675 };
cbb54cce
SH
676 Y.extend(blockhider, Y.Base, blockhider.prototype, {
677 NAME : 'blockhider',
678 ATTRS : {
679 id : {},
680 preference : {},
681 iconVisible : {
682 value : M.util.image_url('t/switch_minus', 'moodle')
683 },
684 iconHidden : {
685 value : M.util.image_url('t/switch_plus', 'moodle')
686 },
687 block : {
688 setter : function(node) {
689 return Y.one(node);
690 }
691 }
692 }
693 });
694 return blockhider;
695 })();
696 new M.util.block_hider(config);
697 });
667c0ae1 698};
cbb54cce 699
b532be2d
DM
700/**
701 * Returns a string registered in advance for usage in JavaScript
702 *
703 * If you do not pass the third parameter, the function will just return
704 * the corresponding value from the M.str object. If the third parameter is
705 * provided, the function performs {$a} placeholder substitution in the
706 * same way as PHP get_string() in Moodle does.
707 *
708 * @param {String} identifier string identifier
709 * @param {String} component the component providing the string
710 * @param {Object|String} a optional variable to populate placeholder with
711 */
712M.util.get_string = function(identifier, component, a) {
713 var stringvalue;
714
715 if (M.cfg.developerdebug) {
716 // creating new instance if YUI is not optimal but it seems to be better way then
717 // require the instance via the function API - note that it is used in rare cases
718 // for debugging only anyway
dd51b3b2
SH
719 // To ensure we don't kill browser performance if hundreds of get_string requests
720 // are made we cache the instance we generate within the M.util namespace.
721 // We don't publicly define the variable so that it doesn't get abused.
722 if (typeof M.util.get_string_yui_instance === 'undefined') {
723 M.util.get_string_yui_instance = new YUI({ debug : true });
724 }
725 var Y = M.util.get_string_yui_instance;
b532be2d
DM
726 }
727
728 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
729 stringvalue = '[[' + identifier + ',' + component + ']]';
730 if (M.cfg.developerdebug) {
731 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
732 }
733 return stringvalue;
734 }
735
736 stringvalue = M.str[component][identifier];
737
738 if (typeof a == 'undefined') {
739 // no placeholder substitution requested
740 return stringvalue;
741 }
742
743 if (typeof a == 'number' || typeof a == 'string') {
744 // replace all occurrences of {$a} with the placeholder value
745 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
746 return stringvalue;
747 }
748
749 if (typeof a == 'object') {
750 // replace {$a->key} placeholders
751 for (var key in a) {
752 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
753 if (M.cfg.developerdebug) {
754 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
755 }
756 continue;
757 }
758 var search = '{$a->' + key + '}';
759 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
760 search = new RegExp(search, 'g');
761 stringvalue = stringvalue.replace(search, a[key]);
762 }
763 return stringvalue;
764 }
765
766 if (M.cfg.developerdebug) {
767 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
768 }
769 return stringvalue;
770};
771
2d16dad0
PS
772/**
773 * Set focus on username or password field of the login form
774 */
775M.util.focus_login_form = function(Y) {
776 var username = Y.one('#username');
777 var password = Y.one('#password');
778
779 if (username == null || password == null) {
780 // something is wrong here
781 return;
782 }
783
784 var curElement = document.activeElement
785 if (curElement == 'undefined') {
786 // legacy browser - skip refocus protection
787 } else if (curElement.tagName == 'INPUT') {
788 // user was probably faster to focus something, do not mess with focus
789 return;
790 }
791
792 if (username.get('value') == '') {
793 username.focus();
794 } else {
795 password.focus();
796 }
797}
798
d8feba6c
RK
799/**
800 * Adds lightbox hidden element that covers the whole node.
801 *
802 * @param {YUI} Y
803 * @param {Node} the node lightbox should be added to
804 * @retun {Node} created lightbox node
805 */
806M.util.add_lightbox = function(Y, node) {
807 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
808
809 // Check if lightbox is already there
810 if (node.one('.lightbox')) {
811 return node.one('.lightbox');
812 }
813
814 node.setStyle('position', 'relative');
815 var waiticon = Y.Node.create('<img />')
816 .setAttrs({
817 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
818 })
819 .setStyles({
820 'position' : 'relative',
821 'top' : '50%'
822 });
823
824 var lightbox = Y.Node.create('<div></div>')
825 .setStyles({
826 'opacity' : '.75',
827 'position' : 'absolute',
828 'width' : '100%',
829 'height' : '100%',
830 'top' : 0,
831 'left' : 0,
832 'backgroundColor' : 'white',
833 'text-align' : 'center'
834 })
835 .setAttribute('class', 'lightbox')
836 .hide();
837
838 lightbox.appendChild(waiticon);
839 node.append(lightbox);
840 return lightbox;
841}
2d16dad0 842
45b364b9
ARN
843/**
844 * Appends a hidden spinner element to the specified node.
845 *
846 * @param {YUI} Y
847 * @param {Node} the node the spinner should be added to
848 * @return {Node} created spinner node
849 */
850M.util.add_spinner = function(Y, node) {
851 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
852
853 // Check if spinner is already there
854 if (node.one('.spinner')) {
855 return node.one('.spinner');
856 }
857
858 var spinner = Y.Node.create('<img />')
859 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
860 .addClass('spinner')
861 .addClass('iconsmall')
862 .hide();
863
864 node.append(spinner);
865 return spinner;
866}
867
38224dcb
PS
868//=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
869
63d28811 870function checkall() {
214f5850 871 var inputs = document.getElementsByTagName('input');
872 for (var i = 0; i < inputs.length; i++) {
873 if (inputs[i].type == 'checkbox') {
af8def57
RT
874 if (inputs[i].disabled || inputs[i].readOnly) {
875 continue;
876 }
214f5850 877 inputs[i].checked = true;
878 }
d2ce367f 879 }
63d28811 880}
881
03f9425f 882function checknone() {
214f5850 883 var inputs = document.getElementsByTagName('input');
884 for (var i = 0; i < inputs.length; i++) {
885 if (inputs[i].type == 'checkbox') {
af8def57
RT
886 if (inputs[i].disabled || inputs[i].readOnly) {
887 continue;
888 }
214f5850 889 inputs[i].checked = false;
890 }
d2ce367f 891 }
03f9425f 892}
893
45caa363 894/**
895 * Either check, or uncheck, all checkboxes inside the element with id is
896 * @param id the id of the container
897 * @param checked the new state, either '' or 'checked'.
898 */
899function select_all_in_element_with_id(id, checked) {
900 var container = document.getElementById(id);
901 if (!container) {
902 return;
903 }
904 var inputs = container.getElementsByTagName('input');
905 for (var i = 0; i < inputs.length; ++i) {
906 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
907 inputs[i].checked = checked;
908 }
909 }
910}
911
8ceb09e0 912function select_all_in(elTagName, elClass, elId) {
d2ce367f 913 var inputs = document.getElementsByTagName('input');
8ceb09e0 914 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
363cb62c 915 for(var i = 0; i < inputs.length; ++i) {
bee40515 916 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
363cb62c 917 inputs[i].checked = 'checked';
918 }
919 }
920}
921
8ceb09e0 922function deselect_all_in(elTagName, elClass, elId) {
363cb62c 923 var inputs = document.getElementsByTagName('INPUT');
8ceb09e0 924 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
363cb62c 925 for(var i = 0; i < inputs.length; ++i) {
bee40515 926 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
363cb62c 927 inputs[i].checked = '';
928 }
929 }
930}
931
932function confirm_if(expr, message) {
933 if(!expr) {
934 return true;
935 }
936 return confirm(message);
937}
47aa42e2 938
939
940/*
941 findParentNode (start, elementName, elementClass, elementID)
50ef8eb9 942
47aa42e2 943 Travels up the DOM hierarchy to find a parent element with the
944 specified tag name, class, and id. All conditions must be met,
945 but any can be ommitted. Returns the BODY element if no match
946 found.
947*/
948function findParentNode(el, elName, elClass, elId) {
45caa363 949 while (el.nodeName.toUpperCase() != 'BODY') {
950 if ((!elName || el.nodeName.toUpperCase() == elName) &&
47aa42e2 951 (!elClass || el.className.indexOf(elClass) != -1) &&
45caa363 952 (!elId || el.id == elId)) {
47aa42e2 953 break;
954 }
955 el = el.parentNode;
956 }
957 return el;
958}
19194f82 959/*
960 findChildNode (start, elementName, elementClass, elementID)
961
962 Travels down the DOM hierarchy to find all child elements with the
963 specified tag name, class, and id. All conditions must be met,
964 but any can be ommitted.
a23f0aaf 965 Doesn't examine children of matches.
19194f82 966*/
967function findChildNodes(start, tagName, elementClass, elementID, elementName) {
968 var children = new Array();
83c9a8a2 969 for (var i = 0; i < start.childNodes.length; i++) {
a23f0aaf 970 var classfound = false;
83c9a8a2 971 var child = start.childNodes[i];
a23f0aaf 972 if((child.nodeType == 1) &&//element node type
b51709c1 973 (elementClass && (typeof(child.className)=='string'))) {
a23f0aaf 974 var childClasses = child.className.split(/\s+/);
b51709c1 975 for (var childClassIndex in childClasses) {
976 if (childClasses[childClassIndex]==elementClass) {
a23f0aaf 977 classfound = true;
978 break;
979 }
980 }
981 }
f07b9627 982 if(child.nodeType == 1) { //element node type
983 if ( (!tagName || child.nodeName == tagName) &&
984 (!elementClass || classfound)&&
985 (!elementID || child.id == elementID) &&
986 (!elementName || child.name == elementName))
987 {
988 children = children.concat(child);
989 } else {
990 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
991 }
19194f82 992 }
19194f82 993 }
994 return children;
995}
47aa42e2 996
54bb33eb 997function unmaskPassword(id) {
239ade45 998 var pw = document.getElementById(id);
54bb33eb 999 var chb = document.getElementById(id+'unmask');
239ade45 1000
1001 try {
1002 // first try IE way - it can not set name attribute later
1003 if (chb.checked) {
6e9989db 1004 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
239ade45 1005 } else {
6e9989db 1006 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
239ade45 1007 }
eba8cd63 1008 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
239ade45 1009 } catch (e) {
1010 var newpw = document.createElement('input');
6e9989db 1011 newpw.setAttribute('autocomplete', 'off');
239ade45 1012 newpw.setAttribute('name', pw.name);
1013 if (chb.checked) {
1014 newpw.setAttribute('type', 'text');
1015 } else {
1016 newpw.setAttribute('type', 'password');
1017 }
eba8cd63 1018 newpw.setAttribute('class', pw.getAttribute('class'));
239ade45 1019 }
1020 newpw.id = pw.id;
1021 newpw.size = pw.size;
1022 newpw.onblur = pw.onblur;
1023 newpw.onchange = pw.onchange;
1024 newpw.value = pw.value;
1025 pw.parentNode.replaceChild(newpw, pw);
1026}
1027
47aa42e2 1028function filterByParent(elCollection, parentFinder) {
1029 var filteredCollection = [];
45caa363 1030 for (var i = 0; i < elCollection.length; ++i) {
47aa42e2 1031 var findParent = parentFinder(elCollection[i]);
4d99adbb 1032 if (findParent.nodeName.toUpperCase() != 'BODY') {
47aa42e2 1033 filteredCollection.push(elCollection[i]);
1034 }
1035 }
1036 return filteredCollection;
1037}
1038
7979105c 1039/*
1040 All this is here just so that IE gets to handle oversized blocks
1041 in a visually pleasing manner. It does a browser detect. So sue me.
1042*/
1043
1044function fix_column_widths() {
1045 var agt = navigator.userAgent.toLowerCase();
1046 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1047 fix_column_width('left-column');
1048 fix_column_width('right-column');
1049 }
1050}
1051
1052function fix_column_width(colName) {
1053 if(column = document.getElementById(colName)) {
1054 if(!column.offsetWidth) {
1055 setTimeout("fix_column_width('" + colName + "')", 20);
1056 return;
1057 }
1058
1059 var width = 0;
1060 var nodes = column.childNodes;
1061
1062 for(i = 0; i < nodes.length; ++i) {
6605ff8c 1063 if(nodes[i].className.indexOf("block") != -1 ) {
7979105c 1064 if(width < nodes[i].offsetWidth) {
1065 width = nodes[i].offsetWidth;
1066 }
1067 }
1068 }
1069
1070 for(i = 0; i < nodes.length; ++i) {
6605ff8c 1071 if(nodes[i].className.indexOf("block") != -1 ) {
7979105c 1072 nodes[i].style.width = width + 'px';
1073 }
1074 }
1075 }
1076}
d13c5938 1077
1078
1079/*
9f439b17 1080 Insert myValue at current cursor position
1081 */
d13c5938 1082function insertAtCursor(myField, myValue) {
9f439b17 1083 // IE support
1084 if (document.selection) {
1085 myField.focus();
1086 sel = document.selection.createRange();
1087 sel.text = myValue;
1088 }
1089 // Mozilla/Netscape support
1090 else if (myField.selectionStart || myField.selectionStart == '0') {
1091 var startPos = myField.selectionStart;
1092 var endPos = myField.selectionEnd;
1093 myField.value = myField.value.substring(0, startPos)
1094 + myValue + myField.value.substring(endPos, myField.value.length);
1095 } else {
1096 myField.value += myValue;
1097 }
d13c5938 1098}
7470d6de 1099
1100
1101/*
c0056e22 1102 Call instead of setting window.onload directly or setting body onload=.
1103 Adds your function to a chain of functions rather than overwriting anything
1104 that exists.
1105*/
7470d6de 1106function addonload(fn) {
1107 var oldhandler=window.onload;
1108 window.onload=function() {
1109 if(oldhandler) oldhandler();
c0056e22 1110 fn();
7470d6de 1111 }
1112}
7d2a0492 1113/**
1114 * Replacement for getElementsByClassName in browsers that aren't cool enough
6f5e0852 1115 *
7d2a0492 1116 * Relying on the built-in getElementsByClassName is far, far faster than
1117 * using YUI.
6f5e0852 1118 *
7d2a0492 1119 * Note: the third argument used to be an object with odd behaviour. It now
1120 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1121 * mimicked if you pass an object.
1122 *
1123 * @param {Node} oElm The top-level node for searching. To search a whole
1124 * document, use `document`.
1125 * @param {String} strTagName filter by tag names
1126 * @param {String} name same as HTML5 spec
1127 */
1128function getElementsByClassName(oElm, strTagName, name) {
1129 // for backwards compatibility
1130 if(typeof name == "object") {
1131 var names = new Array();
1132 for(var i=0; i<name.length; i++) names.push(names[i]);
1133 name = names.join('');
1134 }
1135 // use native implementation if possible
1136 if (oElm.getElementsByClassName && Array.filter) {
1137 if (strTagName == '*') {
1138 return oElm.getElementsByClassName(name);
1139 } else {
1140 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1141 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1142 });
1143 }
1144 }
1145 // native implementation unavailable, fall back to slow method
c849ed1e 1146 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1147 var arrReturnElements = new Array();
1148 var arrRegExpClassNames = new Array();
7d2a0492 1149 var names = name.split(' ');
1150 for(var i=0; i<names.length; i++) {
1151 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
c849ed1e 1152 }
1153 var oElement;
1154 var bMatchesAll;
b51709c1 1155 for(var j=0; j<arrElements.length; j++) {
c849ed1e 1156 oElement = arrElements[j];
1157 bMatchesAll = true;
b51709c1 1158 for(var k=0; k<arrRegExpClassNames.length; k++) {
1159 if(!arrRegExpClassNames[k].test(oElement.className)) {
c849ed1e 1160 bMatchesAll = false;
1161 break;
1162 }
1163 }
b51709c1 1164 if(bMatchesAll) {
c849ed1e 1165 arrReturnElements.push(oElement);
1166 }
1167 }
1168 return (arrReturnElements)
1169}
77241d9b 1170
f8065dd2 1171function openpopup(event, args) {
1172
2cf81209 1173 if (event) {
c7e3e61c
SH
1174 if (event.preventDefault) {
1175 event.preventDefault();
1176 } else {
1177 event.returnValue = false;
1178 }
2cf81209 1179 }
3a42ad12 1180
2cf81209
SH
1181 var fullurl = args.url;
1182 if (!args.url.match(/https?:\/\//)) {
1183 fullurl = M.cfg.wwwroot + args.url;
77241d9b 1184 }
8c598cef
TH
1185 if (args.fullscreen) {
1186 args.options = args.options.
1187 replace(/top=\d+/, 'top=0').
1188 replace(/left=\d+/, 'left=0').
1189 replace(/width=\d+/, 'width=' + screen.availWidth).
1190 replace(/height=\d+/, 'height=' + screen.availHeight);
1191 }
2cf81209
SH
1192 var windowobj = window.open(fullurl,args.name,args.options);
1193 if (!windowobj) {
1194 return true;
1195 }
8c598cef 1196
2cf81209 1197 if (args.fullscreen) {
9f692145
TH
1198 // In some browser / OS combinations (E.g. Chrome on Windows), the
1199 // window initially opens slighly too big. The width and heigh options
1200 // seem to control the area inside the browser window, so what with
1201 // scroll-bars, etc. the actual window is bigger than the screen.
1202 // Therefore, we need to fix things up after the window is open.
1203 var hackcount = 100;
1204 var get_size_exactly_right = function() {
8c598cef 1205 windowobj.moveTo(0, 0);
9f692145
TH
1206 windowobj.resizeTo(screen.availWidth, screen.availHeight);
1207
1208 // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1209 // something like windowobj.resizeTo(1280, 1024) too soon (up to
1210 // about 50ms) after the window is open, then it actually behaves
1211 // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1212 // check that the resize actually worked, and if not, repeatedly try
1213 // again after a short delay until it works (but with a limit of
1214 // hackcount repeats.
1215 if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1216 hackcount -= 1;
1217 setTimeout(get_size_exactly_right, 10);
1218 }
1219 }
1220 setTimeout(get_size_exactly_right, 0);
2cf81209
SH
1221 }
1222 windowobj.focus();
3a42ad12 1223
2cf81209 1224 return false;
77241d9b 1225}
740939ec 1226
b166403f 1227/** Close the current browser window. */
7a5c78e0 1228function close_window(e) {
c7e3e61c
SH
1229 if (e.preventDefault) {
1230 e.preventDefault();
1231 } else {
1232 e.returnValue = false;
1233 }
1db6b9b8 1234 window.close();
b166403f 1235}
1236
9f319372 1237/**
1238 * Used in a couple of modules to hide navigation areas when using AJAX
1239 */
34a2777c 1240
2538037f 1241function show_item(itemid) {
1242 var item = document.getElementById(itemid);
1243 if (item) {
1244 item.style.display = "";
1245 }
1246}
1247
1248function destroy_item(itemid) {
1249 var item = document.getElementById(itemid);
1250 if (item) {
1251 item.parentNode.removeChild(item);
1252 }
1253}
34a2777c 1254/**
1255 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1256 * @param controlid the control id.
1257 */
1258function focuscontrol(controlid) {
1259 var control = document.getElementById(controlid);
1260 if (control) {
1261 control.focus();
1262 }
e29380f3 1263}
1264
e11a8328 1265/**
1266 * Transfers keyboard focus to an HTML element based on the old style style of focus
1267 * This function should be removed as soon as it is no longer used
1268 */
428acddb 1269function old_onload_focus(formid, controlname) {
5f8bce50 1270 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
428acddb 1271 document.forms[formid].elements[controlname].focus();
e11a8328 1272 }
1273}
1274
1bcb7eb5 1275function build_querystring(obj) {
aa281068
AD
1276 return convert_object_to_string(obj, '&');
1277}
1278
1279function build_windowoptionsstring(obj) {
1280 return convert_object_to_string(obj, ',');
1281}
1282
1283function convert_object_to_string(obj, separator) {
1bcb7eb5 1284 if (typeof obj !== 'object') {
1285 return null;
1286 }
1287 var list = [];
1288 for(var k in obj) {
1289 k = encodeURIComponent(k);
1290 var value = obj[k];
1291 if(obj[k] instanceof Array) {
1292 for(var i in value) {
1293 list.push(k+'[]='+encodeURIComponent(value[i]));
1294 }
1295 } else {
1296 list.push(k+'='+encodeURIComponent(value));
1297 }
1298 }
aa281068 1299 return list.join(separator);
1bcb7eb5 1300}
d25e2ca3 1301
1302function stripHTML(str) {
1303 var re = /<\S[^><]*>/g;
1304 var ret = str.replace(re, "");
1305 return ret;
1306}
1307
fd4faf98 1308Number.prototype.fixed=function(n){
1309 with(Math)
1310 return round(Number(this)*pow(10,n))/pow(10,n);
667c0ae1 1311};
fd4faf98 1312function update_progress_bar (id, width, pt, msg, es){
dfd9f745 1313 var percent = pt;
fd4faf98 1314 var status = document.getElementById("status_"+id);
1315 var percent_indicator = document.getElementById("pt_"+id);
1316 var progress_bar = document.getElementById("progress_"+id);
1317 var time_es = document.getElementById("time_"+id);
1318 status.innerHTML = msg;
1319 percent_indicator.innerHTML = percent.fixed(2) + '%';
1320 if(percent == 100) {
1321 progress_bar.style.background = "green";
1322 time_es.style.display = "none";
1323 } else {
1324 progress_bar.style.background = "#FFCC66";
dfd9f745
PS
1325 if (es == '?'){
1326 time_es.innerHTML = "";
fd4faf98 1327 }else {
1328 time_es.innerHTML = es.fixed(2)+" sec";
1329 time_es.style.display
1330 = "block";
1331 }
1332 }
1333 progress_bar.style.width = width + "px";
1334
1335}
bf6c37c7 1336
73b62703
PS
1337
1338// ===== Deprecated core Javascript functions for Moodle ====
1339// DO NOT USE!!!!!!!
e50b4c89 1340// Do not put this stuff in separate file because it only adds extra load on servers!
73b62703 1341
2d65597b
PS
1342/**
1343 * Used in a couple of modules to hide navigation areas when using AJAX
1344 */
1345function hide_item(itemid) {
3b044ba3 1346 // use class='hiddenifjs' instead
2d65597b
PS
1347 var item = document.getElementById(itemid);
1348 if (item) {
1349 item.style.display = "none";
1350 }
1351}
36282d85 1352
2cf81209
SH
1353M.util.help_icon = {
1354 Y : null,
36282d85 1355 instance : null,
2cf81209
SH
1356 add : function(Y, properties) {
1357 this.Y = Y;
61ddb953
RW
1358 properties.node = Y.one('#'+properties.id);
1359 if (properties.node) {
1360 properties.node.on('click', this.display, this, properties);
f855a5d2 1361 }
2cf81209
SH
1362 },
1363 display : function(event, args) {
1364 event.preventDefault();
1365 if (M.util.help_icon.instance === null) {
1366 var Y = M.util.help_icon.Y;
0958759d 1367 Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
2cf81209 1368 var help_content_overlay = {
61ddb953 1369 helplink : null,
2cf81209
SH
1370 overlay : null,
1371 init : function() {
1372
61ddb953 1373 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
2cf81209
SH
1374 // Create an overlay from markup
1375 this.overlay = new Y.Overlay({
1376 headerContent: closebtn,
1377 bodyContent: '',
61ddb953 1378 id: 'helppopupbox',
3a42ad12 1379 width:'400px',
2cf81209 1380 visible : false,
3a42ad12 1381 constrain : true
2cf81209
SH
1382 });
1383 this.overlay.render(Y.one(document.body));
1384
1385 closebtn.on('click', this.overlay.hide, this.overlay);
3a42ad12 1386
2cf81209 1387 var boundingBox = this.overlay.get("boundingBox");
2cf81209
SH
1388
1389 // Hide the menu if the user clicks outside of its content
1390 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1391 var oTarget = event.target;
1392 var menuButton = Y.one("#"+args.id);
3a42ad12 1393
2cf81209
SH
1394 if (!oTarget.compareTo(menuButton) &&
1395 !menuButton.contains(oTarget) &&
1396 !oTarget.compareTo(boundingBox) &&
1397 !boundingBox.contains(oTarget)) {
1398 this.overlay.hide();
1399 }
1400 }, this);
61ddb953
RW
1401
1402 Y.on("key", this.close, closebtn , "down:13", this);
1403 closebtn.on('click', this.close, this);
1404 },
1405
1406 close : function(e) {
1407 e.preventDefault();
1408 this.helplink.focus();
1409 this.overlay.hide();
2cf81209
SH
1410 },
1411
1412 display : function(event, args) {
61ddb953 1413 this.helplink = args.node;
2cf81209 1414 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
61ddb953 1415 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
2cf81209
SH
1416
1417 var fullurl = args.url;
1418 if (!args.url.match(/https?:\/\//)) {
1419 fullurl = M.cfg.wwwroot + args.url;
36282d85 1420 }
36282d85 1421
2cf81209
SH
1422 var ajaxurl = fullurl + '&ajax=1';
1423
1424 var cfg = {
1425 method: 'get',
1426 context : this,
1427 on: {
1428 success: function(id, o, node) {
1429 this.display_callback(o.responseText);
1430 },
1431 failure: function(id, o, node) {
1432 var debuginfo = o.statusText;
1433 if (M.cfg.developerdebug) {
1434 o.statusText += ' (' + ajaxurl + ')';
1435 }
1436 this.display_callback('bodyContent',debuginfo);
1437 }
1438 }
1439 };
36282d85 1440
2cf81209
SH
1441 Y.io(ajaxurl, cfg);
1442 this.overlay.show();
61ddb953
RW
1443
1444 Y.one('#closehelpbox').focus();
2cf81209 1445 },
36282d85 1446
2cf81209 1447 display_callback : function(content) {
683bed91 1448 content = '<div role="alert">' + content + '</div>';
61ddb953 1449 this.overlay.set('bodyContent', content);
2cf81209
SH
1450 },
1451
1452 hideContent : function() {
1453 help = this;
1454 help.overlay.hide();
36282d85 1455 }
667c0ae1 1456 };
2cf81209
SH
1457 help_content_overlay.init();
1458 M.util.help_icon.instance = help_content_overlay;
1459 M.util.help_icon.instance.display(event, args);
1460 });
1461 } else {
1462 M.util.help_icon.instance.display(event, args);
1463 }
1464 },
3a42ad12
RW
1465 init : function(Y) {
1466 this.Y = Y;
36282d85 1467 }
667c0ae1 1468};
d2dbd0c0
SH
1469
1470/**
1471 * Custom menu namespace
1472 */
1473M.core_custom_menu = {
1474 /**
1475 * This method is used to initialise a custom menu given the id that belongs
1476 * to the custom menu's root node.
3a42ad12 1477 *
d2dbd0c0
SH
1478 * @param {YUI} Y
1479 * @param {string} nodeid
1480 */
1481 init : function(Y, nodeid) {
242b78f7
SH
1482 var node = Y.one('#'+nodeid);
1483 if (node) {
1484 Y.use('node-menunav', function(Y) {
1485 // Get the node
1486 // Remove the javascript-disabled class.... obviously javascript is enabled.
1487 node.removeClass('javascript-disabled');
1488 // Initialise the menunav plugin
1489 node.plug(Y.Plugin.NodeMenuNav);
1490 });
1491 }
d2dbd0c0 1492 }
667c0ae1 1493};
572dd8ec
SH
1494
1495/**
1496 * Used to store form manipulation methods and enhancments
1497 */
1498M.form = M.form || {};
1499
1500/**
1501 * Converts a nbsp indented select box into a multi drop down custom control much
1502 * like the custom menu. It also selectable categories on or off.
1503 *
1504 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1505 *
1506 * @param {YUI} Y
1507 * @param {string} id
1508 * @param {Array} options
1509 */
1510M.form.init_smartselect = function(Y, id, options) {
882e66fe
SH
1511 if (!id.match(/^id_/)) {
1512 id = 'id_'+id;
1513 }
1514 var select = Y.one('select#'+id);
572dd8ec
SH
1515 if (!select) {
1516 return false;
1517 }
1518 Y.use('event-delegate',function(){
1519 var smartselect = {
1520 id : id,
1521 structure : [],
1522 options : [],
1523 submenucount : 0,
1524 currentvalue : null,
1525 currenttext : null,
1526 shownevent : null,
1527 cfg : {
1528 selectablecategories : true,
1529 mode : null
1530 },
1531 nodes : {
1532 select : null,
1533 loading : null,
1534 menu : null
1535 },
1536 init : function(Y, id, args, nodes) {
1537 if (typeof(args)=='object') {
1538 for (var i in this.cfg) {
1539 if (args[i] || args[i]===false) {
1540 this.cfg[i] = args[i];
1541 }
1542 }
1543 }
3a42ad12 1544
572dd8ec
SH
1545 // Display a loading message first up
1546 this.nodes.select = nodes.select;
1547
1548 this.currentvalue = this.nodes.select.get('selectedIndex');
1549 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1550
1551 var options = Array();
667c0ae1 1552 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
572dd8ec
SH
1553 this.nodes.select.all('option').each(function(option, index) {
1554 var rawtext = option.get('innerHTML');
1555 var text = rawtext.replace(/^(&nbsp;)*/, '');
1556 if (rawtext === text) {
1557 text = rawtext.replace(/^(\s)*/, '');
1558 var depth = (rawtext.length - text.length ) + 1;
1559 } else {
1560 var depth = ((rawtext.length - text.length )/12)+1;
1561 }
1562 option.set('innerHTML', text);
1563 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1564 }, this);
1565
1566 this.structure = [];
1567 var structcount = 0;
1568 for (var i in options) {
1569 var o = options[i];
1570 if (o.depth == 0) {
1571 this.structure.push(o);
1572 structcount++;
1573 } else {
1574 var d = o.depth;
1575 var current = this.structure[structcount-1];
1576 for (var j = 0; j < o.depth-1;j++) {
1577 if (current && current.children) {
1578 current = current.children[current.children.length-1];
1579 }
1580 }
1581 if (current && current.children) {
1582 current.children.push(o);
1583 }
1584 }
1585 }
1586
1587 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1588 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1589 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1590 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1591
1592 if (this.cfg.mode == null) {
1593 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1594 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1595 this.cfg.mode = 'compact';
1596 } else {
1597 this.cfg.mode = 'spanning';
1598 }
1599 }
1600
1601 if (this.cfg.mode == 'compact') {
1602 this.nodes.menu.addClass('compactmenu');
1603 } else {
1604 this.nodes.menu.addClass('spanningmenu');
1605 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1606 }
1607
1608 Y.one(document.body).append(this.nodes.menu);
1609 var pos = this.nodes.select.getXY();
1610 pos[0] += 1;
1611 this.nodes.menu.setXY(pos);
1612 this.nodes.menu.on('click', this.handle_click, this);
32690ac2
SH
1613
1614 Y.one(window).on('resize', function(){
1615 var pos = this.nodes.select.getXY();
1616 pos[0] += 1;
1617 this.nodes.menu.setXY(pos);
1618 }, this);
572dd8ec
SH
1619 },
1620 generate_menu_content : function() {
1621 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1622 content += this.generate_submenu_content(this.structure[0], true);
1623 content += '</ul></div>';
1624 return content;
1625 },
1626 generate_submenu_content : function(item, rootelement) {
1627 this.submenucount++;
1628 var content = '';
1629 if (item.children.length > 0) {
1630 if (rootelement) {
1631 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1632 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1633 content += '<div class="smartselect_menu_content">';
1634 } else {
1635 content += '<li class="smartselect_submenuitem">';
1636 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1637 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1638 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1639 content += '<div class="smartselect_submenu_content">';
1640 }
1641 content += '<ul>';
1642 for (var i in item.children) {
1643 content += this.generate_submenu_content(item.children[i],false);
1644 }
1645 content += '</ul>';
1646 content += '</div>';
1647 content += '</div>';
1648 if (rootelement) {
1649 } else {
1650 content += '</li>';
1651 }
1652 } else {
1653 content += '<li class="smartselect_menuitem">';
1654 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1655 content += '</li>';
1656 }
1657 return content;
1658 },
1659 select : function(e) {
1660 var t = e.target;
1661 e.halt();
1662 this.currenttext = t.get('innerHTML');
1663 this.currentvalue = t.getAttribute('value');
1664 this.nodes.select.set('selectedIndex', this.currentvalue);
1665 this.hide_menu();
1666 },
1667 handle_click : function(e) {
1668 var target = e.target;
1669 if (target.hasClass('smartselect_mask')) {
1670 this.show_menu(e);
1671 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1672 this.select(e);
1673 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1674 this.show_sub_menu(e);
1675 }
1676 },
1677 show_menu : function(e) {
1678 e.halt();
1679 var menu = e.target.ancestor().one('.smartselect_menu');
1680 menu.addClass('visible');
1681 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1682 },
1683 show_sub_menu : function(e) {
1684 e.halt();
1685 var target = e.target;
1686 if (!target.hasClass('smartselect_submenuitem')) {
1687 target = target.ancestor('.smartselect_submenuitem');
1688 }
1689 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1690 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1691 return;
1692 }
1693 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1694 target.one('.smartselect_submenu').addClass('visible');
1695 },
1696 hide_menu : function() {
1697 this.nodes.menu.all('.visible').removeClass('visible');
1698 if (this.shownevent) {
1699 this.shownevent.detach();
1700 }
1701 }
667c0ae1 1702 };
572dd8ec
SH
1703 smartselect.init(Y, id, options, {select:select});
1704 });
667c0ae1 1705};
3a42ad12 1706
fcd2cbaf
PS
1707/** List of flv players to be loaded */
1708M.util.video_players = [];
1709/** List of mp3 players to be loaded */
1710M.util.audio_players = [];
1711
1712/**
1713 * Add video player
1714 * @param id element id
1715 * @param fileurl media url
1716 * @param width
1717 * @param height
1718 * @param autosize true means detect size from media
1719 */
1720M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1721 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
667c0ae1 1722};
3a42ad12 1723
fcd2cbaf
PS
1724/**
1725 * Add audio player.
1726 * @param id
1727 * @param fileurl
1728 * @param small
1729 */
1730M.util.add_audio_player = function (id, fileurl, small) {
1731 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
667c0ae1 1732};
3a42ad12 1733
fcd2cbaf
PS
1734/**
1735 * Initialise all audio and video player, must be called from page footer.
1736 */
1737M.util.load_flowplayer = function() {
1738 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1739 return;
1740 }
1741 if (typeof(flowplayer) == 'undefined') {
1742 var loaded = false;
1743
1744 var embed_function = function() {
1745 if (loaded || typeof(flowplayer) == 'undefined') {
1746 return;
3a42ad12 1747 }
fcd2cbaf
PS
1748 loaded = true;
1749
1750 var controls = {
1751 autoHide: true
1752 }
1753 /* TODO: add CSS color overrides for the flv flow player */
1754
1755 for(var i=0; i<M.util.video_players.length; i++) {
1756 var video = M.util.video_players[i];
1757 if (video.width > 0 && video.height > 0) {
1dd274dc 1758 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', width: video.width, height: video.height};
fcd2cbaf 1759 } else {
1dd274dc 1760 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf';
fcd2cbaf
PS
1761 }
1762 flowplayer(video.id, src, {
1763 plugins: {controls: controls},
1764 clip: {
1765 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1766 onMetaData: function(clip) {
1767 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1768 clip.mvideo.resized = true;
1769 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1770 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1771 // bad luck, we have to guess - we may not get metadata at all
1772 var width = clip.width;
1773 var height = clip.height;
1774 } else {
1775 var width = clip.metaData.width;
1776 var height = clip.metaData.height;
1777 }
1778 var minwidth = 300; // controls are messed up in smaller objects
1779 if (width < minwidth) {
1780 height = (height * minwidth) / width;
1781 width = minwidth;
1782 }
1783
1784 var object = this._api();
1785 object.width = width;
1786 object.height = height;
1787 }
1788 }
1789 }
1790 });
1791 }
1792 if (M.util.audio_players.length == 0) {
1793 return;
1794 }
1795 var controls = {
1796 autoHide: false,
1797 fullscreen: false,
1798 next: false,
1799 previous: false,
1800 scrubber: true,
1801 play: true,
1802 pause: true,
1803 volume: true,
1804 mute: false,
1805 backgroundGradient: [0.5,0,0.3]
1806 };
1807
1808 var rule;
1809 for (var j=0; j < document.styleSheets.length; j++) {
8f80180d
DM
1810
1811 // To avoid javascript security violation accessing cross domain stylesheets
1812 var allrules = false;
1813 try {
1814 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1815 allrules = document.styleSheets[j].rules;
1816 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1817 allrules = document.styleSheets[j].cssRules;
1818 } else {
1819 // why??
1820 continue;
1821 }
1822 } catch (e) {
fcd2cbaf
PS
1823 continue;
1824 }
8f80180d
DM
1825
1826 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1827 if (!allrules) {
1828 continue;
1829 }
1830
fcd2cbaf
PS
1831 for(var i=0; i<allrules.length; i++) {
1832 rule = '';
1833 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1834 if (typeof(allrules[i].cssText) != 'undefined') {
18382a85 1835 rule = allrules[i].cssText;
fcd2cbaf
PS
1836 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1837 rule = allrules[i].style.cssText;
1838 }
1839 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1840 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1841 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1842 controls[colprop] = rule;
1843 }
1844 }
1845 }
1846 allrules = false;
1847 }
1848
1849 for(i=0; i<M.util.audio_players.length; i++) {
1850 var audio = M.util.audio_players[i];
1851 if (audio.small) {
1852 controls.controlall = false;
1853 controls.height = 15;
1854 controls.time = false;
1855 } else {
1856 controls.controlall = true;
1857 controls.height = 25;
1858 controls.time = true;
1859 }
1dd274dc
PS
1860 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', {
1861 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.10.swf'}},
fcd2cbaf
PS
1862 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1863 });
1864 }
1865 }
1866
1dd274dc
PS
1867 if (M.cfg.jsrev == -1) {
1868 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.11.js';
fcd2cbaf 1869 } else {
1dd274dc 1870 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.11.min.js&rev=' + M.cfg.jsrev;
fcd2cbaf
PS
1871 }
1872 var fileref = document.createElement('script');
1873 fileref.setAttribute('type','text/javascript');
1874 fileref.setAttribute('src', jsurl);
1875 fileref.onload = embed_function;
1876 fileref.onreadystatechange = embed_function;
1877 document.getElementsByTagName('head')[0].appendChild(fileref);
1878 }
00e8d08d 1879};