Merge branch 'MDL-43230-master' of git://github.com/ryanwyllie/moodle
[moodle.git] / lib / javascript-static.js
blob5cd262a9dbc265fc951cc1385cb87067dc2577b9
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     }
12     // Ensure thaat the YUI_config is applied to the main YUI instance.
13     Y.applyConfig(YUI_config);
15 /**
16  * The gallery version to use when loading YUI modules from the gallery.
17  * Will be changed every time when using local galleries.
18  */
19 M.yui.galleryversion = '2010.04.21-21-51';
21 /**
22  * Various utility functions
23  */
24 M.util = M.util || {};
26 /**
27  * Language strings - initialised from page footer.
28  */
29 M.str = M.str || {};
31 /**
32  * Returns url for images.
33  * @param {String} imagename
34  * @param {String} component
35  * @return {String}
36  */
37 M.util.image_url = function(imagename, component) {
39     if (!component || component == '' || component == 'moodle' || component == 'core') {
40         component = 'core';
41     }
43     var url = M.cfg.wwwroot + '/theme/image.php';
44     if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
45         if (!M.cfg.svgicons) {
46             url += '/_s';
47         }
48         url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
49     } else {
50         url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
51         if (!M.cfg.svgicons) {
52             url += '&svg=0';
53         }
54     }
56     return url;
59 M.util.in_array = function(item, array){
60     for( var i = 0; i<array.length; i++){
61         if(item==array[i]){
62             return true;
63         }
64     }
65     return false;
68 /**
69  * Init a collapsible region, see print_collapsible_region in weblib.php
70  * @param {YUI} Y YUI3 instance with all libraries loaded
71  * @param {String} id the HTML id for the div.
72  * @param {String} userpref the user preference that records the state of this box. false if none.
73  * @param {String} strtooltip
74  */
75 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
76     Y.use('anim', function(Y) {
77         new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
78     });
81 /**
82  * Object to handle a collapsible region : instantiate and forget styled object
83  *
84  * @class
85  * @constructor
86  * @param {YUI} Y YUI3 instance with all libraries loaded
87  * @param {String} id The HTML id for the div.
88  * @param {String} userpref The user preference that records the state of this box. false if none.
89  * @param {String} strtooltip
90  */
91 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
92     // Record the pref name
93     this.userpref = userpref;
95     // Find the divs in the document.
96     this.div = Y.one('#'+id);
98     // Get the caption for the collapsible region
99     var caption = this.div.one('#'+id + '_caption');
101     // Create a link
102     var a = Y.Node.create('<a href="#"></a>');
103     a.setAttribute('title', strtooltip);
105     // Get all the nodes from caption, remove them and append them to <a>
106     while (caption.hasChildNodes()) {
107         child = caption.get('firstChild');
108         child.remove();
109         a.append(child);
110     }
111     caption.append(a);
113     // Get the height of the div at this point before we shrink it if required
114     var height = this.div.get('offsetHeight');
115     var collapsedimage = 't/collapsed'; // ltr mode
116     if (right_to_left()) {
117         collapsedimage = 't/collapsed_rtl';
118     } else {
119         collapsedimage = 't/collapsed';
120     }
121     if (this.div.hasClass('collapsed')) {
122         // Add the correct image and record the YUI node created in the process
123         this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
124         // Shrink the div as it is collapsed by default
125         this.div.setStyle('height', caption.get('offsetHeight')+'px');
126     } else {
127         // Add the correct image and record the YUI node created in the process
128         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
129     }
130     a.append(this.icon);
132     // Create the animation.
133     var animation = new Y.Anim({
134         node: this.div,
135         duration: 0.3,
136         easing: Y.Easing.easeBoth,
137         to: {height:caption.get('offsetHeight')},
138         from: {height:height}
139     });
141     // Handler for the animation finishing.
142     animation.on('end', function() {
143         this.div.toggleClass('collapsed');
144         var collapsedimage = 't/collapsed'; // ltr mode
145         if (right_to_left()) {
146             collapsedimage = 't/collapsed_rtl';
147             } else {
148             collapsedimage = 't/collapsed';
149             }
150         if (this.div.hasClass('collapsed')) {
151             this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
152         } else {
153             this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
154         }
155     }, this);
157     // Hook up the event handler.
158     a.on('click', function(e, animation) {
159         e.preventDefault();
160         // Animate to the appropriate size.
161         if (animation.get('running')) {
162             animation.stop();
163         }
164         animation.set('reverse', this.div.hasClass('collapsed'));
165         // Update the user preference.
166         if (this.userpref) {
167             M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
168         }
169         animation.run();
170     }, this, animation);
174  * The user preference that stores the state of this box.
175  * @property userpref
176  * @type String
177  */
178 M.util.CollapsibleRegion.prototype.userpref = null;
181  * The key divs that make up this
182  * @property div
183  * @type Y.Node
184  */
185 M.util.CollapsibleRegion.prototype.div = null;
188  * The key divs that make up this
189  * @property icon
190  * @type Y.Node
191  */
192 M.util.CollapsibleRegion.prototype.icon = null;
195  * Makes a best effort to connect back to Moodle to update a user preference,
196  * however, there is no mechanism for finding out if the update succeeded.
198  * Before you can use this function in your JavsScript, you must have called
199  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
200  * the udpate is allowed, and how to safely clean and submitted values.
202  * @param String name the name of the setting to udpate.
203  * @param String the value to set it to.
204  */
205 M.util.set_user_preference = function(name, value) {
206     YUI().use('io', function(Y) {
207         var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
208                 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
210         // If we are a developer, ensure that failures are reported.
211         var cfg = {
212                 method: 'get',
213                 on: {}
214             };
215         if (M.cfg.developerdebug) {
216             cfg.on.failure = function(id, o, args) {
217                 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
218             }
219         }
221         // Make the request.
222         Y.io(url, cfg);
223     });
227  * Prints a confirmation dialog in the style of DOM.confirm().
229  * @method show_confirm_dialog
230  * @param {EventFacade} e
231  * @param {Object} args
232  * @param {String} args.message The question to ask the user
233  * @param {Function} [args.callback] A callback to apply on confirmation.
234  * @param {Object} [args.scope] The scope to use when calling the callback.
235  * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
236  * @param {String} [args.cancellabel] The label to use on the cancel button.
237  * @param {String} [args.continuelabel] The label to use on the continue button.
238  */
239 M.util.show_confirm_dialog = function(e, args) {
240     var target = e.target;
241     if (e.preventDefault) {
242         e.preventDefault();
243     }
245     YUI().use('moodle-core-notification-confirm', function(Y) {
246         var confirmationDialogue = new M.core.confirm({
247             width: '300px',
248             center: true,
249             modal: true,
250             visible: false,
251             draggable: false,
252             title: M.util.get_string('confirmation', 'admin'),
253             noLabel: M.util.get_string('cancel', 'moodle'),
254             question: args.message
255         });
257         // The dialogue was submitted with a positive value indication.
258         confirmationDialogue.on('complete-yes', function(e) {
259             // Handle any callbacks.
260             if (args.callback) {
261                 if (!Y.Lang.isFunction(args.callback)) {
262                     Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
263                             'warn', 'M.util.show_confirm_dialog');
264                     return;
265                 }
267                 var scope = e.target;
268                 if (Y.Lang.isObject(args.scope)) {
269                     scope = args.scope;
270                 }
272                 var callbackargs = args.callbackargs || [];
273                 args.callback.apply(scope, callbackargs);
274                 return;
275             }
277             var targetancestor = null,
278                 targetform = null;
280             if (target.test('a')) {
281                 window.location = target.get('href');
283             } else if ((targetancestor = target.ancestor('a')) !== null) {
284                 window.location = targetancestor.get('href');
286             } else if (target.test('input') || target.test('button')) {
287                 targetform = target.ancestor('form', true);
288                 if (!targetform) {
289                     return;
290                 }
291                 if (target.get('name') && target.get('value')) {
292                     targetform.append('<input type="hidden" name="' + target.get('name') +
293                                     '" value="' + target.get('value') + '">');
294                 }
295                 targetform.submit();
297             } else if (target.test('form')) {
298                 target.submit();
300             } else {
301                 Y.log("Element of type " + target.get('tagName') +
302                         " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, BUTTON or FORM",
303                         'warn', 'javascript-static');
304             }
305         }, this);
307         if (args.cancellabel) {
308             confirmationDialogue.set('noLabel', args.cancellabel);
309         }
311         if (args.continuelabel) {
312             confirmationDialogue.set('yesLabel', args.continuelabel);
313         }
315         confirmationDialogue.render()
316                 .show();
317     });
320 /** Useful for full embedding of various stuff */
321 M.util.init_maximised_embed = function(Y, id) {
322     var obj = Y.one('#'+id);
323     if (!obj) {
324         return;
325     }
327     var get_htmlelement_size = function(el, prop) {
328         if (Y.Lang.isString(el)) {
329             el = Y.one('#' + el);
330         }
331         // Ensure element exists.
332         if (el) {
333             var val = el.getStyle(prop);
334             if (val == 'auto') {
335                 val = el.getComputedStyle(prop);
336             }
337             val = parseInt(val);
338             if (isNaN(val)) {
339                 return 0;
340             }
341             return val;
342         } else {
343             return 0;
344         }
345     };
347     var resize_object = function() {
348         obj.setStyle('display', 'none');
349         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
351         if (newwidth > 500) {
352             obj.setStyle('width', newwidth  + 'px');
353         } else {
354             obj.setStyle('width', '500px');
355         }
357         var headerheight = get_htmlelement_size('page-header', 'height');
358         var footerheight = get_htmlelement_size('page-footer', 'height');
359         var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
360         if (newheight < 400) {
361             newheight = 400;
362         }
363         obj.setStyle('height', newheight+'px');
364         obj.setStyle('display', '');
365     };
367     resize_object();
368     // fix layout if window resized too
369     Y.use('event-resize', function (Y) {
370         Y.on("windowresize", function() {
371             resize_object();
372         });
373     });
377  * Breaks out all links to the top frame - used in frametop page layout.
378  */
379 M.util.init_frametop = function(Y) {
380     Y.all('a').each(function(node) {
381         node.set('target', '_top');
382     });
383     Y.all('form').each(function(node) {
384         node.set('target', '_top');
385     });
389  * Finds all nodes that match the given CSS selector and attaches events to them
390  * so that they toggle a given classname when clicked.
392  * @param {YUI} Y
393  * @param {string} id An id containing elements to target
394  * @param {string} cssselector A selector to use to find targets
395  * @param {string} toggleclassname A classname to toggle
396  */
397 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
399     if (togglecssselector == '') {
400         togglecssselector = cssselector;
401     }
403     var node = Y.one('#'+id);
404     node.all(cssselector).each(function(n){
405         n.on('click', function(e){
406             e.stopPropagation();
407             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
408                 if (this.test(togglecssselector)) {
409                     this.toggleClass(toggleclassname);
410                 } else {
411                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
412             }
413             }
414         }, n);
415     });
416     // Attach this click event to the node rather than all selectors... will be much better
417     // for performance
418     node.on('click', function(e){
419         if (e.target.hasClass('addtoall')) {
420             this.all(togglecssselector).addClass(toggleclassname);
421         } else if (e.target.hasClass('removefromall')) {
422             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
423         }
424     }, node);
428  * Initialises a colour picker
430  * Designed to be used with admin_setting_configcolourpicker although could be used
431  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
432  * above or below the input (must have the same parent) and then call this with the
433  * id.
435  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
436  * contrib/blocks. For better docs refer to that.
438  * @param {YUI} Y
439  * @param {int} id
440  * @param {object} previewconf
441  */
442 M.util.init_colour_picker = function(Y, id, previewconf) {
443     /**
444      * We need node and event-mouseenter
445      */
446     Y.use('node', 'event-mouseenter', function(){
447         /**
448          * The colour picker object
449          */
450         var colourpicker = {
451             box : null,
452             input : null,
453             image : null,
454             preview : null,
455             current : null,
456             eventClick : null,
457             eventMouseEnter : null,
458             eventMouseLeave : null,
459             eventMouseMove : null,
460             width : 300,
461             height :  100,
462             factor : 5,
463             /**
464              * Initalises the colour picker by putting everything together and wiring the events
465              */
466             init : function() {
467                 this.input = Y.one('#'+id);
468                 this.box = this.input.ancestor().one('.admin_colourpicker');
469                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
470                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
471                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
472                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
473                 this.current = Y.Node.create('<div class="currentcolour"></div>');
474                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
475                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
477                 if (typeof(previewconf) === 'object' && previewconf !== null) {
478                     Y.one('#'+id+'_preview').on('click', function(e){
479                         if (Y.Lang.isString(previewconf.selector)) {
480                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
481                         } else {
482                             for (var i in previewconf.selector) {
483                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
484                             }
485                         }
486                     }, this);
487                 }
489                 this.eventClick = this.image.on('click', this.pickColour, this);
490                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
491             },
492             /**
493              * Starts to follow the mouse once it enter the image
494              */
495             startFollow : function(e) {
496                 this.eventMouseEnter.detach();
497                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
498                 this.eventMouseMove = this.image.on('mousemove', function(e){
499                     this.preview.setStyle('backgroundColor', this.determineColour(e));
500                 }, this);
501             },
502             /**
503              * Stops following the mouse
504              */
505             endFollow : function(e) {
506                 this.eventMouseMove.detach();
507                 this.eventMouseLeave.detach();
508                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
509             },
510             /**
511              * Picks the colour the was clicked on
512              */
513             pickColour : function(e) {
514                 var colour = this.determineColour(e);
515                 this.input.set('value', colour);
516                 this.current.setStyle('backgroundColor', colour);
517             },
518             /**
519              * Calculates the colour fromthe given co-ordinates
520              */
521             determineColour : function(e) {
522                 var eventx = Math.floor(e.pageX-e.target.getX());
523                 var eventy = Math.floor(e.pageY-e.target.getY());
525                 var imagewidth = this.width;
526                 var imageheight = this.height;
527                 var factor = this.factor;
528                 var colour = [255,0,0];
530                 var matrices = [
531                     [  0,  1,  0],
532                     [ -1,  0,  0],
533                     [  0,  0,  1],
534                     [  0, -1,  0],
535                     [  1,  0,  0],
536                     [  0,  0, -1]
537                 ];
539                 var matrixcount = matrices.length;
540                 var limit = Math.round(imagewidth/matrixcount);
541                 var heightbreak = Math.round(imageheight/2);
543                 for (var x = 0; x < imagewidth; x++) {
544                     var divisor = Math.floor(x / limit);
545                     var matrix = matrices[divisor];
547                     colour[0] += matrix[0]*factor;
548                     colour[1] += matrix[1]*factor;
549                     colour[2] += matrix[2]*factor;
551                     if (eventx==x) {
552                         break;
553                     }
554                 }
556                 var pixel = [colour[0], colour[1], colour[2]];
557                 if (eventy < heightbreak) {
558                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
559                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
560                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
561                 } else if (eventy > heightbreak) {
562                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
563                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
564                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
565                 }
567                 return this.convert_rgb_to_hex(pixel);
568             },
569             /**
570              * Converts an RGB value to Hex
571              */
572             convert_rgb_to_hex : function(rgb) {
573                 var hex = '#';
574                 var hexchars = "0123456789ABCDEF";
575                 for (var i=0; i<3; i++) {
576                     var number = Math.abs(rgb[i]);
577                     if (number == 0 || isNaN(number)) {
578                         hex += '00';
579                     } else {
580                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
581                     }
582                 }
583                 return hex;
584             }
585         };
586         /**
587          * Initialise the colour picker :) Hoorah
588          */
589         colourpicker.init();
590     });
593 M.util.init_block_hider = function(Y, config) {
594     Y.use('base', 'node', function(Y) {
595         M.util.block_hider = M.util.block_hider || (function(){
596             var blockhider = function() {
597                 blockhider.superclass.constructor.apply(this, arguments);
598             };
599             blockhider.prototype = {
600                 initializer : function(config) {
601                     this.set('block', '#'+this.get('id'));
602                     var b = this.get('block'),
603                         t = b.one('.title'),
604                         a = null,
605                         hide,
606                         show;
607                     if (t && (a = t.one('.block_action'))) {
608                         hide = Y.Node.create('<img />')
609                             .addClass('block-hider-hide')
610                             .setAttrs({
611                                 alt:        config.tooltipVisible,
612                                 src:        this.get('iconVisible'),
613                                 tabIndex:   0,
614                                 'title':    config.tooltipVisible
615                             });
616                         hide.on('keypress', this.updateStateKey, this, true);
617                         hide.on('click', this.updateState, this, true);
619                         show = Y.Node.create('<img />')
620                             .addClass('block-hider-show')
621                             .setAttrs({
622                                 alt:        config.tooltipHidden,
623                                 src:        this.get('iconHidden'),
624                                 tabIndex:   0,
625                                 'title':    config.tooltipHidden
626                             });
627                         show.on('keypress', this.updateStateKey, this, false);
628                         show.on('click', this.updateState, this, false);
630                         a.insert(show, 0).insert(hide, 0);
631                     }
632                 },
633                 updateState : function(e, hide) {
634                     M.util.set_user_preference(this.get('preference'), hide);
635                     if (hide) {
636                         this.get('block').addClass('hidden');
637                         this.get('block').one('.block-hider-show').focus();
638                     } else {
639                         this.get('block').removeClass('hidden');
640                         this.get('block').one('.block-hider-hide').focus();
641                     }
642                 },
643                 updateStateKey : function(e, hide) {
644                     if (e.keyCode == 13) { //allow hide/show via enter key
645                         this.updateState(this, hide);
646                     }
647                 }
648             };
649             Y.extend(blockhider, Y.Base, blockhider.prototype, {
650                 NAME : 'blockhider',
651                 ATTRS : {
652                     id : {},
653                     preference : {},
654                     iconVisible : {
655                         value : M.util.image_url('t/switch_minus', 'moodle')
656                     },
657                     iconHidden : {
658                         value : M.util.image_url('t/switch_plus', 'moodle')
659                     },
660                     block : {
661                         setter : function(node) {
662                             return Y.one(node);
663                         }
664                     }
665                 }
666             });
667             return blockhider;
668         })();
669         new M.util.block_hider(config);
670     });
674  * @var pending_js - The keys are the list of all pending js actions.
675  * @type Object
676  */
677 M.util.pending_js = [];
678 M.util.complete_js = [];
681  * Register any long running javascript code with a unique identifier.
682  * Should be followed with a call to js_complete with a matching
683  * idenfitier when the code is complete. May also be called with no arguments
684  * to test if there is any js calls pending. This is relied on by behat so that
685  * it can wait for all pending updates before interacting with a page.
686  * @param String uniqid - optional, if provided,
687  *                        registers this identifier until js_complete is called.
688  * @return boolean - True if there is any pending js.
689  */
690 M.util.js_pending = function(uniqid) {
691     if (uniqid !== false) {
692         M.util.pending_js.push(uniqid);
693     }
695     return M.util.pending_js.length;
698 // Start this asap.
699 M.util.js_pending('init');
702  * Register listeners for Y.io start/end so we can wait for them in behat.
703  */
704 YUI.add('moodle-core-io', function(Y) {
705     Y.on('io:start', function(id) {
706         M.util.js_pending('io:' + id);
707     });
708     Y.on('io:end', function(id) {
709         M.util.js_complete('io:' + id);
710     });
711 }, '@VERSION@', {
712     condition: {
713         trigger: 'io-base',
714         when: 'after'
715     }
719  * Unregister any long running javascript code by unique identifier.
720  * This function should form a matching pair with js_pending
722  * @param String uniqid - required, unregisters this identifier
723  * @return boolean - True if there is any pending js.
724  */
725 M.util.js_complete = function(uniqid) {
726     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
727     // the native function. Y.Array polyfills the native function if it does not exist.
728     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
729     if (index >= 0) {
730         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
731     }
733     return M.util.pending_js.length;
737  * Returns a string registered in advance for usage in JavaScript
739  * If you do not pass the third parameter, the function will just return
740  * the corresponding value from the M.str object. If the third parameter is
741  * provided, the function performs {$a} placeholder substitution in the
742  * same way as PHP get_string() in Moodle does.
744  * @param {String} identifier string identifier
745  * @param {String} component the component providing the string
746  * @param {Object|String} a optional variable to populate placeholder with
747  */
748 M.util.get_string = function(identifier, component, a) {
749     var stringvalue;
751     if (M.cfg.developerdebug) {
752         // creating new instance if YUI is not optimal but it seems to be better way then
753         // require the instance via the function API - note that it is used in rare cases
754         // for debugging only anyway
755         // To ensure we don't kill browser performance if hundreds of get_string requests
756         // are made we cache the instance we generate within the M.util namespace.
757         // We don't publicly define the variable so that it doesn't get abused.
758         if (typeof M.util.get_string_yui_instance === 'undefined') {
759             M.util.get_string_yui_instance = new YUI({ debug : true });
760         }
761         var Y = M.util.get_string_yui_instance;
762     }
764     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
765         stringvalue = '[[' + identifier + ',' + component + ']]';
766         if (M.cfg.developerdebug) {
767             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
768         }
769         return stringvalue;
770     }
772     stringvalue = M.str[component][identifier];
774     if (typeof a == 'undefined') {
775         // no placeholder substitution requested
776         return stringvalue;
777     }
779     if (typeof a == 'number' || typeof a == 'string') {
780         // replace all occurrences of {$a} with the placeholder value
781         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
782         return stringvalue;
783     }
785     if (typeof a == 'object') {
786         // replace {$a->key} placeholders
787         for (var key in a) {
788             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
789                 if (M.cfg.developerdebug) {
790                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
791                 }
792                 continue;
793             }
794             var search = '{$a->' + key + '}';
795             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
796             search = new RegExp(search, 'g');
797             stringvalue = stringvalue.replace(search, a[key]);
798         }
799         return stringvalue;
800     }
802     if (M.cfg.developerdebug) {
803         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
804     }
805     return stringvalue;
809  * Set focus on username or password field of the login form
810  */
811 M.util.focus_login_form = function(Y) {
812     var username = Y.one('#username');
813     var password = Y.one('#password');
815     if (username == null || password == null) {
816         // something is wrong here
817         return;
818     }
820     var curElement = document.activeElement
821     if (curElement == 'undefined') {
822         // legacy browser - skip refocus protection
823     } else if (curElement.tagName == 'INPUT') {
824         // user was probably faster to focus something, do not mess with focus
825         return;
826     }
828     if (username.get('value') == '') {
829         username.focus();
830     } else {
831         password.focus();
832     }
836  * Set focus on login error message
837  */
838 M.util.focus_login_error = function(Y) {
839     var errorlog = Y.one('#loginerrormessage');
841     if (errorlog) {
842         errorlog.focus();
843     }
846  * Adds lightbox hidden element that covers the whole node.
848  * @param {YUI} Y
849  * @param {Node} the node lightbox should be added to
850  * @retun {Node} created lightbox node
851  */
852 M.util.add_lightbox = function(Y, node) {
853     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
855     // Check if lightbox is already there
856     if (node.one('.lightbox')) {
857         return node.one('.lightbox');
858     }
860     node.setStyle('position', 'relative');
861     var waiticon = Y.Node.create('<img />')
862     .setAttrs({
863         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
864     })
865     .setStyles({
866         'position' : 'relative',
867         'top' : '50%'
868     });
870     var lightbox = Y.Node.create('<div></div>')
871     .setStyles({
872         'opacity' : '.75',
873         'position' : 'absolute',
874         'width' : '100%',
875         'height' : '100%',
876         'top' : 0,
877         'left' : 0,
878         'backgroundColor' : 'white',
879         'textAlign' : 'center'
880     })
881     .setAttribute('class', 'lightbox')
882     .hide();
884     lightbox.appendChild(waiticon);
885     node.append(lightbox);
886     return lightbox;
890  * Appends a hidden spinner element to the specified node.
892  * @param {YUI} Y
893  * @param {Node} the node the spinner should be added to
894  * @return {Node} created spinner node
895  */
896 M.util.add_spinner = function(Y, node) {
897     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
899     // Check if spinner is already there
900     if (node.one('.spinner')) {
901         return node.one('.spinner');
902     }
904     var spinner = Y.Node.create('<img />')
905         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
906         .addClass('spinner')
907         .addClass('iconsmall')
908         .hide();
910     node.append(spinner);
911     return spinner;
914 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
916 function checkall() {
917     var inputs = document.getElementsByTagName('input');
918     for (var i = 0; i < inputs.length; i++) {
919         if (inputs[i].type == 'checkbox') {
920             if (inputs[i].disabled || inputs[i].readOnly) {
921                 continue;
922             }
923             inputs[i].checked = true;
924         }
925     }
928 function checknone() {
929     var inputs = document.getElementsByTagName('input');
930     for (var i = 0; i < inputs.length; i++) {
931         if (inputs[i].type == 'checkbox') {
932             if (inputs[i].disabled || inputs[i].readOnly) {
933                 continue;
934             }
935             inputs[i].checked = false;
936         }
937     }
941  * Either check, or uncheck, all checkboxes inside the element with id is
942  * @param id the id of the container
943  * @param checked the new state, either '' or 'checked'.
944  */
945 function select_all_in_element_with_id(id, checked) {
946     var container = document.getElementById(id);
947     if (!container) {
948         return;
949     }
950     var inputs = container.getElementsByTagName('input');
951     for (var i = 0; i < inputs.length; ++i) {
952         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
953             inputs[i].checked = checked;
954         }
955     }
958 function select_all_in(elTagName, elClass, elId) {
959     var inputs = document.getElementsByTagName('input');
960     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
961     for(var i = 0; i < inputs.length; ++i) {
962         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
963             inputs[i].checked = 'checked';
964         }
965     }
968 function deselect_all_in(elTagName, elClass, elId) {
969     var inputs = document.getElementsByTagName('INPUT');
970     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
971     for(var i = 0; i < inputs.length; ++i) {
972         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
973             inputs[i].checked = '';
974         }
975     }
978 function confirm_if(expr, message) {
979     if(!expr) {
980         return true;
981     }
982     return confirm(message);
987     findParentNode (start, elementName, elementClass, elementID)
989     Travels up the DOM hierarchy to find a parent element with the
990     specified tag name, class, and id. All conditions must be met,
991     but any can be ommitted. Returns the BODY element if no match
992     found.
994 function findParentNode(el, elName, elClass, elId) {
995     while (el.nodeName.toUpperCase() != 'BODY') {
996         if ((!elName || el.nodeName.toUpperCase() == elName) &&
997             (!elClass || el.className.indexOf(elClass) != -1) &&
998             (!elId || el.id == elId)) {
999             break;
1000         }
1001         el = el.parentNode;
1002     }
1003     return el;
1006 function unmaskPassword(id) {
1007     var pw = document.getElementById(id);
1008     var chb = document.getElementById(id+'unmask');
1010     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1011     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1012     // functionality won't work in IE8 or lower.
1013     // This is a temporary fixed to allow other browsers to function properly.
1014     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1015         if (chb.checked) {
1016             pw.type = "text";
1017         } else {
1018             pw.type = "password";
1019         }
1020     } else {  //IE Browser version 8 or lower
1021         try {
1022             // first try IE way - it can not set name attribute later
1023             if (chb.checked) {
1024               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1025             } else {
1026               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1027             }
1028             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1029         } catch (e) {
1030             var newpw = document.createElement('input');
1031             newpw.setAttribute('autocomplete', 'off');
1032             newpw.setAttribute('name', pw.name);
1033             if (chb.checked) {
1034               newpw.setAttribute('type', 'text');
1035             } else {
1036               newpw.setAttribute('type', 'password');
1037             }
1038             newpw.setAttribute('class', pw.getAttribute('class'));
1039         }
1040         newpw.id = pw.id;
1041         newpw.size = pw.size;
1042         newpw.onblur = pw.onblur;
1043         newpw.onchange = pw.onchange;
1044         newpw.value = pw.value;
1045         pw.parentNode.replaceChild(newpw, pw);
1046     }
1049 function filterByParent(elCollection, parentFinder) {
1050     var filteredCollection = [];
1051     for (var i = 0; i < elCollection.length; ++i) {
1052         var findParent = parentFinder(elCollection[i]);
1053         if (findParent.nodeName.toUpperCase() != 'BODY') {
1054             filteredCollection.push(elCollection[i]);
1055         }
1056     }
1057     return filteredCollection;
1061     All this is here just so that IE gets to handle oversized blocks
1062     in a visually pleasing manner. It does a browser detect. So sue me.
1065 function fix_column_widths() {
1066     var agt = navigator.userAgent.toLowerCase();
1067     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1068         fix_column_width('left-column');
1069         fix_column_width('right-column');
1070     }
1073 function fix_column_width(colName) {
1074     if(column = document.getElementById(colName)) {
1075         if(!column.offsetWidth) {
1076             setTimeout("fix_column_width('" + colName + "')", 20);
1077             return;
1078         }
1080         var width = 0;
1081         var nodes = column.childNodes;
1083         for(i = 0; i < nodes.length; ++i) {
1084             if(nodes[i].className.indexOf("block") != -1 ) {
1085                 if(width < nodes[i].offsetWidth) {
1086                     width = nodes[i].offsetWidth;
1087                 }
1088             }
1089         }
1091         for(i = 0; i < nodes.length; ++i) {
1092             if(nodes[i].className.indexOf("block") != -1 ) {
1093                 nodes[i].style.width = width + 'px';
1094             }
1095         }
1096     }
1101    Insert myValue at current cursor position
1102  */
1103 function insertAtCursor(myField, myValue) {
1104     // IE support
1105     if (document.selection) {
1106         myField.focus();
1107         sel = document.selection.createRange();
1108         sel.text = myValue;
1109     }
1110     // Mozilla/Netscape support
1111     else if (myField.selectionStart || myField.selectionStart == '0') {
1112         var startPos = myField.selectionStart;
1113         var endPos = myField.selectionEnd;
1114         myField.value = myField.value.substring(0, startPos)
1115             + myValue + myField.value.substring(endPos, myField.value.length);
1116     } else {
1117         myField.value += myValue;
1118     }
1122  * Increment a file name.
1124  * @param string file name.
1125  * @param boolean ignoreextension do not extract the extension prior to appending the
1126  *                                suffix. Useful when incrementing folder names.
1127  * @return string the incremented file name.
1128  */
1129 function increment_filename(filename, ignoreextension) {
1130     var extension = '';
1131     var basename = filename;
1133     // Split the file name into the basename + extension.
1134     if (!ignoreextension) {
1135         var dotpos = filename.lastIndexOf('.');
1136         if (dotpos !== -1) {
1137             basename = filename.substr(0, dotpos);
1138             extension = filename.substr(dotpos, filename.length);
1139         }
1140     }
1142     // Look to see if the name already has (NN) at the end of it.
1143     var number = 0;
1144     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1145     if (hasnumber !== null) {
1146         // Note the current number & remove it from the basename.
1147         number = parseInt(hasnumber[2], 10);
1148         basename = hasnumber[1];
1149     }
1151     number++;
1152     var newname = basename + ' (' + number + ')' + extension;
1153     return newname;
1157  * Return whether we are in right to left mode or not.
1159  * @return boolean
1160  */
1161 function right_to_left() {
1162     var body = Y.one('body');
1163     var rtl = false;
1164     if (body && body.hasClass('dir-rtl')) {
1165         rtl = true;
1166     }
1167     return rtl;
1170 function openpopup(event, args) {
1172     if (event) {
1173         if (event.preventDefault) {
1174             event.preventDefault();
1175         } else {
1176             event.returnValue = false;
1177         }
1178     }
1180     // Make sure the name argument is set and valid.
1181     var nameregex = /[^a-z0-9_]/i;
1182     if (typeof args.name !== 'string') {
1183         args.name = '_blank';
1184     } else if (args.name.match(nameregex)) {
1185         // Cleans window name because IE does not support funky ones.
1186         if (M.cfg.developerdebug) {
1187             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1188         }
1189         args.name = args.name.replace(nameregex, '_');
1190     }
1192     var fullurl = args.url;
1193     if (!args.url.match(/https?:\/\//)) {
1194         fullurl = M.cfg.wwwroot + args.url;
1195     }
1196     if (args.fullscreen) {
1197         args.options = args.options.
1198                 replace(/top=\d+/, 'top=0').
1199                 replace(/left=\d+/, 'left=0').
1200                 replace(/width=\d+/, 'width=' + screen.availWidth).
1201                 replace(/height=\d+/, 'height=' + screen.availHeight);
1202     }
1203     var windowobj = window.open(fullurl,args.name,args.options);
1204     if (!windowobj) {
1205         return true;
1206     }
1208     if (args.fullscreen) {
1209         // In some browser / OS combinations (E.g. Chrome on Windows), the
1210         // window initially opens slighly too big. The width and heigh options
1211         // seem to control the area inside the browser window, so what with
1212         // scroll-bars, etc. the actual window is bigger than the screen.
1213         // Therefore, we need to fix things up after the window is open.
1214         var hackcount = 100;
1215         var get_size_exactly_right = function() {
1216             windowobj.moveTo(0, 0);
1217             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1219             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1220             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1221             // about 50ms) after the window is open, then it actually behaves
1222             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1223             // check that the resize actually worked, and if not, repeatedly try
1224             // again after a short delay until it works (but with a limit of
1225             // hackcount repeats.
1226             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1227                 hackcount -= 1;
1228                 setTimeout(get_size_exactly_right, 10);
1229             }
1230         }
1231         setTimeout(get_size_exactly_right, 0);
1232     }
1233     windowobj.focus();
1235     return false;
1238 /** Close the current browser window. */
1239 function close_window(e) {
1240     if (e.preventDefault) {
1241         e.preventDefault();
1242     } else {
1243         e.returnValue = false;
1244     }
1245     window.close();
1249  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1250  * @param controlid the control id.
1251  */
1252 function focuscontrol(controlid) {
1253     var control = document.getElementById(controlid);
1254     if (control) {
1255         control.focus();
1256     }
1260  * Transfers keyboard focus to an HTML element based on the old style style of focus
1261  * This function should be removed as soon as it is no longer used
1262  */
1263 function old_onload_focus(formid, controlname) {
1264     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1265         document.forms[formid].elements[controlname].focus();
1266     }
1269 function build_querystring(obj) {
1270     return convert_object_to_string(obj, '&');
1273 function build_windowoptionsstring(obj) {
1274     return convert_object_to_string(obj, ',');
1277 function convert_object_to_string(obj, separator) {
1278     if (typeof obj !== 'object') {
1279         return null;
1280     }
1281     var list = [];
1282     for(var k in obj) {
1283         k = encodeURIComponent(k);
1284         var value = obj[k];
1285         if(obj[k] instanceof Array) {
1286             for(var i in value) {
1287                 list.push(k+'[]='+encodeURIComponent(value[i]));
1288             }
1289         } else {
1290             list.push(k+'='+encodeURIComponent(value));
1291         }
1292     }
1293     return list.join(separator);
1296 function stripHTML(str) {
1297     var re = /<\S[^><]*>/g;
1298     var ret = str.replace(re, "");
1299     return ret;
1302 function updateProgressBar(id, percent, msg, estimate) {
1303     var event,
1304         el = document.getElementById(id),
1305         eventData = {};
1307     if (!el) {
1308         return;
1309     }
1311     eventData.message = msg;
1312     eventData.percent = percent;
1313     eventData.estimate = estimate;
1315     try {
1316         event = new CustomEvent('update', {
1317             bubbles: false,
1318             cancelable: true,
1319             detail: eventData
1320         });
1321     } catch (exception) {
1322         if (!(exception instanceof TypeError)) {
1323             throw exception;
1324         }
1325         event = document.createEvent('CustomEvent');
1326         event.initCustomEvent('update', false, true, eventData);
1327         event.prototype = window.Event.prototype;
1328     }
1330     el.dispatchEvent(event);
1333 // ===== Deprecated core Javascript functions for Moodle ====
1334 //       DO NOT USE!!!!!!!
1335 // Do not put this stuff in separate file because it only adds extra load on servers!
1338  * @method show_item
1339  * @deprecated since Moodle 2.7.
1340  * @see Y.Node.show
1341  */
1342 function show_item() {
1343     throw new Error('show_item can not be used any more. Please use Y.Node.show.');
1347  * @method destroy_item
1348  * @deprecated since Moodle 2.7.
1349  * @see Y.Node.destroy
1350  */
1351 function destroy_item() {
1352     throw new Error('destroy_item can not be used any more. Please use Y.Node.destroy.');
1356  * @method hide_item
1357  * @deprecated since Moodle 2.7.
1358  * @see Y.Node.hide
1359  */
1360 function hide_item() {
1361     throw new Error('hide_item can not be used any more. Please use Y.Node.hide.');
1365  * @method addonload
1366  * @deprecated since Moodle 2.7 - please do not use this function any more.
1367  */
1368 function addonload() {
1369     throw new Error('addonload can not be used any more.');
1373  * @method getElementsByClassName
1374  * @deprecated Since Moodle 2.7 - please do not use this function any more.
1375  * @see Y.one
1376  * @see Y.all
1377  */
1378 function getElementsByClassName() {
1379     throw new Error('getElementsByClassName can not be used any more. Please use Y.one or Y.all.');
1383  * @method findChildNodes
1384  * @deprecated since Moodle 2.7 - please do not use this function any more.
1385  * @see Y.all
1386  */
1387 function findChildNodes() {
1388     throw new Error('findChildNodes can not be used any more. Please use Y.all.');
1391 M.util.help_popups = {
1392     setup : function(Y) {
1393         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1394     },
1395     open_popup : function(e) {
1396         // Prevent the default page action
1397         e.preventDefault();
1399         // Grab the anchor that was clicked
1400         var anchor = e.target.ancestor('a', true);
1401         var args = {
1402             'name'          : 'popup',
1403             'url'           : anchor.getAttribute('href'),
1404             'options'       : ''
1405         };
1406         var options = [
1407             'height=600',
1408             'width=800',
1409             'top=0',
1410             'left=0',
1411             'menubar=0',
1412             'location=0',
1413             'scrollbars',
1414             'resizable',
1415             'toolbar',
1416             'status',
1417             'directories=0',
1418             'fullscreen=0',
1419             'dependent'
1420         ]
1421         args.options = options.join(',');
1423         openpopup(e, args);
1424     }
1428  * Custom menu namespace
1429  */
1430 M.core_custom_menu = {
1431     /**
1432      * This method is used to initialise a custom menu given the id that belongs
1433      * to the custom menu's root node.
1434      *
1435      * @param {YUI} Y
1436      * @param {string} nodeid
1437      */
1438     init : function(Y, nodeid) {
1439         var node = Y.one('#'+nodeid);
1440         if (node) {
1441             Y.use('node-menunav', function(Y) {
1442                 // Get the node
1443                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1444                 node.removeClass('javascript-disabled');
1445                 // Initialise the menunav plugin
1446                 node.plug(Y.Plugin.NodeMenuNav);
1447             });
1448         }
1449     }
1453  * Used to store form manipulation methods and enhancments
1454  */
1455 M.form = M.form || {};
1458  * Converts a nbsp indented select box into a multi drop down custom control much
1459  * like the custom menu. It also selectable categories on or off.
1461  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1463  * @param {YUI} Y
1464  * @param {string} id
1465  * @param {Array} options
1466  */
1467 M.form.init_smartselect = function(Y, id, options) {
1468     if (!id.match(/^id_/)) {
1469         id = 'id_'+id;
1470     }
1471     var select = Y.one('select#'+id);
1472     if (!select) {
1473         return false;
1474     }
1475     Y.use('event-delegate',function(){
1476         var smartselect = {
1477             id : id,
1478             structure : [],
1479             options : [],
1480             submenucount : 0,
1481             currentvalue : null,
1482             currenttext : null,
1483             shownevent : null,
1484             cfg : {
1485                 selectablecategories : true,
1486                 mode : null
1487             },
1488             nodes : {
1489                 select : null,
1490                 loading : null,
1491                 menu : null
1492             },
1493             init : function(Y, id, args, nodes) {
1494                 if (typeof(args)=='object') {
1495                     for (var i in this.cfg) {
1496                         if (args[i] || args[i]===false) {
1497                             this.cfg[i] = args[i];
1498                         }
1499                     }
1500                 }
1502                 // Display a loading message first up
1503                 this.nodes.select = nodes.select;
1505                 this.currentvalue = this.nodes.select.get('selectedIndex');
1506                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1508                 var options = Array();
1509                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1510                 this.nodes.select.all('option').each(function(option, index) {
1511                     var rawtext = option.get('innerHTML');
1512                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1513                     if (rawtext === text) {
1514                         text = rawtext.replace(/^(\s)*/, '');
1515                         var depth = (rawtext.length - text.length ) + 1;
1516                     } else {
1517                         var depth = ((rawtext.length - text.length )/12)+1;
1518                     }
1519                     option.set('innerHTML', text);
1520                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1521                 }, this);
1523                 this.structure = [];
1524                 var structcount = 0;
1525                 for (var i in options) {
1526                     var o = options[i];
1527                     if (o.depth == 0) {
1528                         this.structure.push(o);
1529                         structcount++;
1530                     } else {
1531                         var d = o.depth;
1532                         var current = this.structure[structcount-1];
1533                         for (var j = 0; j < o.depth-1;j++) {
1534                             if (current && current.children) {
1535                                 current = current.children[current.children.length-1];
1536                             }
1537                         }
1538                         if (current && current.children) {
1539                             current.children.push(o);
1540                         }
1541                     }
1542                 }
1544                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1545                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1546                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1547                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1549                 if (this.cfg.mode == null) {
1550                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1551                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1552                         this.cfg.mode = 'compact';
1553                     } else {
1554                         this.cfg.mode = 'spanning';
1555                     }
1556                 }
1558                 if (this.cfg.mode == 'compact') {
1559                     this.nodes.menu.addClass('compactmenu');
1560                 } else {
1561                     this.nodes.menu.addClass('spanningmenu');
1562                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1563                 }
1565                 Y.one(document.body).append(this.nodes.menu);
1566                 var pos = this.nodes.select.getXY();
1567                 pos[0] += 1;
1568                 this.nodes.menu.setXY(pos);
1569                 this.nodes.menu.on('click', this.handle_click, this);
1571                 Y.one(window).on('resize', function(){
1572                      var pos = this.nodes.select.getXY();
1573                     pos[0] += 1;
1574                     this.nodes.menu.setXY(pos);
1575                  }, this);
1576             },
1577             generate_menu_content : function() {
1578                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1579                 content += this.generate_submenu_content(this.structure[0], true);
1580                 content += '</ul></div>';
1581                 return content;
1582             },
1583             generate_submenu_content : function(item, rootelement) {
1584                 this.submenucount++;
1585                 var content = '';
1586                 if (item.children.length > 0) {
1587                     if (rootelement) {
1588                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1589                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1590                         content += '<div class="smartselect_menu_content">';
1591                     } else {
1592                         content += '<li class="smartselect_submenuitem">';
1593                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1594                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1595                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1596                         content += '<div class="smartselect_submenu_content">';
1597                     }
1598                     content += '<ul>';
1599                     for (var i in item.children) {
1600                         content += this.generate_submenu_content(item.children[i],false);
1601                     }
1602                     content += '</ul>';
1603                     content += '</div>';
1604                     content += '</div>';
1605                     if (rootelement) {
1606                     } else {
1607                         content += '</li>';
1608                     }
1609                 } else {
1610                     content += '<li class="smartselect_menuitem">';
1611                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1612                     content += '</li>';
1613                 }
1614                 return content;
1615             },
1616             select : function(e) {
1617                 var t = e.target;
1618                 e.halt();
1619                 this.currenttext = t.get('innerHTML');
1620                 this.currentvalue = t.getAttribute('value');
1621                 this.nodes.select.set('selectedIndex', this.currentvalue);
1622                 this.hide_menu();
1623             },
1624             handle_click : function(e) {
1625                 var target = e.target;
1626                 if (target.hasClass('smartselect_mask')) {
1627                     this.show_menu(e);
1628                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1629                     this.select(e);
1630                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1631                     this.show_sub_menu(e);
1632                 }
1633             },
1634             show_menu : function(e) {
1635                 e.halt();
1636                 var menu = e.target.ancestor().one('.smartselect_menu');
1637                 menu.addClass('visible');
1638                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1639             },
1640             show_sub_menu : function(e) {
1641                 e.halt();
1642                 var target = e.target;
1643                 if (!target.hasClass('smartselect_submenuitem')) {
1644                     target = target.ancestor('.smartselect_submenuitem');
1645                 }
1646                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1647                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1648                     return;
1649                 }
1650                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1651                 target.one('.smartselect_submenu').addClass('visible');
1652             },
1653             hide_menu : function() {
1654                 this.nodes.menu.all('.visible').removeClass('visible');
1655                 if (this.shownevent) {
1656                     this.shownevent.detach();
1657                 }
1658             }
1659         };
1660         smartselect.init(Y, id, options, {select:select});
1661     });
1664 /** List of flv players to be loaded */
1665 M.util.video_players = [];
1666 /** List of mp3 players to be loaded */
1667 M.util.audio_players = [];
1670  * Add video player
1671  * @param id element id
1672  * @param fileurl media url
1673  * @param width
1674  * @param height
1675  * @param autosize true means detect size from media
1676  */
1677 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1678     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1682  * Add audio player.
1683  * @param id
1684  * @param fileurl
1685  * @param small
1686  */
1687 M.util.add_audio_player = function (id, fileurl, small) {
1688     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1692  * Initialise all audio and video player, must be called from page footer.
1693  */
1694 M.util.load_flowplayer = function() {
1695     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1696         return;
1697     }
1698     if (typeof(flowplayer) == 'undefined') {
1699         var loaded = false;
1701         var embed_function = function() {
1702             if (loaded || typeof(flowplayer) == 'undefined') {
1703                 return;
1704             }
1705             loaded = true;
1707             var controls = {
1708                     url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1709                     autoHide: true
1710             }
1711             /* TODO: add CSS color overrides for the flv flow player */
1713             for(var i=0; i<M.util.video_players.length; i++) {
1714                 var video = M.util.video_players[i];
1715                 if (video.width > 0 && video.height > 0) {
1716                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', width: video.width, height: video.height};
1717                 } else {
1718                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php';
1719                 }
1720                 flowplayer(video.id, src, {
1721                     plugins: {controls: controls},
1722                     clip: {
1723                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1724                         onMetaData: function(clip) {
1725                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1726                                 clip.mvideo.resized = true;
1727                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1728                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1729                                     // bad luck, we have to guess - we may not get metadata at all
1730                                     var width = clip.width;
1731                                     var height = clip.height;
1732                                 } else {
1733                                     var width = clip.metaData.width;
1734                                     var height = clip.metaData.height;
1735                                 }
1736                                 var minwidth = 300; // controls are messed up in smaller objects
1737                                 if (width < minwidth) {
1738                                     height = (height * minwidth) / width;
1739                                     width = minwidth;
1740                                 }
1742                                 var object = this._api();
1743                                 object.width = width;
1744                                 object.height = height;
1745                             }
1746                         }
1747                     }
1748                 });
1749             }
1750             if (M.util.audio_players.length == 0) {
1751                 return;
1752             }
1753             var controls = {
1754                     url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1755                     autoHide: false,
1756                     fullscreen: false,
1757                     next: false,
1758                     previous: false,
1759                     scrubber: true,
1760                     play: true,
1761                     pause: true,
1762                     volume: true,
1763                     mute: false,
1764                     backgroundGradient: [0.5,0,0.3]
1765                 };
1767             var rule;
1768             for (var j=0; j < document.styleSheets.length; j++) {
1770                 // To avoid javascript security violation accessing cross domain stylesheets
1771                 var allrules = false;
1772                 try {
1773                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1774                         allrules = document.styleSheets[j].rules;
1775                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1776                         allrules = document.styleSheets[j].cssRules;
1777                     } else {
1778                         // why??
1779                         continue;
1780                     }
1781                 } catch (e) {
1782                     continue;
1783                 }
1785                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1786                 if (!allrules) {
1787                     continue;
1788                 }
1790                 for(var i=0; i<allrules.length; i++) {
1791                     rule = '';
1792                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1793                         if (typeof(allrules[i].cssText) != 'undefined') {
1794                             rule = allrules[i].cssText;
1795                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1796                             rule = allrules[i].style.cssText;
1797                         }
1798                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1799                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1800                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1801                             controls[colprop] = rule;
1802                         }
1803                     }
1804                 }
1805                 allrules = false;
1806             }
1808             for(i=0; i<M.util.audio_players.length; i++) {
1809                 var audio = M.util.audio_players[i];
1810                 if (audio.small) {
1811                     controls.controlall = false;
1812                     controls.height = 15;
1813                     controls.time = false;
1814                 } else {
1815                     controls.controlall = true;
1816                     controls.height = 25;
1817                     controls.time = true;
1818                 }
1819                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', {
1820                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf.php'}},
1821                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1822                 });
1823             }
1824         }
1826         if (M.cfg.jsrev == -1) {
1827             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1828         } else {
1829             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1830         }
1831         var fileref = document.createElement('script');
1832         fileref.setAttribute('type','text/javascript');
1833         fileref.setAttribute('src', jsurl);
1834         fileref.onload = embed_function;
1835         fileref.onreadystatechange = embed_function;
1836         document.getElementsByTagName('head')[0].appendChild(fileref);
1837     }
1841  * Initiates the listeners for skiplink interaction
1843  * @param {YUI} Y
1844  */
1845 M.util.init_skiplink = function(Y) {
1846     Y.one(Y.config.doc.body).delegate('click', function(e) {
1847         e.preventDefault();
1848         e.stopPropagation();
1849         var node = Y.one(this.getAttribute('href'));
1850         node.setAttribute('tabindex', '-1');
1851         node.focus();
1852         return true;
1853     }, 'a.skip');