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