MDL-52927 qtype ddwtos and ddimageortext focus using keyboard
[moodle.git] / lib / javascript-static.js
blobe5597698ee34b0418bf0d5d1ff32e737a09182bd
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')) {
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, 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                     } else {
638                         this.get('block').removeClass('hidden');
639                     }
640                 },
641                 updateStateKey : function(e, hide) {
642                     if (e.keyCode == 13) { //allow hide/show via enter key
643                         this.updateState(this, hide);
644                     }
645                 }
646             };
647             Y.extend(blockhider, Y.Base, blockhider.prototype, {
648                 NAME : 'blockhider',
649                 ATTRS : {
650                     id : {},
651                     preference : {},
652                     iconVisible : {
653                         value : M.util.image_url('t/switch_minus', 'moodle')
654                     },
655                     iconHidden : {
656                         value : M.util.image_url('t/switch_plus', 'moodle')
657                     },
658                     block : {
659                         setter : function(node) {
660                             return Y.one(node);
661                         }
662                     }
663                 }
664             });
665             return blockhider;
666         })();
667         new M.util.block_hider(config);
668     });
672  * @var pending_js - The keys are the list of all pending js actions.
673  * @type Object
674  */
675 M.util.pending_js = [];
676 M.util.complete_js = [];
679  * Register any long running javascript code with a unique identifier.
680  * Should be followed with a call to js_complete with a matching
681  * idenfitier when the code is complete. May also be called with no arguments
682  * to test if there is any js calls pending. This is relied on by behat so that
683  * it can wait for all pending updates before interacting with a page.
684  * @param String uniqid - optional, if provided,
685  *                        registers this identifier until js_complete is called.
686  * @return boolean - True if there is any pending js.
687  */
688 M.util.js_pending = function(uniqid) {
689     if (uniqid !== false) {
690         M.util.pending_js.push(uniqid);
691     }
693     return M.util.pending_js.length;
696 // Start this asap.
697 M.util.js_pending('init');
700  * Register listeners for Y.io start/end so we can wait for them in behat.
701  */
702 YUI.add('moodle-core-io', function(Y) {
703     Y.on('io:start', function(id) {
704         M.util.js_pending('io:' + id);
705     });
706     Y.on('io:end', function(id) {
707         M.util.js_complete('io:' + id);
708     });
709 }, '@VERSION@', {
710     condition: {
711         trigger: 'io-base',
712         when: 'after'
713     }
717  * Unregister any long running javascript code by unique identifier.
718  * This function should form a matching pair with js_pending
720  * @param String uniqid - required, unregisters this identifier
721  * @return boolean - True if there is any pending js.
722  */
723 M.util.js_complete = function(uniqid) {
724     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
725     // the native function. Y.Array polyfills the native function if it does not exist.
726     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
727     if (index >= 0) {
728         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
729     }
731     return M.util.pending_js.length;
735  * Returns a string registered in advance for usage in JavaScript
737  * If you do not pass the third parameter, the function will just return
738  * the corresponding value from the M.str object. If the third parameter is
739  * provided, the function performs {$a} placeholder substitution in the
740  * same way as PHP get_string() in Moodle does.
742  * @param {String} identifier string identifier
743  * @param {String} component the component providing the string
744  * @param {Object|String} a optional variable to populate placeholder with
745  */
746 M.util.get_string = function(identifier, component, a) {
747     var stringvalue;
749     if (M.cfg.developerdebug) {
750         // creating new instance if YUI is not optimal but it seems to be better way then
751         // require the instance via the function API - note that it is used in rare cases
752         // for debugging only anyway
753         // To ensure we don't kill browser performance if hundreds of get_string requests
754         // are made we cache the instance we generate within the M.util namespace.
755         // We don't publicly define the variable so that it doesn't get abused.
756         if (typeof M.util.get_string_yui_instance === 'undefined') {
757             M.util.get_string_yui_instance = new YUI({ debug : true });
758         }
759         var Y = M.util.get_string_yui_instance;
760     }
762     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
763         stringvalue = '[[' + identifier + ',' + component + ']]';
764         if (M.cfg.developerdebug) {
765             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
766         }
767         return stringvalue;
768     }
770     stringvalue = M.str[component][identifier];
772     if (typeof a == 'undefined') {
773         // no placeholder substitution requested
774         return stringvalue;
775     }
777     if (typeof a == 'number' || typeof a == 'string') {
778         // replace all occurrences of {$a} with the placeholder value
779         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
780         return stringvalue;
781     }
783     if (typeof a == 'object') {
784         // replace {$a->key} placeholders
785         for (var key in a) {
786             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
787                 if (M.cfg.developerdebug) {
788                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
789                 }
790                 continue;
791             }
792             var search = '{$a->' + key + '}';
793             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
794             search = new RegExp(search, 'g');
795             stringvalue = stringvalue.replace(search, a[key]);
796         }
797         return stringvalue;
798     }
800     if (M.cfg.developerdebug) {
801         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
802     }
803     return stringvalue;
807  * Set focus on username or password field of the login form
808  */
809 M.util.focus_login_form = function(Y) {
810     var username = Y.one('#username');
811     var password = Y.one('#password');
813     if (username == null || password == null) {
814         // something is wrong here
815         return;
816     }
818     var curElement = document.activeElement
819     if (curElement == 'undefined') {
820         // legacy browser - skip refocus protection
821     } else if (curElement.tagName == 'INPUT') {
822         // user was probably faster to focus something, do not mess with focus
823         return;
824     }
826     if (username.get('value') == '') {
827         username.focus();
828     } else {
829         password.focus();
830     }
834  * Set focus on login error message
835  */
836 M.util.focus_login_error = function(Y) {
837     var errorlog = Y.one('#loginerrormessage');
839     if (errorlog) {
840         errorlog.focus();
841     }
844  * Adds lightbox hidden element that covers the whole node.
846  * @param {YUI} Y
847  * @param {Node} the node lightbox should be added to
848  * @retun {Node} created lightbox node
849  */
850 M.util.add_lightbox = function(Y, node) {
851     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
853     // Check if lightbox is already there
854     if (node.one('.lightbox')) {
855         return node.one('.lightbox');
856     }
858     node.setStyle('position', 'relative');
859     var waiticon = Y.Node.create('<img />')
860     .setAttrs({
861         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
862     })
863     .setStyles({
864         'position' : 'relative',
865         'top' : '50%'
866     });
868     var lightbox = Y.Node.create('<div></div>')
869     .setStyles({
870         'opacity' : '.75',
871         'position' : 'absolute',
872         'width' : '100%',
873         'height' : '100%',
874         'top' : 0,
875         'left' : 0,
876         'backgroundColor' : 'white',
877         'textAlign' : 'center'
878     })
879     .setAttribute('class', 'lightbox')
880     .hide();
882     lightbox.appendChild(waiticon);
883     node.append(lightbox);
884     return lightbox;
888  * Appends a hidden spinner element to the specified node.
890  * @param {YUI} Y
891  * @param {Node} the node the spinner should be added to
892  * @return {Node} created spinner node
893  */
894 M.util.add_spinner = function(Y, node) {
895     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
897     // Check if spinner is already there
898     if (node.one('.spinner')) {
899         return node.one('.spinner');
900     }
902     var spinner = Y.Node.create('<img />')
903         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
904         .addClass('spinner')
905         .addClass('iconsmall')
906         .hide();
908     node.append(spinner);
909     return spinner;
912 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
914 function checkall() {
915     var inputs = document.getElementsByTagName('input');
916     for (var i = 0; i < inputs.length; i++) {
917         if (inputs[i].type == 'checkbox') {
918             if (inputs[i].disabled || inputs[i].readOnly) {
919                 continue;
920             }
921             inputs[i].checked = true;
922         }
923     }
926 function checknone() {
927     var inputs = document.getElementsByTagName('input');
928     for (var i = 0; i < inputs.length; i++) {
929         if (inputs[i].type == 'checkbox') {
930             if (inputs[i].disabled || inputs[i].readOnly) {
931                 continue;
932             }
933             inputs[i].checked = false;
934         }
935     }
939  * Either check, or uncheck, all checkboxes inside the element with id is
940  * @param id the id of the container
941  * @param checked the new state, either '' or 'checked'.
942  */
943 function select_all_in_element_with_id(id, checked) {
944     var container = document.getElementById(id);
945     if (!container) {
946         return;
947     }
948     var inputs = container.getElementsByTagName('input');
949     for (var i = 0; i < inputs.length; ++i) {
950         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
951             inputs[i].checked = checked;
952         }
953     }
956 function select_all_in(elTagName, elClass, elId) {
957     var inputs = document.getElementsByTagName('input');
958     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
959     for(var i = 0; i < inputs.length; ++i) {
960         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
961             inputs[i].checked = 'checked';
962         }
963     }
966 function deselect_all_in(elTagName, elClass, elId) {
967     var inputs = document.getElementsByTagName('INPUT');
968     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
969     for(var i = 0; i < inputs.length; ++i) {
970         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
971             inputs[i].checked = '';
972         }
973     }
976 function confirm_if(expr, message) {
977     if(!expr) {
978         return true;
979     }
980     return confirm(message);
985     findParentNode (start, elementName, elementClass, elementID)
987     Travels up the DOM hierarchy to find a parent element with the
988     specified tag name, class, and id. All conditions must be met,
989     but any can be ommitted. Returns the BODY element if no match
990     found.
992 function findParentNode(el, elName, elClass, elId) {
993     while (el.nodeName.toUpperCase() != 'BODY') {
994         if ((!elName || el.nodeName.toUpperCase() == elName) &&
995             (!elClass || el.className.indexOf(elClass) != -1) &&
996             (!elId || el.id == elId)) {
997             break;
998         }
999         el = el.parentNode;
1000     }
1001     return el;
1004 function unmaskPassword(id) {
1005     var pw = document.getElementById(id);
1006     var chb = document.getElementById(id+'unmask');
1008     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1009     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1010     // functionality won't work in IE8 or lower.
1011     // This is a temporary fixed to allow other browsers to function properly.
1012     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1013         if (chb.checked) {
1014             pw.type = "text";
1015         } else {
1016             pw.type = "password";
1017         }
1018     } else {  //IE Browser version 8 or lower
1019         try {
1020             // first try IE way - it can not set name attribute later
1021             if (chb.checked) {
1022               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1023             } else {
1024               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1025             }
1026             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1027         } catch (e) {
1028             var newpw = document.createElement('input');
1029             newpw.setAttribute('autocomplete', 'off');
1030             newpw.setAttribute('name', pw.name);
1031             if (chb.checked) {
1032               newpw.setAttribute('type', 'text');
1033             } else {
1034               newpw.setAttribute('type', 'password');
1035             }
1036             newpw.setAttribute('class', pw.getAttribute('class'));
1037         }
1038         newpw.id = pw.id;
1039         newpw.size = pw.size;
1040         newpw.onblur = pw.onblur;
1041         newpw.onchange = pw.onchange;
1042         newpw.value = pw.value;
1043         pw.parentNode.replaceChild(newpw, pw);
1044     }
1047 function filterByParent(elCollection, parentFinder) {
1048     var filteredCollection = [];
1049     for (var i = 0; i < elCollection.length; ++i) {
1050         var findParent = parentFinder(elCollection[i]);
1051         if (findParent.nodeName.toUpperCase() != 'BODY') {
1052             filteredCollection.push(elCollection[i]);
1053         }
1054     }
1055     return filteredCollection;
1059     All this is here just so that IE gets to handle oversized blocks
1060     in a visually pleasing manner. It does a browser detect. So sue me.
1063 function fix_column_widths() {
1064     var agt = navigator.userAgent.toLowerCase();
1065     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1066         fix_column_width('left-column');
1067         fix_column_width('right-column');
1068     }
1071 function fix_column_width(colName) {
1072     if(column = document.getElementById(colName)) {
1073         if(!column.offsetWidth) {
1074             setTimeout("fix_column_width('" + colName + "')", 20);
1075             return;
1076         }
1078         var width = 0;
1079         var nodes = column.childNodes;
1081         for(i = 0; i < nodes.length; ++i) {
1082             if(nodes[i].className.indexOf("block") != -1 ) {
1083                 if(width < nodes[i].offsetWidth) {
1084                     width = nodes[i].offsetWidth;
1085                 }
1086             }
1087         }
1089         for(i = 0; i < nodes.length; ++i) {
1090             if(nodes[i].className.indexOf("block") != -1 ) {
1091                 nodes[i].style.width = width + 'px';
1092             }
1093         }
1094     }
1099    Insert myValue at current cursor position
1100  */
1101 function insertAtCursor(myField, myValue) {
1102     // IE support
1103     if (document.selection) {
1104         myField.focus();
1105         sel = document.selection.createRange();
1106         sel.text = myValue;
1107     }
1108     // Mozilla/Netscape support
1109     else if (myField.selectionStart || myField.selectionStart == '0') {
1110         var startPos = myField.selectionStart;
1111         var endPos = myField.selectionEnd;
1112         myField.value = myField.value.substring(0, startPos)
1113             + myValue + myField.value.substring(endPos, myField.value.length);
1114     } else {
1115         myField.value += myValue;
1116     }
1120  * Increment a file name.
1122  * @param string file name.
1123  * @param boolean ignoreextension do not extract the extension prior to appending the
1124  *                                suffix. Useful when incrementing folder names.
1125  * @return string the incremented file name.
1126  */
1127 function increment_filename(filename, ignoreextension) {
1128     var extension = '';
1129     var basename = filename;
1131     // Split the file name into the basename + extension.
1132     if (!ignoreextension) {
1133         var dotpos = filename.lastIndexOf('.');
1134         if (dotpos !== -1) {
1135             basename = filename.substr(0, dotpos);
1136             extension = filename.substr(dotpos, filename.length);
1137         }
1138     }
1140     // Look to see if the name already has (NN) at the end of it.
1141     var number = 0;
1142     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1143     if (hasnumber !== null) {
1144         // Note the current number & remove it from the basename.
1145         number = parseInt(hasnumber[2], 10);
1146         basename = hasnumber[1];
1147     }
1149     number++;
1150     var newname = basename + ' (' + number + ')' + extension;
1151     return newname;
1155  * Return whether we are in right to left mode or not.
1157  * @return boolean
1158  */
1159 function right_to_left() {
1160     var body = Y.one('body');
1161     var rtl = false;
1162     if (body && body.hasClass('dir-rtl')) {
1163         rtl = true;
1164     }
1165     return rtl;
1168 function openpopup(event, args) {
1170     if (event) {
1171         if (event.preventDefault) {
1172             event.preventDefault();
1173         } else {
1174             event.returnValue = false;
1175         }
1176     }
1178     // Make sure the name argument is set and valid.
1179     var nameregex = /[^a-z0-9_]/i;
1180     if (typeof args.name !== 'string') {
1181         args.name = '_blank';
1182     } else if (args.name.match(nameregex)) {
1183         // Cleans window name because IE does not support funky ones.
1184         if (M.cfg.developerdebug) {
1185             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1186         }
1187         args.name = args.name.replace(nameregex, '_');
1188     }
1190     var fullurl = args.url;
1191     if (!args.url.match(/https?:\/\//)) {
1192         fullurl = M.cfg.wwwroot + args.url;
1193     }
1194     if (args.fullscreen) {
1195         args.options = args.options.
1196                 replace(/top=\d+/, 'top=0').
1197                 replace(/left=\d+/, 'left=0').
1198                 replace(/width=\d+/, 'width=' + screen.availWidth).
1199                 replace(/height=\d+/, 'height=' + screen.availHeight);
1200     }
1201     var windowobj = window.open(fullurl,args.name,args.options);
1202     if (!windowobj) {
1203         return true;
1204     }
1206     if (args.fullscreen) {
1207         // In some browser / OS combinations (E.g. Chrome on Windows), the
1208         // window initially opens slighly too big. The width and heigh options
1209         // seem to control the area inside the browser window, so what with
1210         // scroll-bars, etc. the actual window is bigger than the screen.
1211         // Therefore, we need to fix things up after the window is open.
1212         var hackcount = 100;
1213         var get_size_exactly_right = function() {
1214             windowobj.moveTo(0, 0);
1215             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1217             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1218             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1219             // about 50ms) after the window is open, then it actually behaves
1220             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1221             // check that the resize actually worked, and if not, repeatedly try
1222             // again after a short delay until it works (but with a limit of
1223             // hackcount repeats.
1224             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1225                 hackcount -= 1;
1226                 setTimeout(get_size_exactly_right, 10);
1227             }
1228         }
1229         setTimeout(get_size_exactly_right, 0);
1230     }
1231     windowobj.focus();
1233     return false;
1236 /** Close the current browser window. */
1237 function close_window(e) {
1238     if (e.preventDefault) {
1239         e.preventDefault();
1240     } else {
1241         e.returnValue = false;
1242     }
1243     window.close();
1247  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1248  * @param controlid the control id.
1249  */
1250 function focuscontrol(controlid) {
1251     var control = document.getElementById(controlid);
1252     if (control) {
1253         control.focus();
1254     }
1258  * Transfers keyboard focus to an HTML element based on the old style style of focus
1259  * This function should be removed as soon as it is no longer used
1260  */
1261 function old_onload_focus(formid, controlname) {
1262     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1263         document.forms[formid].elements[controlname].focus();
1264     }
1267 function build_querystring(obj) {
1268     return convert_object_to_string(obj, '&');
1271 function build_windowoptionsstring(obj) {
1272     return convert_object_to_string(obj, ',');
1275 function convert_object_to_string(obj, separator) {
1276     if (typeof obj !== 'object') {
1277         return null;
1278     }
1279     var list = [];
1280     for(var k in obj) {
1281         k = encodeURIComponent(k);
1282         var value = obj[k];
1283         if(obj[k] instanceof Array) {
1284             for(var i in value) {
1285                 list.push(k+'[]='+encodeURIComponent(value[i]));
1286             }
1287         } else {
1288             list.push(k+'='+encodeURIComponent(value));
1289         }
1290     }
1291     return list.join(separator);
1294 function stripHTML(str) {
1295     var re = /<\S[^><]*>/g;
1296     var ret = str.replace(re, "");
1297     return ret;
1300 function updateProgressBar(id, percent, msg, estimate) {
1301     var progressIndicator = Y.one('#' + id);
1302     if (!progressIndicator) {
1303         return;
1304     }
1306     var progressBar = progressIndicator.one('.bar'),
1307         statusIndicator = progressIndicator.one('h2'),
1308         estimateIndicator = progressIndicator.one('p');
1310     statusIndicator.set('innerHTML', Y.Escape.html(msg));
1311     progressBar.set('innerHTML', Y.Escape.html('' + percent + '%'));
1312     if (percent === 100) {
1313         progressIndicator.addClass('progress-success');
1314         estimateIndicator.set('innerHTML', null);
1315     } else {
1316         if (estimate) {
1317             estimateIndicator.set('innerHTML', Y.Escape.html(estimate));
1318         } else {
1319             estimateIndicator.set('innerHTML', null);
1320         }
1321         progressIndicator.removeClass('progress-success');
1322     }
1323     progressBar.setAttribute('aria-valuenow', percent);
1324     progressBar.setStyle('width', percent + '%');
1327 // ===== Deprecated core Javascript functions for Moodle ====
1328 //       DO NOT USE!!!!!!!
1329 // Do not put this stuff in separate file because it only adds extra load on servers!
1332  * @method show_item
1333  * @deprecated since Moodle 2.7.
1334  * @see Y.Node.show
1335  */
1336 function show_item() {
1337     throw new Error('show_item can not be used any more. Please use Y.Node.show.');
1341  * @method destroy_item
1342  * @deprecated since Moodle 2.7.
1343  * @see Y.Node.destroy
1344  */
1345 function destroy_item() {
1346     throw new Error('destroy_item can not be used any more. Please use Y.Node.destroy.');
1350  * @method hide_item
1351  * @deprecated since Moodle 2.7.
1352  * @see Y.Node.hide
1353  */
1354 function hide_item() {
1355     throw new Error('hide_item can not be used any more. Please use Y.Node.hide.');
1359  * @method addonload
1360  * @deprecated since Moodle 2.7 - please do not use this function any more.
1361  */
1362 function addonload() {
1363     throw new Error('addonload can not be used any more.');
1367  * @method getElementsByClassName
1368  * @deprecated Since Moodle 2.7 - please do not use this function any more.
1369  * @see Y.one
1370  * @see Y.all
1371  */
1372 function getElementsByClassName() {
1373     throw new Error('getElementsByClassName can not be used any more. Please use Y.one or Y.all.');
1377  * @method findChildNodes
1378  * @deprecated since Moodle 2.7 - please do not use this function any more.
1379  * @see Y.all
1380  */
1381 function findChildNodes() {
1382     throw new Error('findChildNodes can not be used any more. Please use Y.all.');
1385 M.util.help_popups = {
1386     setup : function(Y) {
1387         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1388     },
1389     open_popup : function(e) {
1390         // Prevent the default page action
1391         e.preventDefault();
1393         // Grab the anchor that was clicked
1394         var anchor = e.target.ancestor('a', true);
1395         var args = {
1396             'name'          : 'popup',
1397             'url'           : anchor.getAttribute('href'),
1398             'options'       : ''
1399         };
1400         var options = [
1401             'height=600',
1402             'width=800',
1403             'top=0',
1404             'left=0',
1405             'menubar=0',
1406             'location=0',
1407             'scrollbars',
1408             'resizable',
1409             'toolbar',
1410             'status',
1411             'directories=0',
1412             'fullscreen=0',
1413             'dependent'
1414         ]
1415         args.options = options.join(',');
1417         openpopup(e, args);
1418     }
1422  * Custom menu namespace
1423  */
1424 M.core_custom_menu = {
1425     /**
1426      * This method is used to initialise a custom menu given the id that belongs
1427      * to the custom menu's root node.
1428      *
1429      * @param {YUI} Y
1430      * @param {string} nodeid
1431      */
1432     init : function(Y, nodeid) {
1433         var node = Y.one('#'+nodeid);
1434         if (node) {
1435             Y.use('node-menunav', function(Y) {
1436                 // Get the node
1437                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1438                 node.removeClass('javascript-disabled');
1439                 // Initialise the menunav plugin
1440                 node.plug(Y.Plugin.NodeMenuNav);
1441             });
1442         }
1443     }
1447  * Used to store form manipulation methods and enhancments
1448  */
1449 M.form = M.form || {};
1452  * Converts a nbsp indented select box into a multi drop down custom control much
1453  * like the custom menu. It also selectable categories on or off.
1455  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1457  * @param {YUI} Y
1458  * @param {string} id
1459  * @param {Array} options
1460  */
1461 M.form.init_smartselect = function(Y, id, options) {
1462     if (!id.match(/^id_/)) {
1463         id = 'id_'+id;
1464     }
1465     var select = Y.one('select#'+id);
1466     if (!select) {
1467         return false;
1468     }
1469     Y.use('event-delegate',function(){
1470         var smartselect = {
1471             id : id,
1472             structure : [],
1473             options : [],
1474             submenucount : 0,
1475             currentvalue : null,
1476             currenttext : null,
1477             shownevent : null,
1478             cfg : {
1479                 selectablecategories : true,
1480                 mode : null
1481             },
1482             nodes : {
1483                 select : null,
1484                 loading : null,
1485                 menu : null
1486             },
1487             init : function(Y, id, args, nodes) {
1488                 if (typeof(args)=='object') {
1489                     for (var i in this.cfg) {
1490                         if (args[i] || args[i]===false) {
1491                             this.cfg[i] = args[i];
1492                         }
1493                     }
1494                 }
1496                 // Display a loading message first up
1497                 this.nodes.select = nodes.select;
1499                 this.currentvalue = this.nodes.select.get('selectedIndex');
1500                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1502                 var options = Array();
1503                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1504                 this.nodes.select.all('option').each(function(option, index) {
1505                     var rawtext = option.get('innerHTML');
1506                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1507                     if (rawtext === text) {
1508                         text = rawtext.replace(/^(\s)*/, '');
1509                         var depth = (rawtext.length - text.length ) + 1;
1510                     } else {
1511                         var depth = ((rawtext.length - text.length )/12)+1;
1512                     }
1513                     option.set('innerHTML', text);
1514                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1515                 }, this);
1517                 this.structure = [];
1518                 var structcount = 0;
1519                 for (var i in options) {
1520                     var o = options[i];
1521                     if (o.depth == 0) {
1522                         this.structure.push(o);
1523                         structcount++;
1524                     } else {
1525                         var d = o.depth;
1526                         var current = this.structure[structcount-1];
1527                         for (var j = 0; j < o.depth-1;j++) {
1528                             if (current && current.children) {
1529                                 current = current.children[current.children.length-1];
1530                             }
1531                         }
1532                         if (current && current.children) {
1533                             current.children.push(o);
1534                         }
1535                     }
1536                 }
1538                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1539                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1540                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1541                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1543                 if (this.cfg.mode == null) {
1544                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1545                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1546                         this.cfg.mode = 'compact';
1547                     } else {
1548                         this.cfg.mode = 'spanning';
1549                     }
1550                 }
1552                 if (this.cfg.mode == 'compact') {
1553                     this.nodes.menu.addClass('compactmenu');
1554                 } else {
1555                     this.nodes.menu.addClass('spanningmenu');
1556                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1557                 }
1559                 Y.one(document.body).append(this.nodes.menu);
1560                 var pos = this.nodes.select.getXY();
1561                 pos[0] += 1;
1562                 this.nodes.menu.setXY(pos);
1563                 this.nodes.menu.on('click', this.handle_click, this);
1565                 Y.one(window).on('resize', function(){
1566                      var pos = this.nodes.select.getXY();
1567                     pos[0] += 1;
1568                     this.nodes.menu.setXY(pos);
1569                  }, this);
1570             },
1571             generate_menu_content : function() {
1572                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1573                 content += this.generate_submenu_content(this.structure[0], true);
1574                 content += '</ul></div>';
1575                 return content;
1576             },
1577             generate_submenu_content : function(item, rootelement) {
1578                 this.submenucount++;
1579                 var content = '';
1580                 if (item.children.length > 0) {
1581                     if (rootelement) {
1582                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1583                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1584                         content += '<div class="smartselect_menu_content">';
1585                     } else {
1586                         content += '<li class="smartselect_submenuitem">';
1587                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1588                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1589                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1590                         content += '<div class="smartselect_submenu_content">';
1591                     }
1592                     content += '<ul>';
1593                     for (var i in item.children) {
1594                         content += this.generate_submenu_content(item.children[i],false);
1595                     }
1596                     content += '</ul>';
1597                     content += '</div>';
1598                     content += '</div>';
1599                     if (rootelement) {
1600                     } else {
1601                         content += '</li>';
1602                     }
1603                 } else {
1604                     content += '<li class="smartselect_menuitem">';
1605                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1606                     content += '</li>';
1607                 }
1608                 return content;
1609             },
1610             select : function(e) {
1611                 var t = e.target;
1612                 e.halt();
1613                 this.currenttext = t.get('innerHTML');
1614                 this.currentvalue = t.getAttribute('value');
1615                 this.nodes.select.set('selectedIndex', this.currentvalue);
1616                 this.hide_menu();
1617             },
1618             handle_click : function(e) {
1619                 var target = e.target;
1620                 if (target.hasClass('smartselect_mask')) {
1621                     this.show_menu(e);
1622                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1623                     this.select(e);
1624                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1625                     this.show_sub_menu(e);
1626                 }
1627             },
1628             show_menu : function(e) {
1629                 e.halt();
1630                 var menu = e.target.ancestor().one('.smartselect_menu');
1631                 menu.addClass('visible');
1632                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1633             },
1634             show_sub_menu : function(e) {
1635                 e.halt();
1636                 var target = e.target;
1637                 if (!target.hasClass('smartselect_submenuitem')) {
1638                     target = target.ancestor('.smartselect_submenuitem');
1639                 }
1640                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1641                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1642                     return;
1643                 }
1644                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1645                 target.one('.smartselect_submenu').addClass('visible');
1646             },
1647             hide_menu : function() {
1648                 this.nodes.menu.all('.visible').removeClass('visible');
1649                 if (this.shownevent) {
1650                     this.shownevent.detach();
1651                 }
1652             }
1653         };
1654         smartselect.init(Y, id, options, {select:select});
1655     });
1658 /** List of flv players to be loaded */
1659 M.util.video_players = [];
1660 /** List of mp3 players to be loaded */
1661 M.util.audio_players = [];
1664  * Add video player
1665  * @param id element id
1666  * @param fileurl media url
1667  * @param width
1668  * @param height
1669  * @param autosize true means detect size from media
1670  */
1671 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1672     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1676  * Add audio player.
1677  * @param id
1678  * @param fileurl
1679  * @param small
1680  */
1681 M.util.add_audio_player = function (id, fileurl, small) {
1682     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1686  * Initialise all audio and video player, must be called from page footer.
1687  */
1688 M.util.load_flowplayer = function() {
1689     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1690         return;
1691     }
1692     if (typeof(flowplayer) == 'undefined') {
1693         var loaded = false;
1695         var embed_function = function() {
1696             if (loaded || typeof(flowplayer) == 'undefined') {
1697                 return;
1698             }
1699             loaded = true;
1701             var controls = {
1702                     url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1703                     autoHide: true
1704             }
1705             /* TODO: add CSS color overrides for the flv flow player */
1707             for(var i=0; i<M.util.video_players.length; i++) {
1708                 var video = M.util.video_players[i];
1709                 if (video.width > 0 && video.height > 0) {
1710                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', width: video.width, height: video.height};
1711                 } else {
1712                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php';
1713                 }
1714                 flowplayer(video.id, src, {
1715                     plugins: {controls: controls},
1716                     clip: {
1717                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1718                         onMetaData: function(clip) {
1719                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1720                                 clip.mvideo.resized = true;
1721                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1722                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1723                                     // bad luck, we have to guess - we may not get metadata at all
1724                                     var width = clip.width;
1725                                     var height = clip.height;
1726                                 } else {
1727                                     var width = clip.metaData.width;
1728                                     var height = clip.metaData.height;
1729                                 }
1730                                 var minwidth = 300; // controls are messed up in smaller objects
1731                                 if (width < minwidth) {
1732                                     height = (height * minwidth) / width;
1733                                     width = minwidth;
1734                                 }
1736                                 var object = this._api();
1737                                 object.width = width;
1738                                 object.height = height;
1739                             }
1740                         }
1741                     }
1742                 });
1743             }
1744             if (M.util.audio_players.length == 0) {
1745                 return;
1746             }
1747             var controls = {
1748                     url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1749                     autoHide: false,
1750                     fullscreen: false,
1751                     next: false,
1752                     previous: false,
1753                     scrubber: true,
1754                     play: true,
1755                     pause: true,
1756                     volume: true,
1757                     mute: false,
1758                     backgroundGradient: [0.5,0,0.3]
1759                 };
1761             var rule;
1762             for (var j=0; j < document.styleSheets.length; j++) {
1764                 // To avoid javascript security violation accessing cross domain stylesheets
1765                 var allrules = false;
1766                 try {
1767                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1768                         allrules = document.styleSheets[j].rules;
1769                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1770                         allrules = document.styleSheets[j].cssRules;
1771                     } else {
1772                         // why??
1773                         continue;
1774                     }
1775                 } catch (e) {
1776                     continue;
1777                 }
1779                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1780                 if (!allrules) {
1781                     continue;
1782                 }
1784                 for(var i=0; i<allrules.length; i++) {
1785                     rule = '';
1786                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1787                         if (typeof(allrules[i].cssText) != 'undefined') {
1788                             rule = allrules[i].cssText;
1789                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1790                             rule = allrules[i].style.cssText;
1791                         }
1792                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1793                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1794                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1795                             controls[colprop] = rule;
1796                         }
1797                     }
1798                 }
1799                 allrules = false;
1800             }
1802             for(i=0; i<M.util.audio_players.length; i++) {
1803                 var audio = M.util.audio_players[i];
1804                 if (audio.small) {
1805                     controls.controlall = false;
1806                     controls.height = 15;
1807                     controls.time = false;
1808                 } else {
1809                     controls.controlall = true;
1810                     controls.height = 25;
1811                     controls.time = true;
1812                 }
1813                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', {
1814                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf.php'}},
1815                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1816                 });
1817             }
1818         }
1820         if (M.cfg.jsrev == -1) {
1821             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1822         } else {
1823             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1824         }
1825         var fileref = document.createElement('script');
1826         fileref.setAttribute('type','text/javascript');
1827         fileref.setAttribute('src', jsurl);
1828         fileref.onload = embed_function;
1829         fileref.onreadystatechange = embed_function;
1830         document.getElementsByTagName('head')[0].appendChild(fileref);
1831     }
1835  * Initiates the listeners for skiplink interaction
1837  * @param {YUI} Y
1838  */
1839 M.util.init_skiplink = function(Y) {
1840     Y.one(Y.config.doc.body).delegate('click', function(e) {
1841         e.preventDefault();
1842         e.stopPropagation();
1843         var node = Y.one(this.getAttribute('href'));
1844         node.setAttribute('tabindex', '-1');
1845         node.focus();
1846         return true;
1847     }, 'a.skip');