MDL-47704 Grades: Behat fix - dropped items have no contribution
[moodle.git] / lib / javascript-static.js
blob38a4dd82d2d00c1314b542518a144e375cb9244e
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('winHeight')) - 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                     if (t && (a = t.one('.block_action'))) {
602                         var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
603                         hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
604                         hide.on('keypress', this.updateStateKey, this, true);
605                         var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
606                         show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
607                         show.on('keypress', this.updateStateKey, this, false);
608                         a.insert(show, 0).insert(hide, 0);
609                     }
610                 },
611                 updateState : function(e, hide) {
612                     M.util.set_user_preference(this.get('preference'), hide);
613                     if (hide) {
614                         this.get('block').addClass('hidden');
615                     } else {
616                         this.get('block').removeClass('hidden');
617                     }
618                 },
619                 updateStateKey : function(e, hide) {
620                     if (e.keyCode == 13) { //allow hide/show via enter key
621                         this.updateState(this, hide);
622                     }
623                 }
624             };
625             Y.extend(blockhider, Y.Base, blockhider.prototype, {
626                 NAME : 'blockhider',
627                 ATTRS : {
628                     id : {},
629                     preference : {},
630                     iconVisible : {
631                         value : M.util.image_url('t/switch_minus', 'moodle')
632                     },
633                     iconHidden : {
634                         value : M.util.image_url('t/switch_plus', 'moodle')
635                     },
636                     block : {
637                         setter : function(node) {
638                             return Y.one(node);
639                         }
640                     }
641                 }
642             });
643             return blockhider;
644         })();
645         new M.util.block_hider(config);
646     });
650  * @var pending_js - The keys are the list of all pending js actions.
651  * @type Object
652  */
653 M.util.pending_js = [];
654 M.util.complete_js = [];
657  * Register any long running javascript code with a unique identifier.
658  * Should be followed with a call to js_complete with a matching
659  * idenfitier when the code is complete. May also be called with no arguments
660  * to test if there is any js calls pending. This is relied on by behat so that
661  * it can wait for all pending updates before interacting with a page.
662  * @param String uniqid - optional, if provided,
663  *                        registers this identifier until js_complete is called.
664  * @return boolean - True if there is any pending js.
665  */
666 M.util.js_pending = function(uniqid) {
667     if (uniqid !== false) {
668         M.util.pending_js.push(uniqid);
669     }
671     return M.util.pending_js.length;
674 // Start this asap.
675 M.util.js_pending('init');
678  * Register listeners for Y.io start/end so we can wait for them in behat.
679  */
680 YUI.add('moodle-core-io', function(Y) {
681     Y.on('io:start', function(id) {
682         M.util.js_pending('io:' + id);
683     });
684     Y.on('io:end', function(id) {
685         M.util.js_complete('io:' + id);
686     });
687 }, '@VERSION@', {
688     condition: {
689         trigger: 'io-base',
690         when: 'after'
691     }
695  * Unregister any long running javascript code by unique identifier.
696  * This function should form a matching pair with js_pending
698  * @param String uniqid - required, unregisters this identifier
699  * @return boolean - True if there is any pending js.
700  */
701 M.util.js_complete = function(uniqid) {
702     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
703     // the native function. Y.Array polyfills the native function if it does not exist.
704     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
705     if (index >= 0) {
706         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
707     }
709     return M.util.pending_js.length;
713  * Returns a string registered in advance for usage in JavaScript
715  * If you do not pass the third parameter, the function will just return
716  * the corresponding value from the M.str object. If the third parameter is
717  * provided, the function performs {$a} placeholder substitution in the
718  * same way as PHP get_string() in Moodle does.
720  * @param {String} identifier string identifier
721  * @param {String} component the component providing the string
722  * @param {Object|String} a optional variable to populate placeholder with
723  */
724 M.util.get_string = function(identifier, component, a) {
725     var stringvalue;
727     if (M.cfg.developerdebug) {
728         // creating new instance if YUI is not optimal but it seems to be better way then
729         // require the instance via the function API - note that it is used in rare cases
730         // for debugging only anyway
731         // To ensure we don't kill browser performance if hundreds of get_string requests
732         // are made we cache the instance we generate within the M.util namespace.
733         // We don't publicly define the variable so that it doesn't get abused.
734         if (typeof M.util.get_string_yui_instance === 'undefined') {
735             M.util.get_string_yui_instance = new YUI({ debug : true });
736         }
737         var Y = M.util.get_string_yui_instance;
738     }
740     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
741         stringvalue = '[[' + identifier + ',' + component + ']]';
742         if (M.cfg.developerdebug) {
743             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
744         }
745         return stringvalue;
746     }
748     stringvalue = M.str[component][identifier];
750     if (typeof a == 'undefined') {
751         // no placeholder substitution requested
752         return stringvalue;
753     }
755     if (typeof a == 'number' || typeof a == 'string') {
756         // replace all occurrences of {$a} with the placeholder value
757         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
758         return stringvalue;
759     }
761     if (typeof a == 'object') {
762         // replace {$a->key} placeholders
763         for (var key in a) {
764             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
765                 if (M.cfg.developerdebug) {
766                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
767                 }
768                 continue;
769             }
770             var search = '{$a->' + key + '}';
771             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
772             search = new RegExp(search, 'g');
773             stringvalue = stringvalue.replace(search, a[key]);
774         }
775         return stringvalue;
776     }
778     if (M.cfg.developerdebug) {
779         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
780     }
781     return stringvalue;
785  * Set focus on username or password field of the login form
786  */
787 M.util.focus_login_form = function(Y) {
788     var username = Y.one('#username');
789     var password = Y.one('#password');
791     if (username == null || password == null) {
792         // something is wrong here
793         return;
794     }
796     var curElement = document.activeElement
797     if (curElement == 'undefined') {
798         // legacy browser - skip refocus protection
799     } else if (curElement.tagName == 'INPUT') {
800         // user was probably faster to focus something, do not mess with focus
801         return;
802     }
804     if (username.get('value') == '') {
805         username.focus();
806     } else {
807         password.focus();
808     }
812  * Set focus on login error message
813  */
814 M.util.focus_login_error = function(Y) {
815     var errorlog = Y.one('#loginerrormessage');
817     if (errorlog) {
818         errorlog.focus();
819     }
822  * Adds lightbox hidden element that covers the whole node.
824  * @param {YUI} Y
825  * @param {Node} the node lightbox should be added to
826  * @retun {Node} created lightbox node
827  */
828 M.util.add_lightbox = function(Y, node) {
829     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
831     // Check if lightbox is already there
832     if (node.one('.lightbox')) {
833         return node.one('.lightbox');
834     }
836     node.setStyle('position', 'relative');
837     var waiticon = Y.Node.create('<img />')
838     .setAttrs({
839         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
840     })
841     .setStyles({
842         'position' : 'relative',
843         'top' : '50%'
844     });
846     var lightbox = Y.Node.create('<div></div>')
847     .setStyles({
848         'opacity' : '.75',
849         'position' : 'absolute',
850         'width' : '100%',
851         'height' : '100%',
852         'top' : 0,
853         'left' : 0,
854         'backgroundColor' : 'white',
855         'textAlign' : 'center'
856     })
857     .setAttribute('class', 'lightbox')
858     .hide();
860     lightbox.appendChild(waiticon);
861     node.append(lightbox);
862     return lightbox;
866  * Appends a hidden spinner element to the specified node.
868  * @param {YUI} Y
869  * @param {Node} the node the spinner should be added to
870  * @return {Node} created spinner node
871  */
872 M.util.add_spinner = function(Y, node) {
873     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
875     // Check if spinner is already there
876     if (node.one('.spinner')) {
877         return node.one('.spinner');
878     }
880     var spinner = Y.Node.create('<img />')
881         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
882         .addClass('spinner')
883         .addClass('iconsmall')
884         .hide();
886     node.append(spinner);
887     return spinner;
890 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
892 function checkall() {
893     var inputs = document.getElementsByTagName('input');
894     for (var i = 0; i < inputs.length; i++) {
895         if (inputs[i].type == 'checkbox') {
896             if (inputs[i].disabled || inputs[i].readOnly) {
897                 continue;
898             }
899             inputs[i].checked = true;
900         }
901     }
904 function checknone() {
905     var inputs = document.getElementsByTagName('input');
906     for (var i = 0; i < inputs.length; i++) {
907         if (inputs[i].type == 'checkbox') {
908             if (inputs[i].disabled || inputs[i].readOnly) {
909                 continue;
910             }
911             inputs[i].checked = false;
912         }
913     }
917  * Either check, or uncheck, all checkboxes inside the element with id is
918  * @param id the id of the container
919  * @param checked the new state, either '' or 'checked'.
920  */
921 function select_all_in_element_with_id(id, checked) {
922     var container = document.getElementById(id);
923     if (!container) {
924         return;
925     }
926     var inputs = container.getElementsByTagName('input');
927     for (var i = 0; i < inputs.length; ++i) {
928         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
929             inputs[i].checked = checked;
930         }
931     }
934 function select_all_in(elTagName, elClass, elId) {
935     var inputs = document.getElementsByTagName('input');
936     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
937     for(var i = 0; i < inputs.length; ++i) {
938         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
939             inputs[i].checked = 'checked';
940         }
941     }
944 function deselect_all_in(elTagName, elClass, elId) {
945     var inputs = document.getElementsByTagName('INPUT');
946     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
947     for(var i = 0; i < inputs.length; ++i) {
948         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
949             inputs[i].checked = '';
950         }
951     }
954 function confirm_if(expr, message) {
955     if(!expr) {
956         return true;
957     }
958     return confirm(message);
963     findParentNode (start, elementName, elementClass, elementID)
965     Travels up the DOM hierarchy to find a parent element with the
966     specified tag name, class, and id. All conditions must be met,
967     but any can be ommitted. Returns the BODY element if no match
968     found.
970 function findParentNode(el, elName, elClass, elId) {
971     while (el.nodeName.toUpperCase() != 'BODY') {
972         if ((!elName || el.nodeName.toUpperCase() == elName) &&
973             (!elClass || el.className.indexOf(elClass) != -1) &&
974             (!elId || el.id == elId)) {
975             break;
976         }
977         el = el.parentNode;
978     }
979     return el;
982     findChildNode (start, elementName, elementClass, elementID)
984     Travels down the DOM hierarchy to find all child elements with the
985     specified tag name, class, and id. All conditions must be met,
986     but any can be ommitted.
987     Doesn't examine children of matches.
989     @deprecated since Moodle 2.7 - please do not use this function any more.
990     @todo MDL-43242 This will be deleted in Moodle 2.9.
991     @see Y.all
993 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
994     Y.log("findChildNodes() is deprecated. Please use Y.all instead.",
995             "warn", "javascript-static.js");
996     var children = new Array();
997     for (var i = 0; i < start.childNodes.length; i++) {
998         var classfound = false;
999         var child = start.childNodes[i];
1000         if((child.nodeType == 1) &&//element node type
1001                   (elementClass && (typeof(child.className)=='string'))) {
1002             var childClasses = child.className.split(/\s+/);
1003             for (var childClassIndex in childClasses) {
1004                 if (childClasses[childClassIndex]==elementClass) {
1005                     classfound = true;
1006                     break;
1007                 }
1008             }
1009         }
1010         if(child.nodeType == 1) { //element node type
1011             if  ( (!tagName || child.nodeName == tagName) &&
1012                 (!elementClass || classfound)&&
1013                 (!elementID || child.id == elementID) &&
1014                 (!elementName || child.name == elementName))
1015             {
1016                 children = children.concat(child);
1017             } else {
1018                 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1019             }
1020         }
1021     }
1022     return children;
1025 function unmaskPassword(id) {
1026     var pw = document.getElementById(id);
1027     var chb = document.getElementById(id+'unmask');
1029     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1030     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1031     // functionality won't work in IE8 or lower.
1032     // This is a temporary fixed to allow other browsers to function properly.
1033     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1034         if (chb.checked) {
1035             pw.type = "text";
1036         } else {
1037             pw.type = "password";
1038         }
1039     } else {  //IE Browser version 8 or lower
1040         try {
1041             // first try IE way - it can not set name attribute later
1042             if (chb.checked) {
1043               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1044             } else {
1045               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1046             }
1047             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1048         } catch (e) {
1049             var newpw = document.createElement('input');
1050             newpw.setAttribute('autocomplete', 'off');
1051             newpw.setAttribute('name', pw.name);
1052             if (chb.checked) {
1053               newpw.setAttribute('type', 'text');
1054             } else {
1055               newpw.setAttribute('type', 'password');
1056             }
1057             newpw.setAttribute('class', pw.getAttribute('class'));
1058         }
1059         newpw.id = pw.id;
1060         newpw.size = pw.size;
1061         newpw.onblur = pw.onblur;
1062         newpw.onchange = pw.onchange;
1063         newpw.value = pw.value;
1064         pw.parentNode.replaceChild(newpw, pw);
1065     }
1068 function filterByParent(elCollection, parentFinder) {
1069     var filteredCollection = [];
1070     for (var i = 0; i < elCollection.length; ++i) {
1071         var findParent = parentFinder(elCollection[i]);
1072         if (findParent.nodeName.toUpperCase() != 'BODY') {
1073             filteredCollection.push(elCollection[i]);
1074         }
1075     }
1076     return filteredCollection;
1080     All this is here just so that IE gets to handle oversized blocks
1081     in a visually pleasing manner. It does a browser detect. So sue me.
1084 function fix_column_widths() {
1085     var agt = navigator.userAgent.toLowerCase();
1086     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1087         fix_column_width('left-column');
1088         fix_column_width('right-column');
1089     }
1092 function fix_column_width(colName) {
1093     if(column = document.getElementById(colName)) {
1094         if(!column.offsetWidth) {
1095             setTimeout("fix_column_width('" + colName + "')", 20);
1096             return;
1097         }
1099         var width = 0;
1100         var nodes = column.childNodes;
1102         for(i = 0; i < nodes.length; ++i) {
1103             if(nodes[i].className.indexOf("block") != -1 ) {
1104                 if(width < nodes[i].offsetWidth) {
1105                     width = nodes[i].offsetWidth;
1106                 }
1107             }
1108         }
1110         for(i = 0; i < nodes.length; ++i) {
1111             if(nodes[i].className.indexOf("block") != -1 ) {
1112                 nodes[i].style.width = width + 'px';
1113             }
1114         }
1115     }
1120    Insert myValue at current cursor position
1121  */
1122 function insertAtCursor(myField, myValue) {
1123     // IE support
1124     if (document.selection) {
1125         myField.focus();
1126         sel = document.selection.createRange();
1127         sel.text = myValue;
1128     }
1129     // Mozilla/Netscape support
1130     else if (myField.selectionStart || myField.selectionStart == '0') {
1131         var startPos = myField.selectionStart;
1132         var endPos = myField.selectionEnd;
1133         myField.value = myField.value.substring(0, startPos)
1134             + myValue + myField.value.substring(endPos, myField.value.length);
1135     } else {
1136         myField.value += myValue;
1137     }
1142         Call instead of setting window.onload directly or setting body onload=.
1143         Adds your function to a chain of functions rather than overwriting anything
1144         that exists.
1145         @deprecated Since Moodle 2.7. This will be removed in Moodle 2.9.
1147 function addonload(fn) {
1148     Y.log('addonload has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1149             'warn', 'javascript-static.js');
1150     var oldhandler=window.onload;
1151     window.onload=function() {
1152         if(oldhandler) oldhandler();
1153             fn();
1154     }
1157  * Replacement for getElementsByClassName in browsers that aren't cool enough
1159  * Relying on the built-in getElementsByClassName is far, far faster than
1160  * using YUI.
1162  * Note: the third argument used to be an object with odd behaviour. It now
1163  * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1164  * mimicked if you pass an object.
1166  * @param {Node} oElm The top-level node for searching. To search a whole
1167  *                    document, use `document`.
1168  * @param {String} strTagName filter by tag names
1169  * @param {String} name same as HTML5 spec
1170  * @deprecated Since Moodle 2.7. This will be removed in Moodle 2.9.
1171  */
1172 function getElementsByClassName(oElm, strTagName, name) {
1173     Y.log('getElementsByClassName has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1174             'warn', 'javascript-static.js');
1175     // for backwards compatibility
1176     if(typeof name == "object") {
1177         var names = new Array();
1178         for(var i=0; i<name.length; i++) names.push(names[i]);
1179         name = names.join('');
1180     }
1181     // use native implementation if possible
1182     if (oElm.getElementsByClassName && Array.filter) {
1183         if (strTagName == '*') {
1184             return oElm.getElementsByClassName(name);
1185         } else {
1186             return Array.filter(oElm.getElementsByClassName(name), function(el) {
1187                 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1188             });
1189         }
1190     }
1191     // native implementation unavailable, fall back to slow method
1192     var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1193     var arrReturnElements = new Array();
1194     var arrRegExpClassNames = new Array();
1195     var names = name.split(' ');
1196     for(var i=0; i<names.length; i++) {
1197         arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1198     }
1199     var oElement;
1200     var bMatchesAll;
1201     for(var j=0; j<arrElements.length; j++) {
1202         oElement = arrElements[j];
1203         bMatchesAll = true;
1204         for(var k=0; k<arrRegExpClassNames.length; k++) {
1205             if(!arrRegExpClassNames[k].test(oElement.className)) {
1206                 bMatchesAll = false;
1207                 break;
1208             }
1209         }
1210         if(bMatchesAll) {
1211             arrReturnElements.push(oElement);
1212         }
1213     }
1214     return (arrReturnElements)
1218  * Increment a file name.
1220  * @param string file name.
1221  * @param boolean ignoreextension do not extract the extension prior to appending the
1222  *                                suffix. Useful when incrementing folder names.
1223  * @return string the incremented file name.
1224  */
1225 function increment_filename(filename, ignoreextension) {
1226     var extension = '';
1227     var basename = filename;
1229     // Split the file name into the basename + extension.
1230     if (!ignoreextension) {
1231         var dotpos = filename.lastIndexOf('.');
1232         if (dotpos !== -1) {
1233             basename = filename.substr(0, dotpos);
1234             extension = filename.substr(dotpos, filename.length);
1235         }
1236     }
1238     // Look to see if the name already has (NN) at the end of it.
1239     var number = 0;
1240     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1241     if (hasnumber !== null) {
1242         // Note the current number & remove it from the basename.
1243         number = parseInt(hasnumber[2], 10);
1244         basename = hasnumber[1];
1245     }
1247     number++;
1248     var newname = basename + ' (' + number + ')' + extension;
1249     return newname;
1253  * Return whether we are in right to left mode or not.
1255  * @return boolean
1256  */
1257 function right_to_left() {
1258     var body = Y.one('body');
1259     var rtl = false;
1260     if (body && body.hasClass('dir-rtl')) {
1261         rtl = true;
1262     }
1263     return rtl;
1266 function openpopup(event, args) {
1268     if (event) {
1269         if (event.preventDefault) {
1270             event.preventDefault();
1271         } else {
1272             event.returnValue = false;
1273         }
1274     }
1276     // Make sure the name argument is set and valid.
1277     var nameregex = /[^a-z0-9_]/i;
1278     if (typeof args.name !== 'string') {
1279         args.name = '_blank';
1280     } else if (args.name.match(nameregex)) {
1281         // Cleans window name because IE does not support funky ones.
1282         if (M.cfg.developerdebug) {
1283             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1284         }
1285         args.name = args.name.replace(nameregex, '_');
1286     }
1288     var fullurl = args.url;
1289     if (!args.url.match(/https?:\/\//)) {
1290         fullurl = M.cfg.wwwroot + args.url;
1291     }
1292     if (args.fullscreen) {
1293         args.options = args.options.
1294                 replace(/top=\d+/, 'top=0').
1295                 replace(/left=\d+/, 'left=0').
1296                 replace(/width=\d+/, 'width=' + screen.availWidth).
1297                 replace(/height=\d+/, 'height=' + screen.availHeight);
1298     }
1299     var windowobj = window.open(fullurl,args.name,args.options);
1300     if (!windowobj) {
1301         return true;
1302     }
1304     if (args.fullscreen) {
1305         // In some browser / OS combinations (E.g. Chrome on Windows), the
1306         // window initially opens slighly too big. The width and heigh options
1307         // seem to control the area inside the browser window, so what with
1308         // scroll-bars, etc. the actual window is bigger than the screen.
1309         // Therefore, we need to fix things up after the window is open.
1310         var hackcount = 100;
1311         var get_size_exactly_right = function() {
1312             windowobj.moveTo(0, 0);
1313             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1315             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1316             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1317             // about 50ms) after the window is open, then it actually behaves
1318             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1319             // check that the resize actually worked, and if not, repeatedly try
1320             // again after a short delay until it works (but with a limit of
1321             // hackcount repeats.
1322             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1323                 hackcount -= 1;
1324                 setTimeout(get_size_exactly_right, 10);
1325             }
1326         }
1327         setTimeout(get_size_exactly_right, 0);
1328     }
1329     windowobj.focus();
1331     return false;
1334 /** Close the current browser window. */
1335 function close_window(e) {
1336     if (e.preventDefault) {
1337         e.preventDefault();
1338     } else {
1339         e.returnValue = false;
1340     }
1341     window.close();
1345  * Used in a couple of modules to hide navigation areas when using AJAX
1346  * @deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1347  */
1348 function show_item(itemid) {
1349     Y.log('show_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1350             'warn', 'javascript-static.js');
1351     var item = Y.one('#' + itemid);
1352     if (item) {
1353         item.show();
1354     }
1357 // Deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1358 function destroy_item(itemid) {
1359     Y.log('destroy_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1360             'warn', 'javascript-static.js');
1361     var item = Y.one('#' + itemid);
1362     if (item) {
1363         item.remove(true);
1364     }
1367  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1368  * @param controlid the control id.
1369  */
1370 function focuscontrol(controlid) {
1371     var control = document.getElementById(controlid);
1372     if (control) {
1373         control.focus();
1374     }
1378  * Transfers keyboard focus to an HTML element based on the old style style of focus
1379  * This function should be removed as soon as it is no longer used
1380  */
1381 function old_onload_focus(formid, controlname) {
1382     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1383         document.forms[formid].elements[controlname].focus();
1384     }
1387 function build_querystring(obj) {
1388     return convert_object_to_string(obj, '&');
1391 function build_windowoptionsstring(obj) {
1392     return convert_object_to_string(obj, ',');
1395 function convert_object_to_string(obj, separator) {
1396     if (typeof obj !== 'object') {
1397         return null;
1398     }
1399     var list = [];
1400     for(var k in obj) {
1401         k = encodeURIComponent(k);
1402         var value = obj[k];
1403         if(obj[k] instanceof Array) {
1404             for(var i in value) {
1405                 list.push(k+'[]='+encodeURIComponent(value[i]));
1406             }
1407         } else {
1408             list.push(k+'='+encodeURIComponent(value));
1409         }
1410     }
1411     return list.join(separator);
1414 function stripHTML(str) {
1415     var re = /<\S[^><]*>/g;
1416     var ret = str.replace(re, "");
1417     return ret;
1420 function updateProgressBar(id, percent, msg, estimate) {
1421     var progressIndicator = Y.one('#' + id);
1422     if (!progressIndicator) {
1423         return;
1424     }
1426     var progressBar = progressIndicator.one('.bar'),
1427         statusIndicator = progressIndicator.one('h2'),
1428         estimateIndicator = progressIndicator.one('p');
1430     statusIndicator.set('innerHTML', Y.Escape.html(msg));
1431     progressBar.set('innerHTML', Y.Escape.html('' + percent + '%'));
1432     if (percent === 100) {
1433         progressIndicator.addClass('progress-success');
1434         estimateIndicator.set('innerHTML', null);
1435     } else {
1436         if (estimate) {
1437             estimateIndicator.set('innerHTML', Y.Escape.html(estimate));
1438         } else {
1439             estimateIndicator.set('innerHTML', null);
1440         }
1441         progressIndicator.removeClass('progress-success');
1442     }
1443     progressBar.setAttribute('aria-valuenow', percent);
1444     progressBar.setStyle('width', percent + '%');
1447 // ===== Deprecated core Javascript functions for Moodle ====
1448 //       DO NOT USE!!!!!!!
1449 // Do not put this stuff in separate file because it only adds extra load on servers!
1452  * Used in a couple of modules to hide navigation areas when using AJAX
1453  * @deprecated since Moodle 2.7. This function will be removed in Moodle 2.9.
1454  */
1455 function hide_item(itemid) {
1456     Y.log('hide_item has been deprecated since Moodle 2.7 and will be removed in Moodle 2.9',
1457             'warn', 'javascript-static.js');
1458     var item = Y.one('#' + itemid);
1459     if (item) {
1460         item.hide();
1461     }
1464 M.util.help_popups = {
1465     setup : function(Y) {
1466         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1467     },
1468     open_popup : function(e) {
1469         // Prevent the default page action
1470         e.preventDefault();
1472         // Grab the anchor that was clicked
1473         var anchor = e.target.ancestor('a', true);
1474         var args = {
1475             'name'          : 'popup',
1476             'url'           : anchor.getAttribute('href'),
1477             'options'       : ''
1478         };
1479         var options = [
1480             'height=600',
1481             'width=800',
1482             'top=0',
1483             'left=0',
1484             'menubar=0',
1485             'location=0',
1486             'scrollbars',
1487             'resizable',
1488             'toolbar',
1489             'status',
1490             'directories=0',
1491             'fullscreen=0',
1492             'dependent'
1493         ]
1494         args.options = options.join(',');
1496         openpopup(e, args);
1497     }
1501  * Custom menu namespace
1502  */
1503 M.core_custom_menu = {
1504     /**
1505      * This method is used to initialise a custom menu given the id that belongs
1506      * to the custom menu's root node.
1507      *
1508      * @param {YUI} Y
1509      * @param {string} nodeid
1510      */
1511     init : function(Y, nodeid) {
1512         var node = Y.one('#'+nodeid);
1513         if (node) {
1514             Y.use('node-menunav', function(Y) {
1515                 // Get the node
1516                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1517                 node.removeClass('javascript-disabled');
1518                 // Initialise the menunav plugin
1519                 node.plug(Y.Plugin.NodeMenuNav);
1520             });
1521         }
1522     }
1526  * Used to store form manipulation methods and enhancments
1527  */
1528 M.form = M.form || {};
1531  * Converts a nbsp indented select box into a multi drop down custom control much
1532  * like the custom menu. It also selectable categories on or off.
1534  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1536  * @param {YUI} Y
1537  * @param {string} id
1538  * @param {Array} options
1539  */
1540 M.form.init_smartselect = function(Y, id, options) {
1541     if (!id.match(/^id_/)) {
1542         id = 'id_'+id;
1543     }
1544     var select = Y.one('select#'+id);
1545     if (!select) {
1546         return false;
1547     }
1548     Y.use('event-delegate',function(){
1549         var smartselect = {
1550             id : id,
1551             structure : [],
1552             options : [],
1553             submenucount : 0,
1554             currentvalue : null,
1555             currenttext : null,
1556             shownevent : null,
1557             cfg : {
1558                 selectablecategories : true,
1559                 mode : null
1560             },
1561             nodes : {
1562                 select : null,
1563                 loading : null,
1564                 menu : null
1565             },
1566             init : function(Y, id, args, nodes) {
1567                 if (typeof(args)=='object') {
1568                     for (var i in this.cfg) {
1569                         if (args[i] || args[i]===false) {
1570                             this.cfg[i] = args[i];
1571                         }
1572                     }
1573                 }
1575                 // Display a loading message first up
1576                 this.nodes.select = nodes.select;
1578                 this.currentvalue = this.nodes.select.get('selectedIndex');
1579                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1581                 var options = Array();
1582                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1583                 this.nodes.select.all('option').each(function(option, index) {
1584                     var rawtext = option.get('innerHTML');
1585                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1586                     if (rawtext === text) {
1587                         text = rawtext.replace(/^(\s)*/, '');
1588                         var depth = (rawtext.length - text.length ) + 1;
1589                     } else {
1590                         var depth = ((rawtext.length - text.length )/12)+1;
1591                     }
1592                     option.set('innerHTML', text);
1593                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1594                 }, this);
1596                 this.structure = [];
1597                 var structcount = 0;
1598                 for (var i in options) {
1599                     var o = options[i];
1600                     if (o.depth == 0) {
1601                         this.structure.push(o);
1602                         structcount++;
1603                     } else {
1604                         var d = o.depth;
1605                         var current = this.structure[structcount-1];
1606                         for (var j = 0; j < o.depth-1;j++) {
1607                             if (current && current.children) {
1608                                 current = current.children[current.children.length-1];
1609                             }
1610                         }
1611                         if (current && current.children) {
1612                             current.children.push(o);
1613                         }
1614                     }
1615                 }
1617                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1618                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1619                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1620                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1622                 if (this.cfg.mode == null) {
1623                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1624                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1625                         this.cfg.mode = 'compact';
1626                     } else {
1627                         this.cfg.mode = 'spanning';
1628                     }
1629                 }
1631                 if (this.cfg.mode == 'compact') {
1632                     this.nodes.menu.addClass('compactmenu');
1633                 } else {
1634                     this.nodes.menu.addClass('spanningmenu');
1635                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1636                 }
1638                 Y.one(document.body).append(this.nodes.menu);
1639                 var pos = this.nodes.select.getXY();
1640                 pos[0] += 1;
1641                 this.nodes.menu.setXY(pos);
1642                 this.nodes.menu.on('click', this.handle_click, this);
1644                 Y.one(window).on('resize', function(){
1645                      var pos = this.nodes.select.getXY();
1646                     pos[0] += 1;
1647                     this.nodes.menu.setXY(pos);
1648                  }, this);
1649             },
1650             generate_menu_content : function() {
1651                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1652                 content += this.generate_submenu_content(this.structure[0], true);
1653                 content += '</ul></div>';
1654                 return content;
1655             },
1656             generate_submenu_content : function(item, rootelement) {
1657                 this.submenucount++;
1658                 var content = '';
1659                 if (item.children.length > 0) {
1660                     if (rootelement) {
1661                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1662                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1663                         content += '<div class="smartselect_menu_content">';
1664                     } else {
1665                         content += '<li class="smartselect_submenuitem">';
1666                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1667                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1668                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1669                         content += '<div class="smartselect_submenu_content">';
1670                     }
1671                     content += '<ul>';
1672                     for (var i in item.children) {
1673                         content += this.generate_submenu_content(item.children[i],false);
1674                     }
1675                     content += '</ul>';
1676                     content += '</div>';
1677                     content += '</div>';
1678                     if (rootelement) {
1679                     } else {
1680                         content += '</li>';
1681                     }
1682                 } else {
1683                     content += '<li class="smartselect_menuitem">';
1684                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1685                     content += '</li>';
1686                 }
1687                 return content;
1688             },
1689             select : function(e) {
1690                 var t = e.target;
1691                 e.halt();
1692                 this.currenttext = t.get('innerHTML');
1693                 this.currentvalue = t.getAttribute('value');
1694                 this.nodes.select.set('selectedIndex', this.currentvalue);
1695                 this.hide_menu();
1696             },
1697             handle_click : function(e) {
1698                 var target = e.target;
1699                 if (target.hasClass('smartselect_mask')) {
1700                     this.show_menu(e);
1701                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1702                     this.select(e);
1703                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1704                     this.show_sub_menu(e);
1705                 }
1706             },
1707             show_menu : function(e) {
1708                 e.halt();
1709                 var menu = e.target.ancestor().one('.smartselect_menu');
1710                 menu.addClass('visible');
1711                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1712             },
1713             show_sub_menu : function(e) {
1714                 e.halt();
1715                 var target = e.target;
1716                 if (!target.hasClass('smartselect_submenuitem')) {
1717                     target = target.ancestor('.smartselect_submenuitem');
1718                 }
1719                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1720                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1721                     return;
1722                 }
1723                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1724                 target.one('.smartselect_submenu').addClass('visible');
1725             },
1726             hide_menu : function() {
1727                 this.nodes.menu.all('.visible').removeClass('visible');
1728                 if (this.shownevent) {
1729                     this.shownevent.detach();
1730                 }
1731             }
1732         };
1733         smartselect.init(Y, id, options, {select:select});
1734     });
1737 /** List of flv players to be loaded */
1738 M.util.video_players = [];
1739 /** List of mp3 players to be loaded */
1740 M.util.audio_players = [];
1743  * Add video player
1744  * @param id element id
1745  * @param fileurl media url
1746  * @param width
1747  * @param height
1748  * @param autosize true means detect size from media
1749  */
1750 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1751     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1755  * Add audio player.
1756  * @param id
1757  * @param fileurl
1758  * @param small
1759  */
1760 M.util.add_audio_player = function (id, fileurl, small) {
1761     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1765  * Initialise all audio and video player, must be called from page footer.
1766  */
1767 M.util.load_flowplayer = function() {
1768     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1769         return;
1770     }
1771     if (typeof(flowplayer) == 'undefined') {
1772         var loaded = false;
1774         var embed_function = function() {
1775             if (loaded || typeof(flowplayer) == 'undefined') {
1776                 return;
1777             }
1778             loaded = true;
1780             var controls = {
1781                     autoHide: true
1782             }
1783             /* TODO: add CSS color overrides for the flv flow player */
1785             for(var i=0; i<M.util.video_players.length; i++) {
1786                 var video = M.util.video_players[i];
1787                 if (video.width > 0 && video.height > 0) {
1788                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', width: video.width, height: video.height};
1789                 } else {
1790                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf';
1791                 }
1792                 flowplayer(video.id, src, {
1793                     plugins: {controls: controls},
1794                     clip: {
1795                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1796                         onMetaData: function(clip) {
1797                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1798                                 clip.mvideo.resized = true;
1799                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1800                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1801                                     // bad luck, we have to guess - we may not get metadata at all
1802                                     var width = clip.width;
1803                                     var height = clip.height;
1804                                 } else {
1805                                     var width = clip.metaData.width;
1806                                     var height = clip.metaData.height;
1807                                 }
1808                                 var minwidth = 300; // controls are messed up in smaller objects
1809                                 if (width < minwidth) {
1810                                     height = (height * minwidth) / width;
1811                                     width = minwidth;
1812                                 }
1814                                 var object = this._api();
1815                                 object.width = width;
1816                                 object.height = height;
1817                             }
1818                         }
1819                     }
1820                 });
1821             }
1822             if (M.util.audio_players.length == 0) {
1823                 return;
1824             }
1825             var controls = {
1826                     autoHide: false,
1827                     fullscreen: false,
1828                     next: false,
1829                     previous: false,
1830                     scrubber: true,
1831                     play: true,
1832                     pause: true,
1833                     volume: true,
1834                     mute: false,
1835                     backgroundGradient: [0.5,0,0.3]
1836                 };
1838             var rule;
1839             for (var j=0; j < document.styleSheets.length; j++) {
1841                 // To avoid javascript security violation accessing cross domain stylesheets
1842                 var allrules = false;
1843                 try {
1844                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1845                         allrules = document.styleSheets[j].rules;
1846                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1847                         allrules = document.styleSheets[j].cssRules;
1848                     } else {
1849                         // why??
1850                         continue;
1851                     }
1852                 } catch (e) {
1853                     continue;
1854                 }
1856                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1857                 if (!allrules) {
1858                     continue;
1859                 }
1861                 for(var i=0; i<allrules.length; i++) {
1862                     rule = '';
1863                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1864                         if (typeof(allrules[i].cssText) != 'undefined') {
1865                             rule = allrules[i].cssText;
1866                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1867                             rule = allrules[i].style.cssText;
1868                         }
1869                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1870                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1871                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1872                             controls[colprop] = rule;
1873                         }
1874                     }
1875                 }
1876                 allrules = false;
1877             }
1879             for(i=0; i<M.util.audio_players.length; i++) {
1880                 var audio = M.util.audio_players[i];
1881                 if (audio.small) {
1882                     controls.controlall = false;
1883                     controls.height = 15;
1884                     controls.time = false;
1885                 } else {
1886                     controls.controlall = true;
1887                     controls.height = 25;
1888                     controls.time = true;
1889                 }
1890                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', {
1891                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf'}},
1892                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1893                 });
1894             }
1895         }
1897         if (M.cfg.jsrev == -1) {
1898             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1899         } else {
1900             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1901         }
1902         var fileref = document.createElement('script');
1903         fileref.setAttribute('type','text/javascript');
1904         fileref.setAttribute('src', jsurl);
1905         fileref.onload = embed_function;
1906         fileref.onreadystatechange = embed_function;
1907         document.getElementsByTagName('head')[0].appendChild(fileref);
1908     }