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