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