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