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