Merge branch 'MDL-23219_22' of git://github.com/timhunt/moodle into MOODLE_22_STABLE
[moodle.git] / lib / javascript-static.js
blob02b2249089e57e0051d55cda9382c5a0f6cbae98
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 laoded from YUI.
6  * @param {Array} modules
7  */
8 M.yui.add_module = function(modules) {
9     for (var modname in modules) {
10         M.yui.loader.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) {
36     var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename;
38     if (M.cfg.themerev > 0) {
39         url = url + '&rev=' + M.cfg.themerev;
40     }
42     if (component && component != '' && component != 'moodle' && component != 'core') {
43         url = url + '&component=' + component;
44     }
46     return url;
49 M.util.in_array = function(item, array){
50     for( var i = 0; i<array.length; i++){
51         if(item==array[i]){
52             return true;
53         }
54     }
55     return false;
58 /**
59  * Init a collapsible region, see print_collapsible_region in weblib.php
60  * @param {YUI} Y YUI3 instance with all libraries loaded
61  * @param {String} id the HTML id for the div.
62  * @param {String} userpref the user preference that records the state of this box. false if none.
63  * @param {String} strtooltip
64  */
65 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
66     Y.use('anim', function(Y) {
67         new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
68     });
71 /**
72  * Object to handle a collapsible region : instantiate and forget styled object
73  *
74  * @class
75  * @constructor
76  * @param {YUI} Y YUI3 instance with all libraries loaded
77  * @param {String} id The HTML id for the div.
78  * @param {String} userpref The user preference that records the state of this box. false if none.
79  * @param {String} strtooltip
80  */
81 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
82     // Record the pref name
83     this.userpref = userpref;
85     // Find the divs in the document.
86     this.div = Y.one('#'+id);
88     // Get the caption for the collapsible region
89     var caption = this.div.one('#'+id + '_caption');
90     caption.setAttribute('title', strtooltip);
92     // Create a link
93     var a = Y.Node.create('<a href="#"></a>');
94     // Create a local scoped lamba function to move nodes to a new link
95     var movenode = function(node){
96         node.remove();
97         a.append(node);
98     };
99     // Apply the lamba function on each of the captions child nodes
100     caption.get('children').each(movenode, this);
101     caption.append(a);
103     // Get the height of the div at this point before we shrink it if required
104     var height = this.div.get('offsetHeight');
105     if (this.div.hasClass('collapsed')) {
106         // Add the correct image and record the YUI node created in the process
107         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
108         // Shrink the div as it is collapsed by default
109         this.div.setStyle('height', caption.get('offsetHeight')+'px');
110     } else {
111         // Add the correct image and record the YUI node created in the process
112         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
113     }
114     a.append(this.icon);
116     // Create the animation.
117     var animation = new Y.Anim({
118         node: this.div,
119         duration: 0.3,
120         easing: Y.Easing.easeBoth,
121         to: {height:caption.get('offsetHeight')},
122         from: {height:height}
123     });
125     // Handler for the animation finishing.
126     animation.on('end', function() {
127         this.div.toggleClass('collapsed');
128         if (this.div.hasClass('collapsed')) {
129             this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
130         } else {
131             this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
132         }
133     }, this);
135     // Hook up the event handler.
136     a.on('click', function(e, animation) {
137         e.preventDefault();
138         // Animate to the appropriate size.
139         if (animation.get('running')) {
140             animation.stop();
141         }
142         animation.set('reverse', this.div.hasClass('collapsed'));
143         // Update the user preference.
144         if (this.userpref) {
145             M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
146         }
147         animation.run();
148     }, this, animation);
152  * The user preference that stores the state of this box.
153  * @property userpref
154  * @type String
155  */
156 M.util.CollapsibleRegion.prototype.userpref = null;
159  * The key divs that make up this
160  * @property div
161  * @type Y.Node
162  */
163 M.util.CollapsibleRegion.prototype.div = null;
166  * The key divs that make up this
167  * @property icon
168  * @type Y.Node
169  */
170 M.util.CollapsibleRegion.prototype.icon = null;
173  * Makes a best effort to connect back to Moodle to update a user preference,
174  * however, there is no mechanism for finding out if the update succeeded.
176  * Before you can use this function in your JavsScript, you must have called
177  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
178  * the udpate is allowed, and how to safely clean and submitted values.
180  * @param String name the name of the setting to udpate.
181  * @param String the value to set it to.
182  */
183 M.util.set_user_preference = function(name, value) {
184     YUI(M.yui.loader).use('io', function(Y) {
185         var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
186                 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
188         // If we are a developer, ensure that failures are reported.
189         var cfg = {
190                 method: 'get',
191                 on: {}
192             };
193         if (M.cfg.developerdebug) {
194             cfg.on.failure = function(id, o, args) {
195                 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
196             }
197         }
199         // Make the request.
200         Y.io(url, cfg);
201     });
205  * Prints a confirmation dialog in the style of DOM.confirm().
206  * @param object event A YUI DOM event or null if launched manually
207  * @param string message The message to show in the dialog
208  * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
209  * @param function fn A JS function to run if YES is clicked.
210  */
211 M.util.show_confirm_dialog = function(e, args) {
212     var target = e.target;
213     if (e.preventDefault) {
214         e.preventDefault();
215     }
217     YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
218         var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
219             {width: '300px',
220               fixedcenter: true,
221               modal: true,
222               visible: false,
223               draggable: false
224             }
225         );
227         simpledialog.setHeader(M.str.admin.confirmation);
228         simpledialog.setBody(args.message);
229         simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
231         var handle_cancel = function() {
232             simpledialog.hide();
233         };
235         var handle_yes = function() {
236             simpledialog.hide();
238             if (args.callback) {
239                 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
240                 var callback = null;
241                 if (Y.Lang.isFunction(args.callback)) {
242                     callback = args.callback;
243                 } else {
244                     callback = eval('('+args.callback+')');
245                 }
247                 if (Y.Lang.isObject(args.scope)) {
248                     var sc = args.scope;
249                 } else {
250                     var sc = e.target;
251                 }
253                 if (args.callbackargs) {
254                     callback.apply(sc, args.callbackargs);
255                 } else {
256                     callback.apply(sc);
257                 }
258                 return;
259             }
261             var targetancestor = null,
262                 targetform = null;
264             if (target.test('a')) {
265                 window.location = target.get('href');
267             } else if ((targetancestor = target.ancestor('a')) !== null) {
268                 window.location = targetancestor.get('href');
270             } else if (target.test('input')) {
271                 targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
272                 // We cannot use target.ancestor('form') on the previous line
273                 // because of http://yuilibrary.com/projects/yui3/ticket/2531561
274                 if (!targetform) {
275                     return;
276                 }
277                 if (target.get('name') && target.get('value')) {
278                     targetform.append('<input type="hidden" name="' + target.get('name') +
279                                     '" value="' + target.get('value') + '">');
280                 }
281                 targetform.submit();
283             } else if (target.get('tagName').toLowerCase() == 'form') {
284                 // We cannot use target.test('form') on the previous line because of
285                 // http://yuilibrary.com/projects/yui3/ticket/2531561
286                 target.submit();
288             } else if (M.cfg.developerdebug) {
289                 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT or FORM");
290             }
291         };
293         if (!args.cancellabel) {
294             args.cancellabel = M.str.moodle.cancel;
295         }
296         if (!args.continuelabel) {
297             args.continuelabel = M.str.moodle.yes;
298         }
300         var buttons = [
301             {text: args.cancellabel,   handler: handle_cancel, isDefault: true},
302             {text: args.continuelabel, handler: handle_yes}
303         ];
305         simpledialog.cfg.queueProperty('buttons', buttons);
307         simpledialog.render(document.body);
308         simpledialog.show();
309     });
312 /** Useful for full embedding of various stuff */
313 M.util.init_maximised_embed = function(Y, id) {
314     var obj = Y.one('#'+id);
315     if (!obj) {
316         return;
317     }
319     var get_htmlelement_size = function(el, prop) {
320         if (Y.Lang.isString(el)) {
321             el = Y.one('#' + el);
322         }
323         var val = el.getStyle(prop);
324         if (val == 'auto') {
325             val = el.getComputedStyle(prop);
326         }
327         return parseInt(val);
328     };
330     var resize_object = function() {
331         obj.setStyle('width', '0px');
332         obj.setStyle('height', '0px');
333         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
335         if (newwidth > 500) {
336             obj.setStyle('width', newwidth  + 'px');
337         } else {
338             obj.setStyle('width', '500px');
339         }
341         var headerheight = get_htmlelement_size('page-header', 'height');
342         var footerheight = get_htmlelement_size('page-footer', 'height');
343         var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
344         if (newheight < 400) {
345             newheight = 400;
346         }
347         obj.setStyle('height', newheight+'px');
348     };
350     resize_object();
351     // fix layout if window resized too
352     window.onresize = function() {
353         resize_object();
354     };
358  * Attach handler to single_select
359  */
360 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
361     Y.use('event-key', function() {
362         var select = Y.one('#'+selectid);
363         if (select) {
364             // Try to get the form by id
365             var form = Y.one('#'+formid) || (function(){
366                 // Hmmm the form's id may have been overriden by an internal input
367                 // with the name id which will KILL IE.
368                 // We need to manually iterate at this point because if the case
369                 // above is true YUI's ancestor method will also kill IE!
370                 var form = select;
371                 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
372                     form = form.ancestor();
373                 }
374                 return form;
375             })();
376             // Make sure we have the form
377             if (form) {
378                 // Create a function to handle our change event
379                 var processchange = function(e, paramobject) {
380                     if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
381                         //prevent event bubbling and detach handlers to prevent multiple submissions caused by double clicking
382                         e.halt();
383                         paramobject.eventkeypress.detach();
384                         paramobject.eventblur.detach();
385                         paramobject.eventchangeorblur.detach();
387                         this.submit();
388                     }
389                 };
390                 // Attach the change event to the keypress, blur, and click actions.
391                 // We don't use the change event because IE fires it on every arrow up/down
392                 // event.... usability
393                 var paramobject = new Object();
394                 paramobject.lastindex = select.get('selectedIndex');
395                 paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
396                 paramobject.eventblur = select.on('blur', processchange, form, paramobject);
397                 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
398                 if (Y.UA.webkit) {
399                     paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
400                 } else {
401                     paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
402                 }
403             }
404         }
405     });
409  * Attach handler to url_select
410  */
411 M.util.init_url_select = function(Y, formid, selectid, nothing) {
412     YUI(M.yui.loader).use('node', function(Y) {
413         Y.on('change', function() {
414             if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
415                 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
416             }
417         },
418         '#'+selectid);
419     });
423  * Breaks out all links to the top frame - used in frametop page layout.
424  */
425 M.util.init_frametop = function(Y) {
426     Y.all('a').each(function(node) {
427         node.set('target', '_top');
428     });
429     Y.all('form').each(function(node) {
430         node.set('target', '_top');
431     });
435  * Finds all nodes that match the given CSS selector and attaches events to them
436  * so that they toggle a given classname when clicked.
438  * @param {YUI} Y
439  * @param {string} id An id containing elements to target
440  * @param {string} cssselector A selector to use to find targets
441  * @param {string} toggleclassname A classname to toggle
442  */
443 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
445     if (togglecssselector == '') {
446         togglecssselector = cssselector;
447     }
449     var node = Y.one('#'+id);
450     node.all(cssselector).each(function(n){
451         n.on('click', function(e){
452             e.stopPropagation();
453             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
454                 if (this.test(togglecssselector)) {
455                     this.toggleClass(toggleclassname);
456                 } else {
457                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
458             }
459             }
460         }, n);
461     });
462     // Attach this click event to the node rather than all selectors... will be much better
463     // for performance
464     node.on('click', function(e){
465         if (e.target.hasClass('addtoall')) {
466             this.all(togglecssselector).addClass(toggleclassname);
467         } else if (e.target.hasClass('removefromall')) {
468             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
469         }
470     }, node);
474  * Initialises a colour picker
476  * Designed to be used with admin_setting_configcolourpicker although could be used
477  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
478  * above or below the input (must have the same parent) and then call this with the
479  * id.
481  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
482  * contrib/blocks. For better docs refer to that.
484  * @param {YUI} Y
485  * @param {int} id
486  * @param {object} previewconf
487  */
488 M.util.init_colour_picker = function(Y, id, previewconf) {
489     /**
490      * We need node and event-mouseenter
491      */
492     Y.use('node', 'event-mouseenter', function(){
493         /**
494          * The colour picker object
495          */
496         var colourpicker = {
497             box : null,
498             input : null,
499             image : null,
500             preview : null,
501             current : null,
502             eventClick : null,
503             eventMouseEnter : null,
504             eventMouseLeave : null,
505             eventMouseMove : null,
506             width : 300,
507             height :  100,
508             factor : 5,
509             /**
510              * Initalises the colour picker by putting everything together and wiring the events
511              */
512             init : function() {
513                 this.input = Y.one('#'+id);
514                 this.box = this.input.ancestor().one('.admin_colourpicker');
515                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
516                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
517                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
518                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
519                 this.current = Y.Node.create('<div class="currentcolour"></div>');
520                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
521                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
523                 if (typeof(previewconf) === 'object' && previewconf !== null) {
524                     Y.one('#'+id+'_preview').on('click', function(e){
525                         if (Y.Lang.isString(previewconf.selector)) {
526                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
527                         } else {
528                             for (var i in previewconf.selector) {
529                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
530                             }
531                         }
532                     }, this);
533                 }
535                 this.eventClick = this.image.on('click', this.pickColour, this);
536                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
537             },
538             /**
539              * Starts to follow the mouse once it enter the image
540              */
541             startFollow : function(e) {
542                 this.eventMouseEnter.detach();
543                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
544                 this.eventMouseMove = this.image.on('mousemove', function(e){
545                     this.preview.setStyle('backgroundColor', this.determineColour(e));
546                 }, this);
547             },
548             /**
549              * Stops following the mouse
550              */
551             endFollow : function(e) {
552                 this.eventMouseMove.detach();
553                 this.eventMouseLeave.detach();
554                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
555             },
556             /**
557              * Picks the colour the was clicked on
558              */
559             pickColour : function(e) {
560                 var colour = this.determineColour(e);
561                 this.input.set('value', colour);
562                 this.current.setStyle('backgroundColor', colour);
563             },
564             /**
565              * Calculates the colour fromthe given co-ordinates
566              */
567             determineColour : function(e) {
568                 var eventx = Math.floor(e.pageX-e.target.getX());
569                 var eventy = Math.floor(e.pageY-e.target.getY());
571                 var imagewidth = this.width;
572                 var imageheight = this.height;
573                 var factor = this.factor;
574                 var colour = [255,0,0];
576                 var matrices = [
577                     [  0,  1,  0],
578                     [ -1,  0,  0],
579                     [  0,  0,  1],
580                     [  0, -1,  0],
581                     [  1,  0,  0],
582                     [  0,  0, -1]
583                 ];
585                 var matrixcount = matrices.length;
586                 var limit = Math.round(imagewidth/matrixcount);
587                 var heightbreak = Math.round(imageheight/2);
589                 for (var x = 0; x < imagewidth; x++) {
590                     var divisor = Math.floor(x / limit);
591                     var matrix = matrices[divisor];
593                     colour[0] += matrix[0]*factor;
594                     colour[1] += matrix[1]*factor;
595                     colour[2] += matrix[2]*factor;
597                     if (eventx==x) {
598                         break;
599                     }
600                 }
602                 var pixel = [colour[0], colour[1], colour[2]];
603                 if (eventy < heightbreak) {
604                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
605                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
606                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
607                 } else if (eventy > heightbreak) {
608                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
609                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
610                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
611                 }
613                 return this.convert_rgb_to_hex(pixel);
614             },
615             /**
616              * Converts an RGB value to Hex
617              */
618             convert_rgb_to_hex : function(rgb) {
619                 var hex = '#';
620                 var hexchars = "0123456789ABCDEF";
621                 for (var i=0; i<3; i++) {
622                     var number = Math.abs(rgb[i]);
623                     if (number == 0 || isNaN(number)) {
624                         hex += '00';
625                     } else {
626                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
627                     }
628                 }
629                 return hex;
630             }
631         };
632         /**
633          * Initialise the colour picker :) Hoorah
634          */
635         colourpicker.init();
636     });
639 M.util.init_block_hider = function(Y, config) {
640     Y.use('base', 'node', function(Y) {
641         M.util.block_hider = M.util.block_hider || (function(){
642             var blockhider = function() {
643                 blockhider.superclass.constructor.apply(this, arguments);
644             };
645             blockhider.prototype = {
646                 initializer : function(config) {
647                     this.set('block', '#'+this.get('id'));
648                     var b = this.get('block'),
649                         t = b.one('.title'),
650                         a = null;
651                     if (t && (a = t.one('.block_action'))) {
652                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
653                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
654                         hide.on('keypress', this.updateStateKey, this, true);
655                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
656                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
657                         show.on('keypress', this.updateStateKey, this, false);
658                         a.insert(show, 0).insert(hide, 0);
659                     }
660                 },
661                 updateState : function(e, hide) {
662                     M.util.set_user_preference(this.get('preference'), hide);
663                     if (hide) {
664                         this.get('block').addClass('hidden');
665                     } else {
666                         this.get('block').removeClass('hidden');
667                     }
668                 },
669                 updateStateKey : function(e, hide) {
670                     if (e.keyCode == 13) { //allow hide/show via enter key
671                         this.updateState(this, hide);
672                     }
673                 }
674             };
675             Y.extend(blockhider, Y.Base, blockhider.prototype, {
676                 NAME : 'blockhider',
677                 ATTRS : {
678                     id : {},
679                     preference : {},
680                     iconVisible : {
681                         value : M.util.image_url('t/switch_minus', 'moodle')
682                     },
683                     iconHidden : {
684                         value : M.util.image_url('t/switch_plus', 'moodle')
685                     },
686                     block : {
687                         setter : function(node) {
688                             return Y.one(node);
689                         }
690                     }
691                 }
692             });
693             return blockhider;
694         })();
695         new M.util.block_hider(config);
696     });
700  * Returns a string registered in advance for usage in JavaScript
702  * If you do not pass the third parameter, the function will just return
703  * the corresponding value from the M.str object. If the third parameter is
704  * provided, the function performs {$a} placeholder substitution in the
705  * same way as PHP get_string() in Moodle does.
707  * @param {String} identifier string identifier
708  * @param {String} component the component providing the string
709  * @param {Object|String} a optional variable to populate placeholder with
710  */
711 M.util.get_string = function(identifier, component, a) {
712     var stringvalue;
714     if (M.cfg.developerdebug) {
715         // creating new instance if YUI is not optimal but it seems to be better way then
716         // require the instance via the function API - note that it is used in rare cases
717         // for debugging only anyway
718         var Y = new YUI({ debug : true });
719     }
721     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
722         stringvalue = '[[' + identifier + ',' + component + ']]';
723         if (M.cfg.developerdebug) {
724             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
725         }
726         return stringvalue;
727     }
729     stringvalue = M.str[component][identifier];
731     if (typeof a == 'undefined') {
732         // no placeholder substitution requested
733         return stringvalue;
734     }
736     if (typeof a == 'number' || typeof a == 'string') {
737         // replace all occurrences of {$a} with the placeholder value
738         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
739         return stringvalue;
740     }
742     if (typeof a == 'object') {
743         // replace {$a->key} placeholders
744         for (var key in a) {
745             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
746                 if (M.cfg.developerdebug) {
747                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
748                 }
749                 continue;
750             }
751             var search = '{$a->' + key + '}';
752             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
753             search = new RegExp(search, 'g');
754             stringvalue = stringvalue.replace(search, a[key]);
755         }
756         return stringvalue;
757     }
759     if (M.cfg.developerdebug) {
760         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
761     }
762     return stringvalue;
766  * Set focus on username or password field of the login form
767  */
768 M.util.focus_login_form = function(Y) {
769     var username = Y.one('#username');
770     var password = Y.one('#password');
772     if (username == null || password == null) {
773         // something is wrong here
774         return;
775     }
777     var curElement = document.activeElement
778     if (curElement == 'undefined') {
779         // legacy browser - skip refocus protection
780     } else if (curElement.tagName == 'INPUT') {
781         // user was probably faster to focus something, do not mess with focus
782         return;
783     }
785     if (username.get('value') == '') {
786         username.focus();
787     } else {
788         password.focus();
789     }
793 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
795 function checkall() {
796     var inputs = document.getElementsByTagName('input');
797     for (var i = 0; i < inputs.length; i++) {
798         if (inputs[i].type == 'checkbox') {
799             if (inputs[i].disabled || inputs[i].readOnly) {
800                 continue;
801             }
802             inputs[i].checked = true;
803         }
804     }
807 function checknone() {
808     var inputs = document.getElementsByTagName('input');
809     for (var i = 0; i < inputs.length; i++) {
810         if (inputs[i].type == 'checkbox') {
811             if (inputs[i].disabled || inputs[i].readOnly) {
812                 continue;
813             }
814             inputs[i].checked = false;
815         }
816     }
820  * Either check, or uncheck, all checkboxes inside the element with id is
821  * @param id the id of the container
822  * @param checked the new state, either '' or 'checked'.
823  */
824 function select_all_in_element_with_id(id, checked) {
825     var container = document.getElementById(id);
826     if (!container) {
827         return;
828     }
829     var inputs = container.getElementsByTagName('input');
830     for (var i = 0; i < inputs.length; ++i) {
831         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
832             inputs[i].checked = checked;
833         }
834     }
837 function select_all_in(elTagName, elClass, elId) {
838     var inputs = document.getElementsByTagName('input');
839     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
840     for(var i = 0; i < inputs.length; ++i) {
841         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
842             inputs[i].checked = 'checked';
843         }
844     }
847 function deselect_all_in(elTagName, elClass, elId) {
848     var inputs = document.getElementsByTagName('INPUT');
849     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
850     for(var i = 0; i < inputs.length; ++i) {
851         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
852             inputs[i].checked = '';
853         }
854     }
857 function confirm_if(expr, message) {
858     if(!expr) {
859         return true;
860     }
861     return confirm(message);
866     findParentNode (start, elementName, elementClass, elementID)
868     Travels up the DOM hierarchy to find a parent element with the
869     specified tag name, class, and id. All conditions must be met,
870     but any can be ommitted. Returns the BODY element if no match
871     found.
873 function findParentNode(el, elName, elClass, elId) {
874     while (el.nodeName.toUpperCase() != 'BODY') {
875         if ((!elName || el.nodeName.toUpperCase() == elName) &&
876             (!elClass || el.className.indexOf(elClass) != -1) &&
877             (!elId || el.id == elId)) {
878             break;
879         }
880         el = el.parentNode;
881     }
882     return el;
885     findChildNode (start, elementName, elementClass, elementID)
887     Travels down the DOM hierarchy to find all child elements with the
888     specified tag name, class, and id. All conditions must be met,
889     but any can be ommitted.
890     Doesn't examine children of matches.
892 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
893     var children = new Array();
894     for (var i = 0; i < start.childNodes.length; i++) {
895         var classfound = false;
896         var child = start.childNodes[i];
897         if((child.nodeType == 1) &&//element node type
898                   (elementClass && (typeof(child.className)=='string'))) {
899             var childClasses = child.className.split(/\s+/);
900             for (var childClassIndex in childClasses) {
901                 if (childClasses[childClassIndex]==elementClass) {
902                     classfound = true;
903                     break;
904                 }
905             }
906         }
907         if(child.nodeType == 1) { //element node type
908             if  ( (!tagName || child.nodeName == tagName) &&
909                 (!elementClass || classfound)&&
910                 (!elementID || child.id == elementID) &&
911                 (!elementName || child.name == elementName))
912             {
913                 children = children.concat(child);
914             } else {
915                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
916             }
917         }
918     }
919     return children;
922 function unmaskPassword(id) {
923   var pw = document.getElementById(id);
924   var chb = document.getElementById(id+'unmask');
926   try {
927     // first try IE way - it can not set name attribute later
928     if (chb.checked) {
929       var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
930     } else {
931       var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
932     }
933     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
934   } catch (e) {
935     var newpw = document.createElement('input');
936     newpw.setAttribute('autocomplete', 'off');
937     newpw.setAttribute('name', pw.name);
938     if (chb.checked) {
939       newpw.setAttribute('type', 'text');
940     } else {
941       newpw.setAttribute('type', 'password');
942     }
943     newpw.setAttribute('class', pw.getAttribute('class'));
944   }
945   newpw.id = pw.id;
946   newpw.size = pw.size;
947   newpw.onblur = pw.onblur;
948   newpw.onchange = pw.onchange;
949   newpw.value = pw.value;
950   pw.parentNode.replaceChild(newpw, pw);
953 function filterByParent(elCollection, parentFinder) {
954     var filteredCollection = [];
955     for (var i = 0; i < elCollection.length; ++i) {
956         var findParent = parentFinder(elCollection[i]);
957         if (findParent.nodeName.toUpperCase() != 'BODY') {
958             filteredCollection.push(elCollection[i]);
959         }
960     }
961     return filteredCollection;
965     All this is here just so that IE gets to handle oversized blocks
966     in a visually pleasing manner. It does a browser detect. So sue me.
969 function fix_column_widths() {
970     var agt = navigator.userAgent.toLowerCase();
971     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
972         fix_column_width('left-column');
973         fix_column_width('right-column');
974     }
977 function fix_column_width(colName) {
978     if(column = document.getElementById(colName)) {
979         if(!column.offsetWidth) {
980             setTimeout("fix_column_width('" + colName + "')", 20);
981             return;
982         }
984         var width = 0;
985         var nodes = column.childNodes;
987         for(i = 0; i < nodes.length; ++i) {
988             if(nodes[i].className.indexOf("block") != -1 ) {
989                 if(width < nodes[i].offsetWidth) {
990                     width = nodes[i].offsetWidth;
991                 }
992             }
993         }
995         for(i = 0; i < nodes.length; ++i) {
996             if(nodes[i].className.indexOf("block") != -1 ) {
997                 nodes[i].style.width = width + 'px';
998             }
999         }
1000     }
1005    Insert myValue at current cursor position
1006  */
1007 function insertAtCursor(myField, myValue) {
1008     // IE support
1009     if (document.selection) {
1010         myField.focus();
1011         sel = document.selection.createRange();
1012         sel.text = myValue;
1013     }
1014     // Mozilla/Netscape support
1015     else if (myField.selectionStart || myField.selectionStart == '0') {
1016         var startPos = myField.selectionStart;
1017         var endPos = myField.selectionEnd;
1018         myField.value = myField.value.substring(0, startPos)
1019             + myValue + myField.value.substring(endPos, myField.value.length);
1020     } else {
1021         myField.value += myValue;
1022     }
1027         Call instead of setting window.onload directly or setting body onload=.
1028         Adds your function to a chain of functions rather than overwriting anything
1029         that exists.
1031 function addonload(fn) {
1032     var oldhandler=window.onload;
1033     window.onload=function() {
1034         if(oldhandler) oldhandler();
1035             fn();
1036     }
1039  * Replacement for getElementsByClassName in browsers that aren't cool enough
1041  * Relying on the built-in getElementsByClassName is far, far faster than
1042  * using YUI.
1044  * Note: the third argument used to be an object with odd behaviour. It now
1045  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1046  * mimicked if you pass an object.
1048  * @param {Node} oElm The top-level node for searching. To search a whole
1049  *                    document, use `document`.
1050  * @param {String} strTagName filter by tag names
1051  * @param {String} name same as HTML5 spec
1052  */
1053 function getElementsByClassName(oElm, strTagName, name) {
1054     // for backwards compatibility
1055     if(typeof name == "object") {
1056         var names = new Array();
1057         for(var i=0; i<name.length; i++) names.push(names[i]);
1058         name = names.join('');
1059     }
1060     // use native implementation if possible
1061     if (oElm.getElementsByClassName && Array.filter) {
1062         if (strTagName == '*') {
1063             return oElm.getElementsByClassName(name);
1064         } else {
1065             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1066                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1067             });
1068         }
1069     }
1070     // native implementation unavailable, fall back to slow method
1071     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1072     var arrReturnElements = new Array();
1073     var arrRegExpClassNames = new Array();
1074     var names = name.split(' ');
1075     for(var i=0; i<names.length; i++) {
1076         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1077     }
1078     var oElement;
1079     var bMatchesAll;
1080     for(var j=0; j<arrElements.length; j++) {
1081         oElement = arrElements[j];
1082         bMatchesAll = true;
1083         for(var k=0; k<arrRegExpClassNames.length; k++) {
1084             if(!arrRegExpClassNames[k].test(oElement.className)) {
1085                 bMatchesAll = false;
1086                 break;
1087             }
1088         }
1089         if(bMatchesAll) {
1090             arrReturnElements.push(oElement);
1091         }
1092     }
1093     return (arrReturnElements)
1096 function openpopup(event, args) {
1098     if (event) {
1099         if (event.preventDefault) {
1100             event.preventDefault();
1101         } else {
1102             event.returnValue = false;
1103         }
1104     }
1106     var fullurl = args.url;
1107     if (!args.url.match(/https?:\/\//)) {
1108         fullurl = M.cfg.wwwroot + args.url;
1109     }
1110     var windowobj = window.open(fullurl,args.name,args.options);
1111     if (!windowobj) {
1112         return true;
1113     }
1114     if (args.fullscreen) {
1115         windowobj.moveTo(0,0);
1116         windowobj.resizeTo(screen.availWidth,screen.availHeight);
1117     }
1118     windowobj.focus();
1120     return false;
1123 /** Close the current browser window. */
1124 function close_window(e) {
1125     if (e.preventDefault) {
1126         e.preventDefault();
1127     } else {
1128         e.returnValue = false;
1129     }
1130     window.close();
1134  * Used in a couple of modules to hide navigation areas when using AJAX
1135  */
1137 function show_item(itemid) {
1138     var item = document.getElementById(itemid);
1139     if (item) {
1140         item.style.display = "";
1141     }
1144 function destroy_item(itemid) {
1145     var item = document.getElementById(itemid);
1146     if (item) {
1147         item.parentNode.removeChild(item);
1148     }
1151  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1152  * @param controlid the control id.
1153  */
1154 function focuscontrol(controlid) {
1155     var control = document.getElementById(controlid);
1156     if (control) {
1157         control.focus();
1158     }
1162  * Transfers keyboard focus to an HTML element based on the old style style of focus
1163  * This function should be removed as soon as it is no longer used
1164  */
1165 function old_onload_focus(formid, controlname) {
1166     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1167         document.forms[formid].elements[controlname].focus();
1168     }
1171 function build_querystring(obj) {
1172     return convert_object_to_string(obj, '&');
1175 function build_windowoptionsstring(obj) {
1176     return convert_object_to_string(obj, ',');
1179 function convert_object_to_string(obj, separator) {
1180     if (typeof obj !== 'object') {
1181         return null;
1182     }
1183     var list = [];
1184     for(var k in obj) {
1185         k = encodeURIComponent(k);
1186         var value = obj[k];
1187         if(obj[k] instanceof Array) {
1188             for(var i in value) {
1189                 list.push(k+'[]='+encodeURIComponent(value[i]));
1190             }
1191         } else {
1192             list.push(k+'='+encodeURIComponent(value));
1193         }
1194     }
1195     return list.join(separator);
1198 function stripHTML(str) {
1199     var re = /<\S[^><]*>/g;
1200     var ret = str.replace(re, "");
1201     return ret;
1204 Number.prototype.fixed=function(n){
1205     with(Math)
1206         return round(Number(this)*pow(10,n))/pow(10,n);
1208 function update_progress_bar (id, width, pt, msg, es){
1209     var percent = pt;
1210     var status = document.getElementById("status_"+id);
1211     var percent_indicator = document.getElementById("pt_"+id);
1212     var progress_bar = document.getElementById("progress_"+id);
1213     var time_es = document.getElementById("time_"+id);
1214     status.innerHTML = msg;
1215     percent_indicator.innerHTML = percent.fixed(2) + '%';
1216     if(percent == 100) {
1217         progress_bar.style.background = "green";
1218         time_es.style.display = "none";
1219     } else {
1220         progress_bar.style.background = "#FFCC66";
1221         if (es == '?'){
1222             time_es.innerHTML = "";
1223         }else {
1224             time_es.innerHTML = es.fixed(2)+" sec";
1225             time_es.style.display
1226                 = "block";
1227         }
1228     }
1229     progress_bar.style.width = width + "px";
1234 // ===== Deprecated core Javascript functions for Moodle ====
1235 //       DO NOT USE!!!!!!!
1236 // Do not put this stuff in separate file because it only adds extra load on servers!
1239  * Used in a couple of modules to hide navigation areas when using AJAX
1240  */
1241 function hide_item(itemid) {
1242     // use class='hiddenifjs' instead
1243     var item = document.getElementById(itemid);
1244     if (item) {
1245         item.style.display = "none";
1246     }
1249 M.util.help_icon = {
1250     Y : null,
1251     instance : null,
1252     add : function(Y, properties) {
1253         this.Y = Y;
1254         properties.node = Y.one('#'+properties.id);
1255         if (properties.node) {
1256             properties.node.on('click', this.display, this, properties);
1257         }
1258     },
1259     display : function(event, args) {
1260         event.preventDefault();
1261         if (M.util.help_icon.instance === null) {
1262             var Y = M.util.help_icon.Y;
1263             Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1264                 var help_content_overlay = {
1265                     helplink : null,
1266                     overlay : null,
1267                     init : function() {
1269                         var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img  src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1270                         // Create an overlay from markup
1271                         this.overlay = new Y.Overlay({
1272                             headerContent: closebtn,
1273                             bodyContent: '',
1274                             id: 'helppopupbox',
1275                             width:'400px',
1276                             visible : false,
1277                             constrain : true
1278                         });
1279                         this.overlay.render(Y.one(document.body));
1281                         closebtn.on('click', this.overlay.hide, this.overlay);
1283                         var boundingBox = this.overlay.get("boundingBox");
1285                         //  Hide the menu if the user clicks outside of its content
1286                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1287                             var oTarget = event.target;
1288                             var menuButton = Y.one("#"+args.id);
1290                             if (!oTarget.compareTo(menuButton) &&
1291                                 !menuButton.contains(oTarget) &&
1292                                 !oTarget.compareTo(boundingBox) &&
1293                                 !boundingBox.contains(oTarget)) {
1294                                 this.overlay.hide();
1295                             }
1296                         }, this);
1298                         Y.on("key", this.close, closebtn , "down:13", this);
1299                         closebtn.on('click', this.close, this);
1300                     },
1302                     close : function(e) {
1303                         e.preventDefault();
1304                         this.helplink.focus();
1305                         this.overlay.hide();
1306                     },
1308                     display : function(event, args) {
1309                         this.helplink = args.node;
1310                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1311                         this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1313                         var fullurl = args.url;
1314                         if (!args.url.match(/https?:\/\//)) {
1315                             fullurl = M.cfg.wwwroot + args.url;
1316                         }
1318                         var ajaxurl = fullurl + '&ajax=1';
1320                         var cfg = {
1321                             method: 'get',
1322                             context : this,
1323                             on: {
1324                                 success: function(id, o, node) {
1325                                     this.display_callback(o.responseText);
1326                                 },
1327                                 failure: function(id, o, node) {
1328                                     var debuginfo = o.statusText;
1329                                     if (M.cfg.developerdebug) {
1330                                         o.statusText += ' (' + ajaxurl + ')';
1331                                     }
1332                                     this.display_callback('bodyContent',debuginfo);
1333                                 }
1334                             }
1335                         };
1337                         Y.io(ajaxurl, cfg);
1338                         this.overlay.show();
1340                         Y.one('#closehelpbox').focus();
1341                     },
1343                     display_callback : function(content) {
1344                         this.overlay.set('bodyContent', content);
1345                     },
1347                     hideContent : function() {
1348                         help = this;
1349                         help.overlay.hide();
1350                     }
1351                 };
1352                 help_content_overlay.init();
1353                 M.util.help_icon.instance = help_content_overlay;
1354                 M.util.help_icon.instance.display(event, args);
1355             });
1356         } else {
1357             M.util.help_icon.instance.display(event, args);
1358         }
1359     },
1360     init : function(Y) {
1361         this.Y = Y;
1362     }
1366  * Custom menu namespace
1367  */
1368 M.core_custom_menu = {
1369     /**
1370      * This method is used to initialise a custom menu given the id that belongs
1371      * to the custom menu's root node.
1372      *
1373      * @param {YUI} Y
1374      * @param {string} nodeid
1375      */
1376     init : function(Y, nodeid) {
1377         var node = Y.one('#'+nodeid);
1378         if (node) {
1379             Y.use('node-menunav', function(Y) {
1380                 // Get the node
1381                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1382                 node.removeClass('javascript-disabled');
1383                 // Initialise the menunav plugin
1384                 node.plug(Y.Plugin.NodeMenuNav);
1385             });
1386         }
1387     }
1391  * Used to store form manipulation methods and enhancments
1392  */
1393 M.form = M.form || {};
1396  * Converts a nbsp indented select box into a multi drop down custom control much
1397  * like the custom menu. It also selectable categories on or off.
1399  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1401  * @param {YUI} Y
1402  * @param {string} id
1403  * @param {Array} options
1404  */
1405 M.form.init_smartselect = function(Y, id, options) {
1406     if (!id.match(/^id_/)) {
1407         id = 'id_'+id;
1408     }
1409     var select = Y.one('select#'+id);
1410     if (!select) {
1411         return false;
1412     }
1413     Y.use('event-delegate',function(){
1414         var smartselect = {
1415             id : id,
1416             structure : [],
1417             options : [],
1418             submenucount : 0,
1419             currentvalue : null,
1420             currenttext : null,
1421             shownevent : null,
1422             cfg : {
1423                 selectablecategories : true,
1424                 mode : null
1425             },
1426             nodes : {
1427                 select : null,
1428                 loading : null,
1429                 menu : null
1430             },
1431             init : function(Y, id, args, nodes) {
1432                 if (typeof(args)=='object') {
1433                     for (var i in this.cfg) {
1434                         if (args[i] || args[i]===false) {
1435                             this.cfg[i] = args[i];
1436                         }
1437                     }
1438                 }
1440                 // Display a loading message first up
1441                 this.nodes.select = nodes.select;
1443                 this.currentvalue = this.nodes.select.get('selectedIndex');
1444                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1446                 var options = Array();
1447                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1448                 this.nodes.select.all('option').each(function(option, index) {
1449                     var rawtext = option.get('innerHTML');
1450                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1451                     if (rawtext === text) {
1452                         text = rawtext.replace(/^(\s)*/, '');
1453                         var depth = (rawtext.length - text.length ) + 1;
1454                     } else {
1455                         var depth = ((rawtext.length - text.length )/12)+1;
1456                     }
1457                     option.set('innerHTML', text);
1458                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1459                 }, this);
1461                 this.structure = [];
1462                 var structcount = 0;
1463                 for (var i in options) {
1464                     var o = options[i];
1465                     if (o.depth == 0) {
1466                         this.structure.push(o);
1467                         structcount++;
1468                     } else {
1469                         var d = o.depth;
1470                         var current = this.structure[structcount-1];
1471                         for (var j = 0; j < o.depth-1;j++) {
1472                             if (current && current.children) {
1473                                 current = current.children[current.children.length-1];
1474                             }
1475                         }
1476                         if (current && current.children) {
1477                             current.children.push(o);
1478                         }
1479                     }
1480                 }
1482                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1483                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1484                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1485                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1487                 if (this.cfg.mode == null) {
1488                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1489                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1490                         this.cfg.mode = 'compact';
1491                     } else {
1492                         this.cfg.mode = 'spanning';
1493                     }
1494                 }
1496                 if (this.cfg.mode == 'compact') {
1497                     this.nodes.menu.addClass('compactmenu');
1498                 } else {
1499                     this.nodes.menu.addClass('spanningmenu');
1500                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1501                 }
1503                 Y.one(document.body).append(this.nodes.menu);
1504                 var pos = this.nodes.select.getXY();
1505                 pos[0] += 1;
1506                 this.nodes.menu.setXY(pos);
1507                 this.nodes.menu.on('click', this.handle_click, this);
1509                 Y.one(window).on('resize', function(){
1510                      var pos = this.nodes.select.getXY();
1511                     pos[0] += 1;
1512                     this.nodes.menu.setXY(pos);
1513                  }, this);
1514             },
1515             generate_menu_content : function() {
1516                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1517                 content += this.generate_submenu_content(this.structure[0], true);
1518                 content += '</ul></div>';
1519                 return content;
1520             },
1521             generate_submenu_content : function(item, rootelement) {
1522                 this.submenucount++;
1523                 var content = '';
1524                 if (item.children.length > 0) {
1525                     if (rootelement) {
1526                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1527                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1528                         content += '<div class="smartselect_menu_content">';
1529                     } else {
1530                         content += '<li class="smartselect_submenuitem">';
1531                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1532                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1533                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1534                         content += '<div class="smartselect_submenu_content">';
1535                     }
1536                     content += '<ul>';
1537                     for (var i in item.children) {
1538                         content += this.generate_submenu_content(item.children[i],false);
1539                     }
1540                     content += '</ul>';
1541                     content += '</div>';
1542                     content += '</div>';
1543                     if (rootelement) {
1544                     } else {
1545                         content += '</li>';
1546                     }
1547                 } else {
1548                     content += '<li class="smartselect_menuitem">';
1549                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1550                     content += '</li>';
1551                 }
1552                 return content;
1553             },
1554             select : function(e) {
1555                 var t = e.target;
1556                 e.halt();
1557                 this.currenttext = t.get('innerHTML');
1558                 this.currentvalue = t.getAttribute('value');
1559                 this.nodes.select.set('selectedIndex', this.currentvalue);
1560                 this.hide_menu();
1561             },
1562             handle_click : function(e) {
1563                 var target = e.target;
1564                 if (target.hasClass('smartselect_mask')) {
1565                     this.show_menu(e);
1566                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1567                     this.select(e);
1568                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1569                     this.show_sub_menu(e);
1570                 }
1571             },
1572             show_menu : function(e) {
1573                 e.halt();
1574                 var menu = e.target.ancestor().one('.smartselect_menu');
1575                 menu.addClass('visible');
1576                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1577             },
1578             show_sub_menu : function(e) {
1579                 e.halt();
1580                 var target = e.target;
1581                 if (!target.hasClass('smartselect_submenuitem')) {
1582                     target = target.ancestor('.smartselect_submenuitem');
1583                 }
1584                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1585                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1586                     return;
1587                 }
1588                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1589                 target.one('.smartselect_submenu').addClass('visible');
1590             },
1591             hide_menu : function() {
1592                 this.nodes.menu.all('.visible').removeClass('visible');
1593                 if (this.shownevent) {
1594                     this.shownevent.detach();
1595                 }
1596             }
1597         };
1598         smartselect.init(Y, id, options, {select:select});
1599     });
1602 /** List of flv players to be loaded */
1603 M.util.video_players = [];
1604 /** List of mp3 players to be loaded */
1605 M.util.audio_players = [];
1608  * Add video player
1609  * @param id element id
1610  * @param fileurl media url
1611  * @param width
1612  * @param height
1613  * @param autosize true means detect size from media
1614  */
1615 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1616     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1620  * Add audio player.
1621  * @param id
1622  * @param fileurl
1623  * @param small
1624  */
1625 M.util.add_audio_player = function (id, fileurl, small) {
1626     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1630  * Initialise all audio and video player, must be called from page footer.
1631  */
1632 M.util.load_flowplayer = function() {
1633     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1634         return;
1635     }
1636     if (typeof(flowplayer) == 'undefined') {
1637         var loaded = false;
1639         var embed_function = function() {
1640             if (loaded || typeof(flowplayer) == 'undefined') {
1641                 return;
1642             }
1643             loaded = true;
1645             var controls = {
1646                     autoHide: true
1647             }
1648             /* TODO: add CSS color overrides for the flv flow player */
1650             for(var i=0; i<M.util.video_players.length; i++) {
1651                 var video = M.util.video_players[i];
1652                 if (video.width > 0 && video.height > 0) {
1653                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1654                 } else {
1655                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1656                 }
1657                 flowplayer(video.id, src, {
1658                     plugins: {controls: controls},
1659                     clip: {
1660                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1661                         onMetaData: function(clip) {
1662                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1663                                 clip.mvideo.resized = true;
1664                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1665                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1666                                     // bad luck, we have to guess - we may not get metadata at all
1667                                     var width = clip.width;
1668                                     var height = clip.height;
1669                                 } else {
1670                                     var width = clip.metaData.width;
1671                                     var height = clip.metaData.height;
1672                                 }
1673                                 var minwidth = 300; // controls are messed up in smaller objects
1674                                 if (width < minwidth) {
1675                                     height = (height * minwidth) / width;
1676                                     width = minwidth;
1677                                 }
1679                                 var object = this._api();
1680                                 object.width = width;
1681                                 object.height = height;
1682                             }
1683                                 }
1684                     }
1685                 });
1686             }
1687             if (M.util.audio_players.length == 0) {
1688                 return;
1689             }
1690             var controls = {
1691                     autoHide: false,
1692                     fullscreen: false,
1693                     next: false,
1694                     previous: false,
1695                     scrubber: true,
1696                     play: true,
1697                     pause: true,
1698                     volume: true,
1699                     mute: false,
1700                     backgroundGradient: [0.5,0,0.3]
1701                 };
1703             var rule;
1704             for (var j=0; j < document.styleSheets.length; j++) {
1705                 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1706                     var allrules = document.styleSheets[j].rules;
1707                 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1708                     var allrules = document.styleSheets[j].cssRules;
1709                 } else {
1710                     // why??
1711                     continue;
1712                 }
1713                 if (!allrules) continue;
1714                 for(var i=0; i<allrules.length; i++) {
1715                     rule = '';
1716                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1717                         if (typeof(allrules[i].cssText) != 'undefined') {
1718                             rule = allrules[i].cssText;
1719                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1720                             rule = allrules[i].style.cssText;
1721                         }
1722                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1723                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1724                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1725                             controls[colprop] = rule;
1726                         }
1727                     }
1728                 }
1729                 allrules = false;
1730             }
1732             for(i=0; i<M.util.audio_players.length; i++) {
1733                 var audio = M.util.audio_players[i];
1734                 if (audio.small) {
1735                     controls.controlall = false;
1736                     controls.height = 15;
1737                     controls.time = false;
1738                 } else {
1739                     controls.controlall = true;
1740                     controls.height = 25;
1741                     controls.time = true;
1742                 }
1743                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1744                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1745                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1746                 });
1747             }
1748         }
1750         if (M.cfg.jsrev == -10) {
1751             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1752         } else {
1753             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1754         }
1755         var fileref = document.createElement('script');
1756         fileref.setAttribute('type','text/javascript');
1757         fileref.setAttribute('src', jsurl);
1758         fileref.onload = embed_function;
1759         fileref.onreadystatechange = embed_function;
1760         document.getElementsByTagName('head')[0].appendChild(fileref);
1761     }