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