weekly release 2.9.3+
[moodle.git] / lib / javascript-static.js
blob92ff0ade0adba14efd0cebb33b18ad0b1dcbf3ff
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('display', 'none');
347         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
349         if (newwidth > 500) {
350             obj.setStyle('width', newwidth  + 'px');
351         } else {
352             obj.setStyle('width', '500px');
353         }
355         var headerheight = get_htmlelement_size('page-header', 'height');
356         var footerheight = get_htmlelement_size('page-footer', 'height');
357         var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
358         if (newheight < 400) {
359             newheight = 400;
360         }
361         obj.setStyle('height', newheight+'px');
362         obj.setStyle('display', '');
363     };
365     resize_object();
366     // fix layout if window resized too
367     Y.use('event-resize', function (Y) {
368         Y.on("windowresize", function() {
369             resize_object();
370         });
371     });
375  * Breaks out all links to the top frame - used in frametop page layout.
376  */
377 M.util.init_frametop = function(Y) {
378     Y.all('a').each(function(node) {
379         node.set('target', '_top');
380     });
381     Y.all('form').each(function(node) {
382         node.set('target', '_top');
383     });
387  * Finds all nodes that match the given CSS selector and attaches events to them
388  * so that they toggle a given classname when clicked.
390  * @param {YUI} Y
391  * @param {string} id An id containing elements to target
392  * @param {string} cssselector A selector to use to find targets
393  * @param {string} toggleclassname A classname to toggle
394  */
395 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
397     if (togglecssselector == '') {
398         togglecssselector = cssselector;
399     }
401     var node = Y.one('#'+id);
402     node.all(cssselector).each(function(n){
403         n.on('click', function(e){
404             e.stopPropagation();
405             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
406                 if (this.test(togglecssselector)) {
407                     this.toggleClass(toggleclassname);
408                 } else {
409                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
410             }
411             }
412         }, n);
413     });
414     // Attach this click event to the node rather than all selectors... will be much better
415     // for performance
416     node.on('click', function(e){
417         if (e.target.hasClass('addtoall')) {
418             this.all(togglecssselector).addClass(toggleclassname);
419         } else if (e.target.hasClass('removefromall')) {
420             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
421         }
422     }, node);
426  * Initialises a colour picker
428  * Designed to be used with admin_setting_configcolourpicker although could be used
429  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
430  * above or below the input (must have the same parent) and then call this with the
431  * id.
433  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
434  * contrib/blocks. For better docs refer to that.
436  * @param {YUI} Y
437  * @param {int} id
438  * @param {object} previewconf
439  */
440 M.util.init_colour_picker = function(Y, id, previewconf) {
441     /**
442      * We need node and event-mouseenter
443      */
444     Y.use('node', 'event-mouseenter', function(){
445         /**
446          * The colour picker object
447          */
448         var colourpicker = {
449             box : null,
450             input : null,
451             image : null,
452             preview : null,
453             current : null,
454             eventClick : null,
455             eventMouseEnter : null,
456             eventMouseLeave : null,
457             eventMouseMove : null,
458             width : 300,
459             height :  100,
460             factor : 5,
461             /**
462              * Initalises the colour picker by putting everything together and wiring the events
463              */
464             init : function() {
465                 this.input = Y.one('#'+id);
466                 this.box = this.input.ancestor().one('.admin_colourpicker');
467                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
468                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
469                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
470                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
471                 this.current = Y.Node.create('<div class="currentcolour"></div>');
472                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
473                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
475                 if (typeof(previewconf) === 'object' && previewconf !== null) {
476                     Y.one('#'+id+'_preview').on('click', function(e){
477                         if (Y.Lang.isString(previewconf.selector)) {
478                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
479                         } else {
480                             for (var i in previewconf.selector) {
481                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
482                             }
483                         }
484                     }, this);
485                 }
487                 this.eventClick = this.image.on('click', this.pickColour, this);
488                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
489             },
490             /**
491              * Starts to follow the mouse once it enter the image
492              */
493             startFollow : function(e) {
494                 this.eventMouseEnter.detach();
495                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
496                 this.eventMouseMove = this.image.on('mousemove', function(e){
497                     this.preview.setStyle('backgroundColor', this.determineColour(e));
498                 }, this);
499             },
500             /**
501              * Stops following the mouse
502              */
503             endFollow : function(e) {
504                 this.eventMouseMove.detach();
505                 this.eventMouseLeave.detach();
506                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
507             },
508             /**
509              * Picks the colour the was clicked on
510              */
511             pickColour : function(e) {
512                 var colour = this.determineColour(e);
513                 this.input.set('value', colour);
514                 this.current.setStyle('backgroundColor', colour);
515             },
516             /**
517              * Calculates the colour fromthe given co-ordinates
518              */
519             determineColour : function(e) {
520                 var eventx = Math.floor(e.pageX-e.target.getX());
521                 var eventy = Math.floor(e.pageY-e.target.getY());
523                 var imagewidth = this.width;
524                 var imageheight = this.height;
525                 var factor = this.factor;
526                 var colour = [255,0,0];
528                 var matrices = [
529                     [  0,  1,  0],
530                     [ -1,  0,  0],
531                     [  0,  0,  1],
532                     [  0, -1,  0],
533                     [  1,  0,  0],
534                     [  0,  0, -1]
535                 ];
537                 var matrixcount = matrices.length;
538                 var limit = Math.round(imagewidth/matrixcount);
539                 var heightbreak = Math.round(imageheight/2);
541                 for (var x = 0; x < imagewidth; x++) {
542                     var divisor = Math.floor(x / limit);
543                     var matrix = matrices[divisor];
545                     colour[0] += matrix[0]*factor;
546                     colour[1] += matrix[1]*factor;
547                     colour[2] += matrix[2]*factor;
549                     if (eventx==x) {
550                         break;
551                     }
552                 }
554                 var pixel = [colour[0], colour[1], colour[2]];
555                 if (eventy < heightbreak) {
556                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
557                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
558                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
559                 } else if (eventy > heightbreak) {
560                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
561                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
562                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
563                 }
565                 return this.convert_rgb_to_hex(pixel);
566             },
567             /**
568              * Converts an RGB value to Hex
569              */
570             convert_rgb_to_hex : function(rgb) {
571                 var hex = '#';
572                 var hexchars = "0123456789ABCDEF";
573                 for (var i=0; i<3; i++) {
574                     var number = Math.abs(rgb[i]);
575                     if (number == 0 || isNaN(number)) {
576                         hex += '00';
577                     } else {
578                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
579                     }
580                 }
581                 return hex;
582             }
583         };
584         /**
585          * Initialise the colour picker :) Hoorah
586          */
587         colourpicker.init();
588     });
591 M.util.init_block_hider = function(Y, config) {
592     Y.use('base', 'node', function(Y) {
593         M.util.block_hider = M.util.block_hider || (function(){
594             var blockhider = function() {
595                 blockhider.superclass.constructor.apply(this, arguments);
596             };
597             blockhider.prototype = {
598                 initializer : function(config) {
599                     this.set('block', '#'+this.get('id'));
600                     var b = this.get('block'),
601                         t = b.one('.title'),
602                         a = null,
603                         hide,
604                         show;
605                     if (t && (a = t.one('.block_action'))) {
606                         hide = Y.Node.create('<img />')
607                             .addClass('block-hider-hide')
608                             .setAttrs({
609                                 alt:        config.tooltipVisible,
610                                 src:        this.get('iconVisible'),
611                                 tabindex:   0,
612                                 'title':    config.tooltipVisible
613                             });
614                         hide.on('keypress', this.updateStateKey, this, true);
615                         hide.on('click', this.updateState, this, true);
617                         show = Y.Node.create('<img />')
618                             .addClass('block-hider-show')
619                             .setAttrs({
620                                 alt:        config.tooltipHidden,
621                                 src:        this.get('iconHidden'),
622                                 tabindex:   0,
623                                 'title':    config.tooltipHidden
624                             });
625                         show.on('keypress', this.updateStateKey, this, false);
626                         show.on('click', this.updateState, this, false);
628                         a.insert(show, 0).insert(hide, 0);
629                     }
630                 },
631                 updateState : function(e, hide) {
632                     M.util.set_user_preference(this.get('preference'), hide);
633                     if (hide) {
634                         this.get('block').addClass('hidden');
635                     } else {
636                         this.get('block').removeClass('hidden');
637                     }
638                 },
639                 updateStateKey : function(e, hide) {
640                     if (e.keyCode == 13) { //allow hide/show via enter key
641                         this.updateState(this, hide);
642                     }
643                 }
644             };
645             Y.extend(blockhider, Y.Base, blockhider.prototype, {
646                 NAME : 'blockhider',
647                 ATTRS : {
648                     id : {},
649                     preference : {},
650                     iconVisible : {
651                         value : M.util.image_url('t/switch_minus', 'moodle')
652                     },
653                     iconHidden : {
654                         value : M.util.image_url('t/switch_plus', 'moodle')
655                     },
656                     block : {
657                         setter : function(node) {
658                             return Y.one(node);
659                         }
660                     }
661                 }
662             });
663             return blockhider;
664         })();
665         new M.util.block_hider(config);
666     });
670  * @var pending_js - The keys are the list of all pending js actions.
671  * @type Object
672  */
673 M.util.pending_js = [];
674 M.util.complete_js = [];
677  * Register any long running javascript code with a unique identifier.
678  * Should be followed with a call to js_complete with a matching
679  * idenfitier when the code is complete. May also be called with no arguments
680  * to test if there is any js calls pending. This is relied on by behat so that
681  * it can wait for all pending updates before interacting with a page.
682  * @param String uniqid - optional, if provided,
683  *                        registers this identifier until js_complete is called.
684  * @return boolean - True if there is any pending js.
685  */
686 M.util.js_pending = function(uniqid) {
687     if (uniqid !== false) {
688         M.util.pending_js.push(uniqid);
689     }
691     return M.util.pending_js.length;
694 // Start this asap.
695 M.util.js_pending('init');
698  * Register listeners for Y.io start/end so we can wait for them in behat.
699  */
700 YUI.add('moodle-core-io', function(Y) {
701     Y.on('io:start', function(id) {
702         M.util.js_pending('io:' + id);
703     });
704     Y.on('io:end', function(id) {
705         M.util.js_complete('io:' + id);
706     });
707 }, '@VERSION@', {
708     condition: {
709         trigger: 'io-base',
710         when: 'after'
711     }
715  * Unregister any long running javascript code by unique identifier.
716  * This function should form a matching pair with js_pending
718  * @param String uniqid - required, unregisters this identifier
719  * @return boolean - True if there is any pending js.
720  */
721 M.util.js_complete = function(uniqid) {
722     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
723     // the native function. Y.Array polyfills the native function if it does not exist.
724     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
725     if (index >= 0) {
726         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
727     }
729     return M.util.pending_js.length;
733  * Returns a string registered in advance for usage in JavaScript
735  * If you do not pass the third parameter, the function will just return
736  * the corresponding value from the M.str object. If the third parameter is
737  * provided, the function performs {$a} placeholder substitution in the
738  * same way as PHP get_string() in Moodle does.
740  * @param {String} identifier string identifier
741  * @param {String} component the component providing the string
742  * @param {Object|String} a optional variable to populate placeholder with
743  */
744 M.util.get_string = function(identifier, component, a) {
745     var stringvalue;
747     if (M.cfg.developerdebug) {
748         // creating new instance if YUI is not optimal but it seems to be better way then
749         // require the instance via the function API - note that it is used in rare cases
750         // for debugging only anyway
751         // To ensure we don't kill browser performance if hundreds of get_string requests
752         // are made we cache the instance we generate within the M.util namespace.
753         // We don't publicly define the variable so that it doesn't get abused.
754         if (typeof M.util.get_string_yui_instance === 'undefined') {
755             M.util.get_string_yui_instance = new YUI({ debug : true });
756         }
757         var Y = M.util.get_string_yui_instance;
758     }
760     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
761         stringvalue = '[[' + identifier + ',' + component + ']]';
762         if (M.cfg.developerdebug) {
763             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
764         }
765         return stringvalue;
766     }
768     stringvalue = M.str[component][identifier];
770     if (typeof a == 'undefined') {
771         // no placeholder substitution requested
772         return stringvalue;
773     }
775     if (typeof a == 'number' || typeof a == 'string') {
776         // replace all occurrences of {$a} with the placeholder value
777         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
778         return stringvalue;
779     }
781     if (typeof a == 'object') {
782         // replace {$a->key} placeholders
783         for (var key in a) {
784             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
785                 if (M.cfg.developerdebug) {
786                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
787                 }
788                 continue;
789             }
790             var search = '{$a->' + key + '}';
791             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
792             search = new RegExp(search, 'g');
793             stringvalue = stringvalue.replace(search, a[key]);
794         }
795         return stringvalue;
796     }
798     if (M.cfg.developerdebug) {
799         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
800     }
801     return stringvalue;
805  * Set focus on username or password field of the login form
806  */
807 M.util.focus_login_form = function(Y) {
808     var username = Y.one('#username');
809     var password = Y.one('#password');
811     if (username == null || password == null) {
812         // something is wrong here
813         return;
814     }
816     var curElement = document.activeElement
817     if (curElement == 'undefined') {
818         // legacy browser - skip refocus protection
819     } else if (curElement.tagName == 'INPUT') {
820         // user was probably faster to focus something, do not mess with focus
821         return;
822     }
824     if (username.get('value') == '') {
825         username.focus();
826     } else {
827         password.focus();
828     }
832  * Set focus on login error message
833  */
834 M.util.focus_login_error = function(Y) {
835     var errorlog = Y.one('#loginerrormessage');
837     if (errorlog) {
838         errorlog.focus();
839     }
842  * Adds lightbox hidden element that covers the whole node.
844  * @param {YUI} Y
845  * @param {Node} the node lightbox should be added to
846  * @retun {Node} created lightbox node
847  */
848 M.util.add_lightbox = function(Y, node) {
849     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
851     // Check if lightbox is already there
852     if (node.one('.lightbox')) {
853         return node.one('.lightbox');
854     }
856     node.setStyle('position', 'relative');
857     var waiticon = Y.Node.create('<img />')
858     .setAttrs({
859         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
860     })
861     .setStyles({
862         'position' : 'relative',
863         'top' : '50%'
864     });
866     var lightbox = Y.Node.create('<div></div>')
867     .setStyles({
868         'opacity' : '.75',
869         'position' : 'absolute',
870         'width' : '100%',
871         'height' : '100%',
872         'top' : 0,
873         'left' : 0,
874         'backgroundColor' : 'white',
875         'textAlign' : 'center'
876     })
877     .setAttribute('class', 'lightbox')
878     .hide();
880     lightbox.appendChild(waiticon);
881     node.append(lightbox);
882     return lightbox;
886  * Appends a hidden spinner element to the specified node.
888  * @param {YUI} Y
889  * @param {Node} the node the spinner should be added to
890  * @return {Node} created spinner node
891  */
892 M.util.add_spinner = function(Y, node) {
893     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
895     // Check if spinner is already there
896     if (node.one('.spinner')) {
897         return node.one('.spinner');
898     }
900     var spinner = Y.Node.create('<img />')
901         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
902         .addClass('spinner')
903         .addClass('iconsmall')
904         .hide();
906     node.append(spinner);
907     return spinner;
910 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
912 function checkall() {
913     var inputs = document.getElementsByTagName('input');
914     for (var i = 0; i < inputs.length; i++) {
915         if (inputs[i].type == 'checkbox') {
916             if (inputs[i].disabled || inputs[i].readOnly) {
917                 continue;
918             }
919             inputs[i].checked = true;
920         }
921     }
924 function checknone() {
925     var inputs = document.getElementsByTagName('input');
926     for (var i = 0; i < inputs.length; i++) {
927         if (inputs[i].type == 'checkbox') {
928             if (inputs[i].disabled || inputs[i].readOnly) {
929                 continue;
930             }
931             inputs[i].checked = false;
932         }
933     }
937  * Either check, or uncheck, all checkboxes inside the element with id is
938  * @param id the id of the container
939  * @param checked the new state, either '' or 'checked'.
940  */
941 function select_all_in_element_with_id(id, checked) {
942     var container = document.getElementById(id);
943     if (!container) {
944         return;
945     }
946     var inputs = container.getElementsByTagName('input');
947     for (var i = 0; i < inputs.length; ++i) {
948         if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
949             inputs[i].checked = checked;
950         }
951     }
954 function select_all_in(elTagName, elClass, elId) {
955     var inputs = document.getElementsByTagName('input');
956     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
957     for(var i = 0; i < inputs.length; ++i) {
958         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
959             inputs[i].checked = 'checked';
960         }
961     }
964 function deselect_all_in(elTagName, elClass, elId) {
965     var inputs = document.getElementsByTagName('INPUT');
966     inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
967     for(var i = 0; i < inputs.length; ++i) {
968         if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
969             inputs[i].checked = '';
970         }
971     }
974 function confirm_if(expr, message) {
975     if(!expr) {
976         return true;
977     }
978     return confirm(message);
983     findParentNode (start, elementName, elementClass, elementID)
985     Travels up the DOM hierarchy to find a parent element with the
986     specified tag name, class, and id. All conditions must be met,
987     but any can be ommitted. Returns the BODY element if no match
988     found.
990 function findParentNode(el, elName, elClass, elId) {
991     while (el.nodeName.toUpperCase() != 'BODY') {
992         if ((!elName || el.nodeName.toUpperCase() == elName) &&
993             (!elClass || el.className.indexOf(elClass) != -1) &&
994             (!elId || el.id == elId)) {
995             break;
996         }
997         el = el.parentNode;
998     }
999     return el;
1002 function unmaskPassword(id) {
1003     var pw = document.getElementById(id);
1004     var chb = document.getElementById(id+'unmask');
1006     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1007     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
1008     // functionality won't work in IE8 or lower.
1009     // This is a temporary fixed to allow other browsers to function properly.
1010     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1011         if (chb.checked) {
1012             pw.type = "text";
1013         } else {
1014             pw.type = "password";
1015         }
1016     } else {  //IE Browser version 8 or lower
1017         try {
1018             // first try IE way - it can not set name attribute later
1019             if (chb.checked) {
1020               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1021             } else {
1022               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1023             }
1024             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1025         } catch (e) {
1026             var newpw = document.createElement('input');
1027             newpw.setAttribute('autocomplete', 'off');
1028             newpw.setAttribute('name', pw.name);
1029             if (chb.checked) {
1030               newpw.setAttribute('type', 'text');
1031             } else {
1032               newpw.setAttribute('type', 'password');
1033             }
1034             newpw.setAttribute('class', pw.getAttribute('class'));
1035         }
1036         newpw.id = pw.id;
1037         newpw.size = pw.size;
1038         newpw.onblur = pw.onblur;
1039         newpw.onchange = pw.onchange;
1040         newpw.value = pw.value;
1041         pw.parentNode.replaceChild(newpw, pw);
1042     }
1045 function filterByParent(elCollection, parentFinder) {
1046     var filteredCollection = [];
1047     for (var i = 0; i < elCollection.length; ++i) {
1048         var findParent = parentFinder(elCollection[i]);
1049         if (findParent.nodeName.toUpperCase() != 'BODY') {
1050             filteredCollection.push(elCollection[i]);
1051         }
1052     }
1053     return filteredCollection;
1057     All this is here just so that IE gets to handle oversized blocks
1058     in a visually pleasing manner. It does a browser detect. So sue me.
1061 function fix_column_widths() {
1062     var agt = navigator.userAgent.toLowerCase();
1063     if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1064         fix_column_width('left-column');
1065         fix_column_width('right-column');
1066     }
1069 function fix_column_width(colName) {
1070     if(column = document.getElementById(colName)) {
1071         if(!column.offsetWidth) {
1072             setTimeout("fix_column_width('" + colName + "')", 20);
1073             return;
1074         }
1076         var width = 0;
1077         var nodes = column.childNodes;
1079         for(i = 0; i < nodes.length; ++i) {
1080             if(nodes[i].className.indexOf("block") != -1 ) {
1081                 if(width < nodes[i].offsetWidth) {
1082                     width = nodes[i].offsetWidth;
1083                 }
1084             }
1085         }
1087         for(i = 0; i < nodes.length; ++i) {
1088             if(nodes[i].className.indexOf("block") != -1 ) {
1089                 nodes[i].style.width = width + 'px';
1090             }
1091         }
1092     }
1097    Insert myValue at current cursor position
1098  */
1099 function insertAtCursor(myField, myValue) {
1100     // IE support
1101     if (document.selection) {
1102         myField.focus();
1103         sel = document.selection.createRange();
1104         sel.text = myValue;
1105     }
1106     // Mozilla/Netscape support
1107     else if (myField.selectionStart || myField.selectionStart == '0') {
1108         var startPos = myField.selectionStart;
1109         var endPos = myField.selectionEnd;
1110         myField.value = myField.value.substring(0, startPos)
1111             + myValue + myField.value.substring(endPos, myField.value.length);
1112     } else {
1113         myField.value += myValue;
1114     }
1118  * Increment a file name.
1120  * @param string file name.
1121  * @param boolean ignoreextension do not extract the extension prior to appending the
1122  *                                suffix. Useful when incrementing folder names.
1123  * @return string the incremented file name.
1124  */
1125 function increment_filename(filename, ignoreextension) {
1126     var extension = '';
1127     var basename = filename;
1129     // Split the file name into the basename + extension.
1130     if (!ignoreextension) {
1131         var dotpos = filename.lastIndexOf('.');
1132         if (dotpos !== -1) {
1133             basename = filename.substr(0, dotpos);
1134             extension = filename.substr(dotpos, filename.length);
1135         }
1136     }
1138     // Look to see if the name already has (NN) at the end of it.
1139     var number = 0;
1140     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1141     if (hasnumber !== null) {
1142         // Note the current number & remove it from the basename.
1143         number = parseInt(hasnumber[2], 10);
1144         basename = hasnumber[1];
1145     }
1147     number++;
1148     var newname = basename + ' (' + number + ')' + extension;
1149     return newname;
1153  * Return whether we are in right to left mode or not.
1155  * @return boolean
1156  */
1157 function right_to_left() {
1158     var body = Y.one('body');
1159     var rtl = false;
1160     if (body && body.hasClass('dir-rtl')) {
1161         rtl = true;
1162     }
1163     return rtl;
1166 function openpopup(event, args) {
1168     if (event) {
1169         if (event.preventDefault) {
1170             event.preventDefault();
1171         } else {
1172             event.returnValue = false;
1173         }
1174     }
1176     // Make sure the name argument is set and valid.
1177     var nameregex = /[^a-z0-9_]/i;
1178     if (typeof args.name !== 'string') {
1179         args.name = '_blank';
1180     } else if (args.name.match(nameregex)) {
1181         // Cleans window name because IE does not support funky ones.
1182         if (M.cfg.developerdebug) {
1183             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1184         }
1185         args.name = args.name.replace(nameregex, '_');
1186     }
1188     var fullurl = args.url;
1189     if (!args.url.match(/https?:\/\//)) {
1190         fullurl = M.cfg.wwwroot + args.url;
1191     }
1192     if (args.fullscreen) {
1193         args.options = args.options.
1194                 replace(/top=\d+/, 'top=0').
1195                 replace(/left=\d+/, 'left=0').
1196                 replace(/width=\d+/, 'width=' + screen.availWidth).
1197                 replace(/height=\d+/, 'height=' + screen.availHeight);
1198     }
1199     var windowobj = window.open(fullurl,args.name,args.options);
1200     if (!windowobj) {
1201         return true;
1202     }
1204     if (args.fullscreen) {
1205         // In some browser / OS combinations (E.g. Chrome on Windows), the
1206         // window initially opens slighly too big. The width and heigh options
1207         // seem to control the area inside the browser window, so what with
1208         // scroll-bars, etc. the actual window is bigger than the screen.
1209         // Therefore, we need to fix things up after the window is open.
1210         var hackcount = 100;
1211         var get_size_exactly_right = function() {
1212             windowobj.moveTo(0, 0);
1213             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1215             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1216             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1217             // about 50ms) after the window is open, then it actually behaves
1218             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1219             // check that the resize actually worked, and if not, repeatedly try
1220             // again after a short delay until it works (but with a limit of
1221             // hackcount repeats.
1222             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1223                 hackcount -= 1;
1224                 setTimeout(get_size_exactly_right, 10);
1225             }
1226         }
1227         setTimeout(get_size_exactly_right, 0);
1228     }
1229     windowobj.focus();
1231     return false;
1234 /** Close the current browser window. */
1235 function close_window(e) {
1236     if (e.preventDefault) {
1237         e.preventDefault();
1238     } else {
1239         e.returnValue = false;
1240     }
1241     window.close();
1245  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1246  * @param controlid the control id.
1247  */
1248 function focuscontrol(controlid) {
1249     var control = document.getElementById(controlid);
1250     if (control) {
1251         control.focus();
1252     }
1256  * Transfers keyboard focus to an HTML element based on the old style style of focus
1257  * This function should be removed as soon as it is no longer used
1258  */
1259 function old_onload_focus(formid, controlname) {
1260     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1261         document.forms[formid].elements[controlname].focus();
1262     }
1265 function build_querystring(obj) {
1266     return convert_object_to_string(obj, '&');
1269 function build_windowoptionsstring(obj) {
1270     return convert_object_to_string(obj, ',');
1273 function convert_object_to_string(obj, separator) {
1274     if (typeof obj !== 'object') {
1275         return null;
1276     }
1277     var list = [];
1278     for(var k in obj) {
1279         k = encodeURIComponent(k);
1280         var value = obj[k];
1281         if(obj[k] instanceof Array) {
1282             for(var i in value) {
1283                 list.push(k+'[]='+encodeURIComponent(value[i]));
1284             }
1285         } else {
1286             list.push(k+'='+encodeURIComponent(value));
1287         }
1288     }
1289     return list.join(separator);
1292 function stripHTML(str) {
1293     var re = /<\S[^><]*>/g;
1294     var ret = str.replace(re, "");
1295     return ret;
1298 function updateProgressBar(id, percent, msg, estimate) {
1299     var progressIndicator = Y.one('#' + id);
1300     if (!progressIndicator) {
1301         return;
1302     }
1304     var progressBar = progressIndicator.one('.bar'),
1305         statusIndicator = progressIndicator.one('h2'),
1306         estimateIndicator = progressIndicator.one('p');
1308     statusIndicator.set('innerHTML', Y.Escape.html(msg));
1309     progressBar.set('innerHTML', Y.Escape.html('' + percent + '%'));
1310     if (percent === 100) {
1311         progressIndicator.addClass('progress-success');
1312         estimateIndicator.set('innerHTML', null);
1313     } else {
1314         if (estimate) {
1315             estimateIndicator.set('innerHTML', Y.Escape.html(estimate));
1316         } else {
1317             estimateIndicator.set('innerHTML', null);
1318         }
1319         progressIndicator.removeClass('progress-success');
1320     }
1321     progressBar.setAttribute('aria-valuenow', percent);
1322     progressBar.setStyle('width', percent + '%');
1325 // ===== Deprecated core Javascript functions for Moodle ====
1326 //       DO NOT USE!!!!!!!
1327 // Do not put this stuff in separate file because it only adds extra load on servers!
1330  * @method show_item
1331  * @deprecated since Moodle 2.7.
1332  * @see Y.Node.show
1333  */
1334 function show_item() {
1335     throw new Error('show_item can not be used any more. Please use Y.Node.show.');
1339  * @method destroy_item
1340  * @deprecated since Moodle 2.7.
1341  * @see Y.Node.destroy
1342  */
1343 function destroy_item() {
1344     throw new Error('destroy_item can not be used any more. Please use Y.Node.destroy.');
1348  * @method hide_item
1349  * @deprecated since Moodle 2.7.
1350  * @see Y.Node.hide
1351  */
1352 function hide_item() {
1353     throw new Error('hide_item can not be used any more. Please use Y.Node.hide.');
1357  * @method addonload
1358  * @deprecated since Moodle 2.7 - please do not use this function any more.
1359  */
1360 function addonload() {
1361     throw new Error('addonload can not be used any more.');
1365  * @method getElementsByClassName
1366  * @deprecated Since Moodle 2.7 - please do not use this function any more.
1367  * @see Y.one
1368  * @see Y.all
1369  */
1370 function getElementsByClassName() {
1371     throw new Error('getElementsByClassName can not be used any more. Please use Y.one or Y.all.');
1375  * @method findChildNodes
1376  * @deprecated since Moodle 2.7 - please do not use this function any more.
1377  * @see Y.all
1378  */
1379 function findChildNodes() {
1380     throw new Error('findChildNodes can not be used any more. Please use Y.all.');
1383 M.util.help_popups = {
1384     setup : function(Y) {
1385         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1386     },
1387     open_popup : function(e) {
1388         // Prevent the default page action
1389         e.preventDefault();
1391         // Grab the anchor that was clicked
1392         var anchor = e.target.ancestor('a', true);
1393         var args = {
1394             'name'          : 'popup',
1395             'url'           : anchor.getAttribute('href'),
1396             'options'       : ''
1397         };
1398         var options = [
1399             'height=600',
1400             'width=800',
1401             'top=0',
1402             'left=0',
1403             'menubar=0',
1404             'location=0',
1405             'scrollbars',
1406             'resizable',
1407             'toolbar',
1408             'status',
1409             'directories=0',
1410             'fullscreen=0',
1411             'dependent'
1412         ]
1413         args.options = options.join(',');
1415         openpopup(e, args);
1416     }
1420  * Custom menu namespace
1421  */
1422 M.core_custom_menu = {
1423     /**
1424      * This method is used to initialise a custom menu given the id that belongs
1425      * to the custom menu's root node.
1426      *
1427      * @param {YUI} Y
1428      * @param {string} nodeid
1429      */
1430     init : function(Y, nodeid) {
1431         var node = Y.one('#'+nodeid);
1432         if (node) {
1433             Y.use('node-menunav', function(Y) {
1434                 // Get the node
1435                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1436                 node.removeClass('javascript-disabled');
1437                 // Initialise the menunav plugin
1438                 node.plug(Y.Plugin.NodeMenuNav);
1439             });
1440         }
1441     }
1445  * Used to store form manipulation methods and enhancments
1446  */
1447 M.form = M.form || {};
1450  * Converts a nbsp indented select box into a multi drop down custom control much
1451  * like the custom menu. It also selectable categories on or off.
1453  * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1455  * @param {YUI} Y
1456  * @param {string} id
1457  * @param {Array} options
1458  */
1459 M.form.init_smartselect = function(Y, id, options) {
1460     if (!id.match(/^id_/)) {
1461         id = 'id_'+id;
1462     }
1463     var select = Y.one('select#'+id);
1464     if (!select) {
1465         return false;
1466     }
1467     Y.use('event-delegate',function(){
1468         var smartselect = {
1469             id : id,
1470             structure : [],
1471             options : [],
1472             submenucount : 0,
1473             currentvalue : null,
1474             currenttext : null,
1475             shownevent : null,
1476             cfg : {
1477                 selectablecategories : true,
1478                 mode : null
1479             },
1480             nodes : {
1481                 select : null,
1482                 loading : null,
1483                 menu : null
1484             },
1485             init : function(Y, id, args, nodes) {
1486                 if (typeof(args)=='object') {
1487                     for (var i in this.cfg) {
1488                         if (args[i] || args[i]===false) {
1489                             this.cfg[i] = args[i];
1490                         }
1491                     }
1492                 }
1494                 // Display a loading message first up
1495                 this.nodes.select = nodes.select;
1497                 this.currentvalue = this.nodes.select.get('selectedIndex');
1498                 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1500                 var options = Array();
1501                 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1502                 this.nodes.select.all('option').each(function(option, index) {
1503                     var rawtext = option.get('innerHTML');
1504                     var text = rawtext.replace(/^(&nbsp;)*/, '');
1505                     if (rawtext === text) {
1506                         text = rawtext.replace(/^(\s)*/, '');
1507                         var depth = (rawtext.length - text.length ) + 1;
1508                     } else {
1509                         var depth = ((rawtext.length - text.length )/12)+1;
1510                     }
1511                     option.set('innerHTML', text);
1512                     options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1513                 }, this);
1515                 this.structure = [];
1516                 var structcount = 0;
1517                 for (var i in options) {
1518                     var o = options[i];
1519                     if (o.depth == 0) {
1520                         this.structure.push(o);
1521                         structcount++;
1522                     } else {
1523                         var d = o.depth;
1524                         var current = this.structure[structcount-1];
1525                         for (var j = 0; j < o.depth-1;j++) {
1526                             if (current && current.children) {
1527                                 current = current.children[current.children.length-1];
1528                             }
1529                         }
1530                         if (current && current.children) {
1531                             current.children.push(o);
1532                         }
1533                     }
1534                 }
1536                 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1537                 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1538                 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1539                 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1541                 if (this.cfg.mode == null) {
1542                     var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1543                     if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1544                         this.cfg.mode = 'compact';
1545                     } else {
1546                         this.cfg.mode = 'spanning';
1547                     }
1548                 }
1550                 if (this.cfg.mode == 'compact') {
1551                     this.nodes.menu.addClass('compactmenu');
1552                 } else {
1553                     this.nodes.menu.addClass('spanningmenu');
1554                     this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1555                 }
1557                 Y.one(document.body).append(this.nodes.menu);
1558                 var pos = this.nodes.select.getXY();
1559                 pos[0] += 1;
1560                 this.nodes.menu.setXY(pos);
1561                 this.nodes.menu.on('click', this.handle_click, this);
1563                 Y.one(window).on('resize', function(){
1564                      var pos = this.nodes.select.getXY();
1565                     pos[0] += 1;
1566                     this.nodes.menu.setXY(pos);
1567                  }, this);
1568             },
1569             generate_menu_content : function() {
1570                 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1571                 content += this.generate_submenu_content(this.structure[0], true);
1572                 content += '</ul></div>';
1573                 return content;
1574             },
1575             generate_submenu_content : function(item, rootelement) {
1576                 this.submenucount++;
1577                 var content = '';
1578                 if (item.children.length > 0) {
1579                     if (rootelement) {
1580                         content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1581                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1582                         content += '<div class="smartselect_menu_content">';
1583                     } else {
1584                         content += '<li class="smartselect_submenuitem">';
1585                         var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1586                         content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1587                         content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1588                         content += '<div class="smartselect_submenu_content">';
1589                     }
1590                     content += '<ul>';
1591                     for (var i in item.children) {
1592                         content += this.generate_submenu_content(item.children[i],false);
1593                     }
1594                     content += '</ul>';
1595                     content += '</div>';
1596                     content += '</div>';
1597                     if (rootelement) {
1598                     } else {
1599                         content += '</li>';
1600                     }
1601                 } else {
1602                     content += '<li class="smartselect_menuitem">';
1603                     content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1604                     content += '</li>';
1605                 }
1606                 return content;
1607             },
1608             select : function(e) {
1609                 var t = e.target;
1610                 e.halt();
1611                 this.currenttext = t.get('innerHTML');
1612                 this.currentvalue = t.getAttribute('value');
1613                 this.nodes.select.set('selectedIndex', this.currentvalue);
1614                 this.hide_menu();
1615             },
1616             handle_click : function(e) {
1617                 var target = e.target;
1618                 if (target.hasClass('smartselect_mask')) {
1619                     this.show_menu(e);
1620                 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1621                     this.select(e);
1622                 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1623                     this.show_sub_menu(e);
1624                 }
1625             },
1626             show_menu : function(e) {
1627                 e.halt();
1628                 var menu = e.target.ancestor().one('.smartselect_menu');
1629                 menu.addClass('visible');
1630                 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1631             },
1632             show_sub_menu : function(e) {
1633                 e.halt();
1634                 var target = e.target;
1635                 if (!target.hasClass('smartselect_submenuitem')) {
1636                     target = target.ancestor('.smartselect_submenuitem');
1637                 }
1638                 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1639                     target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1640                     return;
1641                 }
1642                 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1643                 target.one('.smartselect_submenu').addClass('visible');
1644             },
1645             hide_menu : function() {
1646                 this.nodes.menu.all('.visible').removeClass('visible');
1647                 if (this.shownevent) {
1648                     this.shownevent.detach();
1649                 }
1650             }
1651         };
1652         smartselect.init(Y, id, options, {select:select});
1653     });
1656 /** List of flv players to be loaded */
1657 M.util.video_players = [];
1658 /** List of mp3 players to be loaded */
1659 M.util.audio_players = [];
1662  * Add video player
1663  * @param id element id
1664  * @param fileurl media url
1665  * @param width
1666  * @param height
1667  * @param autosize true means detect size from media
1668  */
1669 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1670     M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1674  * Add audio player.
1675  * @param id
1676  * @param fileurl
1677  * @param small
1678  */
1679 M.util.add_audio_player = function (id, fileurl, small) {
1680     M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1684  * Initialise all audio and video player, must be called from page footer.
1685  */
1686 M.util.load_flowplayer = function() {
1687     if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1688         return;
1689     }
1690     if (typeof(flowplayer) == 'undefined') {
1691         var loaded = false;
1693         var embed_function = function() {
1694             if (loaded || typeof(flowplayer) == 'undefined') {
1695                 return;
1696             }
1697             loaded = true;
1699             var controls = {
1700                     url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1701                     autoHide: true
1702             }
1703             /* TODO: add CSS color overrides for the flv flow player */
1705             for(var i=0; i<M.util.video_players.length; i++) {
1706                 var video = M.util.video_players[i];
1707                 if (video.width > 0 && video.height > 0) {
1708                     var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', width: video.width, height: video.height};
1709                 } else {
1710                     var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php';
1711                 }
1712                 flowplayer(video.id, src, {
1713                     plugins: {controls: controls},
1714                     clip: {
1715                         url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1716                         onMetaData: function(clip) {
1717                             if (clip.mvideo.autosize && !clip.mvideo.resized) {
1718                                 clip.mvideo.resized = true;
1719                                 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1720                                 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1721                                     // bad luck, we have to guess - we may not get metadata at all
1722                                     var width = clip.width;
1723                                     var height = clip.height;
1724                                 } else {
1725                                     var width = clip.metaData.width;
1726                                     var height = clip.metaData.height;
1727                                 }
1728                                 var minwidth = 300; // controls are messed up in smaller objects
1729                                 if (width < minwidth) {
1730                                     height = (height * minwidth) / width;
1731                                     width = minwidth;
1732                                 }
1734                                 var object = this._api();
1735                                 object.width = width;
1736                                 object.height = height;
1737                             }
1738                         }
1739                     }
1740                 });
1741             }
1742             if (M.util.audio_players.length == 0) {
1743                 return;
1744             }
1745             var controls = {
1746                     url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1747                     autoHide: false,
1748                     fullscreen: false,
1749                     next: false,
1750                     previous: false,
1751                     scrubber: true,
1752                     play: true,
1753                     pause: true,
1754                     volume: true,
1755                     mute: false,
1756                     backgroundGradient: [0.5,0,0.3]
1757                 };
1759             var rule;
1760             for (var j=0; j < document.styleSheets.length; j++) {
1762                 // To avoid javascript security violation accessing cross domain stylesheets
1763                 var allrules = false;
1764                 try {
1765                     if (typeof (document.styleSheets[j].rules) != 'undefined') {
1766                         allrules = document.styleSheets[j].rules;
1767                     } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1768                         allrules = document.styleSheets[j].cssRules;
1769                     } else {
1770                         // why??
1771                         continue;
1772                     }
1773                 } catch (e) {
1774                     continue;
1775                 }
1777                 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1778                 if (!allrules) {
1779                     continue;
1780                 }
1782                 for(var i=0; i<allrules.length; i++) {
1783                     rule = '';
1784                     if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1785                         if (typeof(allrules[i].cssText) != 'undefined') {
1786                             rule = allrules[i].cssText;
1787                         } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1788                             rule = allrules[i].style.cssText;
1789                         }
1790                         if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1791                             rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1792                             var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1793                             controls[colprop] = rule;
1794                         }
1795                     }
1796                 }
1797                 allrules = false;
1798             }
1800             for(i=0; i<M.util.audio_players.length; i++) {
1801                 var audio = M.util.audio_players[i];
1802                 if (audio.small) {
1803                     controls.controlall = false;
1804                     controls.height = 15;
1805                     controls.time = false;
1806                 } else {
1807                     controls.controlall = true;
1808                     controls.height = 25;
1809                     controls.time = true;
1810                 }
1811                 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', {
1812                     plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf.php'}},
1813                     clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1814                 });
1815             }
1816         }
1818         if (M.cfg.jsrev == -1) {
1819             var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1820         } else {
1821             var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1822         }
1823         var fileref = document.createElement('script');
1824         fileref.setAttribute('type','text/javascript');
1825         fileref.setAttribute('src', jsurl);
1826         fileref.onload = embed_function;
1827         fileref.onreadystatechange = embed_function;
1828         document.getElementsByTagName('head')[0].appendChild(fileref);
1829     }