MDL-35569 AJAX Move auto-submitting selects to separate YUI module
[moodle.git] / lib / javascript-static.js
blobb381800ac2864e0b52121a75219f306b513aa65f
1 // Miscellaneous core Javascript functions for Moodle
2 // Global M object is initilised in inline javascript
4 /**
5  * Add module to list of available modules that can be loaded from YUI.
6  * @param {Array} modules
7  */
8 M.yui.add_module = function(modules) {
9     for (var modname in modules) {
10         YUI_config.modules[modname] = modules[modname];
11     }
13 /**
14  * The gallery version to use when loading YUI modules from the gallery.
15  * Will be changed every time when using local galleries.
16  */
17 M.yui.galleryversion = '2010.04.21-21-51';
19 /**
20  * Various utility functions
21  */
22 M.util = M.util || {};
24 /**
25  * Language strings - initialised from page footer.
26  */
27 M.str = M.str || {};
29 /**
30  * Returns url for images.
31  * @param {String} imagename
32  * @param {String} component
33  * @return {String}
34  */
35 M.util.image_url = function(imagename, component) {
37     if (!component || component == '' || component == 'moodle' || component == 'core') {
38         component = 'core';
39     }
41     var url = M.cfg.wwwroot + '/theme/image.php';
42     if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
43         if (!M.cfg.svgicons) {
44             url += '/_s';
45         }
46         url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
47     } else {
48         url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
49         if (!M.cfg.svgicons) {
50             url += '&svg=0';
51         }
52     }
54     return url;
57 M.util.in_array = function(item, array){
58     for( var i = 0; i<array.length; i++){
59         if(item==array[i]){
60             return true;
61         }
62     }
63     return false;
66 /**
67  * Init a collapsible region, see print_collapsible_region in weblib.php
68  * @param {YUI} Y YUI3 instance with all libraries loaded
69  * @param {String} id the HTML id for the div.
70  * @param {String} userpref the user preference that records the state of this box. false if none.
71  * @param {String} strtooltip
72  */
73 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
74     Y.use('anim', function(Y) {
75         new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
76     });
79 /**
80  * Object to handle a collapsible region : instantiate and forget styled object
81  *
82  * @class
83  * @constructor
84  * @param {YUI} Y YUI3 instance with all libraries loaded
85  * @param {String} id The HTML id for the div.
86  * @param {String} userpref The user preference that records the state of this box. false if none.
87  * @param {String} strtooltip
88  */
89 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
90     // Record the pref name
91     this.userpref = userpref;
93     // Find the divs in the document.
94     this.div = Y.one('#'+id);
96     // Get the caption for the collapsible region
97     var caption = this.div.one('#'+id + '_caption');
99     // Create a link
100     var a = Y.Node.create('<a href="#"></a>');
101     a.setAttribute('title', strtooltip);
103     // Get all the nodes from caption, remove them and append them to <a>
104     while (caption.hasChildNodes()) {
105         child = caption.get('firstChild');
106         child.remove();
107         a.append(child);
108     }
109     caption.append(a);
111     // Get the height of the div at this point before we shrink it if required
112     var height = this.div.get('offsetHeight');
113     var collapsedimage = 't/collapsed'; // ltr mode
114     if ( Y.one(document.body).hasClass('dir-rtl') ) {
115         collapsedimage = 't/collapsed_rtl';
116     } else {
117         collapsedimage = 't/collapsed';
118     }
119     if (this.div.hasClass('collapsed')) {
120         // Add the correct image and record the YUI node created in the process
121         this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
122         // Shrink the div as it is collapsed by default
123         this.div.setStyle('height', caption.get('offsetHeight')+'px');
124     } else {
125         // Add the correct image and record the YUI node created in the process
126         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
127     }
128     a.append(this.icon);
130     // Create the animation.
131     var animation = new Y.Anim({
132         node: this.div,
133         duration: 0.3,
134         easing: Y.Easing.easeBoth,
135         to: {height:caption.get('offsetHeight')},
136         from: {height:height}
137     });
139     // Handler for the animation finishing.
140     animation.on('end', function() {
141         this.div.toggleClass('collapsed');
142         var collapsedimage = 't/collapsed'; // ltr mode
143         if ( Y.one(document.body).hasClass('dir-rtl') ) {
144             collapsedimage = 't/collapsed_rtl';
145             } else {
146             collapsedimage = 't/collapsed';
147             }
148         if (this.div.hasClass('collapsed')) {
149             this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
150         } else {
151             this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
152         }
153     }, this);
155     // Hook up the event handler.
156     a.on('click', function(e, animation) {
157         e.preventDefault();
158         // Animate to the appropriate size.
159         if (animation.get('running')) {
160             animation.stop();
161         }
162         animation.set('reverse', this.div.hasClass('collapsed'));
163         // Update the user preference.
164         if (this.userpref) {
165             M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
166         }
167         animation.run();
168     }, this, animation);
172  * The user preference that stores the state of this box.
173  * @property userpref
174  * @type String
175  */
176 M.util.CollapsibleRegion.prototype.userpref = null;
179  * The key divs that make up this
180  * @property div
181  * @type Y.Node
182  */
183 M.util.CollapsibleRegion.prototype.div = null;
186  * The key divs that make up this
187  * @property icon
188  * @type Y.Node
189  */
190 M.util.CollapsibleRegion.prototype.icon = null;
193  * Makes a best effort to connect back to Moodle to update a user preference,
194  * however, there is no mechanism for finding out if the update succeeded.
196  * Before you can use this function in your JavsScript, you must have called
197  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
198  * the udpate is allowed, and how to safely clean and submitted values.
200  * @param String name the name of the setting to udpate.
201  * @param String the value to set it to.
202  */
203 M.util.set_user_preference = function(name, value) {
204     YUI().use('io', function(Y) {
205         var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
206                 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
208         // If we are a developer, ensure that failures are reported.
209         var cfg = {
210                 method: 'get',
211                 on: {}
212             };
213         if (M.cfg.developerdebug) {
214             cfg.on.failure = function(id, o, args) {
215                 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
216             }
217         }
219         // Make the request.
220         Y.io(url, cfg);
221     });
225  * Prints a confirmation dialog in the style of DOM.confirm().
226  * @param object event A YUI DOM event or null if launched manually
227  * @param string message The message to show in the dialog
228  * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
229  * @param function fn A JS function to run if YES is clicked.
230  */
231 M.util.show_confirm_dialog = function(e, args) {
232     var target = e.target;
233     if (e.preventDefault) {
234         e.preventDefault();
235     }
237     YUI().use('yui2-container', 'yui2-event', function(Y) {
238         var simpledialog = new Y.YUI2.widget.SimpleDialog('confirmdialog',
239             {width: '300px',
240               fixedcenter: true,
241               modal: true,
242               visible: false,
243               draggable: false
244             }
245         );
247         simpledialog.setHeader(M.str.admin.confirmation);
248         simpledialog.setBody(args.message);
249         simpledialog.cfg.setProperty('icon', Y.YUI2.widget.SimpleDialog.ICON_WARN);
251         var handle_cancel = function() {
252             simpledialog.hide();
253         };
255         var handle_yes = function() {
256             simpledialog.hide();
258             if (args.callback) {
259                 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
260                 var callback = null;
261                 if (Y.Lang.isFunction(args.callback)) {
262                     callback = args.callback;
263                 } else {
264                     callback = eval('('+args.callback+')');
265                 }
267                 if (Y.Lang.isObject(args.scope)) {
268                     var sc = args.scope;
269                 } else {
270                     var sc = e.target;
271                 }
273                 if (args.callbackargs) {
274                     callback.apply(sc, args.callbackargs);
275                 } else {
276                     callback.apply(sc);
277                 }
278                 return;
279             }
281             var targetancestor = null,
282                 targetform = null;
284             if (target.test('a')) {
285                 window.location = target.get('href');
287             } else if ((targetancestor = target.ancestor('a')) !== null) {
288                 window.location = targetancestor.get('href');
290             } else if (target.test('input')) {
291                 targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
292                 // We cannot use target.ancestor('form') on the previous line
293                 // because of http://yuilibrary.com/projects/yui3/ticket/2531561
294                 if (!targetform) {
295                     return;
296                 }
297                 if (target.get('name') && target.get('value')) {
298                     targetform.append('<input type="hidden" name="' + target.get('name') +
299                                     '" value="' + target.get('value') + '">');
300                 }
301                 targetform.submit();
303             } else if (target.get('tagName').toLowerCase() == 'form') {
304                 // We cannot use target.test('form') on the previous line because of
305                 // http://yuilibrary.com/projects/yui3/ticket/2531561
306                 target.submit();
308             } else if (M.cfg.developerdebug) {
309                 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
310             }
311         };
313         if (!args.cancellabel) {
314             args.cancellabel = M.str.moodle.cancel;
315         }
316         if (!args.continuelabel) {
317             args.continuelabel = M.str.moodle.yes;
318         }
320         var buttons = [
321             {text: args.cancellabel,   handler: handle_cancel, isDefault: true},
322             {text: args.continuelabel, handler: handle_yes}
323         ];
325         simpledialog.cfg.queueProperty('buttons', buttons);
327         simpledialog.render(document.body);
328         simpledialog.show();
329     });
332 /** Useful for full embedding of various stuff */
333 M.util.init_maximised_embed = function(Y, id) {
334     var obj = Y.one('#'+id);
335     if (!obj) {
336         return;
337     }
339     var get_htmlelement_size = function(el, prop) {
340         if (Y.Lang.isString(el)) {
341             el = Y.one('#' + el);
342         }
343         var val = el.getStyle(prop);
344         if (val == 'auto') {
345             val = el.getComputedStyle(prop);
346         }
347         return parseInt(val);
348     };
350     var resize_object = function() {
351         obj.setStyle('width', '0px');
352         obj.setStyle('height', '0px');
353         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
355         if (newwidth > 500) {
356             obj.setStyle('width', newwidth  + 'px');
357         } else {
358             obj.setStyle('width', '500px');
359         }
361         var headerheight = get_htmlelement_size('page-header', 'height');
362         var footerheight = get_htmlelement_size('page-footer', 'height');
363         var newheight = parseInt(Y.one('body').get('winHeight')) - footerheight - headerheight - 100;
364         if (newheight < 400) {
365             newheight = 400;
366         }
367         obj.setStyle('height', newheight+'px');
368     };
370     resize_object();
371     // fix layout if window resized too
372     window.onresize = function() {
373         resize_object();
374     };
378  * Attach handler to single_select
380  * This code was deprecated in Moodle 2.4 and will be removed in Moodle 2.6
382  * Please see lib/yui/formautosubmit/formautosubmit.js for its replacement
383  */
384 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
385     if (M.cfg.developerdebug) {
386         Y.log("You are using a deprecated function call (M.util.init_select_autosubmit). Please look at rewriting your call to use moodle-core-formautosubmit");
387     }
388     Y.use('event-key', function() {
389         var select = Y.one('#'+selectid);
390         if (select) {
391             // Try to get the form by id
392             var form = Y.one('#'+formid) || (function(){
393                 // Hmmm the form's id may have been overriden by an internal input
394                 // with the name id which will KILL IE.
395                 // We need to manually iterate at this point because if the case
396                 // above is true YUI's ancestor method will also kill IE!
397                 var form = select;
398                 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
399                     form = form.ancestor();
400                 }
401                 return form;
402             })();
403             // Make sure we have the form
404             if (form) {
405                 var buttonflag = 0;
406                 // Create a function to handle our change event
407                 var processchange = function(e, paramobject) {
408                     if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
409                         // chrome doesn't pick up on a click when selecting an element in a select menu, so we use
410                         // the on change event to fire this function. This just checks to see if a button was
411                         // first pressed before redirecting to the appropriate page.
412                         if (Y.UA.os == 'windows' && Y.UA.chrome){
413                             if (buttonflag == 1) {
414                                 buttonflag = 0;
415                                 this.submit();
416                             }
417                         } else {
418                             this.submit();
419                         }
420                     }
421                     if (e.button == 1) {
422                         buttonflag = 1;
423                     }
424                 };
426                 var changedown = function(e, paramobject) {
427                     if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
428                         if(e.keyCode == 13) {
429                             form.submit();
430                         }
431                     }
432                 }
434                 var paramobject = new Object();
435                 paramobject.lastindex = select.get('selectedIndex');
436                 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
437                 // Bad hack to circumvent problems with different browsers on different systems.
438                 if (Y.UA.os == 'macintosh') {
439                     if(Y.UA.webkit) {
440                         paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
441                     }
442                     paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
443                 } else {
444                     if(Y.UA.os == 'windows' && Y.UA.chrome) {
445                         paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
446                     }
447                     paramobject.eventkeypress = Y.on('keydown', changedown, select, '', form, paramobject);
448                 }
449             }
450         }
451     });
455  * Attach handler to url_select
456  * Deprecated from 2.4 onwards.
457  * Please use @see init_select_autosubmit() for redirecting to a url (above).
458  * This function has accessability issues and also does not use the formid passed through as a parameter.
459  */
460 M.util.init_url_select = function(Y, formid, selectid, nothing) {
461     if (M.cfg.developerdebug) {
462         Y.log("You are using a deprecated function call (M.util.init_url_select). Please look at rewriting your call to use moodle-core-formautosubmit");
463     }
464     YUI().use('node', function(Y) {
465         Y.on('change', function() {
466             if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
467                 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
468             }
469         },
470         '#'+selectid);
471     });
475  * Breaks out all links to the top frame - used in frametop page layout.
476  */
477 M.util.init_frametop = function(Y) {
478     Y.all('a').each(function(node) {
479         node.set('target', '_top');
480     });
481     Y.all('form').each(function(node) {
482         node.set('target', '_top');
483     });
487  * Finds all nodes that match the given CSS selector and attaches events to them
488  * so that they toggle a given classname when clicked.
490  * @param {YUI} Y
491  * @param {string} id An id containing elements to target
492  * @param {string} cssselector A selector to use to find targets
493  * @param {string} toggleclassname A classname to toggle
494  */
495 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
497     if (togglecssselector == '') {
498         togglecssselector = cssselector;
499     }
501     var node = Y.one('#'+id);
502     node.all(cssselector).each(function(n){
503         n.on('click', function(e){
504             e.stopPropagation();
505             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
506                 if (this.test(togglecssselector)) {
507                     this.toggleClass(toggleclassname);
508                 } else {
509                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
510             }
511             }
512         }, n);
513     });
514     // Attach this click event to the node rather than all selectors... will be much better
515     // for performance
516     node.on('click', function(e){
517         if (e.target.hasClass('addtoall')) {
518             this.all(togglecssselector).addClass(toggleclassname);
519         } else if (e.target.hasClass('removefromall')) {
520             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
521         }
522     }, node);
526  * Initialises a colour picker
528  * Designed to be used with admin_setting_configcolourpicker although could be used
529  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
530  * above or below the input (must have the same parent) and then call this with the
531  * id.
533  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
534  * contrib/blocks. For better docs refer to that.
536  * @param {YUI} Y
537  * @param {int} id
538  * @param {object} previewconf
539  */
540 M.util.init_colour_picker = function(Y, id, previewconf) {
541     /**
542      * We need node and event-mouseenter
543      */
544     Y.use('node', 'event-mouseenter', function(){
545         /**
546          * The colour picker object
547          */
548         var colourpicker = {
549             box : null,
550             input : null,
551             image : null,
552             preview : null,
553             current : null,
554             eventClick : null,
555             eventMouseEnter : null,
556             eventMouseLeave : null,
557             eventMouseMove : null,
558             width : 300,
559             height :  100,
560             factor : 5,
561             /**
562              * Initalises the colour picker by putting everything together and wiring the events
563              */
564             init : function() {
565                 this.input = Y.one('#'+id);
566                 this.box = this.input.ancestor().one('.admin_colourpicker');
567                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
568                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
569                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
570                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
571                 this.current = Y.Node.create('<div class="currentcolour"></div>');
572                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
573                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
575                 if (typeof(previewconf) === 'object' && previewconf !== null) {
576                     Y.one('#'+id+'_preview').on('click', function(e){
577                         if (Y.Lang.isString(previewconf.selector)) {
578                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
579                         } else {
580                             for (var i in previewconf.selector) {
581                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
582                             }
583                         }
584                     }, this);
585                 }
587                 this.eventClick = this.image.on('click', this.pickColour, this);
588                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
589             },
590             /**
591              * Starts to follow the mouse once it enter the image
592              */
593             startFollow : function(e) {
594                 this.eventMouseEnter.detach();
595                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
596                 this.eventMouseMove = this.image.on('mousemove', function(e){
597                     this.preview.setStyle('backgroundColor', this.determineColour(e));
598                 }, this);
599             },
600             /**
601              * Stops following the mouse
602              */
603             endFollow : function(e) {
604                 this.eventMouseMove.detach();
605                 this.eventMouseLeave.detach();
606                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
607             },
608             /**
609              * Picks the colour the was clicked on
610              */
611             pickColour : function(e) {
612                 var colour = this.determineColour(e);
613                 this.input.set('value', colour);
614                 this.current.setStyle('backgroundColor', colour);
615             },
616             /**
617              * Calculates the colour fromthe given co-ordinates
618              */
619             determineColour : function(e) {
620                 var eventx = Math.floor(e.pageX-e.target.getX());
621                 var eventy = Math.floor(e.pageY-e.target.getY());
623                 var imagewidth = this.width;
624                 var imageheight = this.height;
625                 var factor = this.factor;
626                 var colour = [255,0,0];
628                 var matrices = [
629                     [  0,  1,  0],
630                     [ -1,  0,  0],
631                     [  0,  0,  1],
632                     [  0, -1,  0],
633                     [  1,  0,  0],
634                     [  0,  0, -1]
635                 ];
637                 var matrixcount = matrices.length;
638                 var limit = Math.round(imagewidth/matrixcount);
639                 var heightbreak = Math.round(imageheight/2);
641                 for (var x = 0; x < imagewidth; x++) {
642                     var divisor = Math.floor(x / limit);
643                     var matrix = matrices[divisor];
645                     colour[0] += matrix[0]*factor;
646                     colour[1] += matrix[1]*factor;
647                     colour[2] += matrix[2]*factor;
649                     if (eventx==x) {
650                         break;
651                     }
652                 }
654                 var pixel = [colour[0], colour[1], colour[2]];
655                 if (eventy < heightbreak) {
656                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
657                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
658                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
659                 } else if (eventy > heightbreak) {
660                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
661                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
662                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
663                 }
665                 return this.convert_rgb_to_hex(pixel);
666             },
667             /**
668              * Converts an RGB value to Hex
669              */
670             convert_rgb_to_hex : function(rgb) {
671                 var hex = '#';
672                 var hexchars = "0123456789ABCDEF";
673                 for (var i=0; i<3; i++) {
674                     var number = Math.abs(rgb[i]);
675                     if (number == 0 || isNaN(number)) {
676                         hex += '00';
677                     } else {
678                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
679                     }
680                 }
681                 return hex;
682             }
683         };
684         /**
685          * Initialise the colour picker :) Hoorah
686          */
687         colourpicker.init();
688     });
691 M.util.init_block_hider = function(Y, config) {
692     Y.use('base', 'node', function(Y) {
693         M.util.block_hider = M.util.block_hider || (function(){
694             var blockhider = function() {
695                 blockhider.superclass.constructor.apply(this, arguments);
696             };
697             blockhider.prototype = {
698                 initializer : function(config) {
699                     this.set('block', '#'+this.get('id'));
700                     var b = this.get('block'),
701                         t = b.one('.title'),
702                         a = null;
703                     if (t && (a = t.one('.block_action'))) {
704                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
705                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
706                         hide.on('keypress', this.updateStateKey, this, true);
707                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
708                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
709                         show.on('keypress', this.updateStateKey, this, false);
710                         a.insert(show, 0).insert(hide, 0);
711                     }
712                 },
713                 updateState : function(e, hide) {
714                     M.util.set_user_preference(this.get('preference'), hide);
715                     if (hide) {
716                         this.get('block').addClass('hidden');
717                     } else {
718                         this.get('block').removeClass('hidden');
719                     }
720                 },
721                 updateStateKey : function(e, hide) {
722                     if (e.keyCode == 13) { //allow hide/show via enter key
723                         this.updateState(this, hide);
724                     }
725                 }
726             };
727             Y.extend(blockhider, Y.Base, blockhider.prototype, {
728                 NAME : 'blockhider',
729                 ATTRS : {
730                     id : {},
731                     preference : {},
732                     iconVisible : {
733                         value : M.util.image_url('t/switch_minus', 'moodle')
734                     },
735                     iconHidden : {
736                         value : M.util.image_url('t/switch_plus', 'moodle')
737                     },
738                     block : {
739                         setter : function(node) {
740                             return Y.one(node);
741                         }
742                     }
743                 }
744             });
745             return blockhider;
746         })();
747         new M.util.block_hider(config);
748     });
752  * Returns a string registered in advance for usage in JavaScript
754  * If you do not pass the third parameter, the function will just return
755  * the corresponding value from the M.str object. If the third parameter is
756  * provided, the function performs {$a} placeholder substitution in the
757  * same way as PHP get_string() in Moodle does.
759  * @param {String} identifier string identifier
760  * @param {String} component the component providing the string
761  * @param {Object|String} a optional variable to populate placeholder with
762  */
763 M.util.get_string = function(identifier, component, a) {
764     var stringvalue;
766     if (M.cfg.developerdebug) {
767         // creating new instance if YUI is not optimal but it seems to be better way then
768         // require the instance via the function API - note that it is used in rare cases
769         // for debugging only anyway
770         // To ensure we don't kill browser performance if hundreds of get_string requests
771         // are made we cache the instance we generate within the M.util namespace.
772         // We don't publicly define the variable so that it doesn't get abused.
773         if (typeof M.util.get_string_yui_instance === 'undefined') {
774             M.util.get_string_yui_instance = new YUI({ debug : true });
775         }
776         var Y = M.util.get_string_yui_instance;
777     }
779     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
780         stringvalue = '[[' + identifier + ',' + component + ']]';
781         if (M.cfg.developerdebug) {
782             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
783         }
784         return stringvalue;
785     }
787     stringvalue = M.str[component][identifier];
789     if (typeof a == 'undefined') {
790         // no placeholder substitution requested
791         return stringvalue;
792     }
794     if (typeof a == 'number' || typeof a == 'string') {
795         // replace all occurrences of {$a} with the placeholder value
796         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
797         return stringvalue;
798     }
800     if (typeof a == 'object') {
801         // replace {$a->key} placeholders
802         for (var key in a) {
803             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
804                 if (M.cfg.developerdebug) {
805                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
806                 }
807                 continue;
808             }
809             var search = '{$a->' + key + '}';
810             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
811             search = new RegExp(search, 'g');
812             stringvalue = stringvalue.replace(search, a[key]);
813         }
814         return stringvalue;
815     }
817     if (M.cfg.developerdebug) {
818         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
819     }
820     return stringvalue;
824  * Set focus on username or password field of the login form
825  */
826 M.util.focus_login_form = function(Y) {
827     var username = Y.one('#username');
828     var password = Y.one('#password');
830     if (username == null || password == null) {
831         // something is wrong here
832         return;
833     }
835     var curElement = document.activeElement
836     if (curElement == 'undefined') {
837         // legacy browser - skip refocus protection
838     } else if (curElement.tagName == 'INPUT') {
839         // user was probably faster to focus something, do not mess with focus
840         return;
841     }
843     if (username.get('value') == '') {
844         username.focus();
845     } else {
846         password.focus();
847     }
851  * Adds lightbox hidden element that covers the whole node.
853  * @param {YUI} Y
854  * @param {Node} the node lightbox should be added to
855  * @retun {Node} created lightbox node
856  */
857 M.util.add_lightbox = function(Y, node) {
858     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
860     // Check if lightbox is already there
861     if (node.one('.lightbox')) {
862         return node.one('.lightbox');
863     }
865     node.setStyle('position', 'relative');
866     var waiticon = Y.Node.create('<img />')
867     .setAttrs({
868         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
869     })
870     .setStyles({
871         'position' : 'relative',
872         'top' : '50%'
873     });
875     var lightbox = Y.Node.create('<div></div>')
876     .setStyles({
877         'opacity' : '.75',
878         'position' : 'absolute',
879         'width' : '100%',
880         'height' : '100%',
881         'top' : 0,
882         'left' : 0,
883         'backgroundColor' : 'white',
884         'text-align' : 'center'
885     })
886     .setAttribute('class', 'lightbox')
887     .hide();
889     lightbox.appendChild(waiticon);
890     node.append(lightbox);
891     return lightbox;
895  * Appends a hidden spinner element to the specified node.
897  * @param {YUI} Y
898  * @param {Node} the node the spinner should be added to
899  * @return {Node} created spinner node
900  */
901 M.util.add_spinner = function(Y, node) {
902     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
904     // Check if spinner is already there
905     if (node.one('.spinner')) {
906         return node.one('.spinner');
907     }
909     var spinner = Y.Node.create('<img />')
910         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
911         .addClass('spinner')
912         .addClass('iconsmall')
913         .hide();
915     node.append(spinner);
916     return spinner;
919 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
921 function checkall() {
922     var inputs = document.getElementsByTagName('input');
923     for (var i = 0; i < inputs.length; i++) {
924         if (inputs[i].type == 'checkbox') {
925             if (inputs[i].disabled || inputs[i].readOnly) {
926                 continue;
927             }
928             inputs[i].checked = true;
929         }
930     }
933 function checknone() {
934     var inputs = document.getElementsByTagName('input');
935     for (var i = 0; i < inputs.length; i++) {
936         if (inputs[i].type == 'checkbox') {
937             if (inputs[i].disabled || inputs[i].readOnly) {
938                 continue;
939             }
940             inputs[i].checked = false;
941         }
942     }
946  * Either check, or uncheck, all checkboxes inside the element with id is
947  * @param id the id of the container
948  * @param checked the new state, either '' or 'checked'.
949  */
950 function select_all_in_element_with_id(id, checked) {
951     var container = document.getElementById(id);
952     if (!container) {
953         return;
954     }
955     var inputs = container.getElementsByTagName('input');
956     for (var i = 0; i < inputs.length; ++i) {
957         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
958             inputs[i].checked = checked;
959         }
960     }
963 function select_all_in(elTagName, elClass, elId) {
964     var inputs = document.getElementsByTagName('input');
965     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
966     for(var i = 0; i < inputs.length; ++i) {
967         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
968             inputs[i].checked = 'checked';
969         }
970     }
973 function deselect_all_in(elTagName, elClass, elId) {
974     var inputs = document.getElementsByTagName('INPUT');
975     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
976     for(var i = 0; i < inputs.length; ++i) {
977         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
978             inputs[i].checked = '';
979         }
980     }
983 function confirm_if(expr, message) {
984     if(!expr) {
985         return true;
986     }
987     return confirm(message);
992     findParentNode (start, elementName, elementClass, elementID)
994     Travels up the DOM hierarchy to find a parent element with the
995     specified tag name, class, and id. All conditions must be met,
996     but any can be ommitted. Returns the BODY element if no match
997     found.
999 function findParentNode(el, elName, elClass, elId) {
1000     while (el.nodeName.toUpperCase() != 'BODY') {
1001         if ((!elName || el.nodeName.toUpperCase() == elName) &&
1002             (!elClass || el.className.indexOf(elClass) != -1) &&
1003             (!elId || el.id == elId)) {
1004             break;
1005         }
1006         el = el.parentNode;
1007     }
1008     return el;
1011     findChildNode (start, elementName, elementClass, elementID)
1013     Travels down the DOM hierarchy to find all child elements with the
1014     specified tag name, class, and id. All conditions must be met,
1015     but any can be ommitted.
1016     Doesn't examine children of matches.
1018 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
1019     var children = new Array();
1020     for (var i = 0; i < start.childNodes.length; i++) {
1021         var classfound = false;
1022         var child = start.childNodes[i];
1023         if((child.nodeType == 1) &&//element node type
1024                   (elementClass && (typeof(child.className)=='string'))) {
1025             var childClasses = child.className.split(/\s+/);
1026             for (var childClassIndex in childClasses) {
1027                 if (childClasses[childClassIndex]==elementClass) {
1028                     classfound = true;
1029                     break;
1030                 }
1031             }
1032         }
1033         if(child.nodeType == 1) { //element node type
1034             if  ( (!tagName || child.nodeName == tagName) &&
1035                 (!elementClass || classfound)&&
1036                 (!elementID || child.id == elementID) &&
1037                 (!elementName || child.name == elementName))
1038             {
1039                 children = children.concat(child);
1040             } else {
1041                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1042             }
1043         }
1044     }
1045     return children;
1048 function unmaskPassword(id) {
1049   var pw = document.getElementById(id);
1050   var chb = document.getElementById(id+'unmask');
1052   try {
1053     // first try IE way - it can not set name attribute later
1054     if (chb.checked) {
1055       var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1056     } else {
1057       var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1058     }
1059     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1060   } catch (e) {
1061     var newpw = document.createElement('input');
1062     newpw.setAttribute('autocomplete', 'off');
1063     newpw.setAttribute('name', pw.name);
1064     if (chb.checked) {
1065       newpw.setAttribute('type', 'text');
1066     } else {
1067       newpw.setAttribute('type', 'password');
1068     }
1069     newpw.setAttribute('class', pw.getAttribute('class'));
1070   }
1071   newpw.id = pw.id;
1072   newpw.size = pw.size;
1073   newpw.onblur = pw.onblur;
1074   newpw.onchange = pw.onchange;
1075   newpw.value = pw.value;
1076   pw.parentNode.replaceChild(newpw, pw);
1079 function filterByParent(elCollection, parentFinder) {
1080     var filteredCollection = [];
1081     for (var i = 0; i < elCollection.length; ++i) {
1082         var findParent = parentFinder(elCollection[i]);
1083         if (findParent.nodeName.toUpperCase() != 'BODY') {
1084             filteredCollection.push(elCollection[i]);
1085         }
1086     }
1087     return filteredCollection;
1091     All this is here just so that IE gets to handle oversized blocks
1092     in a visually pleasing manner. It does a browser detect. So sue me.
1095 function fix_column_widths() {
1096     var agt = navigator.userAgent.toLowerCase();
1097     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1098         fix_column_width('left-column');
1099         fix_column_width('right-column');
1100     }
1103 function fix_column_width(colName) {
1104     if(column = document.getElementById(colName)) {
1105         if(!column.offsetWidth) {
1106             setTimeout("fix_column_width('" + colName + "')", 20);
1107             return;
1108         }
1110         var width = 0;
1111         var nodes = column.childNodes;
1113         for(i = 0; i < nodes.length; ++i) {
1114             if(nodes[i].className.indexOf("block") != -1 ) {
1115                 if(width < nodes[i].offsetWidth) {
1116                     width = nodes[i].offsetWidth;
1117                 }
1118             }
1119         }
1121         for(i = 0; i < nodes.length; ++i) {
1122             if(nodes[i].className.indexOf("block") != -1 ) {
1123                 nodes[i].style.width = width + 'px';
1124             }
1125         }
1126     }
1131    Insert myValue at current cursor position
1132  */
1133 function insertAtCursor(myField, myValue) {
1134     // IE support
1135     if (document.selection) {
1136         myField.focus();
1137         sel = document.selection.createRange();
1138         sel.text = myValue;
1139     }
1140     // Mozilla/Netscape support
1141     else if (myField.selectionStart || myField.selectionStart == '0') {
1142         var startPos = myField.selectionStart;
1143         var endPos = myField.selectionEnd;
1144         myField.value = myField.value.substring(0, startPos)
1145             + myValue + myField.value.substring(endPos, myField.value.length);
1146     } else {
1147         myField.value += myValue;
1148     }
1153         Call instead of setting window.onload directly or setting body onload=.
1154         Adds your function to a chain of functions rather than overwriting anything
1155         that exists.
1157 function addonload(fn) {
1158     var oldhandler=window.onload;
1159     window.onload=function() {
1160         if(oldhandler) oldhandler();
1161             fn();
1162     }
1165  * Replacement for getElementsByClassName in browsers that aren't cool enough
1167  * Relying on the built-in getElementsByClassName is far, far faster than
1168  * using YUI.
1170  * Note: the third argument used to be an object with odd behaviour. It now
1171  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1172  * mimicked if you pass an object.
1174  * @param {Node} oElm The top-level node for searching. To search a whole
1175  *                    document, use `document`.
1176  * @param {String} strTagName filter by tag names
1177  * @param {String} name same as HTML5 spec
1178  */
1179 function getElementsByClassName(oElm, strTagName, name) {
1180     // for backwards compatibility
1181     if(typeof name == "object") {
1182         var names = new Array();
1183         for(var i=0; i<name.length; i++) names.push(names[i]);
1184         name = names.join('');
1185     }
1186     // use native implementation if possible
1187     if (oElm.getElementsByClassName && Array.filter) {
1188         if (strTagName == '*') {
1189             return oElm.getElementsByClassName(name);
1190         } else {
1191             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1192                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1193             });
1194         }
1195     }
1196     // native implementation unavailable, fall back to slow method
1197     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1198     var arrReturnElements = new Array();
1199     var arrRegExpClassNames = new Array();
1200     var names = name.split(' ');
1201     for(var i=0; i<names.length; i++) {
1202         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1203     }
1204     var oElement;
1205     var bMatchesAll;
1206     for(var j=0; j<arrElements.length; j++) {
1207         oElement = arrElements[j];
1208         bMatchesAll = true;
1209         for(var k=0; k<arrRegExpClassNames.length; k++) {
1210             if(!arrRegExpClassNames[k].test(oElement.className)) {
1211                 bMatchesAll = false;
1212                 break;
1213             }
1214         }
1215         if(bMatchesAll) {
1216             arrReturnElements.push(oElement);
1217         }
1218     }
1219     return (arrReturnElements)
1222 function openpopup(event, args) {
1224     if (event) {
1225         if (event.preventDefault) {
1226             event.preventDefault();
1227         } else {
1228             event.returnValue = false;
1229         }
1230     }
1232     // Make sure the name argument is set and valid.
1233     var nameregex = /[^a-z0-9_]/i;
1234     if (typeof args.name !== 'string') {
1235         args.name = '_blank';
1236     } else if (args.name.match(nameregex)) {
1237         // Cleans window name because IE does not support funky ones.
1238         args.name = args.name.replace(nameregex, '_');
1239         if (M.cfg.developerdebug) {
1240             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup()');
1241         }
1242     }
1244     var fullurl = args.url;
1245     if (!args.url.match(/https?:\/\//)) {
1246         fullurl = M.cfg.wwwroot + args.url;
1247     }
1248     if (args.fullscreen) {
1249         args.options = args.options.
1250                 replace(/top=\d+/, 'top=0').
1251                 replace(/left=\d+/, 'left=0').
1252                 replace(/width=\d+/, 'width=' + screen.availWidth).
1253                 replace(/height=\d+/, 'height=' + screen.availHeight);
1254     }
1255     var windowobj = window.open(fullurl,args.name,args.options);
1256     if (!windowobj) {
1257         return true;
1258     }
1260     if (args.fullscreen) {
1261         // In some browser / OS combinations (E.g. Chrome on Windows), the
1262         // window initially opens slighly too big. The width and heigh options
1263         // seem to control the area inside the browser window, so what with
1264         // scroll-bars, etc. the actual window is bigger than the screen.
1265         // Therefore, we need to fix things up after the window is open.
1266         var hackcount = 100;
1267         var get_size_exactly_right = function() {
1268             windowobj.moveTo(0, 0);
1269             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1271             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1272             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1273             // about 50ms) after the window is open, then it actually behaves
1274             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1275             // check that the resize actually worked, and if not, repeatedly try
1276             // again after a short delay until it works (but with a limit of
1277             // hackcount repeats.
1278             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1279                 hackcount -= 1;
1280                 setTimeout(get_size_exactly_right, 10);
1281             }
1282         }
1283         setTimeout(get_size_exactly_right, 0);
1284     }
1285     windowobj.focus();
1287     return false;
1290 /** Close the current browser window. */
1291 function close_window(e) {
1292     if (e.preventDefault) {
1293         e.preventDefault();
1294     } else {
1295         e.returnValue = false;
1296     }
1297     window.close();
1301  * Used in a couple of modules to hide navigation areas when using AJAX
1302  */
1304 function show_item(itemid) {
1305     var item = document.getElementById(itemid);
1306     if (item) {
1307         item.style.display = "";
1308     }
1311 function destroy_item(itemid) {
1312     var item = document.getElementById(itemid);
1313     if (item) {
1314         item.parentNode.removeChild(item);
1315     }
1318  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1319  * @param controlid the control id.
1320  */
1321 function focuscontrol(controlid) {
1322     var control = document.getElementById(controlid);
1323     if (control) {
1324         control.focus();
1325     }
1329  * Transfers keyboard focus to an HTML element based on the old style style of focus
1330  * This function should be removed as soon as it is no longer used
1331  */
1332 function old_onload_focus(formid, controlname) {
1333     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1334         document.forms[formid].elements[controlname].focus();
1335     }
1338 function build_querystring(obj) {
1339     return convert_object_to_string(obj, '&');
1342 function build_windowoptionsstring(obj) {
1343     return convert_object_to_string(obj, ',');
1346 function convert_object_to_string(obj, separator) {
1347     if (typeof obj !== 'object') {
1348         return null;
1349     }
1350     var list = [];
1351     for(var k in obj) {
1352         k = encodeURIComponent(k);
1353         var value = obj[k];
1354         if(obj[k] instanceof Array) {
1355             for(var i in value) {
1356                 list.push(k+'[]='+encodeURIComponent(value[i]));
1357             }
1358         } else {
1359             list.push(k+'='+encodeURIComponent(value));
1360         }
1361     }
1362     return list.join(separator);
1365 function stripHTML(str) {
1366     var re = /<\S[^><]*>/g;
1367     var ret = str.replace(re, "");
1368     return ret;
1371 Number.prototype.fixed=function(n){
1372     with(Math)
1373         return round(Number(this)*pow(10,n))/pow(10,n);
1375 function update_progress_bar (id, width, pt, msg, es){
1376     var percent = pt;
1377     var status = document.getElementById("status_"+id);
1378     var percent_indicator = document.getElementById("pt_"+id);
1379     var progress_bar = document.getElementById("progress_"+id);
1380     var time_es = document.getElementById("time_"+id);
1381     status.innerHTML = msg;
1382     percent_indicator.innerHTML = percent.fixed(2) + '%';
1383     if(percent == 100) {
1384         progress_bar.style.background = "green";
1385         time_es.style.display = "none";
1386     } else {
1387         progress_bar.style.background = "#FFCC66";
1388         if (es == '?'){
1389             time_es.innerHTML = "";
1390         }else {
1391             time_es.innerHTML = es.fixed(2)+" sec";
1392             time_es.style.display
1393                 = "block";
1394         }
1395     }
1396     progress_bar.style.width = width + "px";
1401 // ===== Deprecated core Javascript functions for Moodle ====
1402 //       DO NOT USE!!!!!!!
1403 // Do not put this stuff in separate file because it only adds extra load on servers!
1406  * Used in a couple of modules to hide navigation areas when using AJAX
1407  */
1408 function hide_item(itemid) {
1409     // use class='hiddenifjs' instead
1410     var item = document.getElementById(itemid);
1411     if (item) {
1412         item.style.display = "none";
1413     }
1416 M.util.help_icon = {
1417     Y : null,
1418     instance : null,
1419     add : function(Y, properties) {
1420         this.Y = Y;
1421         properties.node = Y.one('#'+properties.id);
1422         if (properties.node) {
1423             properties.node.on('click', this.display, this, properties);
1424         }
1425     },
1426     display : function(event, args) {
1427         event.preventDefault();
1428         if (M.util.help_icon.instance === null) {
1429             var Y = M.util.help_icon.Y;
1430             Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1431                 var help_content_overlay = {
1432                     helplink : null,
1433                     overlay : null,
1434                     init : function() {
1436                         var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img  src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1437                         // Create an overlay from markup
1438                         this.overlay = new Y.Overlay({
1439                             headerContent: closebtn,
1440                             bodyContent: '',
1441                             id: 'helppopupbox',
1442                             width:'400px',
1443                             visible : false,
1444                             constrain : true
1445                         });
1446                         this.overlay.render(Y.one(document.body));
1448                         closebtn.on('click', this.overlay.hide, this.overlay);
1450                         var boundingBox = this.overlay.get("boundingBox");
1452                         //  Hide the menu if the user clicks outside of its content
1453                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1454                             var oTarget = event.target;
1455                             var menuButton = Y.one("#"+args.id);
1457                             if (!oTarget.compareTo(menuButton) &&
1458                                 !menuButton.contains(oTarget) &&
1459                                 !oTarget.compareTo(boundingBox) &&
1460                                 !boundingBox.contains(oTarget)) {
1461                                 this.overlay.hide();
1462                             }
1463                         }, this);
1465                         Y.on("key", this.close, closebtn , "down:13", this);
1466                         closebtn.on('click', this.close, this);
1467                     },
1469                     close : function(e) {
1470                         e.preventDefault();
1471                         this.helplink.focus();
1472                         this.overlay.hide();
1473                     },
1475                     display : function(event, args) {
1476                         this.helplink = args.node;
1477                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1478                         this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1480                         var fullurl = args.url;
1481                         if (!args.url.match(/https?:\/\//)) {
1482                             fullurl = M.cfg.wwwroot + args.url;
1483                         }
1485                         var ajaxurl = fullurl + '&ajax=1';
1487                         var cfg = {
1488                             method: 'get',
1489                             context : this,
1490                             on: {
1491                                 success: function(id, o, node) {
1492                                     this.display_callback(o.responseText);
1493                                 },
1494                                 failure: function(id, o, node) {
1495                                     var debuginfo = o.statusText;
1496                                     if (M.cfg.developerdebug) {
1497                                         o.statusText += ' (' + ajaxurl + ')';
1498                                     }
1499                                     this.display_callback('bodyContent',debuginfo);
1500                                 }
1501                             }
1502                         };
1504                         Y.io(ajaxurl, cfg);
1505                         this.overlay.show();
1507                         Y.one('#closehelpbox').focus();
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     }
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     }
1559  * Used to store form manipulation methods and enhancments
1560  */
1561 M.form = M.form || {};
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.
1567  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
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     });
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 = [];
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});
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});
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.14.swf', width: video.width, height: video.height};
1822                 } else {
1823                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.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.14.swf', {
1924                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.10.swf'}},
1925                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1926                 });
1927             }
1928         }
1930         if (M.cfg.jsrev == -1) {
1931             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.11.js';
1932         } else {
1933             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.11.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     }