Merge branch 'MDL-44620_master' of git://github.com/dmonllao/moodle into MOODLE_27_STABLE
[moodle.git] / lib / javascript-static.js
blob29a7d9567ce8fb95202cbf5c08b6956e6a5525fc
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().
227  * @method show_confirm_dialog
228  * @param {EventFacade} e
229  * @param {Object} args
230  * @param {String} args.message The question to ask the user
231  * @param {Function} [args.callback] A callback to apply on confirmation.
232  * @param {Object} [args.scope] The scope to use when calling the callback.
233  * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
234  * @param {String} [args.cancellabel] The label to use on the cancel button.
235  * @param {String} [args.continuelabel] The label to use on the continue button.
236  */
237 M.util.show_confirm_dialog = function(e, args) {
238     var target = e.target;
239     if (e.preventDefault) {
240         e.preventDefault();
241     }
243     YUI().use('moodle-core-notification-confirm', function(Y) {
244         var confirmationDialogue = new M.core.confirm({
245             width: '300px',
246             center: true,
247             modal: true,
248             visible: false,
249             draggable: false,
250             title: M.util.get_string('confirmation', 'admin'),
251             noLabel: M.util.get_string('cancel', 'moodle'),
252             question: args.message
253         });
255         // The dialogue was submitted with a positive value indication.
256         confirmationDialogue.on('complete-yes', function(e) {
257             // Handle any callbacks.
258             if (args.callback) {
259                 if (!Y.Lang.isFunction(args.callback)) {
260                     Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
261                             'warn', 'M.util.show_confirm_dialog');
262                     return;
263                 }
265                 var scope = e.target;
266                 if (Y.Lang.isObject(args.scope)) {
267                     scope = args.scope;
268                 }
270                 var callbackargs = args.callbackargs || [];
271                 args.callback.apply(scope, callbackargs);
272                 return;
273             }
275             var targetancestor = null,
276                 targetform = null;
278             if (target.test('a')) {
279                 window.location = target.get('href');
281             } else if ((targetancestor = target.ancestor('a')) !== null) {
282                 window.location = targetancestor.get('href');
284             } else if (target.test('input')) {
285                 targetform = target.ancestor('form', true);
286                 if (!targetform) {
287                     return;
288                 }
289                 if (target.get('name') && target.get('value')) {
290                     targetform.append('<input type="hidden" name="' + target.get('name') +
291                                     '" value="' + target.get('value') + '">');
292                 }
293                 targetform.submit();
295             } else if (target.test('form')) {
296                 target.submit();
298             } else {
299                 Y.log("Element of type " + target.get('tagName') +
300                         " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM",
301                         'warn', 'javascript-static');
302             }
303         }, this);
305         if (args.cancellabel) {
306             confirmationDialogue.set('noLabel', args.cancellabel);
307         }
309         if (args.continuelabel) {
310             confirmationDialogue.set('yesLabel', args.continuelabel);
311         }
313         confirmationDialogue.render()
314                 .show();
315     });
318 /** Useful for full embedding of various stuff */
319 M.util.init_maximised_embed = function(Y, id) {
320     var obj = Y.one('#'+id);
321     if (!obj) {
322         return;
323     }
325     var get_htmlelement_size = function(el, prop) {
326         if (Y.Lang.isString(el)) {
327             el = Y.one('#' + el);
328         }
329         // Ensure element exists.
330         if (el) {
331             var val = el.getStyle(prop);
332             if (val == 'auto') {
333                 val = el.getComputedStyle(prop);
334             }
335             return parseInt(val);
336         } else {
337             return 0;
338         }
339     };
341     var resize_object = function() {
342         obj.setStyle('width', '0px');
343         obj.setStyle('height', '0px');
344         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
346         if (newwidth > 500) {
347             obj.setStyle('width', newwidth  + 'px');
348         } else {
349             obj.setStyle('width', '500px');
350         }
352         var headerheight = get_htmlelement_size('page-header', 'height');
353         var footerheight = get_htmlelement_size('page-footer', 'height');
354         var newheight = parseInt(Y.one('body').get('winHeight')) - footerheight - headerheight - 100;
355         if (newheight < 400) {
356             newheight = 400;
357         }
358         obj.setStyle('height', newheight+'px');
359     };
361     resize_object();
362     // fix layout if window resized too
363     window.onresize = function() {
364         resize_object();
365     };
369  * Breaks out all links to the top frame - used in frametop page layout.
370  */
371 M.util.init_frametop = function(Y) {
372     Y.all('a').each(function(node) {
373         node.set('target', '_top');
374     });
375     Y.all('form').each(function(node) {
376         node.set('target', '_top');
377     });
381  * Finds all nodes that match the given CSS selector and attaches events to them
382  * so that they toggle a given classname when clicked.
384  * @param {YUI} Y
385  * @param {string} id An id containing elements to target
386  * @param {string} cssselector A selector to use to find targets
387  * @param {string} toggleclassname A classname to toggle
388  */
389 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
391     if (togglecssselector == '') {
392         togglecssselector = cssselector;
393     }
395     var node = Y.one('#'+id);
396     node.all(cssselector).each(function(n){
397         n.on('click', function(e){
398             e.stopPropagation();
399             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
400                 if (this.test(togglecssselector)) {
401                     this.toggleClass(toggleclassname);
402                 } else {
403                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
404             }
405             }
406         }, n);
407     });
408     // Attach this click event to the node rather than all selectors... will be much better
409     // for performance
410     node.on('click', function(e){
411         if (e.target.hasClass('addtoall')) {
412             this.all(togglecssselector).addClass(toggleclassname);
413         } else if (e.target.hasClass('removefromall')) {
414             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
415         }
416     }, node);
420  * Initialises a colour picker
422  * Designed to be used with admin_setting_configcolourpicker although could be used
423  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
424  * above or below the input (must have the same parent) and then call this with the
425  * id.
427  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
428  * contrib/blocks. For better docs refer to that.
430  * @param {YUI} Y
431  * @param {int} id
432  * @param {object} previewconf
433  */
434 M.util.init_colour_picker = function(Y, id, previewconf) {
435     /**
436      * We need node and event-mouseenter
437      */
438     Y.use('node', 'event-mouseenter', function(){
439         /**
440          * The colour picker object
441          */
442         var colourpicker = {
443             box : null,
444             input : null,
445             image : null,
446             preview : null,
447             current : null,
448             eventClick : null,
449             eventMouseEnter : null,
450             eventMouseLeave : null,
451             eventMouseMove : null,
452             width : 300,
453             height :  100,
454             factor : 5,
455             /**
456              * Initalises the colour picker by putting everything together and wiring the events
457              */
458             init : function() {
459                 this.input = Y.one('#'+id);
460                 this.box = this.input.ancestor().one('.admin_colourpicker');
461                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
462                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
463                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
464                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
465                 this.current = Y.Node.create('<div class="currentcolour"></div>');
466                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
467                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
469                 if (typeof(previewconf) === 'object' && previewconf !== null) {
470                     Y.one('#'+id+'_preview').on('click', function(e){
471                         if (Y.Lang.isString(previewconf.selector)) {
472                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
473                         } else {
474                             for (var i in previewconf.selector) {
475                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
476                             }
477                         }
478                     }, this);
479                 }
481                 this.eventClick = this.image.on('click', this.pickColour, this);
482                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
483             },
484             /**
485              * Starts to follow the mouse once it enter the image
486              */
487             startFollow : function(e) {
488                 this.eventMouseEnter.detach();
489                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
490                 this.eventMouseMove = this.image.on('mousemove', function(e){
491                     this.preview.setStyle('backgroundColor', this.determineColour(e));
492                 }, this);
493             },
494             /**
495              * Stops following the mouse
496              */
497             endFollow : function(e) {
498                 this.eventMouseMove.detach();
499                 this.eventMouseLeave.detach();
500                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
501             },
502             /**
503              * Picks the colour the was clicked on
504              */
505             pickColour : function(e) {
506                 var colour = this.determineColour(e);
507                 this.input.set('value', colour);
508                 this.current.setStyle('backgroundColor', colour);
509             },
510             /**
511              * Calculates the colour fromthe given co-ordinates
512              */
513             determineColour : function(e) {
514                 var eventx = Math.floor(e.pageX-e.target.getX());
515                 var eventy = Math.floor(e.pageY-e.target.getY());
517                 var imagewidth = this.width;
518                 var imageheight = this.height;
519                 var factor = this.factor;
520                 var colour = [255,0,0];
522                 var matrices = [
523                     [  0,  1,  0],
524                     [ -1,  0,  0],
525                     [  0,  0,  1],
526                     [  0, -1,  0],
527                     [  1,  0,  0],
528                     [  0,  0, -1]
529                 ];
531                 var matrixcount = matrices.length;
532                 var limit = Math.round(imagewidth/matrixcount);
533                 var heightbreak = Math.round(imageheight/2);
535                 for (var x = 0; x < imagewidth; x++) {
536                     var divisor = Math.floor(x / limit);
537                     var matrix = matrices[divisor];
539                     colour[0] += matrix[0]*factor;
540                     colour[1] += matrix[1]*factor;
541                     colour[2] += matrix[2]*factor;
543                     if (eventx==x) {
544                         break;
545                     }
546                 }
548                 var pixel = [colour[0], colour[1], colour[2]];
549                 if (eventy < heightbreak) {
550                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
551                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
552                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
553                 } else if (eventy > heightbreak) {
554                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
555                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
556                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
557                 }
559                 return this.convert_rgb_to_hex(pixel);
560             },
561             /**
562              * Converts an RGB value to Hex
563              */
564             convert_rgb_to_hex : function(rgb) {
565                 var hex = '#';
566                 var hexchars = "0123456789ABCDEF";
567                 for (var i=0; i<3; i++) {
568                     var number = Math.abs(rgb[i]);
569                     if (number == 0 || isNaN(number)) {
570                         hex += '00';
571                     } else {
572                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
573                     }
574                 }
575                 return hex;
576             }
577         };
578         /**
579          * Initialise the colour picker :) Hoorah
580          */
581         colourpicker.init();
582     });
585 M.util.init_block_hider = function(Y, config) {
586     Y.use('base', 'node', function(Y) {
587         M.util.block_hider = M.util.block_hider || (function(){
588             var blockhider = function() {
589                 blockhider.superclass.constructor.apply(this, arguments);
590             };
591             blockhider.prototype = {
592                 initializer : function(config) {
593                     this.set('block', '#'+this.get('id'));
594                     var b = this.get('block'),
595                         t = b.one('.title'),
596                         a = null;
597                     if (t && (a = t.one('.block_action'))) {
598                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
599                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
600                         hide.on('keypress', this.updateStateKey, this, true);
601                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
602                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
603                         show.on('keypress', this.updateStateKey, this, false);
604                         a.insert(show, 0).insert(hide, 0);
605                     }
606                 },
607                 updateState : function(e, hide) {
608                     M.util.set_user_preference(this.get('preference'), hide);
609                     if (hide) {
610                         this.get('block').addClass('hidden');
611                     } else {
612                         this.get('block').removeClass('hidden');
613                     }
614                 },
615                 updateStateKey : function(e, hide) {
616                     if (e.keyCode == 13) { //allow hide/show via enter key
617                         this.updateState(this, hide);
618                     }
619                 }
620             };
621             Y.extend(blockhider, Y.Base, blockhider.prototype, {
622                 NAME : 'blockhider',
623                 ATTRS : {
624                     id : {},
625                     preference : {},
626                     iconVisible : {
627                         value : M.util.image_url('t/switch_minus', 'moodle')
628                     },
629                     iconHidden : {
630                         value : M.util.image_url('t/switch_plus', 'moodle')
631                     },
632                     block : {
633                         setter : function(node) {
634                             return Y.one(node);
635                         }
636                     }
637                 }
638             });
639             return blockhider;
640         })();
641         new M.util.block_hider(config);
642     });
646  * @var pending_js - The keys are the list of all pending js actions.
647  * @type Object
648  */
649 M.util.pending_js = [];
650 M.util.complete_js = [];
653  * Register any long running javascript code with a unique identifier.
654  * Should be followed with a call to js_complete with a matching
655  * idenfitier when the code is complete. May also be called with no arguments
656  * to test if there is any js calls pending. This is relied on by behat so that
657  * it can wait for all pending updates before interacting with a page.
658  * @param String uniqid - optional, if provided,
659  *                        registers this identifier until js_complete is called.
660  * @return boolean - True if there is any pending js.
661  */
662 M.util.js_pending = function(uniqid) {
663     if (uniqid !== false) {
664         M.util.pending_js.push(uniqid);
665     }
667     return M.util.pending_js.length;
670 // Start this asap.
671 M.util.js_pending('init');
674  * Register listeners for Y.io start/end so we can wait for them in behat.
675  */
676 YUI.add('moodle-core-io', function(Y) {
677     Y.on('io:start', function(id) {
678         M.util.js_pending('io:' + id);
679     });
680     Y.on('io:end', function(id) {
681         M.util.js_complete('io:' + id);
682     });
683 }, '@VERSION@', {
684     condition: {
685         trigger: 'io-base',
686         when: 'after'
687     }
691  * Unregister any long running javascript code by unique identifier.
692  * This function should form a matching pair with js_pending
694  * @param String uniqid - required, unregisters this identifier
695  * @return boolean - True if there is any pending js.
696  */
697 M.util.js_complete = function(uniqid) {
698     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
699     // the native function. Y.Array polyfills the native function if it does not exist.
700     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
701     if (index >= 0) {
702         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
703     }
705     return M.util.pending_js.length;
709  * Returns a string registered in advance for usage in JavaScript
711  * If you do not pass the third parameter, the function will just return
712  * the corresponding value from the M.str object. If the third parameter is
713  * provided, the function performs {$a} placeholder substitution in the
714  * same way as PHP get_string() in Moodle does.
716  * @param {String} identifier string identifier
717  * @param {String} component the component providing the string
718  * @param {Object|String} a optional variable to populate placeholder with
719  */
720 M.util.get_string = function(identifier, component, a) {
721     var stringvalue;
723     if (M.cfg.developerdebug) {
724         // creating new instance if YUI is not optimal but it seems to be better way then
725         // require the instance via the function API - note that it is used in rare cases
726         // for debugging only anyway
727         // To ensure we don't kill browser performance if hundreds of get_string requests
728         // are made we cache the instance we generate within the M.util namespace.
729         // We don't publicly define the variable so that it doesn't get abused.
730         if (typeof M.util.get_string_yui_instance === 'undefined') {
731             M.util.get_string_yui_instance = new YUI({ debug : true });
732         }
733         var Y = M.util.get_string_yui_instance;
734     }
736     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
737         stringvalue = '[[' + identifier + ',' + component + ']]';
738         if (M.cfg.developerdebug) {
739             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
740         }
741         return stringvalue;
742     }
744     stringvalue = M.str[component][identifier];
746     if (typeof a == 'undefined') {
747         // no placeholder substitution requested
748         return stringvalue;
749     }
751     if (typeof a == 'number' || typeof a == 'string') {
752         // replace all occurrences of {$a} with the placeholder value
753         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
754         return stringvalue;
755     }
757     if (typeof a == 'object') {
758         // replace {$a->key} placeholders
759         for (var key in a) {
760             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
761                 if (M.cfg.developerdebug) {
762                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
763                 }
764                 continue;
765             }
766             var search = '{$a->' + key + '}';
767             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
768             search = new RegExp(search, 'g');
769             stringvalue = stringvalue.replace(search, a[key]);
770         }
771         return stringvalue;
772     }
774     if (M.cfg.developerdebug) {
775         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
776     }
777     return stringvalue;
781  * Set focus on username or password field of the login form
782  */
783 M.util.focus_login_form = function(Y) {
784     var username = Y.one('#username');
785     var password = Y.one('#password');
787     if (username == null || password == null) {
788         // something is wrong here
789         return;
790     }
792     var curElement = document.activeElement
793     if (curElement == 'undefined') {
794         // legacy browser - skip refocus protection
795     } else if (curElement.tagName == 'INPUT') {
796         // user was probably faster to focus something, do not mess with focus
797         return;
798     }
800     if (username.get('value') == '') {
801         username.focus();
802     } else {
803         password.focus();
804     }
808  * Set focus on login error message
809  */
810 M.util.focus_login_error = function(Y) {
811     var errorlog = Y.one('#loginerrormessage');
813     if (errorlog) {
814         errorlog.focus();
815     }
818  * Adds lightbox hidden element that covers the whole node.
820  * @param {YUI} Y
821  * @param {Node} the node lightbox should be added to
822  * @retun {Node} created lightbox node
823  */
824 M.util.add_lightbox = function(Y, node) {
825     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
827     // Check if lightbox is already there
828     if (node.one('.lightbox')) {
829         return node.one('.lightbox');
830     }
832     node.setStyle('position', 'relative');
833     var waiticon = Y.Node.create('<img />')
834     .setAttrs({
835         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
836     })
837     .setStyles({
838         'position' : 'relative',
839         'top' : '50%'
840     });
842     var lightbox = Y.Node.create('<div></div>')
843     .setStyles({
844         'opacity' : '.75',
845         'position' : 'absolute',
846         'width' : '100%',
847         'height' : '100%',
848         'top' : 0,
849         'left' : 0,
850         'backgroundColor' : 'white',
851         'textAlign' : 'center'
852     })
853     .setAttribute('class', 'lightbox')
854     .hide();
856     lightbox.appendChild(waiticon);
857     node.append(lightbox);
858     return lightbox;
862  * Appends a hidden spinner element to the specified node.
864  * @param {YUI} Y
865  * @param {Node} the node the spinner should be added to
866  * @return {Node} created spinner node
867  */
868 M.util.add_spinner = function(Y, node) {
869     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
871     // Check if spinner is already there
872     if (node.one('.spinner')) {
873         return node.one('.spinner');
874     }
876     var spinner = Y.Node.create('<img />')
877         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
878         .addClass('spinner')
879         .addClass('iconsmall')
880         .hide();
882     node.append(spinner);
883     return spinner;
886 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
888 function checkall() {
889     var inputs = document.getElementsByTagName('input');
890     for (var i = 0; i < inputs.length; i++) {
891         if (inputs[i].type == 'checkbox') {
892             if (inputs[i].disabled || inputs[i].readOnly) {
893                 continue;
894             }
895             inputs[i].checked = true;
896         }
897     }
900 function checknone() {
901     var inputs = document.getElementsByTagName('input');
902     for (var i = 0; i < inputs.length; i++) {
903         if (inputs[i].type == 'checkbox') {
904             if (inputs[i].disabled || inputs[i].readOnly) {
905                 continue;
906             }
907             inputs[i].checked = false;
908         }
909     }
913  * Either check, or uncheck, all checkboxes inside the element with id is
914  * @param id the id of the container
915  * @param checked the new state, either '' or 'checked'.
916  */
917 function select_all_in_element_with_id(id, checked) {
918     var container = document.getElementById(id);
919     if (!container) {
920         return;
921     }
922     var inputs = container.getElementsByTagName('input');
923     for (var i = 0; i < inputs.length; ++i) {
924         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
925             inputs[i].checked = checked;
926         }
927     }
930 function select_all_in(elTagName, elClass, elId) {
931     var inputs = document.getElementsByTagName('input');
932     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
933     for(var i = 0; i < inputs.length; ++i) {
934         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
935             inputs[i].checked = 'checked';
936         }
937     }
940 function deselect_all_in(elTagName, elClass, elId) {
941     var inputs = document.getElementsByTagName('INPUT');
942     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
943     for(var i = 0; i < inputs.length; ++i) {
944         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
945             inputs[i].checked = '';
946         }
947     }
950 function confirm_if(expr, message) {
951     if(!expr) {
952         return true;
953     }
954     return confirm(message);
959     findParentNode (start, elementName, elementClass, elementID)
961     Travels up the DOM hierarchy to find a parent element with the
962     specified tag name, class, and id. All conditions must be met,
963     but any can be ommitted. Returns the BODY element if no match
964     found.
966 function findParentNode(el, elName, elClass, elId) {
967     while (el.nodeName.toUpperCase() != 'BODY') {
968         if ((!elName || el.nodeName.toUpperCase() == elName) &&
969             (!elClass || el.className.indexOf(elClass) != -1) &&
970             (!elId || el.id == elId)) {
971             break;
972         }
973         el = el.parentNode;
974     }
975     return el;
978     findChildNode (start, elementName, elementClass, elementID)
980     Travels down the DOM hierarchy to find all child elements with the
981     specified tag name, class, and id. All conditions must be met,
982     but any can be ommitted.
983     Doesn't examine children of matches.
985     @deprecated since Moodle 2.7 - please do not use this function any more.
986     @todo MDL-43242 This will be deleted in Moodle 2.9.
987     @see Y.all
989 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
990     Y.log("findChildNodes() is deprecated. Please use Y.all instead.",
991             "warn", "javascript-static.js");
992     var children = new Array();
993     for (var i = 0; i < start.childNodes.length; i++) {
994         var classfound = false;
995         var child = start.childNodes[i];
996         if((child.nodeType == 1) &&//element node type
997                   (elementClass && (typeof(child.className)=='string'))) {
998             var childClasses = child.className.split(/\s+/);
999             for (var childClassIndex in childClasses) {
1000                 if (childClasses[childClassIndex]==elementClass) {
1001                     classfound = true;
1002                     break;
1003                 }
1004             }
1005         }
1006         if(child.nodeType == 1) { //element node type
1007             if  ( (!tagName || child.nodeName == tagName) &&
1008                 (!elementClass || classfound)&&
1009                 (!elementID || child.id == elementID) &&
1010                 (!elementName || child.name == elementName))
1011             {
1012                 children = children.concat(child);
1013             } else {
1014                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1015             }
1016         }
1017     }
1018     return children;
1021 function unmaskPassword(id) {
1022     var pw = document.getElementById(id);
1023     var chb = document.getElementById(id+'unmask');
1025     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1026     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1027     // functionality won't work in IE8 or lower.
1028     // This is a temporary fixed to allow other browsers to function properly.
1029     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1030         if (chb.checked) {
1031             pw.type = "text";
1032         } else {
1033             pw.type = "password";
1034         }
1035     } else {  //IE Browser version 8 or lower
1036         try {
1037             // first try IE way - it can not set name attribute later
1038             if (chb.checked) {
1039               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1040             } else {
1041               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1042             }
1043             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1044         } catch (e) {
1045             var newpw = document.createElement('input');
1046             newpw.setAttribute('autocomplete', 'off');
1047             newpw.setAttribute('name', pw.name);
1048             if (chb.checked) {
1049               newpw.setAttribute('type', 'text');
1050             } else {
1051               newpw.setAttribute('type', 'password');
1052             }
1053             newpw.setAttribute('class', pw.getAttribute('class'));
1054         }
1055         newpw.id = pw.id;
1056         newpw.size = pw.size;
1057         newpw.onblur = pw.onblur;
1058         newpw.onchange = pw.onchange;
1059         newpw.value = pw.value;
1060         pw.parentNode.replaceChild(newpw, pw);
1061     }
1064 function filterByParent(elCollection, parentFinder) {
1065     var filteredCollection = [];
1066     for (var i = 0; i < elCollection.length; ++i) {
1067         var findParent = parentFinder(elCollection[i]);
1068         if (findParent.nodeName.toUpperCase() != 'BODY') {
1069             filteredCollection.push(elCollection[i]);
1070         }
1071     }
1072     return filteredCollection;
1076     All this is here just so that IE gets to handle oversized blocks
1077     in a visually pleasing manner. It does a browser detect. So sue me.
1080 function fix_column_widths() {
1081     var agt = navigator.userAgent.toLowerCase();
1082     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1083         fix_column_width('left-column');
1084         fix_column_width('right-column');
1085     }
1088 function fix_column_width(colName) {
1089     if(column = document.getElementById(colName)) {
1090         if(!column.offsetWidth) {
1091             setTimeout("fix_column_width('" + colName + "')", 20);
1092             return;
1093         }
1095         var width = 0;
1096         var nodes = column.childNodes;
1098         for(i = 0; i < nodes.length; ++i) {
1099             if(nodes[i].className.indexOf("block") != -1 ) {
1100                 if(width < nodes[i].offsetWidth) {
1101                     width = nodes[i].offsetWidth;
1102                 }
1103             }
1104         }
1106         for(i = 0; i < nodes.length; ++i) {
1107             if(nodes[i].className.indexOf("block") != -1 ) {
1108                 nodes[i].style.width = width + 'px';
1109             }
1110         }
1111     }
1116    Insert myValue at current cursor position
1117  */
1118 function insertAtCursor(myField, myValue) {
1119     // IE support
1120     if (document.selection) {
1121         myField.focus();
1122         sel = document.selection.createRange();
1123         sel.text = myValue;
1124     }
1125     // Mozilla/Netscape support
1126     else if (myField.selectionStart || myField.selectionStart == '0') {
1127         var startPos = myField.selectionStart;
1128         var endPos = myField.selectionEnd;
1129         myField.value = myField.value.substring(0, startPos)
1130             + myValue + myField.value.substring(endPos, myField.value.length);
1131     } else {
1132         myField.value += myValue;
1133     }
1138         Call instead of setting window.onload directly or setting body onload=.
1139         Adds your function to a chain of functions rather than overwriting anything
1140         that exists.
1141         @deprecated Since Moodle 2.7. This will be removed in Moodle 2.9.
1143 function addonload(fn) {
1144     Y.log('addonload has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1145             'warn', 'javascript-static.js');
1146     var oldhandler=window.onload;
1147     window.onload=function() {
1148         if(oldhandler) oldhandler();
1149             fn();
1150     }
1153  * Replacement for getElementsByClassName in browsers that aren't cool enough
1155  * Relying on the built-in getElementsByClassName is far, far faster than
1156  * using YUI.
1158  * Note: the third argument used to be an object with odd behaviour. It now
1159  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1160  * mimicked if you pass an object.
1162  * @param {Node} oElm The top-level node for searching. To search a whole
1163  *                    document, use `document`.
1164  * @param {String} strTagName filter by tag names
1165  * @param {String} name same as HTML5 spec
1166  * @deprecated Since Moodle 2.7. This will be removed in Moodle 2.9.
1167  */
1168 function getElementsByClassName(oElm, strTagName, name) {
1169     Y.log('getElementsByClassName has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1170             'warn', 'javascript-static.js');
1171     // for backwards compatibility
1172     if(typeof name == "object") {
1173         var names = new Array();
1174         for(var i=0; i<name.length; i++) names.push(names[i]);
1175         name = names.join('');
1176     }
1177     // use native implementation if possible
1178     if (oElm.getElementsByClassName && Array.filter) {
1179         if (strTagName == '*') {
1180             return oElm.getElementsByClassName(name);
1181         } else {
1182             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1183                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1184             });
1185         }
1186     }
1187     // native implementation unavailable, fall back to slow method
1188     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1189     var arrReturnElements = new Array();
1190     var arrRegExpClassNames = new Array();
1191     var names = name.split(' ');
1192     for(var i=0; i<names.length; i++) {
1193         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1194     }
1195     var oElement;
1196     var bMatchesAll;
1197     for(var j=0; j<arrElements.length; j++) {
1198         oElement = arrElements[j];
1199         bMatchesAll = true;
1200         for(var k=0; k<arrRegExpClassNames.length; k++) {
1201             if(!arrRegExpClassNames[k].test(oElement.className)) {
1202                 bMatchesAll = false;
1203                 break;
1204             }
1205         }
1206         if(bMatchesAll) {
1207             arrReturnElements.push(oElement);
1208         }
1209     }
1210     return (arrReturnElements)
1214  * Increment a file name.
1216  * @param string file name.
1217  * @param boolean ignoreextension do not extract the extension prior to appending the
1218  *                                suffix. Useful when incrementing folder names.
1219  * @return string the incremented file name.
1220  */
1221 function increment_filename(filename, ignoreextension) {
1222     var extension = '';
1223     var basename = filename;
1225     // Split the file name into the basename + extension.
1226     if (!ignoreextension) {
1227         var dotpos = filename.lastIndexOf('.');
1228         if (dotpos !== -1) {
1229             basename = filename.substr(0, dotpos);
1230             extension = filename.substr(dotpos, filename.length);
1231         }
1232     }
1234     // Look to see if the name already has (NN) at the end of it.
1235     var number = 0;
1236     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1237     if (hasnumber !== null) {
1238         // Note the current number & remove it from the basename.
1239         number = parseInt(hasnumber[2], 10);
1240         basename = hasnumber[1];
1241     }
1243     number++;
1244     var newname = basename + ' (' + number + ')' + extension;
1245     return newname;
1249  * Return whether we are in right to left mode or not.
1251  * @return boolean
1252  */
1253 function right_to_left() {
1254     var body = Y.one('body');
1255     var rtl = false;
1256     if (body && body.hasClass('dir-rtl')) {
1257         rtl = true;
1258     }
1259     return rtl;
1262 function openpopup(event, args) {
1264     if (event) {
1265         if (event.preventDefault) {
1266             event.preventDefault();
1267         } else {
1268             event.returnValue = false;
1269         }
1270     }
1272     // Make sure the name argument is set and valid.
1273     var nameregex = /[^a-z0-9_]/i;
1274     if (typeof args.name !== 'string') {
1275         args.name = '_blank';
1276     } else if (args.name.match(nameregex)) {
1277         // Cleans window name because IE does not support funky ones.
1278         if (M.cfg.developerdebug) {
1279             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1280         }
1281         args.name = args.name.replace(nameregex, '_');
1282     }
1284     var fullurl = args.url;
1285     if (!args.url.match(/https?:\/\//)) {
1286         fullurl = M.cfg.wwwroot + args.url;
1287     }
1288     if (args.fullscreen) {
1289         args.options = args.options.
1290                 replace(/top=\d+/, 'top=0').
1291                 replace(/left=\d+/, 'left=0').
1292                 replace(/width=\d+/, 'width=' + screen.availWidth).
1293                 replace(/height=\d+/, 'height=' + screen.availHeight);
1294     }
1295     var windowobj = window.open(fullurl,args.name,args.options);
1296     if (!windowobj) {
1297         return true;
1298     }
1300     if (args.fullscreen) {
1301         // In some browser / OS combinations (E.g. Chrome on Windows), the
1302         // window initially opens slighly too big. The width and heigh options
1303         // seem to control the area inside the browser window, so what with
1304         // scroll-bars, etc. the actual window is bigger than the screen.
1305         // Therefore, we need to fix things up after the window is open.
1306         var hackcount = 100;
1307         var get_size_exactly_right = function() {
1308             windowobj.moveTo(0, 0);
1309             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1311             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1312             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1313             // about 50ms) after the window is open, then it actually behaves
1314             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1315             // check that the resize actually worked, and if not, repeatedly try
1316             // again after a short delay until it works (but with a limit of
1317             // hackcount repeats.
1318             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1319                 hackcount -= 1;
1320                 setTimeout(get_size_exactly_right, 10);
1321             }
1322         }
1323         setTimeout(get_size_exactly_right, 0);
1324     }
1325     windowobj.focus();
1327     return false;
1330 /** Close the current browser window. */
1331 function close_window(e) {
1332     if (e.preventDefault) {
1333         e.preventDefault();
1334     } else {
1335         e.returnValue = false;
1336     }
1337     window.close();
1341  * Used in a couple of modules to hide navigation areas when using AJAX
1342  * @deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1343  */
1344 function show_item(itemid) {
1345     Y.log('show_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1346             'warn', 'javascript-static.js');
1347     var item = Y.one('#' + itemid);
1348     if (item) {
1349         item.show();
1350     }
1353 // Deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1354 function destroy_item(itemid) {
1355     Y.log('destroy_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1356             'warn', 'javascript-static.js');
1357     var item = Y.one('#' + itemid);
1358     if (item) {
1359         item.remove(true);
1360     }
1363  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1364  * @param controlid the control id.
1365  */
1366 function focuscontrol(controlid) {
1367     var control = document.getElementById(controlid);
1368     if (control) {
1369         control.focus();
1370     }
1374  * Transfers keyboard focus to an HTML element based on the old style style of focus
1375  * This function should be removed as soon as it is no longer used
1376  */
1377 function old_onload_focus(formid, controlname) {
1378     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1379         document.forms[formid].elements[controlname].focus();
1380     }
1383 function build_querystring(obj) {
1384     return convert_object_to_string(obj, '&');
1387 function build_windowoptionsstring(obj) {
1388     return convert_object_to_string(obj, ',');
1391 function convert_object_to_string(obj, separator) {
1392     if (typeof obj !== 'object') {
1393         return null;
1394     }
1395     var list = [];
1396     for(var k in obj) {
1397         k = encodeURIComponent(k);
1398         var value = obj[k];
1399         if(obj[k] instanceof Array) {
1400             for(var i in value) {
1401                 list.push(k+'[]='+encodeURIComponent(value[i]));
1402             }
1403         } else {
1404             list.push(k+'='+encodeURIComponent(value));
1405         }
1406     }
1407     return list.join(separator);
1410 function stripHTML(str) {
1411     var re = /<\S[^><]*>/g;
1412     var ret = str.replace(re, "");
1413     return ret;
1416 Number.prototype.fixed=function(n){
1417     with(Math)
1418         return round(Number(this)*pow(10,n))/pow(10,n);
1420 function update_progress_bar (id, width, pt, msg, es){
1421     var percent = pt;
1422     var status = document.getElementById("status_"+id);
1423     var percent_indicator = document.getElementById("pt_"+id);
1424     var progress_bar = document.getElementById("progress_"+id);
1425     var time_es = document.getElementById("time_"+id);
1426     status.innerHTML = msg;
1427     percent_indicator.innerHTML = percent.fixed(2) + '%';
1428     if(percent == 100) {
1429         progress_bar.style.background = "green";
1430         time_es.style.display = "none";
1431     } else {
1432         progress_bar.style.background = "#FFCC66";
1433         if (es == '?'){
1434             time_es.innerHTML = "";
1435         }else {
1436             time_es.innerHTML = es.fixed(2)+" sec";
1437             time_es.style.display
1438                 = "block";
1439         }
1440     }
1441     progress_bar.style.width = width + "px";
1446 // ===== Deprecated core Javascript functions for Moodle ====
1447 //       DO NOT USE!!!!!!!
1448 // Do not put this stuff in separate file because it only adds extra load on servers!
1451  * Used in a couple of modules to hide navigation areas when using AJAX
1452  * @deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1453  */
1454 function hide_item(itemid) {
1455     Y.log('hide_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1456             'warn', 'javascript-static.js');
1457     var item = Y.one('#' + itemid);
1458     if (item) {
1459         item.hide();
1460     }
1463 M.util.help_popups = {
1464     setup : function(Y) {
1465         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1466     },
1467     open_popup : function(e) {
1468         // Prevent the default page action
1469         e.preventDefault();
1471         // Grab the anchor that was clicked
1472         var anchor = e.target.ancestor('a', true);
1473         var args = {
1474             'name'          : 'popup',
1475             'url'           : anchor.getAttribute('href'),
1476             'options'       : ''
1477         };
1478         var options = [
1479             'height=600',
1480             'width=800',
1481             'top=0',
1482             'left=0',
1483             'menubar=0',
1484             'location=0',
1485             'scrollbars',
1486             'resizable',
1487             'toolbar',
1488             'status',
1489             'directories=0',
1490             'fullscreen=0',
1491             'dependent'
1492         ]
1493         args.options = options.join(',');
1495         openpopup(e, args);
1496     }
1500  * Custom menu namespace
1501  */
1502 M.core_custom_menu = {
1503     /**
1504      * This method is used to initialise a custom menu given the id that belongs
1505      * to the custom menu's root node.
1506      *
1507      * @param {YUI} Y
1508      * @param {string} nodeid
1509      */
1510     init : function(Y, nodeid) {
1511         var node = Y.one('#'+nodeid);
1512         if (node) {
1513             Y.use('node-menunav', function(Y) {
1514                 // Get the node
1515                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1516                 node.removeClass('javascript-disabled');
1517                 // Initialise the menunav plugin
1518                 node.plug(Y.Plugin.NodeMenuNav);
1519             });
1520         }
1521     }
1525  * Used to store form manipulation methods and enhancments
1526  */
1527 M.form = M.form || {};
1530  * Converts a nbsp indented select box into a multi drop down custom control much
1531  * like the custom menu. It also selectable categories on or off.
1533  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1535  * @param {YUI} Y
1536  * @param {string} id
1537  * @param {Array} options
1538  */
1539 M.form.init_smartselect = function(Y, id, options) {
1540     if (!id.match(/^id_/)) {
1541         id = 'id_'+id;
1542     }
1543     var select = Y.one('select#'+id);
1544     if (!select) {
1545         return false;
1546     }
1547     Y.use('event-delegate',function(){
1548         var smartselect = {
1549             id : id,
1550             structure : [],
1551             options : [],
1552             submenucount : 0,
1553             currentvalue : null,
1554             currenttext : null,
1555             shownevent : null,
1556             cfg : {
1557                 selectablecategories : true,
1558                 mode : null
1559             },
1560             nodes : {
1561                 select : null,
1562                 loading : null,
1563                 menu : null
1564             },
1565             init : function(Y, id, args, nodes) {
1566                 if (typeof(args)=='object') {
1567                     for (var i in this.cfg) {
1568                         if (args[i] || args[i]===false) {
1569                             this.cfg[i] = args[i];
1570                         }
1571                     }
1572                 }
1574                 // Display a loading message first up
1575                 this.nodes.select = nodes.select;
1577                 this.currentvalue = this.nodes.select.get('selectedIndex');
1578                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1580                 var options = Array();
1581                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1582                 this.nodes.select.all('option').each(function(option, index) {
1583                     var rawtext = option.get('innerHTML');
1584                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1585                     if (rawtext === text) {
1586                         text = rawtext.replace(/^(\s)*/, '');
1587                         var depth = (rawtext.length - text.length ) + 1;
1588                     } else {
1589                         var depth = ((rawtext.length - text.length )/12)+1;
1590                     }
1591                     option.set('innerHTML', text);
1592                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1593                 }, this);
1595                 this.structure = [];
1596                 var structcount = 0;
1597                 for (var i in options) {
1598                     var o = options[i];
1599                     if (o.depth == 0) {
1600                         this.structure.push(o);
1601                         structcount++;
1602                     } else {
1603                         var d = o.depth;
1604                         var current = this.structure[structcount-1];
1605                         for (var j = 0; j < o.depth-1;j++) {
1606                             if (current && current.children) {
1607                                 current = current.children[current.children.length-1];
1608                             }
1609                         }
1610                         if (current && current.children) {
1611                             current.children.push(o);
1612                         }
1613                     }
1614                 }
1616                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1617                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1618                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1619                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1621                 if (this.cfg.mode == null) {
1622                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1623                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1624                         this.cfg.mode = 'compact';
1625                     } else {
1626                         this.cfg.mode = 'spanning';
1627                     }
1628                 }
1630                 if (this.cfg.mode == 'compact') {
1631                     this.nodes.menu.addClass('compactmenu');
1632                 } else {
1633                     this.nodes.menu.addClass('spanningmenu');
1634                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1635                 }
1637                 Y.one(document.body).append(this.nodes.menu);
1638                 var pos = this.nodes.select.getXY();
1639                 pos[0] += 1;
1640                 this.nodes.menu.setXY(pos);
1641                 this.nodes.menu.on('click', this.handle_click, this);
1643                 Y.one(window).on('resize', function(){
1644                      var pos = this.nodes.select.getXY();
1645                     pos[0] += 1;
1646                     this.nodes.menu.setXY(pos);
1647                  }, this);
1648             },
1649             generate_menu_content : function() {
1650                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1651                 content += this.generate_submenu_content(this.structure[0], true);
1652                 content += '</ul></div>';
1653                 return content;
1654             },
1655             generate_submenu_content : function(item, rootelement) {
1656                 this.submenucount++;
1657                 var content = '';
1658                 if (item.children.length > 0) {
1659                     if (rootelement) {
1660                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1661                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1662                         content += '<div class="smartselect_menu_content">';
1663                     } else {
1664                         content += '<li class="smartselect_submenuitem">';
1665                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1666                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1667                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1668                         content += '<div class="smartselect_submenu_content">';
1669                     }
1670                     content += '<ul>';
1671                     for (var i in item.children) {
1672                         content += this.generate_submenu_content(item.children[i],false);
1673                     }
1674                     content += '</ul>';
1675                     content += '</div>';
1676                     content += '</div>';
1677                     if (rootelement) {
1678                     } else {
1679                         content += '</li>';
1680                     }
1681                 } else {
1682                     content += '<li class="smartselect_menuitem">';
1683                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1684                     content += '</li>';
1685                 }
1686                 return content;
1687             },
1688             select : function(e) {
1689                 var t = e.target;
1690                 e.halt();
1691                 this.currenttext = t.get('innerHTML');
1692                 this.currentvalue = t.getAttribute('value');
1693                 this.nodes.select.set('selectedIndex', this.currentvalue);
1694                 this.hide_menu();
1695             },
1696             handle_click : function(e) {
1697                 var target = e.target;
1698                 if (target.hasClass('smartselect_mask')) {
1699                     this.show_menu(e);
1700                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1701                     this.select(e);
1702                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1703                     this.show_sub_menu(e);
1704                 }
1705             },
1706             show_menu : function(e) {
1707                 e.halt();
1708                 var menu = e.target.ancestor().one('.smartselect_menu');
1709                 menu.addClass('visible');
1710                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1711             },
1712             show_sub_menu : function(e) {
1713                 e.halt();
1714                 var target = e.target;
1715                 if (!target.hasClass('smartselect_submenuitem')) {
1716                     target = target.ancestor('.smartselect_submenuitem');
1717                 }
1718                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1719                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1720                     return;
1721                 }
1722                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1723                 target.one('.smartselect_submenu').addClass('visible');
1724             },
1725             hide_menu : function() {
1726                 this.nodes.menu.all('.visible').removeClass('visible');
1727                 if (this.shownevent) {
1728                     this.shownevent.detach();
1729                 }
1730             }
1731         };
1732         smartselect.init(Y, id, options, {select:select});
1733     });
1736 /** List of flv players to be loaded */
1737 M.util.video_players = [];
1738 /** List of mp3 players to be loaded */
1739 M.util.audio_players = [];
1742  * Add video player
1743  * @param id element id
1744  * @param fileurl media url
1745  * @param width
1746  * @param height
1747  * @param autosize true means detect size from media
1748  */
1749 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1750     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1754  * Add audio player.
1755  * @param id
1756  * @param fileurl
1757  * @param small
1758  */
1759 M.util.add_audio_player = function (id, fileurl, small) {
1760     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1764  * Initialise all audio and video player, must be called from page footer.
1765  */
1766 M.util.load_flowplayer = function() {
1767     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1768         return;
1769     }
1770     if (typeof(flowplayer) == 'undefined') {
1771         var loaded = false;
1773         var embed_function = function() {
1774             if (loaded || typeof(flowplayer) == 'undefined') {
1775                 return;
1776             }
1777             loaded = true;
1779             var controls = {
1780                     autoHide: true
1781             }
1782             /* TODO: add CSS color overrides for the flv flow player */
1784             for(var i=0; i<M.util.video_players.length; i++) {
1785                 var video = M.util.video_players[i];
1786                 if (video.width > 0 && video.height > 0) {
1787                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', width: video.width, height: video.height};
1788                 } else {
1789                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf';
1790                 }
1791                 flowplayer(video.id, src, {
1792                     plugins: {controls: controls},
1793                     clip: {
1794                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1795                         onMetaData: function(clip) {
1796                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1797                                 clip.mvideo.resized = true;
1798                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1799                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1800                                     // bad luck, we have to guess - we may not get metadata at all
1801                                     var width = clip.width;
1802                                     var height = clip.height;
1803                                 } else {
1804                                     var width = clip.metaData.width;
1805                                     var height = clip.metaData.height;
1806                                 }
1807                                 var minwidth = 300; // controls are messed up in smaller objects
1808                                 if (width < minwidth) {
1809                                     height = (height * minwidth) / width;
1810                                     width = minwidth;
1811                                 }
1813                                 var object = this._api();
1814                                 object.width = width;
1815                                 object.height = height;
1816                             }
1817                         }
1818                     }
1819                 });
1820             }
1821             if (M.util.audio_players.length == 0) {
1822                 return;
1823             }
1824             var controls = {
1825                     autoHide: false,
1826                     fullscreen: false,
1827                     next: false,
1828                     previous: false,
1829                     scrubber: true,
1830                     play: true,
1831                     pause: true,
1832                     volume: true,
1833                     mute: false,
1834                     backgroundGradient: [0.5,0,0.3]
1835                 };
1837             var rule;
1838             for (var j=0; j < document.styleSheets.length; j++) {
1840                 // To avoid javascript security violation accessing cross domain stylesheets
1841                 var allrules = false;
1842                 try {
1843                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1844                         allrules = document.styleSheets[j].rules;
1845                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1846                         allrules = document.styleSheets[j].cssRules;
1847                     } else {
1848                         // why??
1849                         continue;
1850                     }
1851                 } catch (e) {
1852                     continue;
1853                 }
1855                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1856                 if (!allrules) {
1857                     continue;
1858                 }
1860                 for(var i=0; i<allrules.length; i++) {
1861                     rule = '';
1862                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1863                         if (typeof(allrules[i].cssText) != 'undefined') {
1864                             rule = allrules[i].cssText;
1865                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1866                             rule = allrules[i].style.cssText;
1867                         }
1868                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1869                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1870                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1871                             controls[colprop] = rule;
1872                         }
1873                     }
1874                 }
1875                 allrules = false;
1876             }
1878             for(i=0; i<M.util.audio_players.length; i++) {
1879                 var audio = M.util.audio_players[i];
1880                 if (audio.small) {
1881                     controls.controlall = false;
1882                     controls.height = 15;
1883                     controls.time = false;
1884                 } else {
1885                     controls.controlall = true;
1886                     controls.height = 25;
1887                     controls.time = true;
1888                 }
1889                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', {
1890                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf'}},
1891                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1892                 });
1893             }
1894         }
1896         if (M.cfg.jsrev == -1) {
1897             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1898         } else {
1899             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1900         }
1901         var fileref = document.createElement('script');
1902         fileref.setAttribute('type','text/javascript');
1903         fileref.setAttribute('src', jsurl);
1904         fileref.onload = embed_function;
1905         fileref.onreadystatechange = embed_function;
1906         document.getElementsByTagName('head')[0].appendChild(fileref);
1907     }