9a6d8342d7fb43bfb2111505f26f32895d760f89
[moodle.git] / lib / javascript-static.js
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     }
12 };
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;
55 };
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;
64 };
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     });
77 };
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 ( Y.one(document.body).hasClass('dir-rtl') ) {
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 ( Y.one(document.body).hasClass('dir-rtl') ) {
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);
169 };
171 /**
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;
178 /**
179  * The key divs that make up this
180  * @property div
181  * @type Y.Node
182  */
183 M.util.CollapsibleRegion.prototype.div = null;
185 /**
186  * The key divs that make up this
187  * @property icon
188  * @type Y.Node
189  */
190 M.util.CollapsibleRegion.prototype.icon = null;
192 /**
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.
195  *
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.
199  *
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     });
222 };
224 /**
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     });
330 };
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     };
375 };
377 /**
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                     paramobject.lastindex = select.get('selectedIndex');
418                 };
420                 var changedown = function(e, paramobject) {
421                     if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
422                         if(e.keyCode == 13) {
423                             form.submit();
424                         }
425                         paramobject.lastindex = select.get('selectedIndex');
426                     }
427                 }
429                 var paramobject = new Object();
430                 paramobject.lastindex = select.get('selectedIndex');
431                 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
432                 // Bad hack to circumvent problems with different browsers on different systems.
433                 if (Y.UA.os == 'macintosh') {
434                     if(Y.UA.webkit) {
435                         paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
436                     }
437                     paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
438                 } else {
439                     if(Y.UA.os == 'windows' && Y.UA.chrome) {
440                         paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
441                     }
442                     paramobject.eventkeypress = Y.on('keydown', changedown, select, '', form, paramobject);
443                 }
444             }
445         }
446     });
447 };
449 /**
450  * Attach handler to url_select
451  * Deprecated from 2.4 onwards.
452  * Please use @see init_select_autosubmit() for redirecting to a url (above).
453  * This function has accessability issues and also does not use the formid passed through as a parameter.
454  */
455 M.util.init_url_select = function(Y, formid, selectid, nothing) {
456     YUI().use('node', function(Y) {
457         Y.on('change', function() {
458             if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
459                 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
460             }
461         },
462         '#'+selectid);
463     });
464 };
466 /**
467  * Breaks out all links to the top frame - used in frametop page layout.
468  */
469 M.util.init_frametop = function(Y) {
470     Y.all('a').each(function(node) {
471         node.set('target', '_top');
472     });
473     Y.all('form').each(function(node) {
474         node.set('target', '_top');
475     });
476 };
478 /**
479  * Finds all nodes that match the given CSS selector and attaches events to them
480  * so that they toggle a given classname when clicked.
481  *
482  * @param {YUI} Y
483  * @param {string} id An id containing elements to target
484  * @param {string} cssselector A selector to use to find targets
485  * @param {string} toggleclassname A classname to toggle
486  */
487 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
489     if (togglecssselector == '') {
490         togglecssselector = cssselector;
491     }
493     var node = Y.one('#'+id);
494     node.all(cssselector).each(function(n){
495         n.on('click', function(e){
496             e.stopPropagation();
497             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
498                 if (this.test(togglecssselector)) {
499                     this.toggleClass(toggleclassname);
500                 } else {
501                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
502             }
503             }
504         }, n);
505     });
506     // Attach this click event to the node rather than all selectors... will be much better
507     // for performance
508     node.on('click', function(e){
509         if (e.target.hasClass('addtoall')) {
510             this.all(togglecssselector).addClass(toggleclassname);
511         } else if (e.target.hasClass('removefromall')) {
512             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
513         }
514     }, node);
515 };
517 /**
518  * Initialises a colour picker
519  *
520  * Designed to be used with admin_setting_configcolourpicker although could be used
521  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
522  * above or below the input (must have the same parent) and then call this with the
523  * id.
524  *
525  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
526  * contrib/blocks. For better docs refer to that.
527  *
528  * @param {YUI} Y
529  * @param {int} id
530  * @param {object} previewconf
531  */
532 M.util.init_colour_picker = function(Y, id, previewconf) {
533     /**
534      * We need node and event-mouseenter
535      */
536     Y.use('node', 'event-mouseenter', function(){
537         /**
538          * The colour picker object
539          */
540         var colourpicker = {
541             box : null,
542             input : null,
543             image : null,
544             preview : null,
545             current : null,
546             eventClick : null,
547             eventMouseEnter : null,
548             eventMouseLeave : null,
549             eventMouseMove : null,
550             width : 300,
551             height :  100,
552             factor : 5,
553             /**
554              * Initalises the colour picker by putting everything together and wiring the events
555              */
556             init : function() {
557                 this.input = Y.one('#'+id);
558                 this.box = this.input.ancestor().one('.admin_colourpicker');
559                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
560                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
561                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
562                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
563                 this.current = Y.Node.create('<div class="currentcolour"></div>');
564                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
565                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
567                 if (typeof(previewconf) === 'object' && previewconf !== null) {
568                     Y.one('#'+id+'_preview').on('click', function(e){
569                         if (Y.Lang.isString(previewconf.selector)) {
570                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
571                         } else {
572                             for (var i in previewconf.selector) {
573                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
574                             }
575                         }
576                     }, this);
577                 }
579                 this.eventClick = this.image.on('click', this.pickColour, this);
580                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
581             },
582             /**
583              * Starts to follow the mouse once it enter the image
584              */
585             startFollow : function(e) {
586                 this.eventMouseEnter.detach();
587                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
588                 this.eventMouseMove = this.image.on('mousemove', function(e){
589                     this.preview.setStyle('backgroundColor', this.determineColour(e));
590                 }, this);
591             },
592             /**
593              * Stops following the mouse
594              */
595             endFollow : function(e) {
596                 this.eventMouseMove.detach();
597                 this.eventMouseLeave.detach();
598                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
599             },
600             /**
601              * Picks the colour the was clicked on
602              */
603             pickColour : function(e) {
604                 var colour = this.determineColour(e);
605                 this.input.set('value', colour);
606                 this.current.setStyle('backgroundColor', colour);
607             },
608             /**
609              * Calculates the colour fromthe given co-ordinates
610              */
611             determineColour : function(e) {
612                 var eventx = Math.floor(e.pageX-e.target.getX());
613                 var eventy = Math.floor(e.pageY-e.target.getY());
615                 var imagewidth = this.width;
616                 var imageheight = this.height;
617                 var factor = this.factor;
618                 var colour = [255,0,0];
620                 var matrices = [
621                     [  0,  1,  0],
622                     [ -1,  0,  0],
623                     [  0,  0,  1],
624                     [  0, -1,  0],
625                     [  1,  0,  0],
626                     [  0,  0, -1]
627                 ];
629                 var matrixcount = matrices.length;
630                 var limit = Math.round(imagewidth/matrixcount);
631                 var heightbreak = Math.round(imageheight/2);
633                 for (var x = 0; x < imagewidth; x++) {
634                     var divisor = Math.floor(x / limit);
635                     var matrix = matrices[divisor];
637                     colour[0] += matrix[0]*factor;
638                     colour[1] += matrix[1]*factor;
639                     colour[2] += matrix[2]*factor;
641                     if (eventx==x) {
642                         break;
643                     }
644                 }
646                 var pixel = [colour[0], colour[1], colour[2]];
647                 if (eventy < heightbreak) {
648                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
649                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
650                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
651                 } else if (eventy > heightbreak) {
652                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
653                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
654                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
655                 }
657                 return this.convert_rgb_to_hex(pixel);
658             },
659             /**
660              * Converts an RGB value to Hex
661              */
662             convert_rgb_to_hex : function(rgb) {
663                 var hex = '#';
664                 var hexchars = "0123456789ABCDEF";
665                 for (var i=0; i<3; i++) {
666                     var number = Math.abs(rgb[i]);
667                     if (number == 0 || isNaN(number)) {
668                         hex += '00';
669                     } else {
670                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
671                     }
672                 }
673                 return hex;
674             }
675         };
676         /**
677          * Initialise the colour picker :) Hoorah
678          */
679         colourpicker.init();
680     });
681 };
683 M.util.init_block_hider = function(Y, config) {
684     Y.use('base', 'node', function(Y) {
685         M.util.block_hider = M.util.block_hider || (function(){
686             var blockhider = function() {
687                 blockhider.superclass.constructor.apply(this, arguments);
688             };
689             blockhider.prototype = {
690                 initializer : function(config) {
691                     this.set('block', '#'+this.get('id'));
692                     var b = this.get('block'),
693                         t = b.one('.title'),
694                         a = null;
695                     if (t && (a = t.one('.block_action'))) {
696                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
697                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
698                         hide.on('keypress', this.updateStateKey, this, true);
699                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
700                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
701                         show.on('keypress', this.updateStateKey, this, false);
702                         a.insert(show, 0).insert(hide, 0);
703                     }
704                 },
705                 updateState : function(e, hide) {
706                     M.util.set_user_preference(this.get('preference'), hide);
707                     if (hide) {
708                         this.get('block').addClass('hidden');
709                     } else {
710                         this.get('block').removeClass('hidden');
711                     }
712                 },
713                 updateStateKey : function(e, hide) {
714                     if (e.keyCode == 13) { //allow hide/show via enter key
715                         this.updateState(this, hide);
716                     }
717                 }
718             };
719             Y.extend(blockhider, Y.Base, blockhider.prototype, {
720                 NAME : 'blockhider',
721                 ATTRS : {
722                     id : {},
723                     preference : {},
724                     iconVisible : {
725                         value : M.util.image_url('t/switch_minus', 'moodle')
726                     },
727                     iconHidden : {
728                         value : M.util.image_url('t/switch_plus', 'moodle')
729                     },
730                     block : {
731                         setter : function(node) {
732                             return Y.one(node);
733                         }
734                     }
735                 }
736             });
737             return blockhider;
738         })();
739         new M.util.block_hider(config);
740     });
741 };
743 /**
744  * Returns a string registered in advance for usage in JavaScript
745  *
746  * If you do not pass the third parameter, the function will just return
747  * the corresponding value from the M.str object. If the third parameter is
748  * provided, the function performs {$a} placeholder substitution in the
749  * same way as PHP get_string() in Moodle does.
750  *
751  * @param {String} identifier string identifier
752  * @param {String} component the component providing the string
753  * @param {Object|String} a optional variable to populate placeholder with
754  */
755 M.util.get_string = function(identifier, component, a) {
756     var stringvalue;
758     if (M.cfg.developerdebug) {
759         // creating new instance if YUI is not optimal but it seems to be better way then
760         // require the instance via the function API - note that it is used in rare cases
761         // for debugging only anyway
762         // To ensure we don't kill browser performance if hundreds of get_string requests
763         // are made we cache the instance we generate within the M.util namespace.
764         // We don't publicly define the variable so that it doesn't get abused.
765         if (typeof M.util.get_string_yui_instance === 'undefined') {
766             M.util.get_string_yui_instance = new YUI({ debug : true });
767         }
768         var Y = M.util.get_string_yui_instance;
769     }
771     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
772         stringvalue = '[[' + identifier + ',' + component + ']]';
773         if (M.cfg.developerdebug) {
774             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
775         }
776         return stringvalue;
777     }
779     stringvalue = M.str[component][identifier];
781     if (typeof a == 'undefined') {
782         // no placeholder substitution requested
783         return stringvalue;
784     }
786     if (typeof a == 'number' || typeof a == 'string') {
787         // replace all occurrences of {$a} with the placeholder value
788         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
789         return stringvalue;
790     }
792     if (typeof a == 'object') {
793         // replace {$a->key} placeholders
794         for (var key in a) {
795             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
796                 if (M.cfg.developerdebug) {
797                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
798                 }
799                 continue;
800             }
801             var search = '{$a->' + key + '}';
802             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
803             search = new RegExp(search, 'g');
804             stringvalue = stringvalue.replace(search, a[key]);
805         }
806         return stringvalue;
807     }
809     if (M.cfg.developerdebug) {
810         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
811     }
812     return stringvalue;
813 };
815 /**
816  * Set focus on username or password field of the login form
817  */
818 M.util.focus_login_form = function(Y) {
819     var username = Y.one('#username');
820     var password = Y.one('#password');
822     if (username == null || password == null) {
823         // something is wrong here
824         return;
825     }
827     var curElement = document.activeElement
828     if (curElement == 'undefined') {
829         // legacy browser - skip refocus protection
830     } else if (curElement.tagName == 'INPUT') {
831         // user was probably faster to focus something, do not mess with focus
832         return;
833     }
835     if (username.get('value') == '') {
836         username.focus();
837     } else {
838         password.focus();
839     }
842 /**
843  * Adds lightbox hidden element that covers the whole node.
844  *
845  * @param {YUI} Y
846  * @param {Node} the node lightbox should be added to
847  * @retun {Node} created lightbox node
848  */
849 M.util.add_lightbox = function(Y, node) {
850     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
852     // Check if lightbox is already there
853     if (node.one('.lightbox')) {
854         return node.one('.lightbox');
855     }
857     node.setStyle('position', 'relative');
858     var waiticon = Y.Node.create('<img />')
859     .setAttrs({
860         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
861     })
862     .setStyles({
863         'position' : 'relative',
864         'top' : '50%'
865     });
867     var lightbox = Y.Node.create('<div></div>')
868     .setStyles({
869         'opacity' : '.75',
870         'position' : 'absolute',
871         'width' : '100%',
872         'height' : '100%',
873         'top' : 0,
874         'left' : 0,
875         'backgroundColor' : 'white',
876         'textAlign' : 'center'
877     })
878     .setAttribute('class', 'lightbox')
879     .hide();
881     lightbox.appendChild(waiticon);
882     node.append(lightbox);
883     return lightbox;
886 /**
887  * Appends a hidden spinner element to the specified node.
888  *
889  * @param {YUI} Y
890  * @param {Node} the node the spinner should be added to
891  * @return {Node} created spinner node
892  */
893 M.util.add_spinner = function(Y, node) {
894     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
896     // Check if spinner is already there
897     if (node.one('.spinner')) {
898         return node.one('.spinner');
899     }
901     var spinner = Y.Node.create('<img />')
902         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
903         .addClass('spinner')
904         .addClass('iconsmall')
905         .hide();
907     node.append(spinner);
908     return spinner;
911 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
913 function checkall() {
914     var inputs = document.getElementsByTagName('input');
915     for (var i = 0; i < inputs.length; i++) {
916         if (inputs[i].type == 'checkbox') {
917             if (inputs[i].disabled || inputs[i].readOnly) {
918                 continue;
919             }
920             inputs[i].checked = true;
921         }
922     }
925 function checknone() {
926     var inputs = document.getElementsByTagName('input');
927     for (var i = 0; i < inputs.length; i++) {
928         if (inputs[i].type == 'checkbox') {
929             if (inputs[i].disabled || inputs[i].readOnly) {
930                 continue;
931             }
932             inputs[i].checked = false;
933         }
934     }
937 /**
938  * Either check, or uncheck, all checkboxes inside the element with id is
939  * @param id the id of the container
940  * @param checked the new state, either '' or 'checked'.
941  */
942 function select_all_in_element_with_id(id, checked) {
943     var container = document.getElementById(id);
944     if (!container) {
945         return;
946     }
947     var inputs = container.getElementsByTagName('input');
948     for (var i = 0; i < inputs.length; ++i) {
949         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
950             inputs[i].checked = checked;
951         }
952     }
955 function select_all_in(elTagName, elClass, elId) {
956     var inputs = document.getElementsByTagName('input');
957     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
958     for(var i = 0; i < inputs.length; ++i) {
959         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
960             inputs[i].checked = 'checked';
961         }
962     }
965 function deselect_all_in(elTagName, elClass, elId) {
966     var inputs = document.getElementsByTagName('INPUT');
967     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
968     for(var i = 0; i < inputs.length; ++i) {
969         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
970             inputs[i].checked = '';
971         }
972     }
975 function confirm_if(expr, message) {
976     if(!expr) {
977         return true;
978     }
979     return confirm(message);
983 /*
984     findParentNode (start, elementName, elementClass, elementID)
986     Travels up the DOM hierarchy to find a parent element with the
987     specified tag name, class, and id. All conditions must be met,
988     but any can be ommitted. Returns the BODY element if no match
989     found.
990 */
991 function findParentNode(el, elName, elClass, elId) {
992     while (el.nodeName.toUpperCase() != 'BODY') {
993         if ((!elName || el.nodeName.toUpperCase() == elName) &&
994             (!elClass || el.className.indexOf(elClass) != -1) &&
995             (!elId || el.id == elId)) {
996             break;
997         }
998         el = el.parentNode;
999     }
1000     return el;
1002 /*
1003     findChildNode (start, elementName, elementClass, elementID)
1005     Travels down the DOM hierarchy to find all child elements with the
1006     specified tag name, class, and id. All conditions must be met,
1007     but any can be ommitted.
1008     Doesn't examine children of matches.
1009 */
1010 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
1011     var children = new Array();
1012     for (var i = 0; i < start.childNodes.length; i++) {
1013         var classfound = false;
1014         var child = start.childNodes[i];
1015         if((child.nodeType == 1) &&//element node type
1016                   (elementClass && (typeof(child.className)=='string'))) {
1017             var childClasses = child.className.split(/\s+/);
1018             for (var childClassIndex in childClasses) {
1019                 if (childClasses[childClassIndex]==elementClass) {
1020                     classfound = true;
1021                     break;
1022                 }
1023             }
1024         }
1025         if(child.nodeType == 1) { //element node type
1026             if  ( (!tagName || child.nodeName == tagName) &&
1027                 (!elementClass || classfound)&&
1028                 (!elementID || child.id == elementID) &&
1029                 (!elementName || child.name == elementName))
1030             {
1031                 children = children.concat(child);
1032             } else {
1033                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1034             }
1035         }
1036     }
1037     return children;
1040 function unmaskPassword(id) {
1041   var pw = document.getElementById(id);
1042   var chb = document.getElementById(id+'unmask');
1044   try {
1045     // first try IE way - it can not set name attribute later
1046     if (chb.checked) {
1047       var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1048     } else {
1049       var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1050     }
1051     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1052   } catch (e) {
1053     var newpw = document.createElement('input');
1054     newpw.setAttribute('autocomplete', 'off');
1055     newpw.setAttribute('name', pw.name);
1056     if (chb.checked) {
1057       newpw.setAttribute('type', 'text');
1058     } else {
1059       newpw.setAttribute('type', 'password');
1060     }
1061     newpw.setAttribute('class', pw.getAttribute('class'));
1062   }
1063   newpw.id = pw.id;
1064   newpw.size = pw.size;
1065   newpw.onblur = pw.onblur;
1066   newpw.onchange = pw.onchange;
1067   newpw.value = pw.value;
1068   pw.parentNode.replaceChild(newpw, pw);
1071 function filterByParent(elCollection, parentFinder) {
1072     var filteredCollection = [];
1073     for (var i = 0; i < elCollection.length; ++i) {
1074         var findParent = parentFinder(elCollection[i]);
1075         if (findParent.nodeName.toUpperCase() != 'BODY') {
1076             filteredCollection.push(elCollection[i]);
1077         }
1078     }
1079     return filteredCollection;
1082 /*
1083     All this is here just so that IE gets to handle oversized blocks
1084     in a visually pleasing manner. It does a browser detect. So sue me.
1085 */
1087 function fix_column_widths() {
1088     var agt = navigator.userAgent.toLowerCase();
1089     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1090         fix_column_width('left-column');
1091         fix_column_width('right-column');
1092     }
1095 function fix_column_width(colName) {
1096     if(column = document.getElementById(colName)) {
1097         if(!column.offsetWidth) {
1098             setTimeout("fix_column_width('" + colName + "')", 20);
1099             return;
1100         }
1102         var width = 0;
1103         var nodes = column.childNodes;
1105         for(i = 0; i < nodes.length; ++i) {
1106             if(nodes[i].className.indexOf("block") != -1 ) {
1107                 if(width < nodes[i].offsetWidth) {
1108                     width = nodes[i].offsetWidth;
1109                 }
1110             }
1111         }
1113         for(i = 0; i < nodes.length; ++i) {
1114             if(nodes[i].className.indexOf("block") != -1 ) {
1115                 nodes[i].style.width = width + 'px';
1116             }
1117         }
1118     }
1122 /*
1123    Insert myValue at current cursor position
1124  */
1125 function insertAtCursor(myField, myValue) {
1126     // IE support
1127     if (document.selection) {
1128         myField.focus();
1129         sel = document.selection.createRange();
1130         sel.text = myValue;
1131     }
1132     // Mozilla/Netscape support
1133     else if (myField.selectionStart || myField.selectionStart == '0') {
1134         var startPos = myField.selectionStart;
1135         var endPos = myField.selectionEnd;
1136         myField.value = myField.value.substring(0, startPos)
1137             + myValue + myField.value.substring(endPos, myField.value.length);
1138     } else {
1139         myField.value += myValue;
1140     }
1144 /*
1145         Call instead of setting window.onload directly or setting body onload=.
1146         Adds your function to a chain of functions rather than overwriting anything
1147         that exists.
1148 */
1149 function addonload(fn) {
1150     var oldhandler=window.onload;
1151     window.onload=function() {
1152         if(oldhandler) oldhandler();
1153             fn();
1154     }
1156 /**
1157  * Replacement for getElementsByClassName in browsers that aren't cool enough
1158  *
1159  * Relying on the built-in getElementsByClassName is far, far faster than
1160  * using YUI.
1161  *
1162  * Note: the third argument used to be an object with odd behaviour. It now
1163  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1164  * mimicked if you pass an object.
1165  *
1166  * @param {Node} oElm The top-level node for searching. To search a whole
1167  *                    document, use `document`.
1168  * @param {String} strTagName filter by tag names
1169  * @param {String} name same as HTML5 spec
1170  */
1171 function getElementsByClassName(oElm, strTagName, name) {
1172     // for backwards compatibility
1173     if(typeof name == "object") {
1174         var names = new Array();
1175         for(var i=0; i<name.length; i++) names.push(names[i]);
1176         name = names.join('');
1177     }
1178     // use native implementation if possible
1179     if (oElm.getElementsByClassName && Array.filter) {
1180         if (strTagName == '*') {
1181             return oElm.getElementsByClassName(name);
1182         } else {
1183             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1184                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1185             });
1186         }
1187     }
1188     // native implementation unavailable, fall back to slow method
1189     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1190     var arrReturnElements = new Array();
1191     var arrRegExpClassNames = new Array();
1192     var names = name.split(' ');
1193     for(var i=0; i<names.length; i++) {
1194         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1195     }
1196     var oElement;
1197     var bMatchesAll;
1198     for(var j=0; j<arrElements.length; j++) {
1199         oElement = arrElements[j];
1200         bMatchesAll = true;
1201         for(var k=0; k<arrRegExpClassNames.length; k++) {
1202             if(!arrRegExpClassNames[k].test(oElement.className)) {
1203                 bMatchesAll = false;
1204                 break;
1205             }
1206         }
1207         if(bMatchesAll) {
1208             arrReturnElements.push(oElement);
1209         }
1210     }
1211     return (arrReturnElements)
1214 function openpopup(event, args) {
1216     if (event) {
1217         if (event.preventDefault) {
1218             event.preventDefault();
1219         } else {
1220             event.returnValue = false;
1221         }
1222     }
1224     // Make sure the name argument is set and valid.
1225     var nameregex = /[^a-z0-9_]/i;
1226     if (typeof args.name !== 'string') {
1227         args.name = '_blank';
1228     } else if (args.name.match(nameregex)) {
1229         // Cleans window name because IE does not support funky ones.
1230         args.name = args.name.replace(nameregex, '_');
1231         if (M.cfg.developerdebug) {
1232             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup()');
1233         }
1234     }
1236     var fullurl = args.url;
1237     if (!args.url.match(/https?:\/\//)) {
1238         fullurl = M.cfg.wwwroot + args.url;
1239     }
1240     if (args.fullscreen) {
1241         args.options = args.options.
1242                 replace(/top=\d+/, 'top=0').
1243                 replace(/left=\d+/, 'left=0').
1244                 replace(/width=\d+/, 'width=' + screen.availWidth).
1245                 replace(/height=\d+/, 'height=' + screen.availHeight);
1246     }
1247     var windowobj = window.open(fullurl,args.name,args.options);
1248     if (!windowobj) {
1249         return true;
1250     }
1252     if (args.fullscreen) {
1253         // In some browser / OS combinations (E.g. Chrome on Windows), the
1254         // window initially opens slighly too big. The width and heigh options
1255         // seem to control the area inside the browser window, so what with
1256         // scroll-bars, etc. the actual window is bigger than the screen.
1257         // Therefore, we need to fix things up after the window is open.
1258         var hackcount = 100;
1259         var get_size_exactly_right = function() {
1260             windowobj.moveTo(0, 0);
1261             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1263             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1264             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1265             // about 50ms) after the window is open, then it actually behaves
1266             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1267             // check that the resize actually worked, and if not, repeatedly try
1268             // again after a short delay until it works (but with a limit of
1269             // hackcount repeats.
1270             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1271                 hackcount -= 1;
1272                 setTimeout(get_size_exactly_right, 10);
1273             }
1274         }
1275         setTimeout(get_size_exactly_right, 0);
1276     }
1277     windowobj.focus();
1279     return false;
1282 /** Close the current browser window. */
1283 function close_window(e) {
1284     if (e.preventDefault) {
1285         e.preventDefault();
1286     } else {
1287         e.returnValue = false;
1288     }
1289     window.close();
1292 /**
1293  * Used in a couple of modules to hide navigation areas when using AJAX
1294  */
1296 function show_item(itemid) {
1297     var item = document.getElementById(itemid);
1298     if (item) {
1299         item.style.display = "";
1300     }
1303 function destroy_item(itemid) {
1304     var item = document.getElementById(itemid);
1305     if (item) {
1306         item.parentNode.removeChild(item);
1307     }
1309 /**
1310  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1311  * @param controlid the control id.
1312  */
1313 function focuscontrol(controlid) {
1314     var control = document.getElementById(controlid);
1315     if (control) {
1316         control.focus();
1317     }
1320 /**
1321  * Transfers keyboard focus to an HTML element based on the old style style of focus
1322  * This function should be removed as soon as it is no longer used
1323  */
1324 function old_onload_focus(formid, controlname) {
1325     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1326         document.forms[formid].elements[controlname].focus();
1327     }
1330 function build_querystring(obj) {
1331     return convert_object_to_string(obj, '&');
1334 function build_windowoptionsstring(obj) {
1335     return convert_object_to_string(obj, ',');
1338 function convert_object_to_string(obj, separator) {
1339     if (typeof obj !== 'object') {
1340         return null;
1341     }
1342     var list = [];
1343     for(var k in obj) {
1344         k = encodeURIComponent(k);
1345         var value = obj[k];
1346         if(obj[k] instanceof Array) {
1347             for(var i in value) {
1348                 list.push(k+'[]='+encodeURIComponent(value[i]));
1349             }
1350         } else {
1351             list.push(k+'='+encodeURIComponent(value));
1352         }
1353     }
1354     return list.join(separator);
1357 function stripHTML(str) {
1358     var re = /<\S[^><]*>/g;
1359     var ret = str.replace(re, "");
1360     return ret;
1363 Number.prototype.fixed=function(n){
1364     with(Math)
1365         return round(Number(this)*pow(10,n))/pow(10,n);
1366 };
1367 function update_progress_bar (id, width, pt, msg, es){
1368     var percent = pt;
1369     var status = document.getElementById("status_"+id);
1370     var percent_indicator = document.getElementById("pt_"+id);
1371     var progress_bar = document.getElementById("progress_"+id);
1372     var time_es = document.getElementById("time_"+id);
1373     status.innerHTML = msg;
1374     percent_indicator.innerHTML = percent.fixed(2) + '%';
1375     if(percent == 100) {
1376         progress_bar.style.background = "green";
1377         time_es.style.display = "none";
1378     } else {
1379         progress_bar.style.background = "#FFCC66";
1380         if (es == '?'){
1381             time_es.innerHTML = "";
1382         }else {
1383             time_es.innerHTML = es.fixed(2)+" sec";
1384             time_es.style.display
1385                 = "block";
1386         }
1387     }
1388     progress_bar.style.width = width + "px";
1393 // ===== Deprecated core Javascript functions for Moodle ====
1394 //       DO NOT USE!!!!!!!
1395 // Do not put this stuff in separate file because it only adds extra load on servers!
1397 /**
1398  * Used in a couple of modules to hide navigation areas when using AJAX
1399  */
1400 function hide_item(itemid) {
1401     // use class='hiddenifjs' instead
1402     var item = document.getElementById(itemid);
1403     if (item) {
1404         item.style.display = "none";
1405     }
1408 M.util.help_popups = {
1409     setup : function(Y) {
1410         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1411     },
1412     open_popup : function(e) {
1413         // Prevent the default page action
1414         e.preventDefault();
1416         // Grab the anchor that was clicked
1417         var anchor = e.target.ancestor('a', true);
1418         var args = {
1419             'name'          : 'popup',
1420             'url'           : anchor.getAttribute('href'),
1421             'options'       : ''
1422         };
1423         var options = [
1424             'height=600',
1425             'width=800',
1426             'top=0',
1427             'left=0',
1428             'menubar=0',
1429             'location=0',
1430             'scrollbars',
1431             'resizable',
1432             'toolbar',
1433             'status',
1434             'directories=0',
1435             'fullscreen=0',
1436             'dependent'
1437         ]
1438         args.options = options.join(',');
1440         openpopup(e, args);
1441     }
1444 M.util.help_icon = {
1445     Y : null,
1446     instance : null,
1447     add : function(Y, properties) {
1448         this.Y = Y;
1449         properties.node = Y.one('#'+properties.id);
1450         if (properties.node) {
1451             properties.node.on('click', this.display, this, properties);
1452         }
1453     },
1454     display : function(event, args) {
1455         event.preventDefault();
1456         if (M.util.help_icon.instance === null) {
1457             var Y = M.util.help_icon.Y;
1458             Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', 'escape', function(Y) {
1459                 var help_content_overlay = {
1460                     helplink : null,
1461                     overlay : null,
1462                     init : function() {
1464                         var strclose = Y.Escape.html(M.str.form.close);
1465                         var footerbtn = Y.Node.create('<button class="closebtn">'+strclose+'</button>');
1466                         // Create an overlay from markup
1467                         this.overlay = new Y.Overlay({
1468                             footerContent: footerbtn,
1469                             bodyContent: '',
1470                             id: 'helppopupbox',
1471                             width:'400px',
1472                             visible : false,
1473                             constrain : true
1474                         });
1475                         this.overlay.render(Y.one(document.body));
1477                         footerbtn.on('click', this.overlay.hide, this.overlay);
1479                         var boundingBox = this.overlay.get("boundingBox");
1481                         //  Hide the menu if the user clicks outside of its content
1482                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1483                             var oTarget = event.target;
1484                             var menuButton = Y.one("#"+args.id);
1486                             if (!oTarget.compareTo(menuButton) &&
1487                                 !menuButton.contains(oTarget) &&
1488                                 !oTarget.compareTo(boundingBox) &&
1489                                 !boundingBox.contains(oTarget)) {
1490                                 this.overlay.hide();
1491                             }
1492                         }, this);
1493                     },
1495                     close : function(e) {
1496                         e.preventDefault();
1497                         this.helplink.focus();
1498                         this.overlay.hide();
1499                     },
1501                     display : function(event, args) {
1502                         if (Y.one('html').get('dir') == 'rtl') {
1503                             var overlayPosition = [Y.WidgetPositionAlign.TR, Y.WidgetPositionAlign.LC];
1504                         } else {
1505                             var overlayPosition = [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC];
1506                         }
1508                         this.helplink = args.node;
1510                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1511                         this.overlay.set("align", {node:args.node, points: overlayPosition});
1513                         var fullurl = args.url;
1514                         if (!args.url.match(/https?:\/\//)) {
1515                             fullurl = M.cfg.wwwroot + args.url;
1516                         }
1518                         var ajaxurl = fullurl + '&ajax=1';
1520                         var cfg = {
1521                             method: 'get',
1522                             context : this,
1523                             on: {
1524                                 success: function(id, o, node) {
1525                                     this.display_callback(o.responseText);
1526                                 },
1527                                 failure: function(id, o, node) {
1528                                     var debuginfo = o.statusText;
1529                                     if (M.cfg.developerdebug) {
1530                                         o.statusText += ' (' + ajaxurl + ')';
1531                                     }
1532                                     this.display_callback('bodyContent',debuginfo);
1533                                 }
1534                             }
1535                         };
1537                         Y.io(ajaxurl, cfg);
1538                         this.overlay.show();
1539                     },
1541                     display_callback : function(content) {
1542                         content = '<div role="alert">' + content + '</div>';
1543                         this.overlay.set('bodyContent', content);
1544                     },
1546                     hideContent : function() {
1547                         help = this;
1548                         help.overlay.hide();
1549                     }
1550                 };
1551                 help_content_overlay.init();
1552                 M.util.help_icon.instance = help_content_overlay;
1553                 M.util.help_icon.instance.display(event, args);
1554             });
1555         } else {
1556             M.util.help_icon.instance.display(event, args);
1557         }
1558     },
1559     init : function(Y) {
1560         this.Y = Y;
1561     }
1562 };
1564 /**
1565  * Custom menu namespace
1566  */
1567 M.core_custom_menu = {
1568     /**
1569      * This method is used to initialise a custom menu given the id that belongs
1570      * to the custom menu's root node.
1571      *
1572      * @param {YUI} Y
1573      * @param {string} nodeid
1574      */
1575     init : function(Y, nodeid) {
1576         var node = Y.one('#'+nodeid);
1577         if (node) {
1578             Y.use('node-menunav', function(Y) {
1579                 // Get the node
1580                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1581                 node.removeClass('javascript-disabled');
1582                 // Initialise the menunav plugin
1583                 node.plug(Y.Plugin.NodeMenuNav);
1584             });
1585         }
1586     }
1587 };
1589 /**
1590  * Used to store form manipulation methods and enhancments
1591  */
1592 M.form = M.form || {};
1594 /**
1595  * Converts a nbsp indented select box into a multi drop down custom control much
1596  * like the custom menu. It also selectable categories on or off.
1597  *
1598  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1599  *
1600  * @param {YUI} Y
1601  * @param {string} id
1602  * @param {Array} options
1603  */
1604 M.form.init_smartselect = function(Y, id, options) {
1605     if (!id.match(/^id_/)) {
1606         id = 'id_'+id;
1607     }
1608     var select = Y.one('select#'+id);
1609     if (!select) {
1610         return false;
1611     }
1612     Y.use('event-delegate',function(){
1613         var smartselect = {
1614             id : id,
1615             structure : [],
1616             options : [],
1617             submenucount : 0,
1618             currentvalue : null,
1619             currenttext : null,
1620             shownevent : null,
1621             cfg : {
1622                 selectablecategories : true,
1623                 mode : null
1624             },
1625             nodes : {
1626                 select : null,
1627                 loading : null,
1628                 menu : null
1629             },
1630             init : function(Y, id, args, nodes) {
1631                 if (typeof(args)=='object') {
1632                     for (var i in this.cfg) {
1633                         if (args[i] || args[i]===false) {
1634                             this.cfg[i] = args[i];
1635                         }
1636                     }
1637                 }
1639                 // Display a loading message first up
1640                 this.nodes.select = nodes.select;
1642                 this.currentvalue = this.nodes.select.get('selectedIndex');
1643                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1645                 var options = Array();
1646                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1647                 this.nodes.select.all('option').each(function(option, index) {
1648                     var rawtext = option.get('innerHTML');
1649                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1650                     if (rawtext === text) {
1651                         text = rawtext.replace(/^(\s)*/, '');
1652                         var depth = (rawtext.length - text.length ) + 1;
1653                     } else {
1654                         var depth = ((rawtext.length - text.length )/12)+1;
1655                     }
1656                     option.set('innerHTML', text);
1657                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1658                 }, this);
1660                 this.structure = [];
1661                 var structcount = 0;
1662                 for (var i in options) {
1663                     var o = options[i];
1664                     if (o.depth == 0) {
1665                         this.structure.push(o);
1666                         structcount++;
1667                     } else {
1668                         var d = o.depth;
1669                         var current = this.structure[structcount-1];
1670                         for (var j = 0; j < o.depth-1;j++) {
1671                             if (current && current.children) {
1672                                 current = current.children[current.children.length-1];
1673                             }
1674                         }
1675                         if (current && current.children) {
1676                             current.children.push(o);
1677                         }
1678                     }
1679                 }
1681                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1682                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1683                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1684                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1686                 if (this.cfg.mode == null) {
1687                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1688                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1689                         this.cfg.mode = 'compact';
1690                     } else {
1691                         this.cfg.mode = 'spanning';
1692                     }
1693                 }
1695                 if (this.cfg.mode == 'compact') {
1696                     this.nodes.menu.addClass('compactmenu');
1697                 } else {
1698                     this.nodes.menu.addClass('spanningmenu');
1699                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1700                 }
1702                 Y.one(document.body).append(this.nodes.menu);
1703                 var pos = this.nodes.select.getXY();
1704                 pos[0] += 1;
1705                 this.nodes.menu.setXY(pos);
1706                 this.nodes.menu.on('click', this.handle_click, this);
1708                 Y.one(window).on('resize', function(){
1709                      var pos = this.nodes.select.getXY();
1710                     pos[0] += 1;
1711                     this.nodes.menu.setXY(pos);
1712                  }, this);
1713             },
1714             generate_menu_content : function() {
1715                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1716                 content += this.generate_submenu_content(this.structure[0], true);
1717                 content += '</ul></div>';
1718                 return content;
1719             },
1720             generate_submenu_content : function(item, rootelement) {
1721                 this.submenucount++;
1722                 var content = '';
1723                 if (item.children.length > 0) {
1724                     if (rootelement) {
1725                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1726                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1727                         content += '<div class="smartselect_menu_content">';
1728                     } else {
1729                         content += '<li class="smartselect_submenuitem">';
1730                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1731                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1732                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1733                         content += '<div class="smartselect_submenu_content">';
1734                     }
1735                     content += '<ul>';
1736                     for (var i in item.children) {
1737                         content += this.generate_submenu_content(item.children[i],false);
1738                     }
1739                     content += '</ul>';
1740                     content += '</div>';
1741                     content += '</div>';
1742                     if (rootelement) {
1743                     } else {
1744                         content += '</li>';
1745                     }
1746                 } else {
1747                     content += '<li class="smartselect_menuitem">';
1748                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1749                     content += '</li>';
1750                 }
1751                 return content;
1752             },
1753             select : function(e) {
1754                 var t = e.target;
1755                 e.halt();
1756                 this.currenttext = t.get('innerHTML');
1757                 this.currentvalue = t.getAttribute('value');
1758                 this.nodes.select.set('selectedIndex', this.currentvalue);
1759                 this.hide_menu();
1760             },
1761             handle_click : function(e) {
1762                 var target = e.target;
1763                 if (target.hasClass('smartselect_mask')) {
1764                     this.show_menu(e);
1765                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1766                     this.select(e);
1767                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1768                     this.show_sub_menu(e);
1769                 }
1770             },
1771             show_menu : function(e) {
1772                 e.halt();
1773                 var menu = e.target.ancestor().one('.smartselect_menu');
1774                 menu.addClass('visible');
1775                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1776             },
1777             show_sub_menu : function(e) {
1778                 e.halt();
1779                 var target = e.target;
1780                 if (!target.hasClass('smartselect_submenuitem')) {
1781                     target = target.ancestor('.smartselect_submenuitem');
1782                 }
1783                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1784                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1785                     return;
1786                 }
1787                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1788                 target.one('.smartselect_submenu').addClass('visible');
1789             },
1790             hide_menu : function() {
1791                 this.nodes.menu.all('.visible').removeClass('visible');
1792                 if (this.shownevent) {
1793                     this.shownevent.detach();
1794                 }
1795             }
1796         };
1797         smartselect.init(Y, id, options, {select:select});
1798     });
1799 };
1801 /** List of flv players to be loaded */
1802 M.util.video_players = [];
1803 /** List of mp3 players to be loaded */
1804 M.util.audio_players = [];
1806 /**
1807  * Add video player
1808  * @param id element id
1809  * @param fileurl media url
1810  * @param width
1811  * @param height
1812  * @param autosize true means detect size from media
1813  */
1814 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1815     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1816 };
1818 /**
1819  * Add audio player.
1820  * @param id
1821  * @param fileurl
1822  * @param small
1823  */
1824 M.util.add_audio_player = function (id, fileurl, small) {
1825     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1826 };
1828 /**
1829  * Initialise all audio and video player, must be called from page footer.
1830  */
1831 M.util.load_flowplayer = function() {
1832     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1833         return;
1834     }
1835     if (typeof(flowplayer) == 'undefined') {
1836         var loaded = false;
1838         var embed_function = function() {
1839             if (loaded || typeof(flowplayer) == 'undefined') {
1840                 return;
1841             }
1842             loaded = true;
1844             var controls = {
1845                     autoHide: true
1846             }
1847             /* TODO: add CSS color overrides for the flv flow player */
1849             for(var i=0; i<M.util.video_players.length; i++) {
1850                 var video = M.util.video_players[i];
1851                 if (video.width > 0 && video.height > 0) {
1852                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', width: video.width, height: video.height};
1853                 } else {
1854                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf';
1855                 }
1856                 flowplayer(video.id, src, {
1857                     plugins: {controls: controls},
1858                     clip: {
1859                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1860                         onMetaData: function(clip) {
1861                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1862                                 clip.mvideo.resized = true;
1863                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1864                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1865                                     // bad luck, we have to guess - we may not get metadata at all
1866                                     var width = clip.width;
1867                                     var height = clip.height;
1868                                 } else {
1869                                     var width = clip.metaData.width;
1870                                     var height = clip.metaData.height;
1871                                 }
1872                                 var minwidth = 300; // controls are messed up in smaller objects
1873                                 if (width < minwidth) {
1874                                     height = (height * minwidth) / width;
1875                                     width = minwidth;
1876                                 }
1878                                 var object = this._api();
1879                                 object.width = width;
1880                                 object.height = height;
1881                             }
1882                                 }
1883                     }
1884                 });
1885             }
1886             if (M.util.audio_players.length == 0) {
1887                 return;
1888             }
1889             var controls = {
1890                     autoHide: false,
1891                     fullscreen: false,
1892                     next: false,
1893                     previous: false,
1894                     scrubber: true,
1895                     play: true,
1896                     pause: true,
1897                     volume: true,
1898                     mute: false,
1899                     backgroundGradient: [0.5,0,0.3]
1900                 };
1902             var rule;
1903             for (var j=0; j < document.styleSheets.length; j++) {
1905                 // To avoid javascript security violation accessing cross domain stylesheets
1906                 var allrules = false;
1907                 try {
1908                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1909                         allrules = document.styleSheets[j].rules;
1910                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1911                         allrules = document.styleSheets[j].cssRules;
1912                     } else {
1913                         // why??
1914                         continue;
1915                     }
1916                 } catch (e) {
1917                     continue;
1918                 }
1920                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1921                 if (!allrules) {
1922                     continue;
1923                 }
1925                 for(var i=0; i<allrules.length; i++) {
1926                     rule = '';
1927                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1928                         if (typeof(allrules[i].cssText) != 'undefined') {
1929                             rule = allrules[i].cssText;
1930                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1931                             rule = allrules[i].style.cssText;
1932                         }
1933                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1934                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1935                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1936                             controls[colprop] = rule;
1937                         }
1938                     }
1939                 }
1940                 allrules = false;
1941             }
1943             for(i=0; i<M.util.audio_players.length; i++) {
1944                 var audio = M.util.audio_players[i];
1945                 if (audio.small) {
1946                     controls.controlall = false;
1947                     controls.height = 15;
1948                     controls.time = false;
1949                 } else {
1950                     controls.controlall = true;
1951                     controls.height = 25;
1952                     controls.time = true;
1953                 }
1954                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', {
1955                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.10.swf'}},
1956                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1957                 });
1958             }
1959         }
1961         if (M.cfg.jsrev == -1) {
1962             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.11.js';
1963         } else {
1964             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.11.min.js&rev=' + M.cfg.jsrev;
1965         }
1966         var fileref = document.createElement('script');
1967         fileref.setAttribute('type','text/javascript');
1968         fileref.setAttribute('src', jsurl);
1969         fileref.onload = embed_function;
1970         fileref.onreadystatechange = embed_function;
1971         document.getElementsByTagName('head')[0].appendChild(fileref);
1972     }
1973 };