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