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