f8479204633bbc5b144d9fee88b247ea84827f14
[moodle.git] / lib / javascript-static.js
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     }
12 };
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;
47 };
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;
56 };
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     });
69 };
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);
149 };
151 /**
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;
158 /**
159  * The key divs that make up this
160  * @property div
161  * @type Y.Node
162  */
163 M.util.CollapsibleRegion.prototype.div = null;
165 /**
166  * The key divs that make up this
167  * @property icon
168  * @type Y.Node
169  */
170 M.util.CollapsibleRegion.prototype.icon = null;
172 /**
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.
175  *
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.
179  *
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     });
202 };
204 /**
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');
266             } else if ((targetancestor = target.ancestor('a')) !== null) {
267                 window.location = targetancestor.get('href');
268             } else if (target.test('input')) {
269                 targetform = target.ancestor('form');
270                 if (targetform && targetform.submit) {
271                     targetform.submit();
272                 }
273             } else if (M.cfg.developerdebug) {
274                 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT");
275             }
276         };
278         if (!args.cancellabel) {
279             args.cancellabel = M.str.moodle.cancel;
280         }
281         if (!args.continuelabel) {
282             args.continuelabel = M.str.moodle.yes;
283         }
285         var buttons = [
286             {text: args.cancellabel,   handler: handle_cancel, isDefault: true},
287             {text: args.continuelabel, handler: handle_yes}
288         ];
290         simpledialog.cfg.queueProperty('buttons', buttons);
292         simpledialog.render(document.body);
293         simpledialog.show();
294     });
295 };
297 /** Useful for full embedding of various stuff */
298 M.util.init_maximised_embed = function(Y, id) {
299     var obj = Y.one('#'+id);
300     if (!obj) {
301         return;
302     }
304     var get_htmlelement_size = function(el, prop) {
305         if (Y.Lang.isString(el)) {
306             el = Y.one('#' + el);
307         }
308         var val = el.getStyle(prop);
309         if (val == 'auto') {
310             val = el.getComputedStyle(prop);
311         }
312         return parseInt(val);
313     };
315     var resize_object = function() {
316         obj.setStyle('width', '0px');
317         obj.setStyle('height', '0px');
318         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
320         if (newwidth > 500) {
321             obj.setStyle('width', newwidth  + 'px');
322         } else {
323             obj.setStyle('width', '500px');
324         }
326         var headerheight = get_htmlelement_size('page-header', 'height');
327         var footerheight = get_htmlelement_size('page-footer', 'height');
328         var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
329         if (newheight < 400) {
330             newheight = 400;
331         }
332         obj.setStyle('height', newheight+'px');
333     };
335     resize_object();
336     // fix layout if window resized too
337     window.onresize = function() {
338         resize_object();
339     };
340 };
342 /**
343  * Attach handler to single_select
344  */
345 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
346     Y.use('event-key', function() {
347         var select = Y.one('#'+selectid);
348         if (select) {
349             // Try to get the form by id
350             var form = Y.one('#'+formid) || (function(){
351                 // Hmmm the form's id may have been overriden by an internal input
352                 // with the name id which will KILL IE.
353                 // We need to manually iterate at this point because if the case
354                 // above is true YUI's ancestor method will also kill IE!
355                 var form = select;
356                 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
357                     form = form.ancestor();
358                 }
359                 return form;
360             })();
361             // Make sure we have the form
362             if (form) {
363                 // Create a function to handle our change event
364                 var processchange = function(e, lastindex) {
365                     if ((nothing===false || select.get('value') != nothing) && lastindex != select.get('selectedIndex')) {
366                         this.submit();
367                     }
368                 };
369                 // Attach the change event to the keypress, blur, and click actions.
370                 // We don't use the change event because IE fires it on every arrow up/down
371                 // event.... usability
372                 Y.on('key', processchange, select, 'press:13', form, select.get('selectedIndex'));
373                 select.on('blur', processchange, form, select.get('selectedIndex'));
374                 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
375                 if (Y.UA.webkit) {
376                     select.on('change', processchange, form, select.get('selectedIndex'));
377                 } else {
378                     select.on('click', processchange, form, select.get('selectedIndex'));
379                 }
380             }
381         }
382     });
383 };
385 /**
386  * Attach handler to url_select
387  */
388 M.util.init_url_select = function(Y, formid, selectid, nothing) {
389     YUI(M.yui.loader).use('node', function(Y) {
390         Y.on('change', function() {
391             if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
392                 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
393             }
394         },
395         '#'+selectid);
396     });
397 };
399 /**
400  * Breaks out all links to the top frame - used in frametop page layout.
401  */
402 M.util.init_frametop = function(Y) {
403     Y.all('a').each(function(node) {
404         node.set('target', '_top');
405     });
406     Y.all('form').each(function(node) {
407         node.set('target', '_top');
408     });
409 };
411 /**
412  * Finds all nodes that match the given CSS selector and attaches events to them
413  * so that they toggle a given classname when clicked.
414  *
415  * @param {YUI} Y
416  * @param {string} id An id containing elements to target
417  * @param {string} cssselector A selector to use to find targets
418  * @param {string} toggleclassname A classname to toggle
419  */
420 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
422     if (togglecssselector == '') {
423         togglecssselector = cssselector;
424     }
426     var node = Y.one('#'+id);
427     node.all(cssselector).each(function(n){
428         n.on('click', function(e){
429             e.stopPropagation();
430             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
431                 if (this.test(togglecssselector)) {
432                     this.toggleClass(toggleclassname);
433                 } else {
434                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
435             }
436             }
437         }, n);
438     });
439     // Attach this click event to the node rather than all selectors... will be much better
440     // for performance
441     node.on('click', function(e){
442         if (e.target.hasClass('addtoall')) {
443             this.all(togglecssselector).addClass(toggleclassname);
444         } else if (e.target.hasClass('removefromall')) {
445             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
446         }
447     }, node);
448 };
450 /**
451  * Initialises a colour picker
452  *
453  * Designed to be used with admin_setting_configcolourpicker although could be used
454  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
455  * above or below the input (must have the same parent) and then call this with the
456  * id.
457  *
458  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
459  * contrib/blocks. For better docs refer to that.
460  *
461  * @param {YUI} Y
462  * @param {int} id
463  * @param {object} previewconf
464  */
465 M.util.init_colour_picker = function(Y, id, previewconf) {
466     /**
467      * We need node and event-mouseenter
468      */
469     Y.use('node', 'event-mouseenter', function(){
470         /**
471          * The colour picker object
472          */
473         var colourpicker = {
474             box : null,
475             input : null,
476             image : null,
477             preview : null,
478             current : null,
479             eventClick : null,
480             eventMouseEnter : null,
481             eventMouseLeave : null,
482             eventMouseMove : null,
483             width : 300,
484             height :  100,
485             factor : 5,
486             /**
487              * Initalises the colour picker by putting everything together and wiring the events
488              */
489             init : function() {
490                 this.input = Y.one('#'+id);
491                 this.box = this.input.ancestor().one('.admin_colourpicker');
492                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
493                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
494                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
495                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
496                 this.current = Y.Node.create('<div class="currentcolour"></div>');
497                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
498                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
500                 if (typeof(previewconf) === 'object' && previewconf !== null) {
501                     Y.one('#'+id+'_preview').on('click', function(e){
502                         if (Y.Lang.isString(previewconf.selector)) {
503                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
504                         } else {
505                             for (var i in previewconf.selector) {
506                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
507                             }
508                         }
509                     }, this);
510                 }
512                 this.eventClick = this.image.on('click', this.pickColour, this);
513                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
514             },
515             /**
516              * Starts to follow the mouse once it enter the image
517              */
518             startFollow : function(e) {
519                 this.eventMouseEnter.detach();
520                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
521                 this.eventMouseMove = this.image.on('mousemove', function(e){
522                     this.preview.setStyle('backgroundColor', this.determineColour(e));
523                 }, this);
524             },
525             /**
526              * Stops following the mouse
527              */
528             endFollow : function(e) {
529                 this.eventMouseMove.detach();
530                 this.eventMouseLeave.detach();
531                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
532             },
533             /**
534              * Picks the colour the was clicked on
535              */
536             pickColour : function(e) {
537                 var colour = this.determineColour(e);
538                 this.input.set('value', colour);
539                 this.current.setStyle('backgroundColor', colour);
540             },
541             /**
542              * Calculates the colour fromthe given co-ordinates
543              */
544             determineColour : function(e) {
545                 var eventx = Math.floor(e.pageX-e.target.getX());
546                 var eventy = Math.floor(e.pageY-e.target.getY());
548                 var imagewidth = this.width;
549                 var imageheight = this.height;
550                 var factor = this.factor;
551                 var colour = [255,0,0];
553                 var matrices = [
554                     [  0,  1,  0],
555                     [ -1,  0,  0],
556                     [  0,  0,  1],
557                     [  0, -1,  0],
558                     [  1,  0,  0],
559                     [  0,  0, -1]
560                 ];
562                 var matrixcount = matrices.length;
563                 var limit = Math.round(imagewidth/matrixcount);
564                 var heightbreak = Math.round(imageheight/2);
566                 for (var x = 0; x < imagewidth; x++) {
567                     var divisor = Math.floor(x / limit);
568                     var matrix = matrices[divisor];
570                     colour[0] += matrix[0]*factor;
571                     colour[1] += matrix[1]*factor;
572                     colour[2] += matrix[2]*factor;
574                     if (eventx==x) {
575                         break;
576                     }
577                 }
579                 var pixel = [colour[0], colour[1], colour[2]];
580                 if (eventy < heightbreak) {
581                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
582                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
583                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
584                 } else if (eventy > heightbreak) {
585                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
586                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
587                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
588                 }
590                 return this.convert_rgb_to_hex(pixel);
591             },
592             /**
593              * Converts an RGB value to Hex
594              */
595             convert_rgb_to_hex : function(rgb) {
596                 var hex = '#';
597                 var hexchars = "0123456789ABCDEF";
598                 for (var i=0; i<3; i++) {
599                     var number = Math.abs(rgb[i]);
600                     if (number == 0 || isNaN(number)) {
601                         hex += '00';
602                     } else {
603                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
604                     }
605                 }
606                 return hex;
607             }
608         };
609         /**
610          * Initialise the colour picker :) Hoorah
611          */
612         colourpicker.init();
613     });
614 };
616 M.util.init_block_hider = function(Y, config) {
617     Y.use('base', 'node', function(Y) {
618         M.util.block_hider = M.util.block_hider || (function(){
619             var blockhider = function() {
620                 blockhider.superclass.constructor.apply(this, arguments);
621             };
622             blockhider.prototype = {
623                 initializer : function(config) {
624                     this.set('block', '#'+this.get('id'));
625                     var b = this.get('block'),
626                         t = b.one('.title'),
627                         a = null;
628                     if (t && (a = t.one('.block_action'))) {
629                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
630                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
631                         hide.on('keypress', this.updateStateKey, this, true);
632                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
633                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
634                         show.on('keypress', this.updateStateKey, this, false);
635                         a.insert(show, 0).insert(hide, 0);
636                     }
637                 },
638                 updateState : function(e, hide) {
639                     M.util.set_user_preference(this.get('preference'), hide);
640                     if (hide) {
641                         this.get('block').addClass('hidden');
642                     } else {
643                         this.get('block').removeClass('hidden');
644                     }
645                 },
646                 updateStateKey : function(e, hide) {
647                     if (e.keyCode == 13) { //allow hide/show via enter key
648                         this.updateState(this, hide);
649                     }
650                 }
651             };
652             Y.extend(blockhider, Y.Base, blockhider.prototype, {
653                 NAME : 'blockhider',
654                 ATTRS : {
655                     id : {},
656                     preference : {},
657                     iconVisible : {
658                         value : M.util.image_url('t/switch_minus', 'moodle')
659                     },
660                     iconHidden : {
661                         value : M.util.image_url('t/switch_plus', 'moodle')
662                     },
663                     block : {
664                         setter : function(node) {
665                             return Y.one(node);
666                         }
667                     }
668                 }
669             });
670             return blockhider;
671         })();
672         new M.util.block_hider(config);
673     });
674 };
676 /**
677  * Returns a string registered in advance for usage in JavaScript
678  *
679  * If you do not pass the third parameter, the function will just return
680  * the corresponding value from the M.str object. If the third parameter is
681  * provided, the function performs {$a} placeholder substitution in the
682  * same way as PHP get_string() in Moodle does.
683  *
684  * @param {String} identifier string identifier
685  * @param {String} component the component providing the string
686  * @param {Object|String} a optional variable to populate placeholder with
687  */
688 M.util.get_string = function(identifier, component, a) {
689     var stringvalue;
691     if (M.cfg.developerdebug) {
692         // creating new instance if YUI is not optimal but it seems to be better way then
693         // require the instance via the function API - note that it is used in rare cases
694         // for debugging only anyway
695         var Y = new YUI({ debug : true });
696     }
698     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
699         stringvalue = '[[' + identifier + ',' + component + ']]';
700         if (M.cfg.developerdebug) {
701             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
702         }
703         return stringvalue;
704     }
706     stringvalue = M.str[component][identifier];
708     if (typeof a == 'undefined') {
709         // no placeholder substitution requested
710         return stringvalue;
711     }
713     if (typeof a == 'number' || typeof a == 'string') {
714         // replace all occurrences of {$a} with the placeholder value
715         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
716         return stringvalue;
717     }
719     if (typeof a == 'object') {
720         // replace {$a->key} placeholders
721         for (var key in a) {
722             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
723                 if (M.cfg.developerdebug) {
724                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
725                 }
726                 continue;
727             }
728             var search = '{$a->' + key + '}';
729             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
730             search = new RegExp(search, 'g');
731             stringvalue = stringvalue.replace(search, a[key]);
732         }
733         return stringvalue;
734     }
736     if (M.cfg.developerdebug) {
737         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
738     }
739     return stringvalue;
740 };
742 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
744 function checkall() {
745     var inputs = document.getElementsByTagName('input');
746     for (var i = 0; i < inputs.length; i++) {
747         if (inputs[i].type == 'checkbox') {
748             inputs[i].checked = true;
749         }
750     }
753 function checknone() {
754     var inputs = document.getElementsByTagName('input');
755     for (var i = 0; i < inputs.length; i++) {
756         if (inputs[i].type == 'checkbox') {
757             inputs[i].checked = false;
758         }
759     }
762 /**
763  * Either check, or uncheck, all checkboxes inside the element with id is
764  * @param id the id of the container
765  * @param checked the new state, either '' or 'checked'.
766  */
767 function select_all_in_element_with_id(id, checked) {
768     var container = document.getElementById(id);
769     if (!container) {
770         return;
771     }
772     var inputs = container.getElementsByTagName('input');
773     for (var i = 0; i < inputs.length; ++i) {
774         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
775             inputs[i].checked = checked;
776         }
777     }
780 function select_all_in(elTagName, elClass, elId) {
781     var inputs = document.getElementsByTagName('input');
782     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
783     for(var i = 0; i < inputs.length; ++i) {
784         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
785             inputs[i].checked = 'checked';
786         }
787     }
790 function deselect_all_in(elTagName, elClass, elId) {
791     var inputs = document.getElementsByTagName('INPUT');
792     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
793     for(var i = 0; i < inputs.length; ++i) {
794         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
795             inputs[i].checked = '';
796         }
797     }
800 function confirm_if(expr, message) {
801     if(!expr) {
802         return true;
803     }
804     return confirm(message);
808 /*
809     findParentNode (start, elementName, elementClass, elementID)
811     Travels up the DOM hierarchy to find a parent element with the
812     specified tag name, class, and id. All conditions must be met,
813     but any can be ommitted. Returns the BODY element if no match
814     found.
815 */
816 function findParentNode(el, elName, elClass, elId) {
817     while (el.nodeName.toUpperCase() != 'BODY') {
818         if ((!elName || el.nodeName.toUpperCase() == elName) &&
819             (!elClass || el.className.indexOf(elClass) != -1) &&
820             (!elId || el.id == elId)) {
821             break;
822         }
823         el = el.parentNode;
824     }
825     return el;
827 /*
828     findChildNode (start, elementName, elementClass, elementID)
830     Travels down the DOM hierarchy to find all child elements with the
831     specified tag name, class, and id. All conditions must be met,
832     but any can be ommitted.
833     Doesn't examine children of matches.
834 */
835 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
836     var children = new Array();
837     for (var i = 0; i < start.childNodes.length; i++) {
838         var classfound = false;
839         var child = start.childNodes[i];
840         if((child.nodeType == 1) &&//element node type
841                   (elementClass && (typeof(child.className)=='string'))) {
842             var childClasses = child.className.split(/\s+/);
843             for (var childClassIndex in childClasses) {
844                 if (childClasses[childClassIndex]==elementClass) {
845                     classfound = true;
846                     break;
847                 }
848             }
849         }
850         if(child.nodeType == 1) { //element node type
851             if  ( (!tagName || child.nodeName == tagName) &&
852                 (!elementClass || classfound)&&
853                 (!elementID || child.id == elementID) &&
854                 (!elementName || child.name == elementName))
855             {
856                 children = children.concat(child);
857             } else {
858                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
859             }
860         }
861     }
862     return children;
865 function unmaskPassword(id) {
866   var pw = document.getElementById(id);
867   var chb = document.getElementById(id+'unmask');
869   try {
870     // first try IE way - it can not set name attribute later
871     if (chb.checked) {
872       var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
873     } else {
874       var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
875     }
876     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
877   } catch (e) {
878     var newpw = document.createElement('input');
879     newpw.setAttribute('name', pw.name);
880     if (chb.checked) {
881       newpw.setAttribute('type', 'text');
882     } else {
883       newpw.setAttribute('type', 'password');
884     }
885     newpw.setAttribute('class', pw.getAttribute('class'));
886   }
887   newpw.id = pw.id;
888   newpw.size = pw.size;
889   newpw.onblur = pw.onblur;
890   newpw.onchange = pw.onchange;
891   newpw.value = pw.value;
892   pw.parentNode.replaceChild(newpw, pw);
895 function filterByParent(elCollection, parentFinder) {
896     var filteredCollection = [];
897     for (var i = 0; i < elCollection.length; ++i) {
898         var findParent = parentFinder(elCollection[i]);
899         if (findParent.nodeName.toUpperCase != 'BODY') {
900             filteredCollection.push(elCollection[i]);
901         }
902     }
903     return filteredCollection;
906 /*
907     All this is here just so that IE gets to handle oversized blocks
908     in a visually pleasing manner. It does a browser detect. So sue me.
909 */
911 function fix_column_widths() {
912     var agt = navigator.userAgent.toLowerCase();
913     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
914         fix_column_width('left-column');
915         fix_column_width('right-column');
916     }
919 function fix_column_width(colName) {
920     if(column = document.getElementById(colName)) {
921         if(!column.offsetWidth) {
922             setTimeout("fix_column_width('" + colName + "')", 20);
923             return;
924         }
926         var width = 0;
927         var nodes = column.childNodes;
929         for(i = 0; i < nodes.length; ++i) {
930             if(nodes[i].className.indexOf("block") != -1 ) {
931                 if(width < nodes[i].offsetWidth) {
932                     width = nodes[i].offsetWidth;
933                 }
934             }
935         }
937         for(i = 0; i < nodes.length; ++i) {
938             if(nodes[i].className.indexOf("block") != -1 ) {
939                 nodes[i].style.width = width + 'px';
940             }
941         }
942     }
946 /*
947    Insert myValue at current cursor position
948  */
949 function insertAtCursor(myField, myValue) {
950     // IE support
951     if (document.selection) {
952         myField.focus();
953         sel = document.selection.createRange();
954         sel.text = myValue;
955     }
956     // Mozilla/Netscape support
957     else if (myField.selectionStart || myField.selectionStart == '0') {
958         var startPos = myField.selectionStart;
959         var endPos = myField.selectionEnd;
960         myField.value = myField.value.substring(0, startPos)
961             + myValue + myField.value.substring(endPos, myField.value.length);
962     } else {
963         myField.value += myValue;
964     }
968 /*
969         Call instead of setting window.onload directly or setting body onload=.
970         Adds your function to a chain of functions rather than overwriting anything
971         that exists.
972 */
973 function addonload(fn) {
974     var oldhandler=window.onload;
975     window.onload=function() {
976         if(oldhandler) oldhandler();
977             fn();
978     }
980 /**
981  * Replacement for getElementsByClassName in browsers that aren't cool enough
982  *
983  * Relying on the built-in getElementsByClassName is far, far faster than
984  * using YUI.
985  *
986  * Note: the third argument used to be an object with odd behaviour. It now
987  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
988  * mimicked if you pass an object.
989  *
990  * @param {Node} oElm The top-level node for searching. To search a whole
991  *                    document, use `document`.
992  * @param {String} strTagName filter by tag names
993  * @param {String} name same as HTML5 spec
994  */
995 function getElementsByClassName(oElm, strTagName, name) {
996     // for backwards compatibility
997     if(typeof name == "object") {
998         var names = new Array();
999         for(var i=0; i<name.length; i++) names.push(names[i]);
1000         name = names.join('');
1001     }
1002     // use native implementation if possible
1003     if (oElm.getElementsByClassName && Array.filter) {
1004         if (strTagName == '*') {
1005             return oElm.getElementsByClassName(name);
1006         } else {
1007             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1008                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1009             });
1010         }
1011     }
1012     // native implementation unavailable, fall back to slow method
1013     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1014     var arrReturnElements = new Array();
1015     var arrRegExpClassNames = new Array();
1016     var names = name.split(' ');
1017     for(var i=0; i<names.length; i++) {
1018         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1019     }
1020     var oElement;
1021     var bMatchesAll;
1022     for(var j=0; j<arrElements.length; j++) {
1023         oElement = arrElements[j];
1024         bMatchesAll = true;
1025         for(var k=0; k<arrRegExpClassNames.length; k++) {
1026             if(!arrRegExpClassNames[k].test(oElement.className)) {
1027                 bMatchesAll = false;
1028                 break;
1029             }
1030         }
1031         if(bMatchesAll) {
1032             arrReturnElements.push(oElement);
1033         }
1034     }
1035     return (arrReturnElements)
1038 function openpopup(event, args) {
1040     if (event) {
1041         if (event.preventDefault) {
1042             event.preventDefault();
1043         } else {
1044             event.returnValue = false;
1045         }
1046     }
1048     var fullurl = args.url;
1049     if (!args.url.match(/https?:\/\//)) {
1050         fullurl = M.cfg.wwwroot + args.url;
1051     }
1052     var windowobj = window.open(fullurl,args.name,args.options);
1053     if (!windowobj) {
1054         return true;
1055     }
1056     if (args.fullscreen) {
1057         windowobj.moveTo(0,0);
1058         windowobj.resizeTo(screen.availWidth,screen.availHeight);
1059     }
1060     windowobj.focus();
1062     return false;
1065 /** Close the current browser window. */
1066 function close_window(e) {
1067     if (e.preventDefault) {
1068         e.preventDefault();
1069     } else {
1070         e.returnValue = false;
1071     }
1072     window.close();
1075 /**
1076  * Used in a couple of modules to hide navigation areas when using AJAX
1077  */
1079 function show_item(itemid) {
1080     var item = document.getElementById(itemid);
1081     if (item) {
1082         item.style.display = "";
1083     }
1086 function destroy_item(itemid) {
1087     var item = document.getElementById(itemid);
1088     if (item) {
1089         item.parentNode.removeChild(item);
1090     }
1092 /**
1093  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1094  * @param controlid the control id.
1095  */
1096 function focuscontrol(controlid) {
1097     var control = document.getElementById(controlid);
1098     if (control) {
1099         control.focus();
1100     }
1103 /**
1104  * Transfers keyboard focus to an HTML element based on the old style style of focus
1105  * This function should be removed as soon as it is no longer used
1106  */
1107 function old_onload_focus(formid, controlname) {
1108     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1109         document.forms[formid].elements[controlname].focus();
1110     }
1113 function build_querystring(obj) {
1114     return convert_object_to_string(obj, '&');
1117 function build_windowoptionsstring(obj) {
1118     return convert_object_to_string(obj, ',');
1121 function convert_object_to_string(obj, separator) {
1122     if (typeof obj !== 'object') {
1123         return null;
1124     }
1125     var list = [];
1126     for(var k in obj) {
1127         k = encodeURIComponent(k);
1128         var value = obj[k];
1129         if(obj[k] instanceof Array) {
1130             for(var i in value) {
1131                 list.push(k+'[]='+encodeURIComponent(value[i]));
1132             }
1133         } else {
1134             list.push(k+'='+encodeURIComponent(value));
1135         }
1136     }
1137     return list.join(separator);
1140 function stripHTML(str) {
1141     var re = /<\S[^><]*>/g;
1142     var ret = str.replace(re, "");
1143     return ret;
1146 Number.prototype.fixed=function(n){
1147     with(Math)
1148         return round(Number(this)*pow(10,n))/pow(10,n);
1149 };
1150 function update_progress_bar (id, width, pt, msg, es){
1151     var percent = pt;
1152     var status = document.getElementById("status_"+id);
1153     var percent_indicator = document.getElementById("pt_"+id);
1154     var progress_bar = document.getElementById("progress_"+id);
1155     var time_es = document.getElementById("time_"+id);
1156     status.innerHTML = msg;
1157     percent_indicator.innerHTML = percent.fixed(2) + '%';
1158     if(percent == 100) {
1159         progress_bar.style.background = "green";
1160         time_es.style.display = "none";
1161     } else {
1162         progress_bar.style.background = "#FFCC66";
1163         if (es == '?'){
1164             time_es.innerHTML = "";
1165         }else {
1166             time_es.innerHTML = es.fixed(2)+" sec";
1167             time_es.style.display
1168                 = "block";
1169         }
1170     }
1171     progress_bar.style.width = width + "px";
1175 function frame_breakout(e, properties) {
1176     this.setAttribute('target', properties.framename);
1180 // ===== Deprecated core Javascript functions for Moodle ====
1181 //       DO NOT USE!!!!!!!
1182 // Do not put this stuff in separate file because it only adds extra load on servers!
1184 /**
1185  * Used in a couple of modules to hide navigation areas when using AJAX
1186  */
1187 function hide_item(itemid) {
1188     // use class='hiddenifjs' instead
1189     var item = document.getElementById(itemid);
1190     if (item) {
1191         item.style.display = "none";
1192     }
1195 M.util.help_icon = {
1196     Y : null,
1197     instance : null,
1198     add : function(Y, properties) {
1199         this.Y = Y;
1200         properties.node = Y.one('#'+properties.id);
1201         if (properties.node) {
1202             properties.node.on('click', this.display, this, properties);
1203         }
1204     },
1205     display : function(event, args) {
1206         event.preventDefault();
1207         if (M.util.help_icon.instance === null) {
1208             var Y = M.util.help_icon.Y;
1209             Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
1210                 var help_content_overlay = {
1211                     helplink : null,
1212                     overlay : null,
1213                     init : function() {
1215                         var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img  src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1216                         // Create an overlay from markup
1217                         this.overlay = new Y.Overlay({
1218                             headerContent: closebtn,
1219                             bodyContent: '',
1220                             id: 'helppopupbox',
1221                             width:'400px',
1222                             visible : false,
1223                             constrain : true
1224                         });
1225                         this.overlay.render(Y.one(document.body));
1227                         closebtn.on('click', this.overlay.hide, this.overlay);
1229                         var boundingBox = this.overlay.get("boundingBox");
1231                         //  Hide the menu if the user clicks outside of its content
1232                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1233                             var oTarget = event.target;
1234                             var menuButton = Y.one("#"+args.id);
1236                             if (!oTarget.compareTo(menuButton) &&
1237                                 !menuButton.contains(oTarget) &&
1238                                 !oTarget.compareTo(boundingBox) &&
1239                                 !boundingBox.contains(oTarget)) {
1240                                 this.overlay.hide();
1241                             }
1242                         }, this);
1244                         Y.on("key", this.close, closebtn , "down:13", this);
1245                         closebtn.on('click', this.close, this);
1246                     },
1248                     close : function(e) {
1249                         e.preventDefault();
1250                         this.helplink.focus();
1251                         this.overlay.hide();
1252                     },
1254                     display : function(event, args) {
1255                         this.helplink = args.node;
1256                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1257                         this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1259                         var fullurl = args.url;
1260                         if (!args.url.match(/https?:\/\//)) {
1261                             fullurl = M.cfg.wwwroot + args.url;
1262                         }
1264                         var ajaxurl = fullurl + '&ajax=1';
1266                         var cfg = {
1267                             method: 'get',
1268                             context : this,
1269                             on: {
1270                                 success: function(id, o, node) {
1271                                     this.display_callback(o.responseText);
1272                                 },
1273                                 failure: function(id, o, node) {
1274                                     var debuginfo = o.statusText;
1275                                     if (M.cfg.developerdebug) {
1276                                         o.statusText += ' (' + ajaxurl + ')';
1277                                     }
1278                                     this.display_callback('bodyContent',debuginfo);
1279                                 }
1280                             }
1281                         };
1283                         Y.io(ajaxurl, cfg);
1284                         this.overlay.show();
1286                         Y.one('#closehelpbox').focus();
1287                     },
1289                     display_callback : function(content) {
1290                         this.overlay.set('bodyContent', content);
1291                     },
1293                     hideContent : function() {
1294                         help = this;
1295                         help.overlay.hide();
1296                     }
1297                 };
1298                 help_content_overlay.init();
1299                 M.util.help_icon.instance = help_content_overlay;
1300                 M.util.help_icon.instance.display(event, args);
1301             });
1302         } else {
1303             M.util.help_icon.instance.display(event, args);
1304         }
1305     },
1306     init : function(Y) {
1307         this.Y = Y;
1308     }
1309 };
1311 /**
1312  * Custom menu namespace
1313  */
1314 M.core_custom_menu = {
1315     /**
1316      * This method is used to initialise a custom menu given the id that belongs
1317      * to the custom menu's root node.
1318      *
1319      * @param {YUI} Y
1320      * @param {string} nodeid
1321      */
1322     init : function(Y, nodeid) {
1323         var node = Y.one('#'+nodeid);
1324         if (node) {
1325             Y.use('node-menunav', function(Y) {
1326                 // Get the node
1327                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1328                 node.removeClass('javascript-disabled');
1329                 // Initialise the menunav plugin
1330                 node.plug(Y.Plugin.NodeMenuNav);
1331             });
1332         }
1333     }
1334 };
1336 /**
1337  * Used to store form manipulation methods and enhancments
1338  */
1339 M.form = M.form || {};
1341 /**
1342  * Converts a nbsp indented select box into a multi drop down custom control much
1343  * like the custom menu. It also selectable categories on or off.
1344  *
1345  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1346  *
1347  * @param {YUI} Y
1348  * @param {string} id
1349  * @param {Array} options
1350  */
1351 M.form.init_smartselect = function(Y, id, options) {
1352     if (!id.match(/^id_/)) {
1353         id = 'id_'+id;
1354     }
1355     var select = Y.one('select#'+id);
1356     if (!select) {
1357         return false;
1358     }
1359     Y.use('event-delegate',function(){
1360         var smartselect = {
1361             id : id,
1362             structure : [],
1363             options : [],
1364             submenucount : 0,
1365             currentvalue : null,
1366             currenttext : null,
1367             shownevent : null,
1368             cfg : {
1369                 selectablecategories : true,
1370                 mode : null
1371             },
1372             nodes : {
1373                 select : null,
1374                 loading : null,
1375                 menu : null
1376             },
1377             init : function(Y, id, args, nodes) {
1378                 if (typeof(args)=='object') {
1379                     for (var i in this.cfg) {
1380                         if (args[i] || args[i]===false) {
1381                             this.cfg[i] = args[i];
1382                         }
1383                     }
1384                 }
1386                 // Display a loading message first up
1387                 this.nodes.select = nodes.select;
1389                 this.currentvalue = this.nodes.select.get('selectedIndex');
1390                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1392                 var options = Array();
1393                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1394                 this.nodes.select.all('option').each(function(option, index) {
1395                     var rawtext = option.get('innerHTML');
1396                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1397                     if (rawtext === text) {
1398                         text = rawtext.replace(/^(\s)*/, '');
1399                         var depth = (rawtext.length - text.length ) + 1;
1400                     } else {
1401                         var depth = ((rawtext.length - text.length )/12)+1;
1402                     }
1403                     option.set('innerHTML', text);
1404                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1405                 }, this);
1407                 this.structure = [];
1408                 var structcount = 0;
1409                 for (var i in options) {
1410                     var o = options[i];
1411                     if (o.depth == 0) {
1412                         this.structure.push(o);
1413                         structcount++;
1414                     } else {
1415                         var d = o.depth;
1416                         var current = this.structure[structcount-1];
1417                         for (var j = 0; j < o.depth-1;j++) {
1418                             if (current && current.children) {
1419                                 current = current.children[current.children.length-1];
1420                             }
1421                         }
1422                         if (current && current.children) {
1423                             current.children.push(o);
1424                         }
1425                     }
1426                 }
1428                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1429                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1430                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1431                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1433                 if (this.cfg.mode == null) {
1434                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1435                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1436                         this.cfg.mode = 'compact';
1437                     } else {
1438                         this.cfg.mode = 'spanning';
1439                     }
1440                 }
1442                 if (this.cfg.mode == 'compact') {
1443                     this.nodes.menu.addClass('compactmenu');
1444                 } else {
1445                     this.nodes.menu.addClass('spanningmenu');
1446                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1447                 }
1449                 Y.one(document.body).append(this.nodes.menu);
1450                 var pos = this.nodes.select.getXY();
1451                 pos[0] += 1;
1452                 this.nodes.menu.setXY(pos);
1453                 this.nodes.menu.on('click', this.handle_click, this);
1455                 Y.one(window).on('resize', function(){
1456                      var pos = this.nodes.select.getXY();
1457                     pos[0] += 1;
1458                     this.nodes.menu.setXY(pos);
1459                  }, this);
1460             },
1461             generate_menu_content : function() {
1462                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1463                 content += this.generate_submenu_content(this.structure[0], true);
1464                 content += '</ul></div>';
1465                 return content;
1466             },
1467             generate_submenu_content : function(item, rootelement) {
1468                 this.submenucount++;
1469                 var content = '';
1470                 if (item.children.length > 0) {
1471                     if (rootelement) {
1472                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1473                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1474                         content += '<div class="smartselect_menu_content">';
1475                     } else {
1476                         content += '<li class="smartselect_submenuitem">';
1477                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1478                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1479                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1480                         content += '<div class="smartselect_submenu_content">';
1481                     }
1482                     content += '<ul>';
1483                     for (var i in item.children) {
1484                         content += this.generate_submenu_content(item.children[i],false);
1485                     }
1486                     content += '</ul>';
1487                     content += '</div>';
1488                     content += '</div>';
1489                     if (rootelement) {
1490                     } else {
1491                         content += '</li>';
1492                     }
1493                 } else {
1494                     content += '<li class="smartselect_menuitem">';
1495                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1496                     content += '</li>';
1497                 }
1498                 return content;
1499             },
1500             select : function(e) {
1501                 var t = e.target;
1502                 e.halt();
1503                 this.currenttext = t.get('innerHTML');
1504                 this.currentvalue = t.getAttribute('value');
1505                 this.nodes.select.set('selectedIndex', this.currentvalue);
1506                 this.hide_menu();
1507             },
1508             handle_click : function(e) {
1509                 var target = e.target;
1510                 if (target.hasClass('smartselect_mask')) {
1511                     this.show_menu(e);
1512                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1513                     this.select(e);
1514                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1515                     this.show_sub_menu(e);
1516                 }
1517             },
1518             show_menu : function(e) {
1519                 e.halt();
1520                 var menu = e.target.ancestor().one('.smartselect_menu');
1521                 menu.addClass('visible');
1522                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1523             },
1524             show_sub_menu : function(e) {
1525                 e.halt();
1526                 var target = e.target;
1527                 if (!target.hasClass('smartselect_submenuitem')) {
1528                     target = target.ancestor('.smartselect_submenuitem');
1529                 }
1530                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1531                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1532                     return;
1533                 }
1534                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1535                 target.one('.smartselect_submenu').addClass('visible');
1536             },
1537             hide_menu : function() {
1538                 this.nodes.menu.all('.visible').removeClass('visible');
1539                 if (this.shownevent) {
1540                     this.shownevent.detach();
1541                 }
1542             }
1543         };
1544         smartselect.init(Y, id, options, {select:select});
1545     });
1546 };
1548 /** List of flv players to be loaded */
1549 M.util.video_players = [];
1550 /** List of mp3 players to be loaded */
1551 M.util.audio_players = [];
1553 /**
1554  * Add video player
1555  * @param id element id
1556  * @param fileurl media url
1557  * @param width
1558  * @param height
1559  * @param autosize true means detect size from media
1560  */
1561 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1562     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1563 };
1565 /**
1566  * Add audio player.
1567  * @param id
1568  * @param fileurl
1569  * @param small
1570  */
1571 M.util.add_audio_player = function (id, fileurl, small) {
1572     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1573 };
1575 /**
1576  * Initialise all audio and video player, must be called from page footer.
1577  */
1578 M.util.load_flowplayer = function() {
1579     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1580         return;
1581     }
1582     if (typeof(flowplayer) == 'undefined') {
1583         var loaded = false;
1585         var embed_function = function() {
1586             if (loaded || typeof(flowplayer) == 'undefined') {
1587                 return;
1588             }
1589             loaded = true;
1591             var controls = {
1592                     autoHide: true
1593             }
1594             /* TODO: add CSS color overrides for the flv flow player */
1596             for(var i=0; i<M.util.video_players.length; i++) {
1597                 var video = M.util.video_players[i];
1598                 if (video.width > 0 && video.height > 0) {
1599                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1600                 } else {
1601                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1602                 }
1603                 flowplayer(video.id, src, {
1604                     plugins: {controls: controls},
1605                     clip: {
1606                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1607                         onMetaData: function(clip) {
1608                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1609                                 clip.mvideo.resized = true;
1610                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1611                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1612                                     // bad luck, we have to guess - we may not get metadata at all
1613                                     var width = clip.width;
1614                                     var height = clip.height;
1615                                 } else {
1616                                     var width = clip.metaData.width;
1617                                     var height = clip.metaData.height;
1618                                 }
1619                                 var minwidth = 300; // controls are messed up in smaller objects
1620                                 if (width < minwidth) {
1621                                     height = (height * minwidth) / width;
1622                                     width = minwidth;
1623                                 }
1625                                 var object = this._api();
1626                                 object.width = width;
1627                                 object.height = height;
1628                             }
1629                                 }
1630                     }
1631                 });
1632             }
1633             if (M.util.audio_players.length == 0) {
1634                 return;
1635             }
1636             var controls = {
1637                     autoHide: false,
1638                     fullscreen: false,
1639                     next: false,
1640                     previous: false,
1641                     scrubber: true,
1642                     play: true,
1643                     pause: true,
1644                     volume: true,
1645                     mute: false,
1646                     backgroundGradient: [0.5,0,0.3]
1647                 };
1649             var rule;
1650             for (var j=0; j < document.styleSheets.length; j++) {
1651                 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1652                     var allrules = document.styleSheets[j].rules;
1653                 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1654                     var allrules = document.styleSheets[j].cssRules;
1655                 } else {
1656                     // why??
1657                     continue;
1658                 }
1659                 for(var i=0; i<allrules.length; i++) {
1660                     rule = '';
1661                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1662                         if (typeof(allrules[i].cssText) != 'undefined') {
1663                             rule = allrules[i].style.cssText;
1664                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1665                             rule = allrules[i].style.cssText;
1666                         }
1667                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1668                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1669                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1670                             controls[colprop] = rule;
1671                         }
1672                     }
1673                 }
1674                 allrules = false;
1675             }
1677             for(i=0; i<M.util.audio_players.length; i++) {
1678                 var audio = M.util.audio_players[i];
1679                 if (audio.small) {
1680                     controls.controlall = false;
1681                     controls.height = 15;
1682                     controls.time = false;
1683                 } else {
1684                     controls.controlall = true;
1685                     controls.height = 25;
1686                     controls.time = true;
1687                 }
1688                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1689                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1690                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1691                 });
1692             }
1693         }
1695         if (M.cfg.jsrev == -10) {
1696             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1697         } else {
1698             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1699         }
1700         var fileref = document.createElement('script');
1701         fileref.setAttribute('type','text/javascript');
1702         fileref.setAttribute('src', jsurl);
1703         fileref.onload = embed_function;
1704         fileref.onreadystatechange = embed_function;
1705         document.getElementsByTagName('head')[0].appendChild(fileref);
1706     }