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