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