a3d19461890f084c35d8ff1ae94611d0ecd4743b
[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 /**
743  * Set focus on username or password field of the login form
744  */
745 M.util.focus_login_form = function(Y) {
746     var username = Y.one('#username');
747     var password = Y.one('#password');
749     if (username == null || password == null) {
750         // something is wrong here
751         return;
752     }
754     var curElement = document.activeElement
755     if (curElement == 'undefined') {
756         // legacy browser - skip refocus protection
757     } else if (curElement.tagName == 'INPUT') {
758         // user was probably faster to focus something, do not mess with focus
759         return;
760     }
762     if (username.get('value') == '') {
763         username.focus();
764     } else {
765         password.focus();
766     }
770 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
772 function checkall() {
773     var inputs = document.getElementsByTagName('input');
774     for (var i = 0; i < inputs.length; i++) {
775         if (inputs[i].type == 'checkbox') {
776             inputs[i].checked = true;
777         }
778     }
781 function checknone() {
782     var inputs = document.getElementsByTagName('input');
783     for (var i = 0; i < inputs.length; i++) {
784         if (inputs[i].type == 'checkbox') {
785             inputs[i].checked = false;
786         }
787     }
790 /**
791  * Either check, or uncheck, all checkboxes inside the element with id is
792  * @param id the id of the container
793  * @param checked the new state, either '' or 'checked'.
794  */
795 function select_all_in_element_with_id(id, checked) {
796     var container = document.getElementById(id);
797     if (!container) {
798         return;
799     }
800     var inputs = container.getElementsByTagName('input');
801     for (var i = 0; i < inputs.length; ++i) {
802         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
803             inputs[i].checked = checked;
804         }
805     }
808 function select_all_in(elTagName, elClass, elId) {
809     var inputs = document.getElementsByTagName('input');
810     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
811     for(var i = 0; i < inputs.length; ++i) {
812         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
813             inputs[i].checked = 'checked';
814         }
815     }
818 function deselect_all_in(elTagName, elClass, elId) {
819     var inputs = document.getElementsByTagName('INPUT');
820     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
821     for(var i = 0; i < inputs.length; ++i) {
822         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
823             inputs[i].checked = '';
824         }
825     }
828 function confirm_if(expr, message) {
829     if(!expr) {
830         return true;
831     }
832     return confirm(message);
836 /*
837     findParentNode (start, elementName, elementClass, elementID)
839     Travels up the DOM hierarchy to find a parent element with the
840     specified tag name, class, and id. All conditions must be met,
841     but any can be ommitted. Returns the BODY element if no match
842     found.
843 */
844 function findParentNode(el, elName, elClass, elId) {
845     while (el.nodeName.toUpperCase() != 'BODY') {
846         if ((!elName || el.nodeName.toUpperCase() == elName) &&
847             (!elClass || el.className.indexOf(elClass) != -1) &&
848             (!elId || el.id == elId)) {
849             break;
850         }
851         el = el.parentNode;
852     }
853     return el;
855 /*
856     findChildNode (start, elementName, elementClass, elementID)
858     Travels down the DOM hierarchy to find all child elements with the
859     specified tag name, class, and id. All conditions must be met,
860     but any can be ommitted.
861     Doesn't examine children of matches.
862 */
863 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
864     var children = new Array();
865     for (var i = 0; i < start.childNodes.length; i++) {
866         var classfound = false;
867         var child = start.childNodes[i];
868         if((child.nodeType == 1) &&//element node type
869                   (elementClass && (typeof(child.className)=='string'))) {
870             var childClasses = child.className.split(/\s+/);
871             for (var childClassIndex in childClasses) {
872                 if (childClasses[childClassIndex]==elementClass) {
873                     classfound = true;
874                     break;
875                 }
876             }
877         }
878         if(child.nodeType == 1) { //element node type
879             if  ( (!tagName || child.nodeName == tagName) &&
880                 (!elementClass || classfound)&&
881                 (!elementID || child.id == elementID) &&
882                 (!elementName || child.name == elementName))
883             {
884                 children = children.concat(child);
885             } else {
886                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
887             }
888         }
889     }
890     return children;
893 function unmaskPassword(id) {
894   var pw = document.getElementById(id);
895   var chb = document.getElementById(id+'unmask');
897   try {
898     // first try IE way - it can not set name attribute later
899     if (chb.checked) {
900       var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
901     } else {
902       var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
903     }
904     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
905   } catch (e) {
906     var newpw = document.createElement('input');
907     newpw.setAttribute('name', pw.name);
908     if (chb.checked) {
909       newpw.setAttribute('type', 'text');
910     } else {
911       newpw.setAttribute('type', 'password');
912     }
913     newpw.setAttribute('class', pw.getAttribute('class'));
914   }
915   newpw.id = pw.id;
916   newpw.size = pw.size;
917   newpw.onblur = pw.onblur;
918   newpw.onchange = pw.onchange;
919   newpw.value = pw.value;
920   pw.parentNode.replaceChild(newpw, pw);
923 function filterByParent(elCollection, parentFinder) {
924     var filteredCollection = [];
925     for (var i = 0; i < elCollection.length; ++i) {
926         var findParent = parentFinder(elCollection[i]);
927         if (findParent.nodeName.toUpperCase != 'BODY') {
928             filteredCollection.push(elCollection[i]);
929         }
930     }
931     return filteredCollection;
934 /*
935     All this is here just so that IE gets to handle oversized blocks
936     in a visually pleasing manner. It does a browser detect. So sue me.
937 */
939 function fix_column_widths() {
940     var agt = navigator.userAgent.toLowerCase();
941     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
942         fix_column_width('left-column');
943         fix_column_width('right-column');
944     }
947 function fix_column_width(colName) {
948     if(column = document.getElementById(colName)) {
949         if(!column.offsetWidth) {
950             setTimeout("fix_column_width('" + colName + "')", 20);
951             return;
952         }
954         var width = 0;
955         var nodes = column.childNodes;
957         for(i = 0; i < nodes.length; ++i) {
958             if(nodes[i].className.indexOf("block") != -1 ) {
959                 if(width < nodes[i].offsetWidth) {
960                     width = nodes[i].offsetWidth;
961                 }
962             }
963         }
965         for(i = 0; i < nodes.length; ++i) {
966             if(nodes[i].className.indexOf("block") != -1 ) {
967                 nodes[i].style.width = width + 'px';
968             }
969         }
970     }
974 /*
975    Insert myValue at current cursor position
976  */
977 function insertAtCursor(myField, myValue) {
978     // IE support
979     if (document.selection) {
980         myField.focus();
981         sel = document.selection.createRange();
982         sel.text = myValue;
983     }
984     // Mozilla/Netscape support
985     else if (myField.selectionStart || myField.selectionStart == '0') {
986         var startPos = myField.selectionStart;
987         var endPos = myField.selectionEnd;
988         myField.value = myField.value.substring(0, startPos)
989             + myValue + myField.value.substring(endPos, myField.value.length);
990     } else {
991         myField.value += myValue;
992     }
996 /*
997         Call instead of setting window.onload directly or setting body onload=.
998         Adds your function to a chain of functions rather than overwriting anything
999         that exists.
1000 */
1001 function addonload(fn) {
1002     var oldhandler=window.onload;
1003     window.onload=function() {
1004         if(oldhandler) oldhandler();
1005             fn();
1006     }
1008 /**
1009  * Replacement for getElementsByClassName in browsers that aren't cool enough
1010  *
1011  * Relying on the built-in getElementsByClassName is far, far faster than
1012  * using YUI.
1013  *
1014  * Note: the third argument used to be an object with odd behaviour. It now
1015  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1016  * mimicked if you pass an object.
1017  *
1018  * @param {Node} oElm The top-level node for searching. To search a whole
1019  *                    document, use `document`.
1020  * @param {String} strTagName filter by tag names
1021  * @param {String} name same as HTML5 spec
1022  */
1023 function getElementsByClassName(oElm, strTagName, name) {
1024     // for backwards compatibility
1025     if(typeof name == "object") {
1026         var names = new Array();
1027         for(var i=0; i<name.length; i++) names.push(names[i]);
1028         name = names.join('');
1029     }
1030     // use native implementation if possible
1031     if (oElm.getElementsByClassName && Array.filter) {
1032         if (strTagName == '*') {
1033             return oElm.getElementsByClassName(name);
1034         } else {
1035             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1036                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1037             });
1038         }
1039     }
1040     // native implementation unavailable, fall back to slow method
1041     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1042     var arrReturnElements = new Array();
1043     var arrRegExpClassNames = new Array();
1044     var names = name.split(' ');
1045     for(var i=0; i<names.length; i++) {
1046         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1047     }
1048     var oElement;
1049     var bMatchesAll;
1050     for(var j=0; j<arrElements.length; j++) {
1051         oElement = arrElements[j];
1052         bMatchesAll = true;
1053         for(var k=0; k<arrRegExpClassNames.length; k++) {
1054             if(!arrRegExpClassNames[k].test(oElement.className)) {
1055                 bMatchesAll = false;
1056                 break;
1057             }
1058         }
1059         if(bMatchesAll) {
1060             arrReturnElements.push(oElement);
1061         }
1062     }
1063     return (arrReturnElements)
1066 function openpopup(event, args) {
1068     if (event) {
1069         if (event.preventDefault) {
1070             event.preventDefault();
1071         } else {
1072             event.returnValue = false;
1073         }
1074     }
1076     var fullurl = args.url;
1077     if (!args.url.match(/https?:\/\//)) {
1078         fullurl = M.cfg.wwwroot + args.url;
1079     }
1080     var windowobj = window.open(fullurl,args.name,args.options);
1081     if (!windowobj) {
1082         return true;
1083     }
1084     if (args.fullscreen) {
1085         windowobj.moveTo(0,0);
1086         windowobj.resizeTo(screen.availWidth,screen.availHeight);
1087     }
1088     windowobj.focus();
1090     return false;
1093 /** Close the current browser window. */
1094 function close_window(e) {
1095     if (e.preventDefault) {
1096         e.preventDefault();
1097     } else {
1098         e.returnValue = false;
1099     }
1100     window.close();
1103 /**
1104  * Used in a couple of modules to hide navigation areas when using AJAX
1105  */
1107 function show_item(itemid) {
1108     var item = document.getElementById(itemid);
1109     if (item) {
1110         item.style.display = "";
1111     }
1114 function destroy_item(itemid) {
1115     var item = document.getElementById(itemid);
1116     if (item) {
1117         item.parentNode.removeChild(item);
1118     }
1120 /**
1121  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1122  * @param controlid the control id.
1123  */
1124 function focuscontrol(controlid) {
1125     var control = document.getElementById(controlid);
1126     if (control) {
1127         control.focus();
1128     }
1131 /**
1132  * Transfers keyboard focus to an HTML element based on the old style style of focus
1133  * This function should be removed as soon as it is no longer used
1134  */
1135 function old_onload_focus(formid, controlname) {
1136     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1137         document.forms[formid].elements[controlname].focus();
1138     }
1141 function build_querystring(obj) {
1142     return convert_object_to_string(obj, '&');
1145 function build_windowoptionsstring(obj) {
1146     return convert_object_to_string(obj, ',');
1149 function convert_object_to_string(obj, separator) {
1150     if (typeof obj !== 'object') {
1151         return null;
1152     }
1153     var list = [];
1154     for(var k in obj) {
1155         k = encodeURIComponent(k);
1156         var value = obj[k];
1157         if(obj[k] instanceof Array) {
1158             for(var i in value) {
1159                 list.push(k+'[]='+encodeURIComponent(value[i]));
1160             }
1161         } else {
1162             list.push(k+'='+encodeURIComponent(value));
1163         }
1164     }
1165     return list.join(separator);
1168 function stripHTML(str) {
1169     var re = /<\S[^><]*>/g;
1170     var ret = str.replace(re, "");
1171     return ret;
1174 Number.prototype.fixed=function(n){
1175     with(Math)
1176         return round(Number(this)*pow(10,n))/pow(10,n);
1177 };
1178 function update_progress_bar (id, width, pt, msg, es){
1179     var percent = pt;
1180     var status = document.getElementById("status_"+id);
1181     var percent_indicator = document.getElementById("pt_"+id);
1182     var progress_bar = document.getElementById("progress_"+id);
1183     var time_es = document.getElementById("time_"+id);
1184     status.innerHTML = msg;
1185     percent_indicator.innerHTML = percent.fixed(2) + '%';
1186     if(percent == 100) {
1187         progress_bar.style.background = "green";
1188         time_es.style.display = "none";
1189     } else {
1190         progress_bar.style.background = "#FFCC66";
1191         if (es == '?'){
1192             time_es.innerHTML = "";
1193         }else {
1194             time_es.innerHTML = es.fixed(2)+" sec";
1195             time_es.style.display
1196                 = "block";
1197         }
1198     }
1199     progress_bar.style.width = width + "px";
1203 function frame_breakout(e, properties) {
1204     this.setAttribute('target', properties.framename);
1208 // ===== Deprecated core Javascript functions for Moodle ====
1209 //       DO NOT USE!!!!!!!
1210 // Do not put this stuff in separate file because it only adds extra load on servers!
1212 /**
1213  * Used in a couple of modules to hide navigation areas when using AJAX
1214  */
1215 function hide_item(itemid) {
1216     // use class='hiddenifjs' instead
1217     var item = document.getElementById(itemid);
1218     if (item) {
1219         item.style.display = "none";
1220     }
1223 M.util.help_icon = {
1224     Y : null,
1225     instance : null,
1226     add : function(Y, properties) {
1227         this.Y = Y;
1228         properties.node = Y.one('#'+properties.id);
1229         if (properties.node) {
1230             properties.node.on('click', this.display, this, properties);
1231         }
1232     },
1233     display : function(event, args) {
1234         event.preventDefault();
1235         if (M.util.help_icon.instance === null) {
1236             var Y = M.util.help_icon.Y;
1237             Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
1238                 var help_content_overlay = {
1239                     helplink : null,
1240                     overlay : null,
1241                     init : function() {
1243                         var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img  src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1244                         // Create an overlay from markup
1245                         this.overlay = new Y.Overlay({
1246                             headerContent: closebtn,
1247                             bodyContent: '',
1248                             id: 'helppopupbox',
1249                             width:'400px',
1250                             visible : false,
1251                             constrain : true
1252                         });
1253                         this.overlay.render(Y.one(document.body));
1255                         closebtn.on('click', this.overlay.hide, this.overlay);
1257                         var boundingBox = this.overlay.get("boundingBox");
1259                         //  Hide the menu if the user clicks outside of its content
1260                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1261                             var oTarget = event.target;
1262                             var menuButton = Y.one("#"+args.id);
1264                             if (!oTarget.compareTo(menuButton) &&
1265                                 !menuButton.contains(oTarget) &&
1266                                 !oTarget.compareTo(boundingBox) &&
1267                                 !boundingBox.contains(oTarget)) {
1268                                 this.overlay.hide();
1269                             }
1270                         }, this);
1272                         Y.on("key", this.close, closebtn , "down:13", this);
1273                         closebtn.on('click', this.close, this);
1274                     },
1276                     close : function(e) {
1277                         e.preventDefault();
1278                         this.helplink.focus();
1279                         this.overlay.hide();
1280                     },
1282                     display : function(event, args) {
1283                         this.helplink = args.node;
1284                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1285                         this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1287                         var fullurl = args.url;
1288                         if (!args.url.match(/https?:\/\//)) {
1289                             fullurl = M.cfg.wwwroot + args.url;
1290                         }
1292                         var ajaxurl = fullurl + '&ajax=1';
1294                         var cfg = {
1295                             method: 'get',
1296                             context : this,
1297                             on: {
1298                                 success: function(id, o, node) {
1299                                     this.display_callback(o.responseText);
1300                                 },
1301                                 failure: function(id, o, node) {
1302                                     var debuginfo = o.statusText;
1303                                     if (M.cfg.developerdebug) {
1304                                         o.statusText += ' (' + ajaxurl + ')';
1305                                     }
1306                                     this.display_callback('bodyContent',debuginfo);
1307                                 }
1308                             }
1309                         };
1311                         Y.io(ajaxurl, cfg);
1312                         this.overlay.show();
1314                         Y.one('#closehelpbox').focus();
1315                     },
1317                     display_callback : function(content) {
1318                         this.overlay.set('bodyContent', content);
1319                     },
1321                     hideContent : function() {
1322                         help = this;
1323                         help.overlay.hide();
1324                     }
1325                 };
1326                 help_content_overlay.init();
1327                 M.util.help_icon.instance = help_content_overlay;
1328                 M.util.help_icon.instance.display(event, args);
1329             });
1330         } else {
1331             M.util.help_icon.instance.display(event, args);
1332         }
1333     },
1334     init : function(Y) {
1335         this.Y = Y;
1336     }
1337 };
1339 /**
1340  * Custom menu namespace
1341  */
1342 M.core_custom_menu = {
1343     /**
1344      * This method is used to initialise a custom menu given the id that belongs
1345      * to the custom menu's root node.
1346      *
1347      * @param {YUI} Y
1348      * @param {string} nodeid
1349      */
1350     init : function(Y, nodeid) {
1351         var node = Y.one('#'+nodeid);
1352         if (node) {
1353             Y.use('node-menunav', function(Y) {
1354                 // Get the node
1355                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1356                 node.removeClass('javascript-disabled');
1357                 // Initialise the menunav plugin
1358                 node.plug(Y.Plugin.NodeMenuNav);
1359             });
1360         }
1361     }
1362 };
1364 /**
1365  * Used to store form manipulation methods and enhancments
1366  */
1367 M.form = M.form || {};
1369 /**
1370  * Converts a nbsp indented select box into a multi drop down custom control much
1371  * like the custom menu. It also selectable categories on or off.
1372  *
1373  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1374  *
1375  * @param {YUI} Y
1376  * @param {string} id
1377  * @param {Array} options
1378  */
1379 M.form.init_smartselect = function(Y, id, options) {
1380     if (!id.match(/^id_/)) {
1381         id = 'id_'+id;
1382     }
1383     var select = Y.one('select#'+id);
1384     if (!select) {
1385         return false;
1386     }
1387     Y.use('event-delegate',function(){
1388         var smartselect = {
1389             id : id,
1390             structure : [],
1391             options : [],
1392             submenucount : 0,
1393             currentvalue : null,
1394             currenttext : null,
1395             shownevent : null,
1396             cfg : {
1397                 selectablecategories : true,
1398                 mode : null
1399             },
1400             nodes : {
1401                 select : null,
1402                 loading : null,
1403                 menu : null
1404             },
1405             init : function(Y, id, args, nodes) {
1406                 if (typeof(args)=='object') {
1407                     for (var i in this.cfg) {
1408                         if (args[i] || args[i]===false) {
1409                             this.cfg[i] = args[i];
1410                         }
1411                     }
1412                 }
1414                 // Display a loading message first up
1415                 this.nodes.select = nodes.select;
1417                 this.currentvalue = this.nodes.select.get('selectedIndex');
1418                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1420                 var options = Array();
1421                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1422                 this.nodes.select.all('option').each(function(option, index) {
1423                     var rawtext = option.get('innerHTML');
1424                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1425                     if (rawtext === text) {
1426                         text = rawtext.replace(/^(\s)*/, '');
1427                         var depth = (rawtext.length - text.length ) + 1;
1428                     } else {
1429                         var depth = ((rawtext.length - text.length )/12)+1;
1430                     }
1431                     option.set('innerHTML', text);
1432                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1433                 }, this);
1435                 this.structure = [];
1436                 var structcount = 0;
1437                 for (var i in options) {
1438                     var o = options[i];
1439                     if (o.depth == 0) {
1440                         this.structure.push(o);
1441                         structcount++;
1442                     } else {
1443                         var d = o.depth;
1444                         var current = this.structure[structcount-1];
1445                         for (var j = 0; j < o.depth-1;j++) {
1446                             if (current && current.children) {
1447                                 current = current.children[current.children.length-1];
1448                             }
1449                         }
1450                         if (current && current.children) {
1451                             current.children.push(o);
1452                         }
1453                     }
1454                 }
1456                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1457                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1458                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1459                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1461                 if (this.cfg.mode == null) {
1462                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1463                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1464                         this.cfg.mode = 'compact';
1465                     } else {
1466                         this.cfg.mode = 'spanning';
1467                     }
1468                 }
1470                 if (this.cfg.mode == 'compact') {
1471                     this.nodes.menu.addClass('compactmenu');
1472                 } else {
1473                     this.nodes.menu.addClass('spanningmenu');
1474                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1475                 }
1477                 Y.one(document.body).append(this.nodes.menu);
1478                 var pos = this.nodes.select.getXY();
1479                 pos[0] += 1;
1480                 this.nodes.menu.setXY(pos);
1481                 this.nodes.menu.on('click', this.handle_click, this);
1483                 Y.one(window).on('resize', function(){
1484                      var pos = this.nodes.select.getXY();
1485                     pos[0] += 1;
1486                     this.nodes.menu.setXY(pos);
1487                  }, this);
1488             },
1489             generate_menu_content : function() {
1490                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1491                 content += this.generate_submenu_content(this.structure[0], true);
1492                 content += '</ul></div>';
1493                 return content;
1494             },
1495             generate_submenu_content : function(item, rootelement) {
1496                 this.submenucount++;
1497                 var content = '';
1498                 if (item.children.length > 0) {
1499                     if (rootelement) {
1500                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1501                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1502                         content += '<div class="smartselect_menu_content">';
1503                     } else {
1504                         content += '<li class="smartselect_submenuitem">';
1505                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1506                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1507                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1508                         content += '<div class="smartselect_submenu_content">';
1509                     }
1510                     content += '<ul>';
1511                     for (var i in item.children) {
1512                         content += this.generate_submenu_content(item.children[i],false);
1513                     }
1514                     content += '</ul>';
1515                     content += '</div>';
1516                     content += '</div>';
1517                     if (rootelement) {
1518                     } else {
1519                         content += '</li>';
1520                     }
1521                 } else {
1522                     content += '<li class="smartselect_menuitem">';
1523                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1524                     content += '</li>';
1525                 }
1526                 return content;
1527             },
1528             select : function(e) {
1529                 var t = e.target;
1530                 e.halt();
1531                 this.currenttext = t.get('innerHTML');
1532                 this.currentvalue = t.getAttribute('value');
1533                 this.nodes.select.set('selectedIndex', this.currentvalue);
1534                 this.hide_menu();
1535             },
1536             handle_click : function(e) {
1537                 var target = e.target;
1538                 if (target.hasClass('smartselect_mask')) {
1539                     this.show_menu(e);
1540                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1541                     this.select(e);
1542                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1543                     this.show_sub_menu(e);
1544                 }
1545             },
1546             show_menu : function(e) {
1547                 e.halt();
1548                 var menu = e.target.ancestor().one('.smartselect_menu');
1549                 menu.addClass('visible');
1550                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1551             },
1552             show_sub_menu : function(e) {
1553                 e.halt();
1554                 var target = e.target;
1555                 if (!target.hasClass('smartselect_submenuitem')) {
1556                     target = target.ancestor('.smartselect_submenuitem');
1557                 }
1558                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1559                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1560                     return;
1561                 }
1562                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1563                 target.one('.smartselect_submenu').addClass('visible');
1564             },
1565             hide_menu : function() {
1566                 this.nodes.menu.all('.visible').removeClass('visible');
1567                 if (this.shownevent) {
1568                     this.shownevent.detach();
1569                 }
1570             }
1571         };
1572         smartselect.init(Y, id, options, {select:select});
1573     });
1574 };
1576 /** List of flv players to be loaded */
1577 M.util.video_players = [];
1578 /** List of mp3 players to be loaded */
1579 M.util.audio_players = [];
1581 /**
1582  * Add video player
1583  * @param id element id
1584  * @param fileurl media url
1585  * @param width
1586  * @param height
1587  * @param autosize true means detect size from media
1588  */
1589 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1590     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1591 };
1593 /**
1594  * Add audio player.
1595  * @param id
1596  * @param fileurl
1597  * @param small
1598  */
1599 M.util.add_audio_player = function (id, fileurl, small) {
1600     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1601 };
1603 /**
1604  * Initialise all audio and video player, must be called from page footer.
1605  */
1606 M.util.load_flowplayer = function() {
1607     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1608         return;
1609     }
1610     if (typeof(flowplayer) == 'undefined') {
1611         var loaded = false;
1613         var embed_function = function() {
1614             if (loaded || typeof(flowplayer) == 'undefined') {
1615                 return;
1616             }
1617             loaded = true;
1619             var controls = {
1620                     autoHide: true
1621             }
1622             /* TODO: add CSS color overrides for the flv flow player */
1624             for(var i=0; i<M.util.video_players.length; i++) {
1625                 var video = M.util.video_players[i];
1626                 if (video.width > 0 && video.height > 0) {
1627                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1628                 } else {
1629                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1630                 }
1631                 flowplayer(video.id, src, {
1632                     plugins: {controls: controls},
1633                     clip: {
1634                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1635                         onMetaData: function(clip) {
1636                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1637                                 clip.mvideo.resized = true;
1638                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1639                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1640                                     // bad luck, we have to guess - we may not get metadata at all
1641                                     var width = clip.width;
1642                                     var height = clip.height;
1643                                 } else {
1644                                     var width = clip.metaData.width;
1645                                     var height = clip.metaData.height;
1646                                 }
1647                                 var minwidth = 300; // controls are messed up in smaller objects
1648                                 if (width < minwidth) {
1649                                     height = (height * minwidth) / width;
1650                                     width = minwidth;
1651                                 }
1653                                 var object = this._api();
1654                                 object.width = width;
1655                                 object.height = height;
1656                             }
1657                                 }
1658                     }
1659                 });
1660             }
1661             if (M.util.audio_players.length == 0) {
1662                 return;
1663             }
1664             var controls = {
1665                     autoHide: false,
1666                     fullscreen: false,
1667                     next: false,
1668                     previous: false,
1669                     scrubber: true,
1670                     play: true,
1671                     pause: true,
1672                     volume: true,
1673                     mute: false,
1674                     backgroundGradient: [0.5,0,0.3]
1675                 };
1677             var rule;
1678             for (var j=0; j < document.styleSheets.length; j++) {
1679                 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1680                     var allrules = document.styleSheets[j].rules;
1681                 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1682                     var allrules = document.styleSheets[j].cssRules;
1683                 } else {
1684                     // why??
1685                     continue;
1686                 }
1687                 for(var i=0; i<allrules.length; i++) {
1688                     rule = '';
1689                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1690                         if (typeof(allrules[i].cssText) != 'undefined') {
1691                             rule = allrules[i].style.cssText;
1692                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1693                             rule = allrules[i].style.cssText;
1694                         }
1695                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1696                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1697                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1698                             controls[colprop] = rule;
1699                         }
1700                     }
1701                 }
1702                 allrules = false;
1703             }
1705             for(i=0; i<M.util.audio_players.length; i++) {
1706                 var audio = M.util.audio_players[i];
1707                 if (audio.small) {
1708                     controls.controlall = false;
1709                     controls.height = 15;
1710                     controls.time = false;
1711                 } else {
1712                     controls.controlall = true;
1713                     controls.height = 25;
1714                     controls.time = true;
1715                 }
1716                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1717                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1718                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1719                 });
1720             }
1721         }
1723         if (M.cfg.jsrev == -10) {
1724             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1725         } else {
1726             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1727         }
1728         var fileref = document.createElement('script');
1729         fileref.setAttribute('type','text/javascript');
1730         fileref.setAttribute('src', jsurl);
1731         fileref.onload = embed_function;
1732         fileref.onreadystatechange = embed_function;
1733         document.getElementsByTagName('head')[0].appendChild(fileref);
1734     }