MDL-35537 - Right align registration text on login page, when in RTL mode
[moodle.git] / lib / javascript-static.js
blobc9d2efd19e61f662e474c13de0d011bfe5da855b
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');
91     // Create a link
92     var a = Y.Node.create('<a href="#"></a>');
93     a.setAttribute('title', strtooltip);
95     // Get all the nodes from caption, remove them and append them to <a>
96     while (caption.hasChildNodes()) {
97         child = caption.get('firstChild');
98         child.remove();
99         a.append(child);
100     }
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         // To ensure we don't kill browser performance if hundreds of get_string requests
719         // are made we cache the instance we generate within the M.util namespace.
720         // We don't publicly define the variable so that it doesn't get abused.
721         if (typeof M.util.get_string_yui_instance === 'undefined') {
722             M.util.get_string_yui_instance = new YUI({ debug : true });
723         }
724         var Y = M.util.get_string_yui_instance;
725     }
727     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
728         stringvalue = '[[' + identifier + ',' + component + ']]';
729         if (M.cfg.developerdebug) {
730             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
731         }
732         return stringvalue;
733     }
735     stringvalue = M.str[component][identifier];
737     if (typeof a == 'undefined') {
738         // no placeholder substitution requested
739         return stringvalue;
740     }
742     if (typeof a == 'number' || typeof a == 'string') {
743         // replace all occurrences of {$a} with the placeholder value
744         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
745         return stringvalue;
746     }
748     if (typeof a == 'object') {
749         // replace {$a->key} placeholders
750         for (var key in a) {
751             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
752                 if (M.cfg.developerdebug) {
753                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
754                 }
755                 continue;
756             }
757             var search = '{$a->' + key + '}';
758             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
759             search = new RegExp(search, 'g');
760             stringvalue = stringvalue.replace(search, a[key]);
761         }
762         return stringvalue;
763     }
765     if (M.cfg.developerdebug) {
766         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
767     }
768     return stringvalue;
772  * Set focus on username or password field of the login form
773  */
774 M.util.focus_login_form = function(Y) {
775     var username = Y.one('#username');
776     var password = Y.one('#password');
778     if (username == null || password == null) {
779         // something is wrong here
780         return;
781     }
783     var curElement = document.activeElement
784     if (curElement == 'undefined') {
785         // legacy browser - skip refocus protection
786     } else if (curElement.tagName == 'INPUT') {
787         // user was probably faster to focus something, do not mess with focus
788         return;
789     }
791     if (username.get('value') == '') {
792         username.focus();
793     } else {
794         password.focus();
795     }
799 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
801 function checkall() {
802     var inputs = document.getElementsByTagName('input');
803     for (var i = 0; i < inputs.length; i++) {
804         if (inputs[i].type == 'checkbox') {
805             if (inputs[i].disabled || inputs[i].readOnly) {
806                 continue;
807             }
808             inputs[i].checked = true;
809         }
810     }
813 function checknone() {
814     var inputs = document.getElementsByTagName('input');
815     for (var i = 0; i < inputs.length; i++) {
816         if (inputs[i].type == 'checkbox') {
817             if (inputs[i].disabled || inputs[i].readOnly) {
818                 continue;
819             }
820             inputs[i].checked = false;
821         }
822     }
826  * Either check, or uncheck, all checkboxes inside the element with id is
827  * @param id the id of the container
828  * @param checked the new state, either '' or 'checked'.
829  */
830 function select_all_in_element_with_id(id, checked) {
831     var container = document.getElementById(id);
832     if (!container) {
833         return;
834     }
835     var inputs = container.getElementsByTagName('input');
836     for (var i = 0; i < inputs.length; ++i) {
837         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
838             inputs[i].checked = checked;
839         }
840     }
843 function select_all_in(elTagName, elClass, elId) {
844     var inputs = document.getElementsByTagName('input');
845     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
846     for(var i = 0; i < inputs.length; ++i) {
847         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
848             inputs[i].checked = 'checked';
849         }
850     }
853 function deselect_all_in(elTagName, elClass, elId) {
854     var inputs = document.getElementsByTagName('INPUT');
855     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
856     for(var i = 0; i < inputs.length; ++i) {
857         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
858             inputs[i].checked = '';
859         }
860     }
863 function confirm_if(expr, message) {
864     if(!expr) {
865         return true;
866     }
867     return confirm(message);
872     findParentNode (start, elementName, elementClass, elementID)
874     Travels up the DOM hierarchy to find a parent element with the
875     specified tag name, class, and id. All conditions must be met,
876     but any can be ommitted. Returns the BODY element if no match
877     found.
879 function findParentNode(el, elName, elClass, elId) {
880     while (el.nodeName.toUpperCase() != 'BODY') {
881         if ((!elName || el.nodeName.toUpperCase() == elName) &&
882             (!elClass || el.className.indexOf(elClass) != -1) &&
883             (!elId || el.id == elId)) {
884             break;
885         }
886         el = el.parentNode;
887     }
888     return el;
891     findChildNode (start, elementName, elementClass, elementID)
893     Travels down the DOM hierarchy to find all child elements with the
894     specified tag name, class, and id. All conditions must be met,
895     but any can be ommitted.
896     Doesn't examine children of matches.
898 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
899     var children = new Array();
900     for (var i = 0; i < start.childNodes.length; i++) {
901         var classfound = false;
902         var child = start.childNodes[i];
903         if((child.nodeType == 1) &&//element node type
904                   (elementClass && (typeof(child.className)=='string'))) {
905             var childClasses = child.className.split(/\s+/);
906             for (var childClassIndex in childClasses) {
907                 if (childClasses[childClassIndex]==elementClass) {
908                     classfound = true;
909                     break;
910                 }
911             }
912         }
913         if(child.nodeType == 1) { //element node type
914             if  ( (!tagName || child.nodeName == tagName) &&
915                 (!elementClass || classfound)&&
916                 (!elementID || child.id == elementID) &&
917                 (!elementName || child.name == elementName))
918             {
919                 children = children.concat(child);
920             } else {
921                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
922             }
923         }
924     }
925     return children;
928 function unmaskPassword(id) {
929   var pw = document.getElementById(id);
930   var chb = document.getElementById(id+'unmask');
932   try {
933     // first try IE way - it can not set name attribute later
934     if (chb.checked) {
935       var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
936     } else {
937       var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
938     }
939     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
940   } catch (e) {
941     var newpw = document.createElement('input');
942     newpw.setAttribute('autocomplete', 'off');
943     newpw.setAttribute('name', pw.name);
944     if (chb.checked) {
945       newpw.setAttribute('type', 'text');
946     } else {
947       newpw.setAttribute('type', 'password');
948     }
949     newpw.setAttribute('class', pw.getAttribute('class'));
950   }
951   newpw.id = pw.id;
952   newpw.size = pw.size;
953   newpw.onblur = pw.onblur;
954   newpw.onchange = pw.onchange;
955   newpw.value = pw.value;
956   pw.parentNode.replaceChild(newpw, pw);
959 function filterByParent(elCollection, parentFinder) {
960     var filteredCollection = [];
961     for (var i = 0; i < elCollection.length; ++i) {
962         var findParent = parentFinder(elCollection[i]);
963         if (findParent.nodeName.toUpperCase() != 'BODY') {
964             filteredCollection.push(elCollection[i]);
965         }
966     }
967     return filteredCollection;
971     All this is here just so that IE gets to handle oversized blocks
972     in a visually pleasing manner. It does a browser detect. So sue me.
975 function fix_column_widths() {
976     var agt = navigator.userAgent.toLowerCase();
977     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
978         fix_column_width('left-column');
979         fix_column_width('right-column');
980     }
983 function fix_column_width(colName) {
984     if(column = document.getElementById(colName)) {
985         if(!column.offsetWidth) {
986             setTimeout("fix_column_width('" + colName + "')", 20);
987             return;
988         }
990         var width = 0;
991         var nodes = column.childNodes;
993         for(i = 0; i < nodes.length; ++i) {
994             if(nodes[i].className.indexOf("block") != -1 ) {
995                 if(width < nodes[i].offsetWidth) {
996                     width = nodes[i].offsetWidth;
997                 }
998             }
999         }
1001         for(i = 0; i < nodes.length; ++i) {
1002             if(nodes[i].className.indexOf("block") != -1 ) {
1003                 nodes[i].style.width = width + 'px';
1004             }
1005         }
1006     }
1011    Insert myValue at current cursor position
1012  */
1013 function insertAtCursor(myField, myValue) {
1014     // IE support
1015     if (document.selection) {
1016         myField.focus();
1017         sel = document.selection.createRange();
1018         sel.text = myValue;
1019     }
1020     // Mozilla/Netscape support
1021     else if (myField.selectionStart || myField.selectionStart == '0') {
1022         var startPos = myField.selectionStart;
1023         var endPos = myField.selectionEnd;
1024         myField.value = myField.value.substring(0, startPos)
1025             + myValue + myField.value.substring(endPos, myField.value.length);
1026     } else {
1027         myField.value += myValue;
1028     }
1033         Call instead of setting window.onload directly or setting body onload=.
1034         Adds your function to a chain of functions rather than overwriting anything
1035         that exists.
1037 function addonload(fn) {
1038     var oldhandler=window.onload;
1039     window.onload=function() {
1040         if(oldhandler) oldhandler();
1041             fn();
1042     }
1045  * Replacement for getElementsByClassName in browsers that aren't cool enough
1047  * Relying on the built-in getElementsByClassName is far, far faster than
1048  * using YUI.
1050  * Note: the third argument used to be an object with odd behaviour. It now
1051  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1052  * mimicked if you pass an object.
1054  * @param {Node} oElm The top-level node for searching. To search a whole
1055  *                    document, use `document`.
1056  * @param {String} strTagName filter by tag names
1057  * @param {String} name same as HTML5 spec
1058  */
1059 function getElementsByClassName(oElm, strTagName, name) {
1060     // for backwards compatibility
1061     if(typeof name == "object") {
1062         var names = new Array();
1063         for(var i=0; i<name.length; i++) names.push(names[i]);
1064         name = names.join('');
1065     }
1066     // use native implementation if possible
1067     if (oElm.getElementsByClassName && Array.filter) {
1068         if (strTagName == '*') {
1069             return oElm.getElementsByClassName(name);
1070         } else {
1071             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1072                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1073             });
1074         }
1075     }
1076     // native implementation unavailable, fall back to slow method
1077     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1078     var arrReturnElements = new Array();
1079     var arrRegExpClassNames = new Array();
1080     var names = name.split(' ');
1081     for(var i=0; i<names.length; i++) {
1082         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1083     }
1084     var oElement;
1085     var bMatchesAll;
1086     for(var j=0; j<arrElements.length; j++) {
1087         oElement = arrElements[j];
1088         bMatchesAll = true;
1089         for(var k=0; k<arrRegExpClassNames.length; k++) {
1090             if(!arrRegExpClassNames[k].test(oElement.className)) {
1091                 bMatchesAll = false;
1092                 break;
1093             }
1094         }
1095         if(bMatchesAll) {
1096             arrReturnElements.push(oElement);
1097         }
1098     }
1099     return (arrReturnElements)
1102 function openpopup(event, args) {
1104     if (event) {
1105         if (event.preventDefault) {
1106             event.preventDefault();
1107         } else {
1108             event.returnValue = false;
1109         }
1110     }
1112     // Cleans window name because IE does not support funky ones.
1113     var nameregex = /[^a-z0-9_]/i;
1114     if (args.name.match(nameregex)) {
1115         args.name = args.name.replace(nameregex, '_');
1116         if (M.cfg.developerdebug) {
1117             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup()');
1118         }
1119     }
1121     var fullurl = args.url;
1122     if (!args.url.match(/https?:\/\//)) {
1123         fullurl = M.cfg.wwwroot + args.url;
1124     }
1125     if (args.fullscreen) {
1126         args.options = args.options.
1127                 replace(/top=\d+/, 'top=0').
1128                 replace(/left=\d+/, 'left=0').
1129                 replace(/width=\d+/, 'width=' + screen.availWidth).
1130                 replace(/height=\d+/, 'height=' + screen.availHeight);
1131     }
1132     var windowobj = window.open(fullurl,args.name,args.options);
1133     if (!windowobj) {
1134         return true;
1135     }
1137     if (args.fullscreen) {
1138         // In some browser / OS combinations (E.g. Chrome on Windows), the
1139         // window initially opens slighly too big. The width and heigh options
1140         // seem to control the area inside the browser window, so what with
1141         // scroll-bars, etc. the actual window is bigger than the screen.
1142         // Therefore, we need to fix things up after the window is open.
1143         var hackcount = 100;
1144         var get_size_exactly_right = function() {
1145             windowobj.moveTo(0, 0);
1146             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1148             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1149             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1150             // about 50ms) after the window is open, then it actually behaves
1151             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1152             // check that the resize actually worked, and if not, repeatedly try
1153             // again after a short delay until it works (but with a limit of
1154             // hackcount repeats.
1155             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1156                 hackcount -= 1;
1157                 setTimeout(get_size_exactly_right, 10);
1158             }
1159         }
1160         setTimeout(get_size_exactly_right, 0);
1161     }
1162     windowobj.focus();
1164     return false;
1167 /** Close the current browser window. */
1168 function close_window(e) {
1169     if (e.preventDefault) {
1170         e.preventDefault();
1171     } else {
1172         e.returnValue = false;
1173     }
1174     window.close();
1178  * Used in a couple of modules to hide navigation areas when using AJAX
1179  */
1181 function show_item(itemid) {
1182     var item = document.getElementById(itemid);
1183     if (item) {
1184         item.style.display = "";
1185     }
1188 function destroy_item(itemid) {
1189     var item = document.getElementById(itemid);
1190     if (item) {
1191         item.parentNode.removeChild(item);
1192     }
1195  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1196  * @param controlid the control id.
1197  */
1198 function focuscontrol(controlid) {
1199     var control = document.getElementById(controlid);
1200     if (control) {
1201         control.focus();
1202     }
1206  * Transfers keyboard focus to an HTML element based on the old style style of focus
1207  * This function should be removed as soon as it is no longer used
1208  */
1209 function old_onload_focus(formid, controlname) {
1210     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1211         document.forms[formid].elements[controlname].focus();
1212     }
1215 function build_querystring(obj) {
1216     return convert_object_to_string(obj, '&');
1219 function build_windowoptionsstring(obj) {
1220     return convert_object_to_string(obj, ',');
1223 function convert_object_to_string(obj, separator) {
1224     if (typeof obj !== 'object') {
1225         return null;
1226     }
1227     var list = [];
1228     for(var k in obj) {
1229         k = encodeURIComponent(k);
1230         var value = obj[k];
1231         if(obj[k] instanceof Array) {
1232             for(var i in value) {
1233                 list.push(k+'[]='+encodeURIComponent(value[i]));
1234             }
1235         } else {
1236             list.push(k+'='+encodeURIComponent(value));
1237         }
1238     }
1239     return list.join(separator);
1242 function stripHTML(str) {
1243     var re = /<\S[^><]*>/g;
1244     var ret = str.replace(re, "");
1245     return ret;
1248 Number.prototype.fixed=function(n){
1249     with(Math)
1250         return round(Number(this)*pow(10,n))/pow(10,n);
1252 function update_progress_bar (id, width, pt, msg, es){
1253     var percent = pt;
1254     var status = document.getElementById("status_"+id);
1255     var percent_indicator = document.getElementById("pt_"+id);
1256     var progress_bar = document.getElementById("progress_"+id);
1257     var time_es = document.getElementById("time_"+id);
1258     status.innerHTML = msg;
1259     percent_indicator.innerHTML = percent.fixed(2) + '%';
1260     if(percent == 100) {
1261         progress_bar.style.background = "green";
1262         time_es.style.display = "none";
1263     } else {
1264         progress_bar.style.background = "#FFCC66";
1265         if (es == '?'){
1266             time_es.innerHTML = "";
1267         }else {
1268             time_es.innerHTML = es.fixed(2)+" sec";
1269             time_es.style.display
1270                 = "block";
1271         }
1272     }
1273     progress_bar.style.width = width + "px";
1278 // ===== Deprecated core Javascript functions for Moodle ====
1279 //       DO NOT USE!!!!!!!
1280 // Do not put this stuff in separate file because it only adds extra load on servers!
1283  * Used in a couple of modules to hide navigation areas when using AJAX
1284  */
1285 function hide_item(itemid) {
1286     // use class='hiddenifjs' instead
1287     var item = document.getElementById(itemid);
1288     if (item) {
1289         item.style.display = "none";
1290     }
1293 M.util.help_icon = {
1294     Y : null,
1295     instance : null,
1296     add : function(Y, properties) {
1297         this.Y = Y;
1298         properties.node = Y.one('#'+properties.id);
1299         if (properties.node) {
1300             properties.node.on('click', this.display, this, properties);
1301         }
1302     },
1303     display : function(event, args) {
1304         event.preventDefault();
1305         if (M.util.help_icon.instance === null) {
1306             var Y = M.util.help_icon.Y;
1307             Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1308                 var help_content_overlay = {
1309                     helplink : null,
1310                     overlay : null,
1311                     init : function() {
1313                         var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img  src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1314                         // Create an overlay from markup
1315                         this.overlay = new Y.Overlay({
1316                             headerContent: closebtn,
1317                             bodyContent: '',
1318                             id: 'helppopupbox',
1319                             width:'400px',
1320                             visible : false,
1321                             constrain : true
1322                         });
1323                         this.overlay.render(Y.one(document.body));
1325                         closebtn.on('click', this.overlay.hide, this.overlay);
1327                         var boundingBox = this.overlay.get("boundingBox");
1329                         //  Hide the menu if the user clicks outside of its content
1330                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1331                             var oTarget = event.target;
1332                             var menuButton = Y.one("#"+args.id);
1334                             if (!oTarget.compareTo(menuButton) &&
1335                                 !menuButton.contains(oTarget) &&
1336                                 !oTarget.compareTo(boundingBox) &&
1337                                 !boundingBox.contains(oTarget)) {
1338                                 this.overlay.hide();
1339                             }
1340                         }, this);
1342                         Y.on("key", this.close, closebtn , "down:13", this);
1343                         closebtn.on('click', this.close, this);
1344                     },
1346                     close : function(e) {
1347                         e.preventDefault();
1348                         this.helplink.focus();
1349                         this.overlay.hide();
1350                     },
1352                     display : function(event, args) {
1353                         this.helplink = args.node;
1354                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1355                         this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1357                         var fullurl = args.url;
1358                         if (!args.url.match(/https?:\/\//)) {
1359                             fullurl = M.cfg.wwwroot + args.url;
1360                         }
1362                         var ajaxurl = fullurl + '&ajax=1';
1364                         var cfg = {
1365                             method: 'get',
1366                             context : this,
1367                             on: {
1368                                 success: function(id, o, node) {
1369                                     this.display_callback(o.responseText);
1370                                 },
1371                                 failure: function(id, o, node) {
1372                                     var debuginfo = o.statusText;
1373                                     if (M.cfg.developerdebug) {
1374                                         o.statusText += ' (' + ajaxurl + ')';
1375                                     }
1376                                     this.display_callback('bodyContent',debuginfo);
1377                                 }
1378                             }
1379                         };
1381                         Y.io(ajaxurl, cfg);
1382                         this.overlay.show();
1384                         Y.one('#closehelpbox').focus();
1385                     },
1387                     display_callback : function(content) {
1388                         content = '<div role="alert">' + content + '</div>';
1389                         this.overlay.set('bodyContent', content);
1390                     },
1392                     hideContent : function() {
1393                         help = this;
1394                         help.overlay.hide();
1395                     }
1396                 };
1397                 help_content_overlay.init();
1398                 M.util.help_icon.instance = help_content_overlay;
1399                 M.util.help_icon.instance.display(event, args);
1400             });
1401         } else {
1402             M.util.help_icon.instance.display(event, args);
1403         }
1404     },
1405     init : function(Y) {
1406         this.Y = Y;
1407     }
1411  * Custom menu namespace
1412  */
1413 M.core_custom_menu = {
1414     /**
1415      * This method is used to initialise a custom menu given the id that belongs
1416      * to the custom menu's root node.
1417      *
1418      * @param {YUI} Y
1419      * @param {string} nodeid
1420      */
1421     init : function(Y, nodeid) {
1422         var node = Y.one('#'+nodeid);
1423         if (node) {
1424             Y.use('node-menunav', function(Y) {
1425                 // Get the node
1426                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1427                 node.removeClass('javascript-disabled');
1428                 // Initialise the menunav plugin
1429                 node.plug(Y.Plugin.NodeMenuNav);
1430             });
1431         }
1432     }
1436  * Used to store form manipulation methods and enhancments
1437  */
1438 M.form = M.form || {};
1441  * Converts a nbsp indented select box into a multi drop down custom control much
1442  * like the custom menu. It also selectable categories on or off.
1444  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1446  * @param {YUI} Y
1447  * @param {string} id
1448  * @param {Array} options
1449  */
1450 M.form.init_smartselect = function(Y, id, options) {
1451     if (!id.match(/^id_/)) {
1452         id = 'id_'+id;
1453     }
1454     var select = Y.one('select#'+id);
1455     if (!select) {
1456         return false;
1457     }
1458     Y.use('event-delegate',function(){
1459         var smartselect = {
1460             id : id,
1461             structure : [],
1462             options : [],
1463             submenucount : 0,
1464             currentvalue : null,
1465             currenttext : null,
1466             shownevent : null,
1467             cfg : {
1468                 selectablecategories : true,
1469                 mode : null
1470             },
1471             nodes : {
1472                 select : null,
1473                 loading : null,
1474                 menu : null
1475             },
1476             init : function(Y, id, args, nodes) {
1477                 if (typeof(args)=='object') {
1478                     for (var i in this.cfg) {
1479                         if (args[i] || args[i]===false) {
1480                             this.cfg[i] = args[i];
1481                         }
1482                     }
1483                 }
1485                 // Display a loading message first up
1486                 this.nodes.select = nodes.select;
1488                 this.currentvalue = this.nodes.select.get('selectedIndex');
1489                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1491                 var options = Array();
1492                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1493                 this.nodes.select.all('option').each(function(option, index) {
1494                     var rawtext = option.get('innerHTML');
1495                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1496                     if (rawtext === text) {
1497                         text = rawtext.replace(/^(\s)*/, '');
1498                         var depth = (rawtext.length - text.length ) + 1;
1499                     } else {
1500                         var depth = ((rawtext.length - text.length )/12)+1;
1501                     }
1502                     option.set('innerHTML', text);
1503                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1504                 }, this);
1506                 this.structure = [];
1507                 var structcount = 0;
1508                 for (var i in options) {
1509                     var o = options[i];
1510                     if (o.depth == 0) {
1511                         this.structure.push(o);
1512                         structcount++;
1513                     } else {
1514                         var d = o.depth;
1515                         var current = this.structure[structcount-1];
1516                         for (var j = 0; j < o.depth-1;j++) {
1517                             if (current && current.children) {
1518                                 current = current.children[current.children.length-1];
1519                             }
1520                         }
1521                         if (current && current.children) {
1522                             current.children.push(o);
1523                         }
1524                     }
1525                 }
1527                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1528                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1529                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1530                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1532                 if (this.cfg.mode == null) {
1533                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1534                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1535                         this.cfg.mode = 'compact';
1536                     } else {
1537                         this.cfg.mode = 'spanning';
1538                     }
1539                 }
1541                 if (this.cfg.mode == 'compact') {
1542                     this.nodes.menu.addClass('compactmenu');
1543                 } else {
1544                     this.nodes.menu.addClass('spanningmenu');
1545                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1546                 }
1548                 Y.one(document.body).append(this.nodes.menu);
1549                 var pos = this.nodes.select.getXY();
1550                 pos[0] += 1;
1551                 this.nodes.menu.setXY(pos);
1552                 this.nodes.menu.on('click', this.handle_click, this);
1554                 Y.one(window).on('resize', function(){
1555                      var pos = this.nodes.select.getXY();
1556                     pos[0] += 1;
1557                     this.nodes.menu.setXY(pos);
1558                  }, this);
1559             },
1560             generate_menu_content : function() {
1561                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1562                 content += this.generate_submenu_content(this.structure[0], true);
1563                 content += '</ul></div>';
1564                 return content;
1565             },
1566             generate_submenu_content : function(item, rootelement) {
1567                 this.submenucount++;
1568                 var content = '';
1569                 if (item.children.length > 0) {
1570                     if (rootelement) {
1571                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1572                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1573                         content += '<div class="smartselect_menu_content">';
1574                     } else {
1575                         content += '<li class="smartselect_submenuitem">';
1576                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1577                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1578                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1579                         content += '<div class="smartselect_submenu_content">';
1580                     }
1581                     content += '<ul>';
1582                     for (var i in item.children) {
1583                         content += this.generate_submenu_content(item.children[i],false);
1584                     }
1585                     content += '</ul>';
1586                     content += '</div>';
1587                     content += '</div>';
1588                     if (rootelement) {
1589                     } else {
1590                         content += '</li>';
1591                     }
1592                 } else {
1593                     content += '<li class="smartselect_menuitem">';
1594                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1595                     content += '</li>';
1596                 }
1597                 return content;
1598             },
1599             select : function(e) {
1600                 var t = e.target;
1601                 e.halt();
1602                 this.currenttext = t.get('innerHTML');
1603                 this.currentvalue = t.getAttribute('value');
1604                 this.nodes.select.set('selectedIndex', this.currentvalue);
1605                 this.hide_menu();
1606             },
1607             handle_click : function(e) {
1608                 var target = e.target;
1609                 if (target.hasClass('smartselect_mask')) {
1610                     this.show_menu(e);
1611                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1612                     this.select(e);
1613                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1614                     this.show_sub_menu(e);
1615                 }
1616             },
1617             show_menu : function(e) {
1618                 e.halt();
1619                 var menu = e.target.ancestor().one('.smartselect_menu');
1620                 menu.addClass('visible');
1621                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1622             },
1623             show_sub_menu : function(e) {
1624                 e.halt();
1625                 var target = e.target;
1626                 if (!target.hasClass('smartselect_submenuitem')) {
1627                     target = target.ancestor('.smartselect_submenuitem');
1628                 }
1629                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1630                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1631                     return;
1632                 }
1633                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1634                 target.one('.smartselect_submenu').addClass('visible');
1635             },
1636             hide_menu : function() {
1637                 this.nodes.menu.all('.visible').removeClass('visible');
1638                 if (this.shownevent) {
1639                     this.shownevent.detach();
1640                 }
1641             }
1642         };
1643         smartselect.init(Y, id, options, {select:select});
1644     });
1647 /** List of flv players to be loaded */
1648 M.util.video_players = [];
1649 /** List of mp3 players to be loaded */
1650 M.util.audio_players = [];
1653  * Add video player
1654  * @param id element id
1655  * @param fileurl media url
1656  * @param width
1657  * @param height
1658  * @param autosize true means detect size from media
1659  */
1660 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1661     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1665  * Add audio player.
1666  * @param id
1667  * @param fileurl
1668  * @param small
1669  */
1670 M.util.add_audio_player = function (id, fileurl, small) {
1671     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1675  * Initialise all audio and video player, must be called from page footer.
1676  */
1677 M.util.load_flowplayer = function() {
1678     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1679         return;
1680     }
1681     if (typeof(flowplayer) == 'undefined') {
1682         var loaded = false;
1684         var embed_function = function() {
1685             if (loaded || typeof(flowplayer) == 'undefined') {
1686                 return;
1687             }
1688             loaded = true;
1690             var controls = {
1691                     autoHide: true
1692             }
1693             /* TODO: add CSS color overrides for the flv flow player */
1695             for(var i=0; i<M.util.video_players.length; i++) {
1696                 var video = M.util.video_players[i];
1697                 if (video.width > 0 && video.height > 0) {
1698                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1699                 } else {
1700                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1701                 }
1702                 flowplayer(video.id, src, {
1703                     plugins: {controls: controls},
1704                     clip: {
1705                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1706                         onMetaData: function(clip) {
1707                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1708                                 clip.mvideo.resized = true;
1709                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1710                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1711                                     // bad luck, we have to guess - we may not get metadata at all
1712                                     var width = clip.width;
1713                                     var height = clip.height;
1714                                 } else {
1715                                     var width = clip.metaData.width;
1716                                     var height = clip.metaData.height;
1717                                 }
1718                                 var minwidth = 300; // controls are messed up in smaller objects
1719                                 if (width < minwidth) {
1720                                     height = (height * minwidth) / width;
1721                                     width = minwidth;
1722                                 }
1724                                 var object = this._api();
1725                                 object.width = width;
1726                                 object.height = height;
1727                             }
1728                                 }
1729                     }
1730                 });
1731             }
1732             if (M.util.audio_players.length == 0) {
1733                 return;
1734             }
1735             var controls = {
1736                     autoHide: false,
1737                     fullscreen: false,
1738                     next: false,
1739                     previous: false,
1740                     scrubber: true,
1741                     play: true,
1742                     pause: true,
1743                     volume: true,
1744                     mute: false,
1745                     backgroundGradient: [0.5,0,0.3]
1746                 };
1748             var rule;
1749             for (var j=0; j < document.styleSheets.length; j++) {
1751                 // To avoid javascript security violation accessing cross domain stylesheets
1752                 var allrules = false;
1753                 try {
1754                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1755                         allrules = document.styleSheets[j].rules;
1756                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1757                         allrules = document.styleSheets[j].cssRules;
1758                     } else {
1759                         // why??
1760                         continue;
1761                     }
1762                 } catch (e) {
1763                     continue;
1764                 }
1766                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1767                 if (!allrules) {
1768                     continue;
1769                 }
1771                 for(var i=0; i<allrules.length; i++) {
1772                     rule = '';
1773                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1774                         if (typeof(allrules[i].cssText) != 'undefined') {
1775                             rule = allrules[i].cssText;
1776                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1777                             rule = allrules[i].style.cssText;
1778                         }
1779                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1780                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1781                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1782                             controls[colprop] = rule;
1783                         }
1784                     }
1785                 }
1786                 allrules = false;
1787             }
1789             for(i=0; i<M.util.audio_players.length; i++) {
1790                 var audio = M.util.audio_players[i];
1791                 if (audio.small) {
1792                     controls.controlall = false;
1793                     controls.height = 15;
1794                     controls.time = false;
1795                 } else {
1796                     controls.controlall = true;
1797                     controls.height = 25;
1798                     controls.time = true;
1799                 }
1800                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1801                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1802                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1803                 });
1804             }
1805         }
1807         if (M.cfg.jsrev == -10) {
1808             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1809         } else {
1810             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1811         }
1812         var fileref = document.createElement('script');
1813         fileref.setAttribute('type','text/javascript');
1814         fileref.setAttribute('src', jsurl);
1815         fileref.onload = embed_function;
1816         fileref.onreadystatechange = embed_function;
1817         document.getElementsByTagName('head')[0].appendChild(fileref);
1818     }