Merge branch 'wip-mdl-50675-m28' of https://github.com/rajeshtaneja/moodle into MOODL...
[moodle.git] / lib / javascript-static.js
blobbe47fdd5f7e1f035aae4cbf67e6f5020ce67613d
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     }
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     var url = M.cfg.wwwroot + '/theme/image.php';
42     if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
43         if (!M.cfg.svgicons) {
44             url += '/_s';
45         }
46         url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
47     } else {
48         url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
49         if (!M.cfg.svgicons) {
50             url += '&svg=0';
51         }
52     }
54     return url;
57 M.util.in_array = function(item, array){
58     for( var i = 0; i<array.length; i++){
59         if(item==array[i]){
60             return true;
61         }
62     }
63     return false;
66 /**
67  * Init a collapsible region, see print_collapsible_region in weblib.php
68  * @param {YUI} Y YUI3 instance with all libraries loaded
69  * @param {String} id the HTML id for the div.
70  * @param {String} userpref the user preference that records the state of this box. false if none.
71  * @param {String} strtooltip
72  */
73 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
74     Y.use('anim', function(Y) {
75         new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
76     });
79 /**
80  * Object to handle a collapsible region : instantiate and forget styled object
81  *
82  * @class
83  * @constructor
84  * @param {YUI} Y YUI3 instance with all libraries loaded
85  * @param {String} id The HTML id for the div.
86  * @param {String} userpref The user preference that records the state of this box. false if none.
87  * @param {String} strtooltip
88  */
89 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
90     // Record the pref name
91     this.userpref = userpref;
93     // Find the divs in the document.
94     this.div = Y.one('#'+id);
96     // Get the caption for the collapsible region
97     var caption = this.div.one('#'+id + '_caption');
99     // Create a link
100     var a = Y.Node.create('<a href="#"></a>');
101     a.setAttribute('title', strtooltip);
103     // Get all the nodes from caption, remove them and append them to <a>
104     while (caption.hasChildNodes()) {
105         child = caption.get('firstChild');
106         child.remove();
107         a.append(child);
108     }
109     caption.append(a);
111     // Get the height of the div at this point before we shrink it if required
112     var height = this.div.get('offsetHeight');
113     var collapsedimage = 't/collapsed'; // ltr mode
114     if (right_to_left()) {
115         collapsedimage = 't/collapsed_rtl';
116     } else {
117         collapsedimage = 't/collapsed';
118     }
119     if (this.div.hasClass('collapsed')) {
120         // Add the correct image and record the YUI node created in the process
121         this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
122         // Shrink the div as it is collapsed by default
123         this.div.setStyle('height', caption.get('offsetHeight')+'px');
124     } else {
125         // Add the correct image and record the YUI node created in the process
126         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
127     }
128     a.append(this.icon);
130     // Create the animation.
131     var animation = new Y.Anim({
132         node: this.div,
133         duration: 0.3,
134         easing: Y.Easing.easeBoth,
135         to: {height:caption.get('offsetHeight')},
136         from: {height:height}
137     });
139     // Handler for the animation finishing.
140     animation.on('end', function() {
141         this.div.toggleClass('collapsed');
142         var collapsedimage = 't/collapsed'; // ltr mode
143         if (right_to_left()) {
144             collapsedimage = 't/collapsed_rtl';
145             } else {
146             collapsedimage = 't/collapsed';
147             }
148         if (this.div.hasClass('collapsed')) {
149             this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
150         } else {
151             this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
152         }
153     }, this);
155     // Hook up the event handler.
156     a.on('click', function(e, animation) {
157         e.preventDefault();
158         // Animate to the appropriate size.
159         if (animation.get('running')) {
160             animation.stop();
161         }
162         animation.set('reverse', this.div.hasClass('collapsed'));
163         // Update the user preference.
164         if (this.userpref) {
165             M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
166         }
167         animation.run();
168     }, this, animation);
172  * The user preference that stores the state of this box.
173  * @property userpref
174  * @type String
175  */
176 M.util.CollapsibleRegion.prototype.userpref = null;
179  * The key divs that make up this
180  * @property div
181  * @type Y.Node
182  */
183 M.util.CollapsibleRegion.prototype.div = null;
186  * The key divs that make up this
187  * @property icon
188  * @type Y.Node
189  */
190 M.util.CollapsibleRegion.prototype.icon = null;
193  * Makes a best effort to connect back to Moodle to update a user preference,
194  * however, there is no mechanism for finding out if the update succeeded.
196  * Before you can use this function in your JavsScript, you must have called
197  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
198  * the udpate is allowed, and how to safely clean and submitted values.
200  * @param String name the name of the setting to udpate.
201  * @param String the value to set it to.
202  */
203 M.util.set_user_preference = function(name, value) {
204     YUI().use('io', function(Y) {
205         var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
206                 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
208         // If we are a developer, ensure that failures are reported.
209         var cfg = {
210                 method: 'get',
211                 on: {}
212             };
213         if (M.cfg.developerdebug) {
214             cfg.on.failure = function(id, o, args) {
215                 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
216             }
217         }
219         // Make the request.
220         Y.io(url, cfg);
221     });
225  * Prints a confirmation dialog in the style of DOM.confirm().
227  * @method show_confirm_dialog
228  * @param {EventFacade} e
229  * @param {Object} args
230  * @param {String} args.message The question to ask the user
231  * @param {Function} [args.callback] A callback to apply on confirmation.
232  * @param {Object} [args.scope] The scope to use when calling the callback.
233  * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
234  * @param {String} [args.cancellabel] The label to use on the cancel button.
235  * @param {String} [args.continuelabel] The label to use on the continue button.
236  */
237 M.util.show_confirm_dialog = function(e, args) {
238     var target = e.target;
239     if (e.preventDefault) {
240         e.preventDefault();
241     }
243     YUI().use('moodle-core-notification-confirm', function(Y) {
244         var confirmationDialogue = new M.core.confirm({
245             width: '300px',
246             center: true,
247             modal: true,
248             visible: false,
249             draggable: false,
250             title: M.util.get_string('confirmation', 'admin'),
251             noLabel: M.util.get_string('cancel', 'moodle'),
252             question: args.message
253         });
255         // The dialogue was submitted with a positive value indication.
256         confirmationDialogue.on('complete-yes', function(e) {
257             // Handle any callbacks.
258             if (args.callback) {
259                 if (!Y.Lang.isFunction(args.callback)) {
260                     Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
261                             'warn', 'M.util.show_confirm_dialog');
262                     return;
263                 }
265                 var scope = e.target;
266                 if (Y.Lang.isObject(args.scope)) {
267                     scope = args.scope;
268                 }
270                 var callbackargs = args.callbackargs || [];
271                 args.callback.apply(scope, callbackargs);
272                 return;
273             }
275             var targetancestor = null,
276                 targetform = null;
278             if (target.test('a')) {
279                 window.location = target.get('href');
281             } else if ((targetancestor = target.ancestor('a')) !== null) {
282                 window.location = targetancestor.get('href');
284             } else if (target.test('input')) {
285                 targetform = target.ancestor('form', true);
286                 if (!targetform) {
287                     return;
288                 }
289                 if (target.get('name') && target.get('value')) {
290                     targetform.append('<input type="hidden" name="' + target.get('name') +
291                                     '" value="' + target.get('value') + '">');
292                 }
293                 targetform.submit();
295             } else if (target.test('form')) {
296                 target.submit();
298             } else {
299                 Y.log("Element of type " + target.get('tagName') +
300                         " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM",
301                         'warn', 'javascript-static');
302             }
303         }, this);
305         if (args.cancellabel) {
306             confirmationDialogue.set('noLabel', args.cancellabel);
307         }
309         if (args.continuelabel) {
310             confirmationDialogue.set('yesLabel', args.continuelabel);
311         }
313         confirmationDialogue.render()
314                 .show();
315     });
318 /** Useful for full embedding of various stuff */
319 M.util.init_maximised_embed = function(Y, id) {
320     var obj = Y.one('#'+id);
321     if (!obj) {
322         return;
323     }
325     var get_htmlelement_size = function(el, prop) {
326         if (Y.Lang.isString(el)) {
327             el = Y.one('#' + el);
328         }
329         // Ensure element exists.
330         if (el) {
331             var val = el.getStyle(prop);
332             if (val == 'auto') {
333                 val = el.getComputedStyle(prop);
334             }
335             val = parseInt(val);
336             if (isNaN(val)) {
337                 return 0;
338             }
339             return val;
340         } else {
341             return 0;
342         }
343     };
345     var resize_object = function() {
346         obj.setStyle('width', '0px');
347         obj.setStyle('height', '0px');
348         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
350         if (newwidth > 500) {
351             obj.setStyle('width', newwidth  + 'px');
352         } else {
353             obj.setStyle('width', '500px');
354         }
356         var headerheight = get_htmlelement_size('page-header', 'height');
357         var footerheight = get_htmlelement_size('page-footer', 'height');
358         var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
359         if (newheight < 400) {
360             newheight = 400;
361         }
362         obj.setStyle('height', newheight+'px');
363     };
365     resize_object();
366     // fix layout if window resized too
367     window.onresize = function() {
368         resize_object();
369     };
373  * Breaks out all links to the top frame - used in frametop page layout.
374  */
375 M.util.init_frametop = function(Y) {
376     Y.all('a').each(function(node) {
377         node.set('target', '_top');
378     });
379     Y.all('form').each(function(node) {
380         node.set('target', '_top');
381     });
385  * Finds all nodes that match the given CSS selector and attaches events to them
386  * so that they toggle a given classname when clicked.
388  * @param {YUI} Y
389  * @param {string} id An id containing elements to target
390  * @param {string} cssselector A selector to use to find targets
391  * @param {string} toggleclassname A classname to toggle
392  */
393 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
395     if (togglecssselector == '') {
396         togglecssselector = cssselector;
397     }
399     var node = Y.one('#'+id);
400     node.all(cssselector).each(function(n){
401         n.on('click', function(e){
402             e.stopPropagation();
403             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
404                 if (this.test(togglecssselector)) {
405                     this.toggleClass(toggleclassname);
406                 } else {
407                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
408             }
409             }
410         }, n);
411     });
412     // Attach this click event to the node rather than all selectors... will be much better
413     // for performance
414     node.on('click', function(e){
415         if (e.target.hasClass('addtoall')) {
416             this.all(togglecssselector).addClass(toggleclassname);
417         } else if (e.target.hasClass('removefromall')) {
418             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
419         }
420     }, node);
424  * Initialises a colour picker
426  * Designed to be used with admin_setting_configcolourpicker although could be used
427  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
428  * above or below the input (must have the same parent) and then call this with the
429  * id.
431  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
432  * contrib/blocks. For better docs refer to that.
434  * @param {YUI} Y
435  * @param {int} id
436  * @param {object} previewconf
437  */
438 M.util.init_colour_picker = function(Y, id, previewconf) {
439     /**
440      * We need node and event-mouseenter
441      */
442     Y.use('node', 'event-mouseenter', function(){
443         /**
444          * The colour picker object
445          */
446         var colourpicker = {
447             box : null,
448             input : null,
449             image : null,
450             preview : null,
451             current : null,
452             eventClick : null,
453             eventMouseEnter : null,
454             eventMouseLeave : null,
455             eventMouseMove : null,
456             width : 300,
457             height :  100,
458             factor : 5,
459             /**
460              * Initalises the colour picker by putting everything together and wiring the events
461              */
462             init : function() {
463                 this.input = Y.one('#'+id);
464                 this.box = this.input.ancestor().one('.admin_colourpicker');
465                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
466                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
467                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
468                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
469                 this.current = Y.Node.create('<div class="currentcolour"></div>');
470                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
471                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
473                 if (typeof(previewconf) === 'object' && previewconf !== null) {
474                     Y.one('#'+id+'_preview').on('click', function(e){
475                         if (Y.Lang.isString(previewconf.selector)) {
476                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
477                         } else {
478                             for (var i in previewconf.selector) {
479                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
480                             }
481                         }
482                     }, this);
483                 }
485                 this.eventClick = this.image.on('click', this.pickColour, this);
486                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
487             },
488             /**
489              * Starts to follow the mouse once it enter the image
490              */
491             startFollow : function(e) {
492                 this.eventMouseEnter.detach();
493                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
494                 this.eventMouseMove = this.image.on('mousemove', function(e){
495                     this.preview.setStyle('backgroundColor', this.determineColour(e));
496                 }, this);
497             },
498             /**
499              * Stops following the mouse
500              */
501             endFollow : function(e) {
502                 this.eventMouseMove.detach();
503                 this.eventMouseLeave.detach();
504                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
505             },
506             /**
507              * Picks the colour the was clicked on
508              */
509             pickColour : function(e) {
510                 var colour = this.determineColour(e);
511                 this.input.set('value', colour);
512                 this.current.setStyle('backgroundColor', colour);
513             },
514             /**
515              * Calculates the colour fromthe given co-ordinates
516              */
517             determineColour : function(e) {
518                 var eventx = Math.floor(e.pageX-e.target.getX());
519                 var eventy = Math.floor(e.pageY-e.target.getY());
521                 var imagewidth = this.width;
522                 var imageheight = this.height;
523                 var factor = this.factor;
524                 var colour = [255,0,0];
526                 var matrices = [
527                     [  0,  1,  0],
528                     [ -1,  0,  0],
529                     [  0,  0,  1],
530                     [  0, -1,  0],
531                     [  1,  0,  0],
532                     [  0,  0, -1]
533                 ];
535                 var matrixcount = matrices.length;
536                 var limit = Math.round(imagewidth/matrixcount);
537                 var heightbreak = Math.round(imageheight/2);
539                 for (var x = 0; x < imagewidth; x++) {
540                     var divisor = Math.floor(x / limit);
541                     var matrix = matrices[divisor];
543                     colour[0] += matrix[0]*factor;
544                     colour[1] += matrix[1]*factor;
545                     colour[2] += matrix[2]*factor;
547                     if (eventx==x) {
548                         break;
549                     }
550                 }
552                 var pixel = [colour[0], colour[1], colour[2]];
553                 if (eventy < heightbreak) {
554                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
555                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
556                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
557                 } else if (eventy > heightbreak) {
558                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
559                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
560                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
561                 }
563                 return this.convert_rgb_to_hex(pixel);
564             },
565             /**
566              * Converts an RGB value to Hex
567              */
568             convert_rgb_to_hex : function(rgb) {
569                 var hex = '#';
570                 var hexchars = "0123456789ABCDEF";
571                 for (var i=0; i<3; i++) {
572                     var number = Math.abs(rgb[i]);
573                     if (number == 0 || isNaN(number)) {
574                         hex += '00';
575                     } else {
576                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
577                     }
578                 }
579                 return hex;
580             }
581         };
582         /**
583          * Initialise the colour picker :) Hoorah
584          */
585         colourpicker.init();
586     });
589 M.util.init_block_hider = function(Y, config) {
590     Y.use('base', 'node', function(Y) {
591         M.util.block_hider = M.util.block_hider || (function(){
592             var blockhider = function() {
593                 blockhider.superclass.constructor.apply(this, arguments);
594             };
595             blockhider.prototype = {
596                 initializer : function(config) {
597                     this.set('block', '#'+this.get('id'));
598                     var b = this.get('block'),
599                         t = b.one('.title'),
600                         a = null,
601                         hide,
602                         show;
603                     if (t && (a = t.one('.block_action'))) {
604                         hide = Y.Node.create('<img />')
605                             .addClass('block-hider-hide')
606                             .setAttrs({
607                                 alt:        config.tooltipVisible,
608                                 src:        this.get('iconVisible'),
609                                 tabindex:   0,
610                                 'title':    config.tooltipVisible
611                             });
612                         hide.on('keypress', this.updateStateKey, this, true);
613                         hide.on('click', this.updateState, this, true);
615                         show = Y.Node.create('<img />')
616                             .addClass('block-hider-show')
617                             .setAttrs({
618                                 alt:        config.tooltipHidden,
619                                 src:        this.get('iconHidden'),
620                                 tabindex:   0,
621                                 'title':    config.tooltipHidden
622                             });
623                         show.on('keypress', this.updateStateKey, this, false);
624                         show.on('click', this.updateState, this, false);
626                         a.insert(show, 0).insert(hide, 0);
627                     }
628                 },
629                 updateState : function(e, hide) {
630                     M.util.set_user_preference(this.get('preference'), hide);
631                     if (hide) {
632                         this.get('block').addClass('hidden');
633                     } else {
634                         this.get('block').removeClass('hidden');
635                     }
636                 },
637                 updateStateKey : function(e, hide) {
638                     if (e.keyCode == 13) { //allow hide/show via enter key
639                         this.updateState(this, hide);
640                     }
641                 }
642             };
643             Y.extend(blockhider, Y.Base, blockhider.prototype, {
644                 NAME : 'blockhider',
645                 ATTRS : {
646                     id : {},
647                     preference : {},
648                     iconVisible : {
649                         value : M.util.image_url('t/switch_minus', 'moodle')
650                     },
651                     iconHidden : {
652                         value : M.util.image_url('t/switch_plus', 'moodle')
653                     },
654                     block : {
655                         setter : function(node) {
656                             return Y.one(node);
657                         }
658                     }
659                 }
660             });
661             return blockhider;
662         })();
663         new M.util.block_hider(config);
664     });
668  * @var pending_js - The keys are the list of all pending js actions.
669  * @type Object
670  */
671 M.util.pending_js = [];
672 M.util.complete_js = [];
675  * Register any long running javascript code with a unique identifier.
676  * Should be followed with a call to js_complete with a matching
677  * idenfitier when the code is complete. May also be called with no arguments
678  * to test if there is any js calls pending. This is relied on by behat so that
679  * it can wait for all pending updates before interacting with a page.
680  * @param String uniqid - optional, if provided,
681  *                        registers this identifier until js_complete is called.
682  * @return boolean - True if there is any pending js.
683  */
684 M.util.js_pending = function(uniqid) {
685     if (uniqid !== false) {
686         M.util.pending_js.push(uniqid);
687     }
689     return M.util.pending_js.length;
692 // Start this asap.
693 M.util.js_pending('init');
696  * Register listeners for Y.io start/end so we can wait for them in behat.
697  */
698 YUI.add('moodle-core-io', function(Y) {
699     Y.on('io:start', function(id) {
700         M.util.js_pending('io:' + id);
701     });
702     Y.on('io:end', function(id) {
703         M.util.js_complete('io:' + id);
704     });
705 }, '@VERSION@', {
706     condition: {
707         trigger: 'io-base',
708         when: 'after'
709     }
713  * Unregister any long running javascript code by unique identifier.
714  * This function should form a matching pair with js_pending
716  * @param String uniqid - required, unregisters this identifier
717  * @return boolean - True if there is any pending js.
718  */
719 M.util.js_complete = function(uniqid) {
720     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
721     // the native function. Y.Array polyfills the native function if it does not exist.
722     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
723     if (index >= 0) {
724         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
725     }
727     return M.util.pending_js.length;
731  * Returns a string registered in advance for usage in JavaScript
733  * If you do not pass the third parameter, the function will just return
734  * the corresponding value from the M.str object. If the third parameter is
735  * provided, the function performs {$a} placeholder substitution in the
736  * same way as PHP get_string() in Moodle does.
738  * @param {String} identifier string identifier
739  * @param {String} component the component providing the string
740  * @param {Object|String} a optional variable to populate placeholder with
741  */
742 M.util.get_string = function(identifier, component, a) {
743     var stringvalue;
745     if (M.cfg.developerdebug) {
746         // creating new instance if YUI is not optimal but it seems to be better way then
747         // require the instance via the function API - note that it is used in rare cases
748         // for debugging only anyway
749         // To ensure we don't kill browser performance if hundreds of get_string requests
750         // are made we cache the instance we generate within the M.util namespace.
751         // We don't publicly define the variable so that it doesn't get abused.
752         if (typeof M.util.get_string_yui_instance === 'undefined') {
753             M.util.get_string_yui_instance = new YUI({ debug : true });
754         }
755         var Y = M.util.get_string_yui_instance;
756     }
758     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
759         stringvalue = '[[' + identifier + ',' + component + ']]';
760         if (M.cfg.developerdebug) {
761             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
762         }
763         return stringvalue;
764     }
766     stringvalue = M.str[component][identifier];
768     if (typeof a == 'undefined') {
769         // no placeholder substitution requested
770         return stringvalue;
771     }
773     if (typeof a == 'number' || typeof a == 'string') {
774         // replace all occurrences of {$a} with the placeholder value
775         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
776         return stringvalue;
777     }
779     if (typeof a == 'object') {
780         // replace {$a->key} placeholders
781         for (var key in a) {
782             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
783                 if (M.cfg.developerdebug) {
784                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
785                 }
786                 continue;
787             }
788             var search = '{$a->' + key + '}';
789             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
790             search = new RegExp(search, 'g');
791             stringvalue = stringvalue.replace(search, a[key]);
792         }
793         return stringvalue;
794     }
796     if (M.cfg.developerdebug) {
797         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
798     }
799     return stringvalue;
803  * Set focus on username or password field of the login form
804  */
805 M.util.focus_login_form = function(Y) {
806     var username = Y.one('#username');
807     var password = Y.one('#password');
809     if (username == null || password == null) {
810         // something is wrong here
811         return;
812     }
814     var curElement = document.activeElement
815     if (curElement == 'undefined') {
816         // legacy browser - skip refocus protection
817     } else if (curElement.tagName == 'INPUT') {
818         // user was probably faster to focus something, do not mess with focus
819         return;
820     }
822     if (username.get('value') == '') {
823         username.focus();
824     } else {
825         password.focus();
826     }
830  * Set focus on login error message
831  */
832 M.util.focus_login_error = function(Y) {
833     var errorlog = Y.one('#loginerrormessage');
835     if (errorlog) {
836         errorlog.focus();
837     }
840  * Adds lightbox hidden element that covers the whole node.
842  * @param {YUI} Y
843  * @param {Node} the node lightbox should be added to
844  * @retun {Node} created lightbox node
845  */
846 M.util.add_lightbox = function(Y, node) {
847     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
849     // Check if lightbox is already there
850     if (node.one('.lightbox')) {
851         return node.one('.lightbox');
852     }
854     node.setStyle('position', 'relative');
855     var waiticon = Y.Node.create('<img />')
856     .setAttrs({
857         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
858     })
859     .setStyles({
860         'position' : 'relative',
861         'top' : '50%'
862     });
864     var lightbox = Y.Node.create('<div></div>')
865     .setStyles({
866         'opacity' : '.75',
867         'position' : 'absolute',
868         'width' : '100%',
869         'height' : '100%',
870         'top' : 0,
871         'left' : 0,
872         'backgroundColor' : 'white',
873         'textAlign' : 'center'
874     })
875     .setAttribute('class', 'lightbox')
876     .hide();
878     lightbox.appendChild(waiticon);
879     node.append(lightbox);
880     return lightbox;
884  * Appends a hidden spinner element to the specified node.
886  * @param {YUI} Y
887  * @param {Node} the node the spinner should be added to
888  * @return {Node} created spinner node
889  */
890 M.util.add_spinner = function(Y, node) {
891     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
893     // Check if spinner is already there
894     if (node.one('.spinner')) {
895         return node.one('.spinner');
896     }
898     var spinner = Y.Node.create('<img />')
899         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
900         .addClass('spinner')
901         .addClass('iconsmall')
902         .hide();
904     node.append(spinner);
905     return spinner;
908 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
910 function checkall() {
911     var inputs = document.getElementsByTagName('input');
912     for (var i = 0; i < inputs.length; i++) {
913         if (inputs[i].type == 'checkbox') {
914             if (inputs[i].disabled || inputs[i].readOnly) {
915                 continue;
916             }
917             inputs[i].checked = true;
918         }
919     }
922 function checknone() {
923     var inputs = document.getElementsByTagName('input');
924     for (var i = 0; i < inputs.length; i++) {
925         if (inputs[i].type == 'checkbox') {
926             if (inputs[i].disabled || inputs[i].readOnly) {
927                 continue;
928             }
929             inputs[i].checked = false;
930         }
931     }
935  * Either check, or uncheck, all checkboxes inside the element with id is
936  * @param id the id of the container
937  * @param checked the new state, either '' or 'checked'.
938  */
939 function select_all_in_element_with_id(id, checked) {
940     var container = document.getElementById(id);
941     if (!container) {
942         return;
943     }
944     var inputs = container.getElementsByTagName('input');
945     for (var i = 0; i < inputs.length; ++i) {
946         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
947             inputs[i].checked = checked;
948         }
949     }
952 function select_all_in(elTagName, elClass, elId) {
953     var inputs = document.getElementsByTagName('input');
954     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
955     for(var i = 0; i < inputs.length; ++i) {
956         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
957             inputs[i].checked = 'checked';
958         }
959     }
962 function deselect_all_in(elTagName, elClass, elId) {
963     var inputs = document.getElementsByTagName('INPUT');
964     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
965     for(var i = 0; i < inputs.length; ++i) {
966         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
967             inputs[i].checked = '';
968         }
969     }
972 function confirm_if(expr, message) {
973     if(!expr) {
974         return true;
975     }
976     return confirm(message);
981     findParentNode (start, elementName, elementClass, elementID)
983     Travels up the DOM hierarchy to find a parent element with the
984     specified tag name, class, and id. All conditions must be met,
985     but any can be ommitted. Returns the BODY element if no match
986     found.
988 function findParentNode(el, elName, elClass, elId) {
989     while (el.nodeName.toUpperCase() != 'BODY') {
990         if ((!elName || el.nodeName.toUpperCase() == elName) &&
991             (!elClass || el.className.indexOf(elClass) != -1) &&
992             (!elId || el.id == elId)) {
993             break;
994         }
995         el = el.parentNode;
996     }
997     return el;
1000     findChildNode (start, elementName, elementClass, elementID)
1002     Travels down the DOM hierarchy to find all child elements with the
1003     specified tag name, class, and id. All conditions must be met,
1004     but any can be ommitted.
1005     Doesn't examine children of matches.
1007     @deprecated since Moodle 2.7 - please do not use this function any more.
1008     @todo MDL-43242 This will be deleted in Moodle 2.9.
1009     @see Y.all
1011 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
1012     Y.log("findChildNodes() is deprecated. Please use Y.all instead.",
1013             "warn", "javascript-static.js");
1014     var children = new Array();
1015     for (var i = 0; i < start.childNodes.length; i++) {
1016         var classfound = false;
1017         var child = start.childNodes[i];
1018         if((child.nodeType == 1) &&//element node type
1019                   (elementClass && (typeof(child.className)=='string'))) {
1020             var childClasses = child.className.split(/\s+/);
1021             for (var childClassIndex in childClasses) {
1022                 if (childClasses[childClassIndex]==elementClass) {
1023                     classfound = true;
1024                     break;
1025                 }
1026             }
1027         }
1028         if(child.nodeType == 1) { //element node type
1029             if  ( (!tagName || child.nodeName == tagName) &&
1030                 (!elementClass || classfound)&&
1031                 (!elementID || child.id == elementID) &&
1032                 (!elementName || child.name == elementName))
1033             {
1034                 children = children.concat(child);
1035             } else {
1036                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1037             }
1038         }
1039     }
1040     return children;
1043 function unmaskPassword(id) {
1044     var pw = document.getElementById(id);
1045     var chb = document.getElementById(id+'unmask');
1047     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1048     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1049     // functionality won't work in IE8 or lower.
1050     // This is a temporary fixed to allow other browsers to function properly.
1051     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1052         if (chb.checked) {
1053             pw.type = "text";
1054         } else {
1055             pw.type = "password";
1056         }
1057     } else {  //IE Browser version 8 or lower
1058         try {
1059             // first try IE way - it can not set name attribute later
1060             if (chb.checked) {
1061               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1062             } else {
1063               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1064             }
1065             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1066         } catch (e) {
1067             var newpw = document.createElement('input');
1068             newpw.setAttribute('autocomplete', 'off');
1069             newpw.setAttribute('name', pw.name);
1070             if (chb.checked) {
1071               newpw.setAttribute('type', 'text');
1072             } else {
1073               newpw.setAttribute('type', 'password');
1074             }
1075             newpw.setAttribute('class', pw.getAttribute('class'));
1076         }
1077         newpw.id = pw.id;
1078         newpw.size = pw.size;
1079         newpw.onblur = pw.onblur;
1080         newpw.onchange = pw.onchange;
1081         newpw.value = pw.value;
1082         pw.parentNode.replaceChild(newpw, pw);
1083     }
1086 function filterByParent(elCollection, parentFinder) {
1087     var filteredCollection = [];
1088     for (var i = 0; i < elCollection.length; ++i) {
1089         var findParent = parentFinder(elCollection[i]);
1090         if (findParent.nodeName.toUpperCase() != 'BODY') {
1091             filteredCollection.push(elCollection[i]);
1092         }
1093     }
1094     return filteredCollection;
1098     All this is here just so that IE gets to handle oversized blocks
1099     in a visually pleasing manner. It does a browser detect. So sue me.
1102 function fix_column_widths() {
1103     var agt = navigator.userAgent.toLowerCase();
1104     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1105         fix_column_width('left-column');
1106         fix_column_width('right-column');
1107     }
1110 function fix_column_width(colName) {
1111     if(column = document.getElementById(colName)) {
1112         if(!column.offsetWidth) {
1113             setTimeout("fix_column_width('" + colName + "')", 20);
1114             return;
1115         }
1117         var width = 0;
1118         var nodes = column.childNodes;
1120         for(i = 0; i < nodes.length; ++i) {
1121             if(nodes[i].className.indexOf("block") != -1 ) {
1122                 if(width < nodes[i].offsetWidth) {
1123                     width = nodes[i].offsetWidth;
1124                 }
1125             }
1126         }
1128         for(i = 0; i < nodes.length; ++i) {
1129             if(nodes[i].className.indexOf("block") != -1 ) {
1130                 nodes[i].style.width = width + 'px';
1131             }
1132         }
1133     }
1138    Insert myValue at current cursor position
1139  */
1140 function insertAtCursor(myField, myValue) {
1141     // IE support
1142     if (document.selection) {
1143         myField.focus();
1144         sel = document.selection.createRange();
1145         sel.text = myValue;
1146     }
1147     // Mozilla/Netscape support
1148     else if (myField.selectionStart || myField.selectionStart == '0') {
1149         var startPos = myField.selectionStart;
1150         var endPos = myField.selectionEnd;
1151         myField.value = myField.value.substring(0, startPos)
1152             + myValue + myField.value.substring(endPos, myField.value.length);
1153     } else {
1154         myField.value += myValue;
1155     }
1160         Call instead of setting window.onload directly or setting body onload=.
1161         Adds your function to a chain of functions rather than overwriting anything
1162         that exists.
1163         @deprecated Since Moodle 2.7. This will be removed in Moodle 2.9.
1165 function addonload(fn) {
1166     Y.log('addonload has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1167             'warn', 'javascript-static.js');
1168     var oldhandler=window.onload;
1169     window.onload=function() {
1170         if(oldhandler) oldhandler();
1171             fn();
1172     }
1175  * Replacement for getElementsByClassName in browsers that aren't cool enough
1177  * Relying on the built-in getElementsByClassName is far, far faster than
1178  * using YUI.
1180  * Note: the third argument used to be an object with odd behaviour. It now
1181  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1182  * mimicked if you pass an object.
1184  * @param {Node} oElm The top-level node for searching. To search a whole
1185  *                    document, use `document`.
1186  * @param {String} strTagName filter by tag names
1187  * @param {String} name same as HTML5 spec
1188  * @deprecated Since Moodle 2.7. This will be removed in Moodle 2.9.
1189  */
1190 function getElementsByClassName(oElm, strTagName, name) {
1191     Y.log('getElementsByClassName has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1192             'warn', 'javascript-static.js');
1193     // for backwards compatibility
1194     if(typeof name == "object") {
1195         var names = new Array();
1196         for(var i=0; i<name.length; i++) names.push(names[i]);
1197         name = names.join('');
1198     }
1199     // use native implementation if possible
1200     if (oElm.getElementsByClassName && Array.filter) {
1201         if (strTagName == '*') {
1202             return oElm.getElementsByClassName(name);
1203         } else {
1204             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1205                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1206             });
1207         }
1208     }
1209     // native implementation unavailable, fall back to slow method
1210     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1211     var arrReturnElements = new Array();
1212     var arrRegExpClassNames = new Array();
1213     var names = name.split(' ');
1214     for(var i=0; i<names.length; i++) {
1215         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1216     }
1217     var oElement;
1218     var bMatchesAll;
1219     for(var j=0; j<arrElements.length; j++) {
1220         oElement = arrElements[j];
1221         bMatchesAll = true;
1222         for(var k=0; k<arrRegExpClassNames.length; k++) {
1223             if(!arrRegExpClassNames[k].test(oElement.className)) {
1224                 bMatchesAll = false;
1225                 break;
1226             }
1227         }
1228         if(bMatchesAll) {
1229             arrReturnElements.push(oElement);
1230         }
1231     }
1232     return (arrReturnElements)
1236  * Increment a file name.
1238  * @param string file name.
1239  * @param boolean ignoreextension do not extract the extension prior to appending the
1240  *                                suffix. Useful when incrementing folder names.
1241  * @return string the incremented file name.
1242  */
1243 function increment_filename(filename, ignoreextension) {
1244     var extension = '';
1245     var basename = filename;
1247     // Split the file name into the basename + extension.
1248     if (!ignoreextension) {
1249         var dotpos = filename.lastIndexOf('.');
1250         if (dotpos !== -1) {
1251             basename = filename.substr(0, dotpos);
1252             extension = filename.substr(dotpos, filename.length);
1253         }
1254     }
1256     // Look to see if the name already has (NN) at the end of it.
1257     var number = 0;
1258     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1259     if (hasnumber !== null) {
1260         // Note the current number & remove it from the basename.
1261         number = parseInt(hasnumber[2], 10);
1262         basename = hasnumber[1];
1263     }
1265     number++;
1266     var newname = basename + ' (' + number + ')' + extension;
1267     return newname;
1271  * Return whether we are in right to left mode or not.
1273  * @return boolean
1274  */
1275 function right_to_left() {
1276     var body = Y.one('body');
1277     var rtl = false;
1278     if (body && body.hasClass('dir-rtl')) {
1279         rtl = true;
1280     }
1281     return rtl;
1284 function openpopup(event, args) {
1286     if (event) {
1287         if (event.preventDefault) {
1288             event.preventDefault();
1289         } else {
1290             event.returnValue = false;
1291         }
1292     }
1294     // Make sure the name argument is set and valid.
1295     var nameregex = /[^a-z0-9_]/i;
1296     if (typeof args.name !== 'string') {
1297         args.name = '_blank';
1298     } else if (args.name.match(nameregex)) {
1299         // Cleans window name because IE does not support funky ones.
1300         if (M.cfg.developerdebug) {
1301             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1302         }
1303         args.name = args.name.replace(nameregex, '_');
1304     }
1306     var fullurl = args.url;
1307     if (!args.url.match(/https?:\/\//)) {
1308         fullurl = M.cfg.wwwroot + args.url;
1309     }
1310     if (args.fullscreen) {
1311         args.options = args.options.
1312                 replace(/top=\d+/, 'top=0').
1313                 replace(/left=\d+/, 'left=0').
1314                 replace(/width=\d+/, 'width=' + screen.availWidth).
1315                 replace(/height=\d+/, 'height=' + screen.availHeight);
1316     }
1317     var windowobj = window.open(fullurl,args.name,args.options);
1318     if (!windowobj) {
1319         return true;
1320     }
1322     if (args.fullscreen) {
1323         // In some browser / OS combinations (E.g. Chrome on Windows), the
1324         // window initially opens slighly too big. The width and heigh options
1325         // seem to control the area inside the browser window, so what with
1326         // scroll-bars, etc. the actual window is bigger than the screen.
1327         // Therefore, we need to fix things up after the window is open.
1328         var hackcount = 100;
1329         var get_size_exactly_right = function() {
1330             windowobj.moveTo(0, 0);
1331             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1333             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1334             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1335             // about 50ms) after the window is open, then it actually behaves
1336             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1337             // check that the resize actually worked, and if not, repeatedly try
1338             // again after a short delay until it works (but with a limit of
1339             // hackcount repeats.
1340             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1341                 hackcount -= 1;
1342                 setTimeout(get_size_exactly_right, 10);
1343             }
1344         }
1345         setTimeout(get_size_exactly_right, 0);
1346     }
1347     windowobj.focus();
1349     return false;
1352 /** Close the current browser window. */
1353 function close_window(e) {
1354     if (e.preventDefault) {
1355         e.preventDefault();
1356     } else {
1357         e.returnValue = false;
1358     }
1359     window.close();
1363  * Used in a couple of modules to hide navigation areas when using AJAX
1364  * @deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1365  */
1366 function show_item(itemid) {
1367     Y.log('show_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1368             'warn', 'javascript-static.js');
1369     var item = Y.one('#' + itemid);
1370     if (item) {
1371         item.show();
1372     }
1375 // Deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1376 function destroy_item(itemid) {
1377     Y.log('destroy_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1378             'warn', 'javascript-static.js');
1379     var item = Y.one('#' + itemid);
1380     if (item) {
1381         item.remove(true);
1382     }
1385  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1386  * @param controlid the control id.
1387  */
1388 function focuscontrol(controlid) {
1389     var control = document.getElementById(controlid);
1390     if (control) {
1391         control.focus();
1392     }
1396  * Transfers keyboard focus to an HTML element based on the old style style of focus
1397  * This function should be removed as soon as it is no longer used
1398  */
1399 function old_onload_focus(formid, controlname) {
1400     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1401         document.forms[formid].elements[controlname].focus();
1402     }
1405 function build_querystring(obj) {
1406     return convert_object_to_string(obj, '&');
1409 function build_windowoptionsstring(obj) {
1410     return convert_object_to_string(obj, ',');
1413 function convert_object_to_string(obj, separator) {
1414     if (typeof obj !== 'object') {
1415         return null;
1416     }
1417     var list = [];
1418     for(var k in obj) {
1419         k = encodeURIComponent(k);
1420         var value = obj[k];
1421         if(obj[k] instanceof Array) {
1422             for(var i in value) {
1423                 list.push(k+'[]='+encodeURIComponent(value[i]));
1424             }
1425         } else {
1426             list.push(k+'='+encodeURIComponent(value));
1427         }
1428     }
1429     return list.join(separator);
1432 function stripHTML(str) {
1433     var re = /<\S[^><]*>/g;
1434     var ret = str.replace(re, "");
1435     return ret;
1438 function updateProgressBar(id, percent, msg, estimate) {
1439     var progressIndicator = Y.one('#' + id);
1440     if (!progressIndicator) {
1441         return;
1442     }
1444     var progressBar = progressIndicator.one('.bar'),
1445         statusIndicator = progressIndicator.one('h2'),
1446         estimateIndicator = progressIndicator.one('p');
1448     statusIndicator.set('innerHTML', Y.Escape.html(msg));
1449     progressBar.set('innerHTML', Y.Escape.html('' + percent + '%'));
1450     if (percent === 100) {
1451         progressIndicator.addClass('progress-success');
1452         estimateIndicator.set('innerHTML', null);
1453     } else {
1454         if (estimate) {
1455             estimateIndicator.set('innerHTML', Y.Escape.html(estimate));
1456         } else {
1457             estimateIndicator.set('innerHTML', null);
1458         }
1459         progressIndicator.removeClass('progress-success');
1460     }
1461     progressBar.setAttribute('aria-valuenow', percent);
1462     progressBar.setStyle('width', percent + '%');
1465 // ===== Deprecated core Javascript functions for Moodle ====
1466 //       DO NOT USE!!!!!!!
1467 // Do not put this stuff in separate file because it only adds extra load on servers!
1470  * Used in a couple of modules to hide navigation areas when using AJAX
1471  * @deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1472  */
1473 function hide_item(itemid) {
1474     Y.log('hide_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1475             'warn', 'javascript-static.js');
1476     var item = Y.one('#' + itemid);
1477     if (item) {
1478         item.hide();
1479     }
1482 M.util.help_popups = {
1483     setup : function(Y) {
1484         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1485     },
1486     open_popup : function(e) {
1487         // Prevent the default page action
1488         e.preventDefault();
1490         // Grab the anchor that was clicked
1491         var anchor = e.target.ancestor('a', true);
1492         var args = {
1493             'name'          : 'popup',
1494             'url'           : anchor.getAttribute('href'),
1495             'options'       : ''
1496         };
1497         var options = [
1498             'height=600',
1499             'width=800',
1500             'top=0',
1501             'left=0',
1502             'menubar=0',
1503             'location=0',
1504             'scrollbars',
1505             'resizable',
1506             'toolbar',
1507             'status',
1508             'directories=0',
1509             'fullscreen=0',
1510             'dependent'
1511         ]
1512         args.options = options.join(',');
1514         openpopup(e, args);
1515     }
1519  * Custom menu namespace
1520  */
1521 M.core_custom_menu = {
1522     /**
1523      * This method is used to initialise a custom menu given the id that belongs
1524      * to the custom menu's root node.
1525      *
1526      * @param {YUI} Y
1527      * @param {string} nodeid
1528      */
1529     init : function(Y, nodeid) {
1530         var node = Y.one('#'+nodeid);
1531         if (node) {
1532             Y.use('node-menunav', function(Y) {
1533                 // Get the node
1534                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1535                 node.removeClass('javascript-disabled');
1536                 // Initialise the menunav plugin
1537                 node.plug(Y.Plugin.NodeMenuNav);
1538             });
1539         }
1540     }
1544  * Used to store form manipulation methods and enhancments
1545  */
1546 M.form = M.form || {};
1549  * Converts a nbsp indented select box into a multi drop down custom control much
1550  * like the custom menu. It also selectable categories on or off.
1552  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1554  * @param {YUI} Y
1555  * @param {string} id
1556  * @param {Array} options
1557  */
1558 M.form.init_smartselect = function(Y, id, options) {
1559     if (!id.match(/^id_/)) {
1560         id = 'id_'+id;
1561     }
1562     var select = Y.one('select#'+id);
1563     if (!select) {
1564         return false;
1565     }
1566     Y.use('event-delegate',function(){
1567         var smartselect = {
1568             id : id,
1569             structure : [],
1570             options : [],
1571             submenucount : 0,
1572             currentvalue : null,
1573             currenttext : null,
1574             shownevent : null,
1575             cfg : {
1576                 selectablecategories : true,
1577                 mode : null
1578             },
1579             nodes : {
1580                 select : null,
1581                 loading : null,
1582                 menu : null
1583             },
1584             init : function(Y, id, args, nodes) {
1585                 if (typeof(args)=='object') {
1586                     for (var i in this.cfg) {
1587                         if (args[i] || args[i]===false) {
1588                             this.cfg[i] = args[i];
1589                         }
1590                     }
1591                 }
1593                 // Display a loading message first up
1594                 this.nodes.select = nodes.select;
1596                 this.currentvalue = this.nodes.select.get('selectedIndex');
1597                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1599                 var options = Array();
1600                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1601                 this.nodes.select.all('option').each(function(option, index) {
1602                     var rawtext = option.get('innerHTML');
1603                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1604                     if (rawtext === text) {
1605                         text = rawtext.replace(/^(\s)*/, '');
1606                         var depth = (rawtext.length - text.length ) + 1;
1607                     } else {
1608                         var depth = ((rawtext.length - text.length )/12)+1;
1609                     }
1610                     option.set('innerHTML', text);
1611                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1612                 }, this);
1614                 this.structure = [];
1615                 var structcount = 0;
1616                 for (var i in options) {
1617                     var o = options[i];
1618                     if (o.depth == 0) {
1619                         this.structure.push(o);
1620                         structcount++;
1621                     } else {
1622                         var d = o.depth;
1623                         var current = this.structure[structcount-1];
1624                         for (var j = 0; j < o.depth-1;j++) {
1625                             if (current && current.children) {
1626                                 current = current.children[current.children.length-1];
1627                             }
1628                         }
1629                         if (current && current.children) {
1630                             current.children.push(o);
1631                         }
1632                     }
1633                 }
1635                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1636                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1637                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1638                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1640                 if (this.cfg.mode == null) {
1641                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1642                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1643                         this.cfg.mode = 'compact';
1644                     } else {
1645                         this.cfg.mode = 'spanning';
1646                     }
1647                 }
1649                 if (this.cfg.mode == 'compact') {
1650                     this.nodes.menu.addClass('compactmenu');
1651                 } else {
1652                     this.nodes.menu.addClass('spanningmenu');
1653                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1654                 }
1656                 Y.one(document.body).append(this.nodes.menu);
1657                 var pos = this.nodes.select.getXY();
1658                 pos[0] += 1;
1659                 this.nodes.menu.setXY(pos);
1660                 this.nodes.menu.on('click', this.handle_click, this);
1662                 Y.one(window).on('resize', function(){
1663                      var pos = this.nodes.select.getXY();
1664                     pos[0] += 1;
1665                     this.nodes.menu.setXY(pos);
1666                  }, this);
1667             },
1668             generate_menu_content : function() {
1669                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1670                 content += this.generate_submenu_content(this.structure[0], true);
1671                 content += '</ul></div>';
1672                 return content;
1673             },
1674             generate_submenu_content : function(item, rootelement) {
1675                 this.submenucount++;
1676                 var content = '';
1677                 if (item.children.length > 0) {
1678                     if (rootelement) {
1679                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1680                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1681                         content += '<div class="smartselect_menu_content">';
1682                     } else {
1683                         content += '<li class="smartselect_submenuitem">';
1684                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1685                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1686                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1687                         content += '<div class="smartselect_submenu_content">';
1688                     }
1689                     content += '<ul>';
1690                     for (var i in item.children) {
1691                         content += this.generate_submenu_content(item.children[i],false);
1692                     }
1693                     content += '</ul>';
1694                     content += '</div>';
1695                     content += '</div>';
1696                     if (rootelement) {
1697                     } else {
1698                         content += '</li>';
1699                     }
1700                 } else {
1701                     content += '<li class="smartselect_menuitem">';
1702                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1703                     content += '</li>';
1704                 }
1705                 return content;
1706             },
1707             select : function(e) {
1708                 var t = e.target;
1709                 e.halt();
1710                 this.currenttext = t.get('innerHTML');
1711                 this.currentvalue = t.getAttribute('value');
1712                 this.nodes.select.set('selectedIndex', this.currentvalue);
1713                 this.hide_menu();
1714             },
1715             handle_click : function(e) {
1716                 var target = e.target;
1717                 if (target.hasClass('smartselect_mask')) {
1718                     this.show_menu(e);
1719                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1720                     this.select(e);
1721                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1722                     this.show_sub_menu(e);
1723                 }
1724             },
1725             show_menu : function(e) {
1726                 e.halt();
1727                 var menu = e.target.ancestor().one('.smartselect_menu');
1728                 menu.addClass('visible');
1729                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1730             },
1731             show_sub_menu : function(e) {
1732                 e.halt();
1733                 var target = e.target;
1734                 if (!target.hasClass('smartselect_submenuitem')) {
1735                     target = target.ancestor('.smartselect_submenuitem');
1736                 }
1737                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1738                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1739                     return;
1740                 }
1741                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1742                 target.one('.smartselect_submenu').addClass('visible');
1743             },
1744             hide_menu : function() {
1745                 this.nodes.menu.all('.visible').removeClass('visible');
1746                 if (this.shownevent) {
1747                     this.shownevent.detach();
1748                 }
1749             }
1750         };
1751         smartselect.init(Y, id, options, {select:select});
1752     });
1755 /** List of flv players to be loaded */
1756 M.util.video_players = [];
1757 /** List of mp3 players to be loaded */
1758 M.util.audio_players = [];
1761  * Add video player
1762  * @param id element id
1763  * @param fileurl media url
1764  * @param width
1765  * @param height
1766  * @param autosize true means detect size from media
1767  */
1768 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1769     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1773  * Add audio player.
1774  * @param id
1775  * @param fileurl
1776  * @param small
1777  */
1778 M.util.add_audio_player = function (id, fileurl, small) {
1779     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1783  * Initialise all audio and video player, must be called from page footer.
1784  */
1785 M.util.load_flowplayer = function() {
1786     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1787         return;
1788     }
1789     if (typeof(flowplayer) == 'undefined') {
1790         var loaded = false;
1792         var embed_function = function() {
1793             if (loaded || typeof(flowplayer) == 'undefined') {
1794                 return;
1795             }
1796             loaded = true;
1798             var controls = {
1799                     autoHide: true
1800             }
1801             /* TODO: add CSS color overrides for the flv flow player */
1803             for(var i=0; i<M.util.video_players.length; i++) {
1804                 var video = M.util.video_players[i];
1805                 if (video.width > 0 && video.height > 0) {
1806                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', width: video.width, height: video.height};
1807                 } else {
1808                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf';
1809                 }
1810                 flowplayer(video.id, src, {
1811                     plugins: {controls: controls},
1812                     clip: {
1813                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1814                         onMetaData: function(clip) {
1815                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1816                                 clip.mvideo.resized = true;
1817                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1818                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1819                                     // bad luck, we have to guess - we may not get metadata at all
1820                                     var width = clip.width;
1821                                     var height = clip.height;
1822                                 } else {
1823                                     var width = clip.metaData.width;
1824                                     var height = clip.metaData.height;
1825                                 }
1826                                 var minwidth = 300; // controls are messed up in smaller objects
1827                                 if (width < minwidth) {
1828                                     height = (height * minwidth) / width;
1829                                     width = minwidth;
1830                                 }
1832                                 var object = this._api();
1833                                 object.width = width;
1834                                 object.height = height;
1835                             }
1836                         }
1837                     }
1838                 });
1839             }
1840             if (M.util.audio_players.length == 0) {
1841                 return;
1842             }
1843             var controls = {
1844                     autoHide: false,
1845                     fullscreen: false,
1846                     next: false,
1847                     previous: false,
1848                     scrubber: true,
1849                     play: true,
1850                     pause: true,
1851                     volume: true,
1852                     mute: false,
1853                     backgroundGradient: [0.5,0,0.3]
1854                 };
1856             var rule;
1857             for (var j=0; j < document.styleSheets.length; j++) {
1859                 // To avoid javascript security violation accessing cross domain stylesheets
1860                 var allrules = false;
1861                 try {
1862                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1863                         allrules = document.styleSheets[j].rules;
1864                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1865                         allrules = document.styleSheets[j].cssRules;
1866                     } else {
1867                         // why??
1868                         continue;
1869                     }
1870                 } catch (e) {
1871                     continue;
1872                 }
1874                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1875                 if (!allrules) {
1876                     continue;
1877                 }
1879                 for(var i=0; i<allrules.length; i++) {
1880                     rule = '';
1881                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1882                         if (typeof(allrules[i].cssText) != 'undefined') {
1883                             rule = allrules[i].cssText;
1884                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1885                             rule = allrules[i].style.cssText;
1886                         }
1887                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1888                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1889                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1890                             controls[colprop] = rule;
1891                         }
1892                     }
1893                 }
1894                 allrules = false;
1895             }
1897             for(i=0; i<M.util.audio_players.length; i++) {
1898                 var audio = M.util.audio_players[i];
1899                 if (audio.small) {
1900                     controls.controlall = false;
1901                     controls.height = 15;
1902                     controls.time = false;
1903                 } else {
1904                     controls.controlall = true;
1905                     controls.height = 25;
1906                     controls.time = true;
1907                 }
1908                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', {
1909                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf'}},
1910                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1911                 });
1912             }
1913         }
1915         if (M.cfg.jsrev == -1) {
1916             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1917         } else {
1918             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1919         }
1920         var fileref = document.createElement('script');
1921         fileref.setAttribute('type','text/javascript');
1922         fileref.setAttribute('src', jsurl);
1923         fileref.onload = embed_function;
1924         fileref.onreadystatechange = embed_function;
1925         document.getElementsByTagName('head')[0].appendChild(fileref);
1926     }