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