MDL-30912 - lib - A tidy up of the submit url jump functions for greater ease with...
[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) {
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;
48 };
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;
57 };
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     });
70 };
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);
150 };
152 /**
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;
159 /**
160  * The key divs that make up this
161  * @property div
162  * @type Y.Node
163  */
164 M.util.CollapsibleRegion.prototype.div = null;
166 /**
167  * The key divs that make up this
168  * @property icon
169  * @type Y.Node
170  */
171 M.util.CollapsibleRegion.prototype.icon = null;
173 /**
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.
176  *
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.
180  *
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     });
203 };
205 /**
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     });
311 };
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     };
356 };
358 /**
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.eventchangeorblur.detach();
385                         this.submit();
386                     }
387                 };
389                 var changedown = function(e, paramobject) {
390                     if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
391                         if(e.keyCode == 13) {
392                             form.submit();
393                         }
394                     }
395                 }
397                 // Attach the change event to the keydown and click actions.
398                 // We don't use the change event because IE fires it on every arrow up/down
399                 // event.... usability
400                 var paramobject = new Object();
401                 paramobject.lastindex = select.get('selectedIndex');
402                 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
403                 paramobject.eventkeypress = Y.on('keydown', changedown, select, '', form, paramobject);
404             }
405         }
406     });
407 };
409 /**
410  * Attach handler to url_select
411  * Deprecated from 2.3 onwards.
412  * Please use @see init_select_autosubmit() for redirecting to a url (above).
413  * This function has accessability issues and also does not use the formid passed through as a parameter.
414  */
415 M.util.init_url_select = function(Y, formid, selectid, nothing) {
416     YUI(M.yui.loader).use('node', function(Y) {
417         Y.on('change', function() {
418             if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
419                 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
420             }
421         },
422         '#'+selectid);
423     });
424 };
426 /**
427  * Breaks out all links to the top frame - used in frametop page layout.
428  */
429 M.util.init_frametop = function(Y) {
430     Y.all('a').each(function(node) {
431         node.set('target', '_top');
432     });
433     Y.all('form').each(function(node) {
434         node.set('target', '_top');
435     });
436 };
438 /**
439  * Finds all nodes that match the given CSS selector and attaches events to them
440  * so that they toggle a given classname when clicked.
441  *
442  * @param {YUI} Y
443  * @param {string} id An id containing elements to target
444  * @param {string} cssselector A selector to use to find targets
445  * @param {string} toggleclassname A classname to toggle
446  */
447 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
449     if (togglecssselector == '') {
450         togglecssselector = cssselector;
451     }
453     var node = Y.one('#'+id);
454     node.all(cssselector).each(function(n){
455         n.on('click', function(e){
456             e.stopPropagation();
457             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
458                 if (this.test(togglecssselector)) {
459                     this.toggleClass(toggleclassname);
460                 } else {
461                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
462             }
463             }
464         }, n);
465     });
466     // Attach this click event to the node rather than all selectors... will be much better
467     // for performance
468     node.on('click', function(e){
469         if (e.target.hasClass('addtoall')) {
470             this.all(togglecssselector).addClass(toggleclassname);
471         } else if (e.target.hasClass('removefromall')) {
472             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
473         }
474     }, node);
475 };
477 /**
478  * Initialises a colour picker
479  *
480  * Designed to be used with admin_setting_configcolourpicker although could be used
481  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
482  * above or below the input (must have the same parent) and then call this with the
483  * id.
484  *
485  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
486  * contrib/blocks. For better docs refer to that.
487  *
488  * @param {YUI} Y
489  * @param {int} id
490  * @param {object} previewconf
491  */
492 M.util.init_colour_picker = function(Y, id, previewconf) {
493     /**
494      * We need node and event-mouseenter
495      */
496     Y.use('node', 'event-mouseenter', function(){
497         /**
498          * The colour picker object
499          */
500         var colourpicker = {
501             box : null,
502             input : null,
503             image : null,
504             preview : null,
505             current : null,
506             eventClick : null,
507             eventMouseEnter : null,
508             eventMouseLeave : null,
509             eventMouseMove : null,
510             width : 300,
511             height :  100,
512             factor : 5,
513             /**
514              * Initalises the colour picker by putting everything together and wiring the events
515              */
516             init : function() {
517                 this.input = Y.one('#'+id);
518                 this.box = this.input.ancestor().one('.admin_colourpicker');
519                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
520                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
521                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
522                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
523                 this.current = Y.Node.create('<div class="currentcolour"></div>');
524                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
525                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
527                 if (typeof(previewconf) === 'object' && previewconf !== null) {
528                     Y.one('#'+id+'_preview').on('click', function(e){
529                         if (Y.Lang.isString(previewconf.selector)) {
530                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
531                         } else {
532                             for (var i in previewconf.selector) {
533                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
534                             }
535                         }
536                     }, this);
537                 }
539                 this.eventClick = this.image.on('click', this.pickColour, this);
540                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
541             },
542             /**
543              * Starts to follow the mouse once it enter the image
544              */
545             startFollow : function(e) {
546                 this.eventMouseEnter.detach();
547                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
548                 this.eventMouseMove = this.image.on('mousemove', function(e){
549                     this.preview.setStyle('backgroundColor', this.determineColour(e));
550                 }, this);
551             },
552             /**
553              * Stops following the mouse
554              */
555             endFollow : function(e) {
556                 this.eventMouseMove.detach();
557                 this.eventMouseLeave.detach();
558                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
559             },
560             /**
561              * Picks the colour the was clicked on
562              */
563             pickColour : function(e) {
564                 var colour = this.determineColour(e);
565                 this.input.set('value', colour);
566                 this.current.setStyle('backgroundColor', colour);
567             },
568             /**
569              * Calculates the colour fromthe given co-ordinates
570              */
571             determineColour : function(e) {
572                 var eventx = Math.floor(e.pageX-e.target.getX());
573                 var eventy = Math.floor(e.pageY-e.target.getY());
575                 var imagewidth = this.width;
576                 var imageheight = this.height;
577                 var factor = this.factor;
578                 var colour = [255,0,0];
580                 var matrices = [
581                     [  0,  1,  0],
582                     [ -1,  0,  0],
583                     [  0,  0,  1],
584                     [  0, -1,  0],
585                     [  1,  0,  0],
586                     [  0,  0, -1]
587                 ];
589                 var matrixcount = matrices.length;
590                 var limit = Math.round(imagewidth/matrixcount);
591                 var heightbreak = Math.round(imageheight/2);
593                 for (var x = 0; x < imagewidth; x++) {
594                     var divisor = Math.floor(x / limit);
595                     var matrix = matrices[divisor];
597                     colour[0] += matrix[0]*factor;
598                     colour[1] += matrix[1]*factor;
599                     colour[2] += matrix[2]*factor;
601                     if (eventx==x) {
602                         break;
603                     }
604                 }
606                 var pixel = [colour[0], colour[1], colour[2]];
607                 if (eventy < heightbreak) {
608                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
609                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
610                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
611                 } else if (eventy > heightbreak) {
612                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
613                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
614                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
615                 }
617                 return this.convert_rgb_to_hex(pixel);
618             },
619             /**
620              * Converts an RGB value to Hex
621              */
622             convert_rgb_to_hex : function(rgb) {
623                 var hex = '#';
624                 var hexchars = "0123456789ABCDEF";
625                 for (var i=0; i<3; i++) {
626                     var number = Math.abs(rgb[i]);
627                     if (number == 0 || isNaN(number)) {
628                         hex += '00';
629                     } else {
630                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
631                     }
632                 }
633                 return hex;
634             }
635         };
636         /**
637          * Initialise the colour picker :) Hoorah
638          */
639         colourpicker.init();
640     });
641 };
643 M.util.init_block_hider = function(Y, config) {
644     Y.use('base', 'node', function(Y) {
645         M.util.block_hider = M.util.block_hider || (function(){
646             var blockhider = function() {
647                 blockhider.superclass.constructor.apply(this, arguments);
648             };
649             blockhider.prototype = {
650                 initializer : function(config) {
651                     this.set('block', '#'+this.get('id'));
652                     var b = this.get('block'),
653                         t = b.one('.title'),
654                         a = null;
655                     if (t && (a = t.one('.block_action'))) {
656                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
657                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
658                         hide.on('keypress', this.updateStateKey, this, true);
659                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
660                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
661                         show.on('keypress', this.updateStateKey, this, false);
662                         a.insert(show, 0).insert(hide, 0);
663                     }
664                 },
665                 updateState : function(e, hide) {
666                     M.util.set_user_preference(this.get('preference'), hide);
667                     if (hide) {
668                         this.get('block').addClass('hidden');
669                     } else {
670                         this.get('block').removeClass('hidden');
671                     }
672                 },
673                 updateStateKey : function(e, hide) {
674                     if (e.keyCode == 13) { //allow hide/show via enter key
675                         this.updateState(this, hide);
676                     }
677                 }
678             };
679             Y.extend(blockhider, Y.Base, blockhider.prototype, {
680                 NAME : 'blockhider',
681                 ATTRS : {
682                     id : {},
683                     preference : {},
684                     iconVisible : {
685                         value : M.util.image_url('t/switch_minus', 'moodle')
686                     },
687                     iconHidden : {
688                         value : M.util.image_url('t/switch_plus', 'moodle')
689                     },
690                     block : {
691                         setter : function(node) {
692                             return Y.one(node);
693                         }
694                     }
695                 }
696             });
697             return blockhider;
698         })();
699         new M.util.block_hider(config);
700     });
701 };
703 /**
704  * Returns a string registered in advance for usage in JavaScript
705  *
706  * If you do not pass the third parameter, the function will just return
707  * the corresponding value from the M.str object. If the third parameter is
708  * provided, the function performs {$a} placeholder substitution in the
709  * same way as PHP get_string() in Moodle does.
710  *
711  * @param {String} identifier string identifier
712  * @param {String} component the component providing the string
713  * @param {Object|String} a optional variable to populate placeholder with
714  */
715 M.util.get_string = function(identifier, component, a) {
716     var stringvalue;
718     if (M.cfg.developerdebug) {
719         // creating new instance if YUI is not optimal but it seems to be better way then
720         // require the instance via the function API - note that it is used in rare cases
721         // for debugging only anyway
722         var Y = new YUI({ debug : true });
723     }
725     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
726         stringvalue = '[[' + identifier + ',' + component + ']]';
727         if (M.cfg.developerdebug) {
728             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
729         }
730         return stringvalue;
731     }
733     stringvalue = M.str[component][identifier];
735     if (typeof a == 'undefined') {
736         // no placeholder substitution requested
737         return stringvalue;
738     }
740     if (typeof a == 'number' || typeof a == 'string') {
741         // replace all occurrences of {$a} with the placeholder value
742         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
743         return stringvalue;
744     }
746     if (typeof a == 'object') {
747         // replace {$a->key} placeholders
748         for (var key in a) {
749             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
750                 if (M.cfg.developerdebug) {
751                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
752                 }
753                 continue;
754             }
755             var search = '{$a->' + key + '}';
756             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
757             search = new RegExp(search, 'g');
758             stringvalue = stringvalue.replace(search, a[key]);
759         }
760         return stringvalue;
761     }
763     if (M.cfg.developerdebug) {
764         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
765     }
766     return stringvalue;
767 };
769 /**
770  * Set focus on username or password field of the login form
771  */
772 M.util.focus_login_form = function(Y) {
773     var username = Y.one('#username');
774     var password = Y.one('#password');
776     if (username == null || password == null) {
777         // something is wrong here
778         return;
779     }
781     var curElement = document.activeElement
782     if (curElement == 'undefined') {
783         // legacy browser - skip refocus protection
784     } else if (curElement.tagName == 'INPUT') {
785         // user was probably faster to focus something, do not mess with focus
786         return;
787     }
789     if (username.get('value') == '') {
790         username.focus();
791     } else {
792         password.focus();
793     }
796 /**
797  * Adds lightbox hidden element that covers the whole node.
798  *
799  * @param {YUI} Y
800  * @param {Node} the node lightbox should be added to
801  * @retun {Node} created lightbox node
802  */
803 M.util.add_lightbox = function(Y, node) {
804     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
806     // Check if lightbox is already there
807     if (node.one('.lightbox')) {
808         return node.one('.lightbox');
809     }
811     node.setStyle('position', 'relative');
812     var waiticon = Y.Node.create('<img />')
813     .setAttrs({
814         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
815     })
816     .setStyles({
817         'position' : 'relative',
818         'top' : '50%'
819     });
821     var lightbox = Y.Node.create('<div></div>')
822     .setStyles({
823         'opacity' : '.75',
824         'position' : 'absolute',
825         'width' : '100%',
826         'height' : '100%',
827         'top' : 0,
828         'left' : 0,
829         'backgroundColor' : 'white',
830         'text-align' : 'center'
831     })
832     .setAttribute('class', 'lightbox')
833     .hide();
835     lightbox.appendChild(waiticon);
836     node.append(lightbox);
837     return lightbox;
840 /**
841  * Appends a hidden spinner element to the specified node.
842  *
843  * @param {YUI} Y
844  * @param {Node} the node the spinner should be added to
845  * @return {Node} created spinner node
846  */
847 M.util.add_spinner = function(Y, node) {
848     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
850     // Check if spinner is already there
851     if (node.one('.spinner')) {
852         return node.one('.spinner');
853     }
855     var spinner = Y.Node.create('<img />')
856         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
857         .addClass('spinner')
858         .addClass('iconsmall')
859         .hide();
861     node.append(spinner);
862     return spinner;
865 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
867 function checkall() {
868     var inputs = document.getElementsByTagName('input');
869     for (var i = 0; i < inputs.length; i++) {
870         if (inputs[i].type == 'checkbox') {
871             if (inputs[i].disabled || inputs[i].readOnly) {
872                 continue;
873             }
874             inputs[i].checked = true;
875         }
876     }
879 function checknone() {
880     var inputs = document.getElementsByTagName('input');
881     for (var i = 0; i < inputs.length; i++) {
882         if (inputs[i].type == 'checkbox') {
883             if (inputs[i].disabled || inputs[i].readOnly) {
884                 continue;
885             }
886             inputs[i].checked = false;
887         }
888     }
891 /**
892  * Either check, or uncheck, all checkboxes inside the element with id is
893  * @param id the id of the container
894  * @param checked the new state, either '' or 'checked'.
895  */
896 function select_all_in_element_with_id(id, checked) {
897     var container = document.getElementById(id);
898     if (!container) {
899         return;
900     }
901     var inputs = container.getElementsByTagName('input');
902     for (var i = 0; i < inputs.length; ++i) {
903         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
904             inputs[i].checked = checked;
905         }
906     }
909 function select_all_in(elTagName, elClass, elId) {
910     var inputs = document.getElementsByTagName('input');
911     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
912     for(var i = 0; i < inputs.length; ++i) {
913         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
914             inputs[i].checked = 'checked';
915         }
916     }
919 function deselect_all_in(elTagName, elClass, elId) {
920     var inputs = document.getElementsByTagName('INPUT');
921     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
922     for(var i = 0; i < inputs.length; ++i) {
923         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
924             inputs[i].checked = '';
925         }
926     }
929 function confirm_if(expr, message) {
930     if(!expr) {
931         return true;
932     }
933     return confirm(message);
937 /*
938     findParentNode (start, elementName, elementClass, elementID)
940     Travels up the DOM hierarchy to find a parent element with the
941     specified tag name, class, and id. All conditions must be met,
942     but any can be ommitted. Returns the BODY element if no match
943     found.
944 */
945 function findParentNode(el, elName, elClass, elId) {
946     while (el.nodeName.toUpperCase() != 'BODY') {
947         if ((!elName || el.nodeName.toUpperCase() == elName) &&
948             (!elClass || el.className.indexOf(elClass) != -1) &&
949             (!elId || el.id == elId)) {
950             break;
951         }
952         el = el.parentNode;
953     }
954     return el;
956 /*
957     findChildNode (start, elementName, elementClass, elementID)
959     Travels down the DOM hierarchy to find all child elements with the
960     specified tag name, class, and id. All conditions must be met,
961     but any can be ommitted.
962     Doesn't examine children of matches.
963 */
964 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
965     var children = new Array();
966     for (var i = 0; i < start.childNodes.length; i++) {
967         var classfound = false;
968         var child = start.childNodes[i];
969         if((child.nodeType == 1) &&//element node type
970                   (elementClass && (typeof(child.className)=='string'))) {
971             var childClasses = child.className.split(/\s+/);
972             for (var childClassIndex in childClasses) {
973                 if (childClasses[childClassIndex]==elementClass) {
974                     classfound = true;
975                     break;
976                 }
977             }
978         }
979         if(child.nodeType == 1) { //element node type
980             if  ( (!tagName || child.nodeName == tagName) &&
981                 (!elementClass || classfound)&&
982                 (!elementID || child.id == elementID) &&
983                 (!elementName || child.name == elementName))
984             {
985                 children = children.concat(child);
986             } else {
987                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
988             }
989         }
990     }
991     return children;
994 function unmaskPassword(id) {
995   var pw = document.getElementById(id);
996   var chb = document.getElementById(id+'unmask');
998   try {
999     // first try IE way - it can not set name attribute later
1000     if (chb.checked) {
1001       var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1002     } else {
1003       var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1004     }
1005     newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1006   } catch (e) {
1007     var newpw = document.createElement('input');
1008     newpw.setAttribute('autocomplete', 'off');
1009     newpw.setAttribute('name', pw.name);
1010     if (chb.checked) {
1011       newpw.setAttribute('type', 'text');
1012     } else {
1013       newpw.setAttribute('type', 'password');
1014     }
1015     newpw.setAttribute('class', pw.getAttribute('class'));
1016   }
1017   newpw.id = pw.id;
1018   newpw.size = pw.size;
1019   newpw.onblur = pw.onblur;
1020   newpw.onchange = pw.onchange;
1021   newpw.value = pw.value;
1022   pw.parentNode.replaceChild(newpw, pw);
1025 function filterByParent(elCollection, parentFinder) {
1026     var filteredCollection = [];
1027     for (var i = 0; i < elCollection.length; ++i) {
1028         var findParent = parentFinder(elCollection[i]);
1029         if (findParent.nodeName.toUpperCase() != 'BODY') {
1030             filteredCollection.push(elCollection[i]);
1031         }
1032     }
1033     return filteredCollection;
1036 /*
1037     All this is here just so that IE gets to handle oversized blocks
1038     in a visually pleasing manner. It does a browser detect. So sue me.
1039 */
1041 function fix_column_widths() {
1042     var agt = navigator.userAgent.toLowerCase();
1043     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1044         fix_column_width('left-column');
1045         fix_column_width('right-column');
1046     }
1049 function fix_column_width(colName) {
1050     if(column = document.getElementById(colName)) {
1051         if(!column.offsetWidth) {
1052             setTimeout("fix_column_width('" + colName + "')", 20);
1053             return;
1054         }
1056         var width = 0;
1057         var nodes = column.childNodes;
1059         for(i = 0; i < nodes.length; ++i) {
1060             if(nodes[i].className.indexOf("block") != -1 ) {
1061                 if(width < nodes[i].offsetWidth) {
1062                     width = nodes[i].offsetWidth;
1063                 }
1064             }
1065         }
1067         for(i = 0; i < nodes.length; ++i) {
1068             if(nodes[i].className.indexOf("block") != -1 ) {
1069                 nodes[i].style.width = width + 'px';
1070             }
1071         }
1072     }
1076 /*
1077    Insert myValue at current cursor position
1078  */
1079 function insertAtCursor(myField, myValue) {
1080     // IE support
1081     if (document.selection) {
1082         myField.focus();
1083         sel = document.selection.createRange();
1084         sel.text = myValue;
1085     }
1086     // Mozilla/Netscape support
1087     else if (myField.selectionStart || myField.selectionStart == '0') {
1088         var startPos = myField.selectionStart;
1089         var endPos = myField.selectionEnd;
1090         myField.value = myField.value.substring(0, startPos)
1091             + myValue + myField.value.substring(endPos, myField.value.length);
1092     } else {
1093         myField.value += myValue;
1094     }
1098 /*
1099         Call instead of setting window.onload directly or setting body onload=.
1100         Adds your function to a chain of functions rather than overwriting anything
1101         that exists.
1102 */
1103 function addonload(fn) {
1104     var oldhandler=window.onload;
1105     window.onload=function() {
1106         if(oldhandler) oldhandler();
1107             fn();
1108     }
1110 /**
1111  * Replacement for getElementsByClassName in browsers that aren't cool enough
1112  *
1113  * Relying on the built-in getElementsByClassName is far, far faster than
1114  * using YUI.
1115  *
1116  * Note: the third argument used to be an object with odd behaviour. It now
1117  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1118  * mimicked if you pass an object.
1119  *
1120  * @param {Node} oElm The top-level node for searching. To search a whole
1121  *                    document, use `document`.
1122  * @param {String} strTagName filter by tag names
1123  * @param {String} name same as HTML5 spec
1124  */
1125 function getElementsByClassName(oElm, strTagName, name) {
1126     // for backwards compatibility
1127     if(typeof name == "object") {
1128         var names = new Array();
1129         for(var i=0; i<name.length; i++) names.push(names[i]);
1130         name = names.join('');
1131     }
1132     // use native implementation if possible
1133     if (oElm.getElementsByClassName && Array.filter) {
1134         if (strTagName == '*') {
1135             return oElm.getElementsByClassName(name);
1136         } else {
1137             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1138                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1139             });
1140         }
1141     }
1142     // native implementation unavailable, fall back to slow method
1143     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1144     var arrReturnElements = new Array();
1145     var arrRegExpClassNames = new Array();
1146     var names = name.split(' ');
1147     for(var i=0; i<names.length; i++) {
1148         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1149     }
1150     var oElement;
1151     var bMatchesAll;
1152     for(var j=0; j<arrElements.length; j++) {
1153         oElement = arrElements[j];
1154         bMatchesAll = true;
1155         for(var k=0; k<arrRegExpClassNames.length; k++) {
1156             if(!arrRegExpClassNames[k].test(oElement.className)) {
1157                 bMatchesAll = false;
1158                 break;
1159             }
1160         }
1161         if(bMatchesAll) {
1162             arrReturnElements.push(oElement);
1163         }
1164     }
1165     return (arrReturnElements)
1168 function openpopup(event, args) {
1170     if (event) {
1171         if (event.preventDefault) {
1172             event.preventDefault();
1173         } else {
1174             event.returnValue = false;
1175         }
1176     }
1178     var fullurl = args.url;
1179     if (!args.url.match(/https?:\/\//)) {
1180         fullurl = M.cfg.wwwroot + args.url;
1181     }
1182     var windowobj = window.open(fullurl,args.name,args.options);
1183     if (!windowobj) {
1184         return true;
1185     }
1186     if (args.fullscreen) {
1187         windowobj.moveTo(0,0);
1188         windowobj.resizeTo(screen.availWidth,screen.availHeight);
1189     }
1190     windowobj.focus();
1192     return false;
1195 /** Close the current browser window. */
1196 function close_window(e) {
1197     if (e.preventDefault) {
1198         e.preventDefault();
1199     } else {
1200         e.returnValue = false;
1201     }
1202     window.close();
1205 /**
1206  * Used in a couple of modules to hide navigation areas when using AJAX
1207  */
1209 function show_item(itemid) {
1210     var item = document.getElementById(itemid);
1211     if (item) {
1212         item.style.display = "";
1213     }
1216 function destroy_item(itemid) {
1217     var item = document.getElementById(itemid);
1218     if (item) {
1219         item.parentNode.removeChild(item);
1220     }
1222 /**
1223  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1224  * @param controlid the control id.
1225  */
1226 function focuscontrol(controlid) {
1227     var control = document.getElementById(controlid);
1228     if (control) {
1229         control.focus();
1230     }
1233 /**
1234  * Transfers keyboard focus to an HTML element based on the old style style of focus
1235  * This function should be removed as soon as it is no longer used
1236  */
1237 function old_onload_focus(formid, controlname) {
1238     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1239         document.forms[formid].elements[controlname].focus();
1240     }
1243 function build_querystring(obj) {
1244     return convert_object_to_string(obj, '&');
1247 function build_windowoptionsstring(obj) {
1248     return convert_object_to_string(obj, ',');
1251 function convert_object_to_string(obj, separator) {
1252     if (typeof obj !== 'object') {
1253         return null;
1254     }
1255     var list = [];
1256     for(var k in obj) {
1257         k = encodeURIComponent(k);
1258         var value = obj[k];
1259         if(obj[k] instanceof Array) {
1260             for(var i in value) {
1261                 list.push(k+'[]='+encodeURIComponent(value[i]));
1262             }
1263         } else {
1264             list.push(k+'='+encodeURIComponent(value));
1265         }
1266     }
1267     return list.join(separator);
1270 function stripHTML(str) {
1271     var re = /<\S[^><]*>/g;
1272     var ret = str.replace(re, "");
1273     return ret;
1276 Number.prototype.fixed=function(n){
1277     with(Math)
1278         return round(Number(this)*pow(10,n))/pow(10,n);
1279 };
1280 function update_progress_bar (id, width, pt, msg, es){
1281     var percent = pt;
1282     var status = document.getElementById("status_"+id);
1283     var percent_indicator = document.getElementById("pt_"+id);
1284     var progress_bar = document.getElementById("progress_"+id);
1285     var time_es = document.getElementById("time_"+id);
1286     status.innerHTML = msg;
1287     percent_indicator.innerHTML = percent.fixed(2) + '%';
1288     if(percent == 100) {
1289         progress_bar.style.background = "green";
1290         time_es.style.display = "none";
1291     } else {
1292         progress_bar.style.background = "#FFCC66";
1293         if (es == '?'){
1294             time_es.innerHTML = "";
1295         }else {
1296             time_es.innerHTML = es.fixed(2)+" sec";
1297             time_es.style.display
1298                 = "block";
1299         }
1300     }
1301     progress_bar.style.width = width + "px";
1306 // ===== Deprecated core Javascript functions for Moodle ====
1307 //       DO NOT USE!!!!!!!
1308 // Do not put this stuff in separate file because it only adds extra load on servers!
1310 /**
1311  * Used in a couple of modules to hide navigation areas when using AJAX
1312  */
1313 function hide_item(itemid) {
1314     // use class='hiddenifjs' instead
1315     var item = document.getElementById(itemid);
1316     if (item) {
1317         item.style.display = "none";
1318     }
1321 M.util.help_icon = {
1322     Y : null,
1323     instance : null,
1324     add : function(Y, properties) {
1325         this.Y = Y;
1326         properties.node = Y.one('#'+properties.id);
1327         if (properties.node) {
1328             properties.node.on('click', this.display, this, properties);
1329         }
1330     },
1331     display : function(event, args) {
1332         event.preventDefault();
1333         if (M.util.help_icon.instance === null) {
1334             var Y = M.util.help_icon.Y;
1335             Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1336                 var help_content_overlay = {
1337                     helplink : null,
1338                     overlay : null,
1339                     init : function() {
1341                         var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img  src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1342                         // Create an overlay from markup
1343                         this.overlay = new Y.Overlay({
1344                             headerContent: closebtn,
1345                             bodyContent: '',
1346                             id: 'helppopupbox',
1347                             width:'400px',
1348                             visible : false,
1349                             constrain : true
1350                         });
1351                         this.overlay.render(Y.one(document.body));
1353                         closebtn.on('click', this.overlay.hide, this.overlay);
1355                         var boundingBox = this.overlay.get("boundingBox");
1357                         //  Hide the menu if the user clicks outside of its content
1358                         boundingBox.get("ownerDocument").on("mousedown", function (event) {
1359                             var oTarget = event.target;
1360                             var menuButton = Y.one("#"+args.id);
1362                             if (!oTarget.compareTo(menuButton) &&
1363                                 !menuButton.contains(oTarget) &&
1364                                 !oTarget.compareTo(boundingBox) &&
1365                                 !boundingBox.contains(oTarget)) {
1366                                 this.overlay.hide();
1367                             }
1368                         }, this);
1370                         Y.on("key", this.close, closebtn , "down:13", this);
1371                         closebtn.on('click', this.close, this);
1372                     },
1374                     close : function(e) {
1375                         e.preventDefault();
1376                         this.helplink.focus();
1377                         this.overlay.hide();
1378                     },
1380                     display : function(event, args) {
1381                         this.helplink = args.node;
1382                         this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1383                         this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1385                         var fullurl = args.url;
1386                         if (!args.url.match(/https?:\/\//)) {
1387                             fullurl = M.cfg.wwwroot + args.url;
1388                         }
1390                         var ajaxurl = fullurl + '&ajax=1';
1392                         var cfg = {
1393                             method: 'get',
1394                             context : this,
1395                             on: {
1396                                 success: function(id, o, node) {
1397                                     this.display_callback(o.responseText);
1398                                 },
1399                                 failure: function(id, o, node) {
1400                                     var debuginfo = o.statusText;
1401                                     if (M.cfg.developerdebug) {
1402                                         o.statusText += ' (' + ajaxurl + ')';
1403                                     }
1404                                     this.display_callback('bodyContent',debuginfo);
1405                                 }
1406                             }
1407                         };
1409                         Y.io(ajaxurl, cfg);
1410                         this.overlay.show();
1412                         Y.one('#closehelpbox').focus();
1413                     },
1415                     display_callback : function(content) {
1416                         this.overlay.set('bodyContent', content);
1417                     },
1419                     hideContent : function() {
1420                         help = this;
1421                         help.overlay.hide();
1422                     }
1423                 };
1424                 help_content_overlay.init();
1425                 M.util.help_icon.instance = help_content_overlay;
1426                 M.util.help_icon.instance.display(event, args);
1427             });
1428         } else {
1429             M.util.help_icon.instance.display(event, args);
1430         }
1431     },
1432     init : function(Y) {
1433         this.Y = Y;
1434     }
1435 };
1437 /**
1438  * Custom menu namespace
1439  */
1440 M.core_custom_menu = {
1441     /**
1442      * This method is used to initialise a custom menu given the id that belongs
1443      * to the custom menu's root node.
1444      *
1445      * @param {YUI} Y
1446      * @param {string} nodeid
1447      */
1448     init : function(Y, nodeid) {
1449         var node = Y.one('#'+nodeid);
1450         if (node) {
1451             Y.use('node-menunav', function(Y) {
1452                 // Get the node
1453                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1454                 node.removeClass('javascript-disabled');
1455                 // Initialise the menunav plugin
1456                 node.plug(Y.Plugin.NodeMenuNav);
1457             });
1458         }
1459     }
1460 };
1462 /**
1463  * Used to store form manipulation methods and enhancments
1464  */
1465 M.form = M.form || {};
1467 /**
1468  * Converts a nbsp indented select box into a multi drop down custom control much
1469  * like the custom menu. It also selectable categories on or off.
1470  *
1471  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1472  *
1473  * @param {YUI} Y
1474  * @param {string} id
1475  * @param {Array} options
1476  */
1477 M.form.init_smartselect = function(Y, id, options) {
1478     if (!id.match(/^id_/)) {
1479         id = 'id_'+id;
1480     }
1481     var select = Y.one('select#'+id);
1482     if (!select) {
1483         return false;
1484     }
1485     Y.use('event-delegate',function(){
1486         var smartselect = {
1487             id : id,
1488             structure : [],
1489             options : [],
1490             submenucount : 0,
1491             currentvalue : null,
1492             currenttext : null,
1493             shownevent : null,
1494             cfg : {
1495                 selectablecategories : true,
1496                 mode : null
1497             },
1498             nodes : {
1499                 select : null,
1500                 loading : null,
1501                 menu : null
1502             },
1503             init : function(Y, id, args, nodes) {
1504                 if (typeof(args)=='object') {
1505                     for (var i in this.cfg) {
1506                         if (args[i] || args[i]===false) {
1507                             this.cfg[i] = args[i];
1508                         }
1509                     }
1510                 }
1512                 // Display a loading message first up
1513                 this.nodes.select = nodes.select;
1515                 this.currentvalue = this.nodes.select.get('selectedIndex');
1516                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1518                 var options = Array();
1519                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1520                 this.nodes.select.all('option').each(function(option, index) {
1521                     var rawtext = option.get('innerHTML');
1522                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1523                     if (rawtext === text) {
1524                         text = rawtext.replace(/^(\s)*/, '');
1525                         var depth = (rawtext.length - text.length ) + 1;
1526                     } else {
1527                         var depth = ((rawtext.length - text.length )/12)+1;
1528                     }
1529                     option.set('innerHTML', text);
1530                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1531                 }, this);
1533                 this.structure = [];
1534                 var structcount = 0;
1535                 for (var i in options) {
1536                     var o = options[i];
1537                     if (o.depth == 0) {
1538                         this.structure.push(o);
1539                         structcount++;
1540                     } else {
1541                         var d = o.depth;
1542                         var current = this.structure[structcount-1];
1543                         for (var j = 0; j < o.depth-1;j++) {
1544                             if (current && current.children) {
1545                                 current = current.children[current.children.length-1];
1546                             }
1547                         }
1548                         if (current && current.children) {
1549                             current.children.push(o);
1550                         }
1551                     }
1552                 }
1554                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1555                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1556                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1557                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1559                 if (this.cfg.mode == null) {
1560                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1561                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1562                         this.cfg.mode = 'compact';
1563                     } else {
1564                         this.cfg.mode = 'spanning';
1565                     }
1566                 }
1568                 if (this.cfg.mode == 'compact') {
1569                     this.nodes.menu.addClass('compactmenu');
1570                 } else {
1571                     this.nodes.menu.addClass('spanningmenu');
1572                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1573                 }
1575                 Y.one(document.body).append(this.nodes.menu);
1576                 var pos = this.nodes.select.getXY();
1577                 pos[0] += 1;
1578                 this.nodes.menu.setXY(pos);
1579                 this.nodes.menu.on('click', this.handle_click, this);
1581                 Y.one(window).on('resize', function(){
1582                      var pos = this.nodes.select.getXY();
1583                     pos[0] += 1;
1584                     this.nodes.menu.setXY(pos);
1585                  }, this);
1586             },
1587             generate_menu_content : function() {
1588                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1589                 content += this.generate_submenu_content(this.structure[0], true);
1590                 content += '</ul></div>';
1591                 return content;
1592             },
1593             generate_submenu_content : function(item, rootelement) {
1594                 this.submenucount++;
1595                 var content = '';
1596                 if (item.children.length > 0) {
1597                     if (rootelement) {
1598                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1599                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1600                         content += '<div class="smartselect_menu_content">';
1601                     } else {
1602                         content += '<li class="smartselect_submenuitem">';
1603                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1604                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1605                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1606                         content += '<div class="smartselect_submenu_content">';
1607                     }
1608                     content += '<ul>';
1609                     for (var i in item.children) {
1610                         content += this.generate_submenu_content(item.children[i],false);
1611                     }
1612                     content += '</ul>';
1613                     content += '</div>';
1614                     content += '</div>';
1615                     if (rootelement) {
1616                     } else {
1617                         content += '</li>';
1618                     }
1619                 } else {
1620                     content += '<li class="smartselect_menuitem">';
1621                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1622                     content += '</li>';
1623                 }
1624                 return content;
1625             },
1626             select : function(e) {
1627                 var t = e.target;
1628                 e.halt();
1629                 this.currenttext = t.get('innerHTML');
1630                 this.currentvalue = t.getAttribute('value');
1631                 this.nodes.select.set('selectedIndex', this.currentvalue);
1632                 this.hide_menu();
1633             },
1634             handle_click : function(e) {
1635                 var target = e.target;
1636                 if (target.hasClass('smartselect_mask')) {
1637                     this.show_menu(e);
1638                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1639                     this.select(e);
1640                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1641                     this.show_sub_menu(e);
1642                 }
1643             },
1644             show_menu : function(e) {
1645                 e.halt();
1646                 var menu = e.target.ancestor().one('.smartselect_menu');
1647                 menu.addClass('visible');
1648                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1649             },
1650             show_sub_menu : function(e) {
1651                 e.halt();
1652                 var target = e.target;
1653                 if (!target.hasClass('smartselect_submenuitem')) {
1654                     target = target.ancestor('.smartselect_submenuitem');
1655                 }
1656                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1657                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1658                     return;
1659                 }
1660                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1661                 target.one('.smartselect_submenu').addClass('visible');
1662             },
1663             hide_menu : function() {
1664                 this.nodes.menu.all('.visible').removeClass('visible');
1665                 if (this.shownevent) {
1666                     this.shownevent.detach();
1667                 }
1668             }
1669         };
1670         smartselect.init(Y, id, options, {select:select});
1671     });
1672 };
1674 /** List of flv players to be loaded */
1675 M.util.video_players = [];
1676 /** List of mp3 players to be loaded */
1677 M.util.audio_players = [];
1679 /**
1680  * Add video player
1681  * @param id element id
1682  * @param fileurl media url
1683  * @param width
1684  * @param height
1685  * @param autosize true means detect size from media
1686  */
1687 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1688     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1689 };
1691 /**
1692  * Add audio player.
1693  * @param id
1694  * @param fileurl
1695  * @param small
1696  */
1697 M.util.add_audio_player = function (id, fileurl, small) {
1698     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1699 };
1701 /**
1702  * Initialise all audio and video player, must be called from page footer.
1703  */
1704 M.util.load_flowplayer = function() {
1705     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1706         return;
1707     }
1708     if (typeof(flowplayer) == 'undefined') {
1709         var loaded = false;
1711         var embed_function = function() {
1712             if (loaded || typeof(flowplayer) == 'undefined') {
1713                 return;
1714             }
1715             loaded = true;
1717             var controls = {
1718                     autoHide: true
1719             }
1720             /* TODO: add CSS color overrides for the flv flow player */
1722             for(var i=0; i<M.util.video_players.length; i++) {
1723                 var video = M.util.video_players[i];
1724                 if (video.width > 0 && video.height > 0) {
1725                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf', width: video.width, height: video.height};
1726                 } else {
1727                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf';
1728                 }
1729                 flowplayer(video.id, src, {
1730                     plugins: {controls: controls},
1731                     clip: {
1732                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1733                         onMetaData: function(clip) {
1734                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1735                                 clip.mvideo.resized = true;
1736                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1737                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1738                                     // bad luck, we have to guess - we may not get metadata at all
1739                                     var width = clip.width;
1740                                     var height = clip.height;
1741                                 } else {
1742                                     var width = clip.metaData.width;
1743                                     var height = clip.metaData.height;
1744                                 }
1745                                 var minwidth = 300; // controls are messed up in smaller objects
1746                                 if (width < minwidth) {
1747                                     height = (height * minwidth) / width;
1748                                     width = minwidth;
1749                                 }
1751                                 var object = this._api();
1752                                 object.width = width;
1753                                 object.height = height;
1754                             }
1755                                 }
1756                     }
1757                 });
1758             }
1759             if (M.util.audio_players.length == 0) {
1760                 return;
1761             }
1762             var controls = {
1763                     autoHide: false,
1764                     fullscreen: false,
1765                     next: false,
1766                     previous: false,
1767                     scrubber: true,
1768                     play: true,
1769                     pause: true,
1770                     volume: true,
1771                     mute: false,
1772                     backgroundGradient: [0.5,0,0.3]
1773                 };
1775             var rule;
1776             for (var j=0; j < document.styleSheets.length; j++) {
1777                 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1778                     var allrules = document.styleSheets[j].rules;
1779                 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1780                     var allrules = document.styleSheets[j].cssRules;
1781                 } else {
1782                     // why??
1783                     continue;
1784                 }
1785                 for(var i=0; i<allrules.length; i++) {
1786                     rule = '';
1787                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1788                         if (typeof(allrules[i].cssText) != 'undefined') {
1789                             rule = allrules[i].cssText;
1790                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1791                             rule = allrules[i].style.cssText;
1792                         }
1793                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1794                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1795                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1796                             controls[colprop] = rule;
1797                         }
1798                     }
1799                 }
1800                 allrules = false;
1801             }
1803             for(i=0; i<M.util.audio_players.length; i++) {
1804                 var audio = M.util.audio_players[i];
1805                 if (audio.small) {
1806                     controls.controlall = false;
1807                     controls.height = 15;
1808                     controls.time = false;
1809                 } else {
1810                     controls.controlall = true;
1811                     controls.height = 25;
1812                     controls.time = true;
1813                 }
1814                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf', {
1815                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.8.swf'}},
1816                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1817                 });
1818             }
1819         }
1821         if (M.cfg.jsrev == -10) {
1822             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.8.min.js';
1823         } else {
1824             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.8.min.js&rev=' + M.cfg.jsrev;
1825         }
1826         var fileref = document.createElement('script');
1827         fileref.setAttribute('type','text/javascript');
1828         fileref.setAttribute('src', jsurl);
1829         fileref.onload = embed_function;
1830         fileref.onreadystatechange = embed_function;
1831         document.getElementsByTagName('head')[0].appendChild(fileref);
1832     }
1833 };