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