Moodle release 2.6.4
[moodle.git] / lib / javascript-static.js
blob5d023cd0f7a58d9882a70dc36b2686620165e05e
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  * @var pending_js - The keys are the list of all pending js actions.
760  * @type Object
761  */
762 M.util.pending_js = [];
763 M.util.complete_js = [];
766  * Register any long running javascript code with a unique identifier.
767  * Should be followed with a call to js_complete with a matching
768  * idenfitier when the code is complete. May also be called with no arguments
769  * to test if there is any js calls pending. This is relied on by behat so that
770  * it can wait for all pending updates before interacting with a page.
771  * @param String uniqid - optional, if provided,
772  *                        registers this identifier until js_complete is called.
773  * @return boolean - True if there is any pending js.
774  */
775 M.util.js_pending = function(uniqid) {
776     if (uniqid !== false) {
777         M.util.pending_js.push(uniqid);
778     }
780     return M.util.pending_js.length;
784  * Register listeners for Y.io start/end so we can wait for them in behat.
785  */
786 M.util.js_watch_io = function() {
787     YUI.add('moodle-core-io', function(Y) {
788         Y.on('io:start', function(id) {
789             M.util.js_pending('io:' + id);
790         });
791         Y.on('io:end', function(id) {
792             M.util.js_complete('io:' + id);
793         });
794     });
795     YUI.applyConfig({
796         modules: {
797             'moodle-core-io': {
798                 after: ['io-base']
799             },
800             'io-base': {
801                 requires: ['moodle-core-io']
802             }
803         }
804     });
808 // Start this asap.
809 M.util.js_pending('init');
810 M.util.js_watch_io();
813  * Unregister any long running javascript code by unique identifier.
814  * This function should form a matching pair with js_pending
816  * @param String uniqid - required, unregisters this identifier
817  * @return boolean - True if there is any pending js.
818  */
819 M.util.js_complete = function(uniqid) {
820     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
821     // the native function. Y.Array polyfills the native function if it does not exist.
822     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
823     if (index >= 0) {
824         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
825     }
827     return M.util.pending_js.length;
831  * Returns a string registered in advance for usage in JavaScript
833  * If you do not pass the third parameter, the function will just return
834  * the corresponding value from the M.str object. If the third parameter is
835  * provided, the function performs {$a} placeholder substitution in the
836  * same way as PHP get_string() in Moodle does.
838  * @param {String} identifier string identifier
839  * @param {String} component the component providing the string
840  * @param {Object|String} a optional variable to populate placeholder with
841  */
842 M.util.get_string = function(identifier, component, a) {
843     var stringvalue;
845     if (M.cfg.developerdebug) {
846         // creating new instance if YUI is not optimal but it seems to be better way then
847         // require the instance via the function API - note that it is used in rare cases
848         // for debugging only anyway
849         // To ensure we don't kill browser performance if hundreds of get_string requests
850         // are made we cache the instance we generate within the M.util namespace.
851         // We don't publicly define the variable so that it doesn't get abused.
852         if (typeof M.util.get_string_yui_instance === 'undefined') {
853             M.util.get_string_yui_instance = new YUI({ debug : true });
854         }
855         var Y = M.util.get_string_yui_instance;
856     }
858     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
859         stringvalue = '[[' + identifier + ',' + component + ']]';
860         if (M.cfg.developerdebug) {
861             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
862         }
863         return stringvalue;
864     }
866     stringvalue = M.str[component][identifier];
868     if (typeof a == 'undefined') {
869         // no placeholder substitution requested
870         return stringvalue;
871     }
873     if (typeof a == 'number' || typeof a == 'string') {
874         // replace all occurrences of {$a} with the placeholder value
875         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
876         return stringvalue;
877     }
879     if (typeof a == 'object') {
880         // replace {$a->key} placeholders
881         for (var key in a) {
882             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
883                 if (M.cfg.developerdebug) {
884                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
885                 }
886                 continue;
887             }
888             var search = '{$a->' + key + '}';
889             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
890             search = new RegExp(search, 'g');
891             stringvalue = stringvalue.replace(search, a[key]);
892         }
893         return stringvalue;
894     }
896     if (M.cfg.developerdebug) {
897         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
898     }
899     return stringvalue;
903  * Set focus on username or password field of the login form
904  */
905 M.util.focus_login_form = function(Y) {
906     var username = Y.one('#username');
907     var password = Y.one('#password');
909     if (username == null || password == null) {
910         // something is wrong here
911         return;
912     }
914     var curElement = document.activeElement
915     if (curElement == 'undefined') {
916         // legacy browser - skip refocus protection
917     } else if (curElement.tagName == 'INPUT') {
918         // user was probably faster to focus something, do not mess with focus
919         return;
920     }
922     if (username.get('value') == '') {
923         username.focus();
924     } else {
925         password.focus();
926     }
930  * Set focus on login error message
931  */
932 M.util.focus_login_error = function(Y) {
933     var errorlog = Y.one('#loginerrormessage');
935     if (errorlog) {
936         errorlog.focus();
937     }
940  * Adds lightbox hidden element that covers the whole node.
942  * @param {YUI} Y
943  * @param {Node} the node lightbox should be added to
944  * @retun {Node} created lightbox node
945  */
946 M.util.add_lightbox = function(Y, node) {
947     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
949     // Check if lightbox is already there
950     if (node.one('.lightbox')) {
951         return node.one('.lightbox');
952     }
954     node.setStyle('position', 'relative');
955     var waiticon = Y.Node.create('<img />')
956     .setAttrs({
957         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
958     })
959     .setStyles({
960         'position' : 'relative',
961         'top' : '50%'
962     });
964     var lightbox = Y.Node.create('<div></div>')
965     .setStyles({
966         'opacity' : '.75',
967         'position' : 'absolute',
968         'width' : '100%',
969         'height' : '100%',
970         'top' : 0,
971         'left' : 0,
972         'backgroundColor' : 'white',
973         'textAlign' : 'center'
974     })
975     .setAttribute('class', 'lightbox')
976     .hide();
978     lightbox.appendChild(waiticon);
979     node.append(lightbox);
980     return lightbox;
984  * Appends a hidden spinner element to the specified node.
986  * @param {YUI} Y
987  * @param {Node} the node the spinner should be added to
988  * @return {Node} created spinner node
989  */
990 M.util.add_spinner = function(Y, node) {
991     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
993     // Check if spinner is already there
994     if (node.one('.spinner')) {
995         return node.one('.spinner');
996     }
998     var spinner = Y.Node.create('<img />')
999         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
1000         .addClass('spinner')
1001         .addClass('iconsmall')
1002         .hide();
1004     node.append(spinner);
1005     return spinner;
1008 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
1010 function checkall() {
1011     var inputs = document.getElementsByTagName('input');
1012     for (var i = 0; i < inputs.length; i++) {
1013         if (inputs[i].type == 'checkbox') {
1014             if (inputs[i].disabled || inputs[i].readOnly) {
1015                 continue;
1016             }
1017             inputs[i].checked = true;
1018         }
1019     }
1022 function checknone() {
1023     var inputs = document.getElementsByTagName('input');
1024     for (var i = 0; i < inputs.length; i++) {
1025         if (inputs[i].type == 'checkbox') {
1026             if (inputs[i].disabled || inputs[i].readOnly) {
1027                 continue;
1028             }
1029             inputs[i].checked = false;
1030         }
1031     }
1035  * Either check, or uncheck, all checkboxes inside the element with id is
1036  * @param id the id of the container
1037  * @param checked the new state, either '' or 'checked'.
1038  */
1039 function select_all_in_element_with_id(id, checked) {
1040     var container = document.getElementById(id);
1041     if (!container) {
1042         return;
1043     }
1044     var inputs = container.getElementsByTagName('input');
1045     for (var i = 0; i < inputs.length; ++i) {
1046         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
1047             inputs[i].checked = checked;
1048         }
1049     }
1052 function select_all_in(elTagName, elClass, elId) {
1053     var inputs = document.getElementsByTagName('input');
1054     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
1055     for(var i = 0; i < inputs.length; ++i) {
1056         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
1057             inputs[i].checked = 'checked';
1058         }
1059     }
1062 function deselect_all_in(elTagName, elClass, elId) {
1063     var inputs = document.getElementsByTagName('INPUT');
1064     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
1065     for(var i = 0; i < inputs.length; ++i) {
1066         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
1067             inputs[i].checked = '';
1068         }
1069     }
1072 function confirm_if(expr, message) {
1073     if(!expr) {
1074         return true;
1075     }
1076     return confirm(message);
1081     findParentNode (start, elementName, elementClass, elementID)
1083     Travels up the DOM hierarchy to find a parent element with the
1084     specified tag name, class, and id. All conditions must be met,
1085     but any can be ommitted. Returns the BODY element if no match
1086     found.
1088 function findParentNode(el, elName, elClass, elId) {
1089     while (el.nodeName.toUpperCase() != 'BODY') {
1090         if ((!elName || el.nodeName.toUpperCase() == elName) &&
1091             (!elClass || el.className.indexOf(elClass) != -1) &&
1092             (!elId || el.id == elId)) {
1093             break;
1094         }
1095         el = el.parentNode;
1096     }
1097     return el;
1100     findChildNode (start, elementName, elementClass, elementID)
1102     Travels down the DOM hierarchy to find all child elements with the
1103     specified tag name, class, and id. All conditions must be met,
1104     but any can be ommitted.
1105     Doesn't examine children of matches.
1107 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
1108     var children = new Array();
1109     for (var i = 0; i < start.childNodes.length; i++) {
1110         var classfound = false;
1111         var child = start.childNodes[i];
1112         if((child.nodeType == 1) &&//element node type
1113                   (elementClass && (typeof(child.className)=='string'))) {
1114             var childClasses = child.className.split(/\s+/);
1115             for (var childClassIndex in childClasses) {
1116                 if (childClasses[childClassIndex]==elementClass) {
1117                     classfound = true;
1118                     break;
1119                 }
1120             }
1121         }
1122         if(child.nodeType == 1) { //element node type
1123             if  ( (!tagName || child.nodeName == tagName) &&
1124                 (!elementClass || classfound)&&
1125                 (!elementID || child.id == elementID) &&
1126                 (!elementName || child.name == elementName))
1127             {
1128                 children = children.concat(child);
1129             } else {
1130                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1131             }
1132         }
1133     }
1134     return children;
1137 function unmaskPassword(id) {
1138     var pw = document.getElementById(id);
1139     var chb = document.getElementById(id+'unmask');
1141     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1142     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1143     // functionality won't work in IE8 or lower.
1144     // This is a temporary fixed to allow other browsers to function properly.
1145     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1146         if (chb.checked) {
1147             pw.type = "text";
1148         } else {
1149             pw.type = "password";
1150         }
1151     } else {  //IE Browser version 8 or lower
1152         try {
1153             // first try IE way - it can not set name attribute later
1154             if (chb.checked) {
1155               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1156             } else {
1157               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1158             }
1159             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1160         } catch (e) {
1161             var newpw = document.createElement('input');
1162             newpw.setAttribute('autocomplete', 'off');
1163             newpw.setAttribute('name', pw.name);
1164             if (chb.checked) {
1165               newpw.setAttribute('type', 'text');
1166             } else {
1167               newpw.setAttribute('type', 'password');
1168             }
1169             newpw.setAttribute('class', pw.getAttribute('class'));
1170         }
1171         newpw.id = pw.id;
1172         newpw.size = pw.size;
1173         newpw.onblur = pw.onblur;
1174         newpw.onchange = pw.onchange;
1175         newpw.value = pw.value;
1176         pw.parentNode.replaceChild(newpw, pw);
1177     }
1180 function filterByParent(elCollection, parentFinder) {
1181     var filteredCollection = [];
1182     for (var i = 0; i < elCollection.length; ++i) {
1183         var findParent = parentFinder(elCollection[i]);
1184         if (findParent.nodeName.toUpperCase() != 'BODY') {
1185             filteredCollection.push(elCollection[i]);
1186         }
1187     }
1188     return filteredCollection;
1192     All this is here just so that IE gets to handle oversized blocks
1193     in a visually pleasing manner. It does a browser detect. So sue me.
1196 function fix_column_widths() {
1197     var agt = navigator.userAgent.toLowerCase();
1198     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1199         fix_column_width('left-column');
1200         fix_column_width('right-column');
1201     }
1204 function fix_column_width(colName) {
1205     if(column = document.getElementById(colName)) {
1206         if(!column.offsetWidth) {
1207             setTimeout("fix_column_width('" + colName + "')", 20);
1208             return;
1209         }
1211         var width = 0;
1212         var nodes = column.childNodes;
1214         for(i = 0; i < nodes.length; ++i) {
1215             if(nodes[i].className.indexOf("block") != -1 ) {
1216                 if(width < nodes[i].offsetWidth) {
1217                     width = nodes[i].offsetWidth;
1218                 }
1219             }
1220         }
1222         for(i = 0; i < nodes.length; ++i) {
1223             if(nodes[i].className.indexOf("block") != -1 ) {
1224                 nodes[i].style.width = width + 'px';
1225             }
1226         }
1227     }
1232    Insert myValue at current cursor position
1233  */
1234 function insertAtCursor(myField, myValue) {
1235     // IE support
1236     if (document.selection) {
1237         myField.focus();
1238         sel = document.selection.createRange();
1239         sel.text = myValue;
1240     }
1241     // Mozilla/Netscape support
1242     else if (myField.selectionStart || myField.selectionStart == '0') {
1243         var startPos = myField.selectionStart;
1244         var endPos = myField.selectionEnd;
1245         myField.value = myField.value.substring(0, startPos)
1246             + myValue + myField.value.substring(endPos, myField.value.length);
1247     } else {
1248         myField.value += myValue;
1249     }
1254         Call instead of setting window.onload directly or setting body onload=.
1255         Adds your function to a chain of functions rather than overwriting anything
1256         that exists.
1258 function addonload(fn) {
1259     var oldhandler=window.onload;
1260     window.onload=function() {
1261         if(oldhandler) oldhandler();
1262             fn();
1263     }
1266  * Replacement for getElementsByClassName in browsers that aren't cool enough
1268  * Relying on the built-in getElementsByClassName is far, far faster than
1269  * using YUI.
1271  * Note: the third argument used to be an object with odd behaviour. It now
1272  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1273  * mimicked if you pass an object.
1275  * @param {Node} oElm The top-level node for searching. To search a whole
1276  *                    document, use `document`.
1277  * @param {String} strTagName filter by tag names
1278  * @param {String} name same as HTML5 spec
1279  */
1280 function getElementsByClassName(oElm, strTagName, name) {
1281     // for backwards compatibility
1282     if(typeof name == "object") {
1283         var names = new Array();
1284         for(var i=0; i<name.length; i++) names.push(names[i]);
1285         name = names.join('');
1286     }
1287     // use native implementation if possible
1288     if (oElm.getElementsByClassName && Array.filter) {
1289         if (strTagName == '*') {
1290             return oElm.getElementsByClassName(name);
1291         } else {
1292             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1293                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1294             });
1295         }
1296     }
1297     // native implementation unavailable, fall back to slow method
1298     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1299     var arrReturnElements = new Array();
1300     var arrRegExpClassNames = new Array();
1301     var names = name.split(' ');
1302     for(var i=0; i<names.length; i++) {
1303         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1304     }
1305     var oElement;
1306     var bMatchesAll;
1307     for(var j=0; j<arrElements.length; j++) {
1308         oElement = arrElements[j];
1309         bMatchesAll = true;
1310         for(var k=0; k<arrRegExpClassNames.length; k++) {
1311             if(!arrRegExpClassNames[k].test(oElement.className)) {
1312                 bMatchesAll = false;
1313                 break;
1314             }
1315         }
1316         if(bMatchesAll) {
1317             arrReturnElements.push(oElement);
1318         }
1319     }
1320     return (arrReturnElements)
1324  * Increment a file name.
1326  * @param string file name.
1327  * @param boolean ignoreextension do not extract the extension prior to appending the
1328  *                                suffix. Useful when incrementing folder names.
1329  * @return string the incremented file name.
1330  */
1331 function increment_filename(filename, ignoreextension) {
1332     var extension = '';
1333     var basename = filename;
1335     // Split the file name into the basename + extension.
1336     if (!ignoreextension) {
1337         var dotpos = filename.lastIndexOf('.');
1338         if (dotpos !== -1) {
1339             basename = filename.substr(0, dotpos);
1340             extension = filename.substr(dotpos, filename.length);
1341         }
1342     }
1344     // Look to see if the name already has (NN) at the end of it.
1345     var number = 0;
1346     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1347     if (hasnumber !== null) {
1348         // Note the current number & remove it from the basename.
1349         number = parseInt(hasnumber[2], 10);
1350         basename = hasnumber[1];
1351     }
1353     number++;
1354     var newname = basename + ' (' + number + ')' + extension;
1355     return newname;
1359  * Return whether we are in right to left mode or not.
1361  * @return boolean
1362  */
1363 function right_to_left() {
1364     var body = Y.one('body');
1365     var rtl = false;
1366     if (body && body.hasClass('dir-rtl')) {
1367         rtl = true;
1368     }
1369     return rtl;
1372 function openpopup(event, args) {
1374     if (event) {
1375         if (event.preventDefault) {
1376             event.preventDefault();
1377         } else {
1378             event.returnValue = false;
1379         }
1380     }
1382     // Make sure the name argument is set and valid.
1383     var nameregex = /[^a-z0-9_]/i;
1384     if (typeof args.name !== 'string') {
1385         args.name = '_blank';
1386     } else if (args.name.match(nameregex)) {
1387         // Cleans window name because IE does not support funky ones.
1388         if (M.cfg.developerdebug) {
1389             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1390         }
1391         args.name = args.name.replace(nameregex, '_');
1392     }
1394     var fullurl = args.url;
1395     if (!args.url.match(/https?:\/\//)) {
1396         fullurl = M.cfg.wwwroot + args.url;
1397     }
1398     if (args.fullscreen) {
1399         args.options = args.options.
1400                 replace(/top=\d+/, 'top=0').
1401                 replace(/left=\d+/, 'left=0').
1402                 replace(/width=\d+/, 'width=' + screen.availWidth).
1403                 replace(/height=\d+/, 'height=' + screen.availHeight);
1404     }
1405     var windowobj = window.open(fullurl,args.name,args.options);
1406     if (!windowobj) {
1407         return true;
1408     }
1410     if (args.fullscreen) {
1411         // In some browser / OS combinations (E.g. Chrome on Windows), the
1412         // window initially opens slighly too big. The width and heigh options
1413         // seem to control the area inside the browser window, so what with
1414         // scroll-bars, etc. the actual window is bigger than the screen.
1415         // Therefore, we need to fix things up after the window is open.
1416         var hackcount = 100;
1417         var get_size_exactly_right = function() {
1418             windowobj.moveTo(0, 0);
1419             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1421             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1422             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1423             // about 50ms) after the window is open, then it actually behaves
1424             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1425             // check that the resize actually worked, and if not, repeatedly try
1426             // again after a short delay until it works (but with a limit of
1427             // hackcount repeats.
1428             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1429                 hackcount -= 1;
1430                 setTimeout(get_size_exactly_right, 10);
1431             }
1432         }
1433         setTimeout(get_size_exactly_right, 0);
1434     }
1435     windowobj.focus();
1437     return false;
1440 /** Close the current browser window. */
1441 function close_window(e) {
1442     if (e.preventDefault) {
1443         e.preventDefault();
1444     } else {
1445         e.returnValue = false;
1446     }
1447     window.close();
1451  * Used in a couple of modules to hide navigation areas when using AJAX
1452  */
1454 function show_item(itemid) {
1455     var item = document.getElementById(itemid);
1456     if (item) {
1457         item.style.display = "";
1458     }
1461 function destroy_item(itemid) {
1462     var item = document.getElementById(itemid);
1463     if (item) {
1464         item.parentNode.removeChild(item);
1465     }
1468  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1469  * @param controlid the control id.
1470  */
1471 function focuscontrol(controlid) {
1472     var control = document.getElementById(controlid);
1473     if (control) {
1474         control.focus();
1475     }
1479  * Transfers keyboard focus to an HTML element based on the old style style of focus
1480  * This function should be removed as soon as it is no longer used
1481  */
1482 function old_onload_focus(formid, controlname) {
1483     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1484         document.forms[formid].elements[controlname].focus();
1485     }
1488 function build_querystring(obj) {
1489     return convert_object_to_string(obj, '&');
1492 function build_windowoptionsstring(obj) {
1493     return convert_object_to_string(obj, ',');
1496 function convert_object_to_string(obj, separator) {
1497     if (typeof obj !== 'object') {
1498         return null;
1499     }
1500     var list = [];
1501     for(var k in obj) {
1502         k = encodeURIComponent(k);
1503         var value = obj[k];
1504         if(obj[k] instanceof Array) {
1505             for(var i in value) {
1506                 list.push(k+'[]='+encodeURIComponent(value[i]));
1507             }
1508         } else {
1509             list.push(k+'='+encodeURIComponent(value));
1510         }
1511     }
1512     return list.join(separator);
1515 function stripHTML(str) {
1516     var re = /<\S[^><]*>/g;
1517     var ret = str.replace(re, "");
1518     return ret;
1521 Number.prototype.fixed=function(n){
1522     with(Math)
1523         return round(Number(this)*pow(10,n))/pow(10,n);
1525 function update_progress_bar (id, width, pt, msg, es){
1526     var percent = pt;
1527     var status = document.getElementById("status_"+id);
1528     var percent_indicator = document.getElementById("pt_"+id);
1529     var progress_bar = document.getElementById("progress_"+id);
1530     var time_es = document.getElementById("time_"+id);
1531     status.innerHTML = msg;
1532     percent_indicator.innerHTML = percent.fixed(2) + '%';
1533     if(percent == 100) {
1534         progress_bar.style.background = "green";
1535         time_es.style.display = "none";
1536     } else {
1537         progress_bar.style.background = "#FFCC66";
1538         if (es == '?'){
1539             time_es.innerHTML = "";
1540         }else {
1541             time_es.innerHTML = es.fixed(2)+" sec";
1542             time_es.style.display
1543                 = "block";
1544         }
1545     }
1546     progress_bar.style.width = width + "px";
1551 // ===== Deprecated core Javascript functions for Moodle ====
1552 //       DO NOT USE!!!!!!!
1553 // Do not put this stuff in separate file because it only adds extra load on servers!
1556  * Used in a couple of modules to hide navigation areas when using AJAX
1557  */
1558 function hide_item(itemid) {
1559     // use class='hiddenifjs' instead
1560     var item = document.getElementById(itemid);
1561     if (item) {
1562         item.style.display = "none";
1563     }
1566 M.util.help_popups = {
1567     setup : function(Y) {
1568         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1569     },
1570     open_popup : function(e) {
1571         // Prevent the default page action
1572         e.preventDefault();
1574         // Grab the anchor that was clicked
1575         var anchor = e.target.ancestor('a', true);
1576         var args = {
1577             'name'          : 'popup',
1578             'url'           : anchor.getAttribute('href'),
1579             'options'       : ''
1580         };
1581         var options = [
1582             'height=600',
1583             'width=800',
1584             'top=0',
1585             'left=0',
1586             'menubar=0',
1587             'location=0',
1588             'scrollbars',
1589             'resizable',
1590             'toolbar',
1591             'status',
1592             'directories=0',
1593             'fullscreen=0',
1594             'dependent'
1595         ]
1596         args.options = options.join(',');
1598         openpopup(e, args);
1599     }
1603  * This code bas been deprecated and will be removed from Moodle 2.7
1605  * Please see lib/yui/popuphelp/popuphelp.js for its replacement
1606  */
1607 M.util.help_icon = {
1608     initialised : false,
1609     setup : function(Y, properties) {
1610         this.add(Y, properties);
1611     },
1612     add : function(Y) {
1613         if (M.cfg.developerdebug) {
1614             Y.log("You are using a deprecated function call (M.util.help_icon.add). " +
1615                     "Please look at rewriting your call to support lib/yui/popuphelp/popuphelp.js");
1616         }
1617         if (!this.initialised) {
1618             YUI().use('moodle-core-popuphelp', function() {
1619                 M.core.init_popuphelp([]);
1620             });
1621         }
1622         this.initialised = true;
1623     }
1627  * Custom menu namespace
1628  */
1629 M.core_custom_menu = {
1630     /**
1631      * This method is used to initialise a custom menu given the id that belongs
1632      * to the custom menu's root node.
1633      *
1634      * @param {YUI} Y
1635      * @param {string} nodeid
1636      */
1637     init : function(Y, nodeid) {
1638         var node = Y.one('#'+nodeid);
1639         if (node) {
1640             Y.use('node-menunav', function(Y) {
1641                 // Get the node
1642                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1643                 node.removeClass('javascript-disabled');
1644                 // Initialise the menunav plugin
1645                 node.plug(Y.Plugin.NodeMenuNav);
1646             });
1647         }
1648     }
1652  * Used to store form manipulation methods and enhancments
1653  */
1654 M.form = M.form || {};
1657  * Converts a nbsp indented select box into a multi drop down custom control much
1658  * like the custom menu. It also selectable categories on or off.
1660  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1662  * @param {YUI} Y
1663  * @param {string} id
1664  * @param {Array} options
1665  */
1666 M.form.init_smartselect = function(Y, id, options) {
1667     if (!id.match(/^id_/)) {
1668         id = 'id_'+id;
1669     }
1670     var select = Y.one('select#'+id);
1671     if (!select) {
1672         return false;
1673     }
1674     Y.use('event-delegate',function(){
1675         var smartselect = {
1676             id : id,
1677             structure : [],
1678             options : [],
1679             submenucount : 0,
1680             currentvalue : null,
1681             currenttext : null,
1682             shownevent : null,
1683             cfg : {
1684                 selectablecategories : true,
1685                 mode : null
1686             },
1687             nodes : {
1688                 select : null,
1689                 loading : null,
1690                 menu : null
1691             },
1692             init : function(Y, id, args, nodes) {
1693                 if (typeof(args)=='object') {
1694                     for (var i in this.cfg) {
1695                         if (args[i] || args[i]===false) {
1696                             this.cfg[i] = args[i];
1697                         }
1698                     }
1699                 }
1701                 // Display a loading message first up
1702                 this.nodes.select = nodes.select;
1704                 this.currentvalue = this.nodes.select.get('selectedIndex');
1705                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1707                 var options = Array();
1708                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1709                 this.nodes.select.all('option').each(function(option, index) {
1710                     var rawtext = option.get('innerHTML');
1711                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1712                     if (rawtext === text) {
1713                         text = rawtext.replace(/^(\s)*/, '');
1714                         var depth = (rawtext.length - text.length ) + 1;
1715                     } else {
1716                         var depth = ((rawtext.length - text.length )/12)+1;
1717                     }
1718                     option.set('innerHTML', text);
1719                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1720                 }, this);
1722                 this.structure = [];
1723                 var structcount = 0;
1724                 for (var i in options) {
1725                     var o = options[i];
1726                     if (o.depth == 0) {
1727                         this.structure.push(o);
1728                         structcount++;
1729                     } else {
1730                         var d = o.depth;
1731                         var current = this.structure[structcount-1];
1732                         for (var j = 0; j < o.depth-1;j++) {
1733                             if (current && current.children) {
1734                                 current = current.children[current.children.length-1];
1735                             }
1736                         }
1737                         if (current && current.children) {
1738                             current.children.push(o);
1739                         }
1740                     }
1741                 }
1743                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1744                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1745                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1746                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1748                 if (this.cfg.mode == null) {
1749                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1750                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1751                         this.cfg.mode = 'compact';
1752                     } else {
1753                         this.cfg.mode = 'spanning';
1754                     }
1755                 }
1757                 if (this.cfg.mode == 'compact') {
1758                     this.nodes.menu.addClass('compactmenu');
1759                 } else {
1760                     this.nodes.menu.addClass('spanningmenu');
1761                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1762                 }
1764                 Y.one(document.body).append(this.nodes.menu);
1765                 var pos = this.nodes.select.getXY();
1766                 pos[0] += 1;
1767                 this.nodes.menu.setXY(pos);
1768                 this.nodes.menu.on('click', this.handle_click, this);
1770                 Y.one(window).on('resize', function(){
1771                      var pos = this.nodes.select.getXY();
1772                     pos[0] += 1;
1773                     this.nodes.menu.setXY(pos);
1774                  }, this);
1775             },
1776             generate_menu_content : function() {
1777                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1778                 content += this.generate_submenu_content(this.structure[0], true);
1779                 content += '</ul></div>';
1780                 return content;
1781             },
1782             generate_submenu_content : function(item, rootelement) {
1783                 this.submenucount++;
1784                 var content = '';
1785                 if (item.children.length > 0) {
1786                     if (rootelement) {
1787                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1788                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1789                         content += '<div class="smartselect_menu_content">';
1790                     } else {
1791                         content += '<li class="smartselect_submenuitem">';
1792                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1793                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1794                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1795                         content += '<div class="smartselect_submenu_content">';
1796                     }
1797                     content += '<ul>';
1798                     for (var i in item.children) {
1799                         content += this.generate_submenu_content(item.children[i],false);
1800                     }
1801                     content += '</ul>';
1802                     content += '</div>';
1803                     content += '</div>';
1804                     if (rootelement) {
1805                     } else {
1806                         content += '</li>';
1807                     }
1808                 } else {
1809                     content += '<li class="smartselect_menuitem">';
1810                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1811                     content += '</li>';
1812                 }
1813                 return content;
1814             },
1815             select : function(e) {
1816                 var t = e.target;
1817                 e.halt();
1818                 this.currenttext = t.get('innerHTML');
1819                 this.currentvalue = t.getAttribute('value');
1820                 this.nodes.select.set('selectedIndex', this.currentvalue);
1821                 this.hide_menu();
1822             },
1823             handle_click : function(e) {
1824                 var target = e.target;
1825                 if (target.hasClass('smartselect_mask')) {
1826                     this.show_menu(e);
1827                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1828                     this.select(e);
1829                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1830                     this.show_sub_menu(e);
1831                 }
1832             },
1833             show_menu : function(e) {
1834                 e.halt();
1835                 var menu = e.target.ancestor().one('.smartselect_menu');
1836                 menu.addClass('visible');
1837                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1838             },
1839             show_sub_menu : function(e) {
1840                 e.halt();
1841                 var target = e.target;
1842                 if (!target.hasClass('smartselect_submenuitem')) {
1843                     target = target.ancestor('.smartselect_submenuitem');
1844                 }
1845                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1846                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1847                     return;
1848                 }
1849                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1850                 target.one('.smartselect_submenu').addClass('visible');
1851             },
1852             hide_menu : function() {
1853                 this.nodes.menu.all('.visible').removeClass('visible');
1854                 if (this.shownevent) {
1855                     this.shownevent.detach();
1856                 }
1857             }
1858         };
1859         smartselect.init(Y, id, options, {select:select});
1860     });
1863 /** List of flv players to be loaded */
1864 M.util.video_players = [];
1865 /** List of mp3 players to be loaded */
1866 M.util.audio_players = [];
1869  * Add video player
1870  * @param id element id
1871  * @param fileurl media url
1872  * @param width
1873  * @param height
1874  * @param autosize true means detect size from media
1875  */
1876 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1877     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1881  * Add audio player.
1882  * @param id
1883  * @param fileurl
1884  * @param small
1885  */
1886 M.util.add_audio_player = function (id, fileurl, small) {
1887     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1891  * Initialise all audio and video player, must be called from page footer.
1892  */
1893 M.util.load_flowplayer = function() {
1894     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1895         return;
1896     }
1897     if (typeof(flowplayer) == 'undefined') {
1898         var loaded = false;
1900         var embed_function = function() {
1901             if (loaded || typeof(flowplayer) == 'undefined') {
1902                 return;
1903             }
1904             loaded = true;
1906             var controls = {
1907                     autoHide: true
1908             }
1909             /* TODO: add CSS color overrides for the flv flow player */
1911             for(var i=0; i<M.util.video_players.length; i++) {
1912                 var video = M.util.video_players[i];
1913                 if (video.width > 0 && video.height > 0) {
1914                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', width: video.width, height: video.height};
1915                 } else {
1916                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf';
1917                 }
1918                 flowplayer(video.id, src, {
1919                     plugins: {controls: controls},
1920                     clip: {
1921                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1922                         onMetaData: function(clip) {
1923                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1924                                 clip.mvideo.resized = true;
1925                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1926                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1927                                     // bad luck, we have to guess - we may not get metadata at all
1928                                     var width = clip.width;
1929                                     var height = clip.height;
1930                                 } else {
1931                                     var width = clip.metaData.width;
1932                                     var height = clip.metaData.height;
1933                                 }
1934                                 var minwidth = 300; // controls are messed up in smaller objects
1935                                 if (width < minwidth) {
1936                                     height = (height * minwidth) / width;
1937                                     width = minwidth;
1938                                 }
1940                                 var object = this._api();
1941                                 object.width = width;
1942                                 object.height = height;
1943                             }
1944                         }
1945                     }
1946                 });
1947             }
1948             if (M.util.audio_players.length == 0) {
1949                 return;
1950             }
1951             var controls = {
1952                     autoHide: false,
1953                     fullscreen: false,
1954                     next: false,
1955                     previous: false,
1956                     scrubber: true,
1957                     play: true,
1958                     pause: true,
1959                     volume: true,
1960                     mute: false,
1961                     backgroundGradient: [0.5,0,0.3]
1962                 };
1964             var rule;
1965             for (var j=0; j < document.styleSheets.length; j++) {
1967                 // To avoid javascript security violation accessing cross domain stylesheets
1968                 var allrules = false;
1969                 try {
1970                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1971                         allrules = document.styleSheets[j].rules;
1972                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1973                         allrules = document.styleSheets[j].cssRules;
1974                     } else {
1975                         // why??
1976                         continue;
1977                     }
1978                 } catch (e) {
1979                     continue;
1980                 }
1982                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1983                 if (!allrules) {
1984                     continue;
1985                 }
1987                 for(var i=0; i<allrules.length; i++) {
1988                     rule = '';
1989                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1990                         if (typeof(allrules[i].cssText) != 'undefined') {
1991                             rule = allrules[i].cssText;
1992                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1993                             rule = allrules[i].style.cssText;
1994                         }
1995                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1996                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1997                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1998                             controls[colprop] = rule;
1999                         }
2000                     }
2001                 }
2002                 allrules = false;
2003             }
2005             for(i=0; i<M.util.audio_players.length; i++) {
2006                 var audio = M.util.audio_players[i];
2007                 if (audio.small) {
2008                     controls.controlall = false;
2009                     controls.height = 15;
2010                     controls.time = false;
2011                 } else {
2012                     controls.controlall = true;
2013                     controls.height = 25;
2014                     controls.time = true;
2015                 }
2016                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', {
2017                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf'}},
2018                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
2019                 });
2020             }
2021         }
2023         if (M.cfg.jsrev == -1) {
2024             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
2025         } else {
2026             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
2027         }
2028         var fileref = document.createElement('script');
2029         fileref.setAttribute('type','text/javascript');
2030         fileref.setAttribute('src', jsurl);
2031         fileref.onload = embed_function;
2032         fileref.onreadystatechange = embed_function;
2033         document.getElementsByTagName('head')[0].appendChild(fileref);
2034     }