a9df26abe258909131baa5598b7c28746ac22e54
[moodle.git] / lib / javascript-static.js
1 // Miscellaneous core Javascript functions for Moodle
2 // Global M object is initilised in inline javascript
4 /**
5  * Add module to list of available modules that can be loaded from YUI.
6  * @param {Array} modules
7  */
8 M.yui.add_module = function(modules) {
9     for (var modname in modules) {
10         YUI_config.modules[modname] = modules[modname];
11     }
12     // Ensure thaat the YUI_config is applied to the main YUI instance.
13     Y.applyConfig(YUI_config);
14 };
15 /**
16  * The gallery version to use when loading YUI modules from the gallery.
17  * Will be changed every time when using local galleries.
18  */
19 M.yui.galleryversion = '2010.04.21-21-51';
21 /**
22  * Various utility functions
23  */
24 M.util = M.util || {};
26 /**
27  * Language strings - initialised from page footer.
28  */
29 M.str = M.str || {};
31 /**
32  * Returns url for images.
33  * @param {String} imagename
34  * @param {String} component
35  * @return {String}
36  */
37 M.util.image_url = function(imagename, component) {
39     if (!component || component == '' || component == 'moodle' || component == 'core') {
40         component = 'core';
41     }
43     var url = M.cfg.wwwroot + '/theme/image.php';
44     if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
45         if (!M.cfg.svgicons) {
46             url += '/_s';
47         }
48         url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
49     } else {
50         url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
51         if (!M.cfg.svgicons) {
52             url += '&svg=0';
53         }
54     }
56     return url;
57 };
59 M.util.in_array = function(item, array){
60     for( var i = 0; i<array.length; i++){
61         if(item==array[i]){
62             return true;
63         }
64     }
65     return false;
66 };
68 /**
69  * Init a collapsible region, see print_collapsible_region in weblib.php
70  * @param {YUI} Y YUI3 instance with all libraries loaded
71  * @param {String} id the HTML id for the div.
72  * @param {String} userpref the user preference that records the state of this box. false if none.
73  * @param {String} strtooltip
74  */
75 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
76     Y.use('anim', function(Y) {
77         new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
78     });
79 };
81 /**
82  * Object to handle a collapsible region : instantiate and forget styled object
83  *
84  * @class
85  * @constructor
86  * @param {YUI} Y YUI3 instance with all libraries loaded
87  * @param {String} id The HTML id for the div.
88  * @param {String} userpref The user preference that records the state of this box. false if none.
89  * @param {String} strtooltip
90  */
91 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
92     // Record the pref name
93     this.userpref = userpref;
95     // Find the divs in the document.
96     this.div = Y.one('#'+id);
98     // Get the caption for the collapsible region
99     var caption = this.div.one('#'+id + '_caption');
101     // Create a link
102     var a = Y.Node.create('<a href="#"></a>');
103     a.setAttribute('title', strtooltip);
105     // Get all the nodes from caption, remove them and append them to <a>
106     while (caption.hasChildNodes()) {
107         child = caption.get('firstChild');
108         child.remove();
109         a.append(child);
110     }
111     caption.append(a);
113     // Get the height of the div at this point before we shrink it if required
114     var height = this.div.get('offsetHeight');
115     var collapsedimage = 't/collapsed'; // ltr mode
116     if (right_to_left()) {
117         collapsedimage = 't/collapsed_rtl';
118     } else {
119         collapsedimage = 't/collapsed';
120     }
121     if (this.div.hasClass('collapsed')) {
122         // Add the correct image and record the YUI node created in the process
123         this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
124         // Shrink the div as it is collapsed by default
125         this.div.setStyle('height', caption.get('offsetHeight')+'px');
126     } else {
127         // Add the correct image and record the YUI node created in the process
128         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
129     }
130     a.append(this.icon);
132     // Create the animation.
133     var animation = new Y.Anim({
134         node: this.div,
135         duration: 0.3,
136         easing: Y.Easing.easeBoth,
137         to: {height:caption.get('offsetHeight')},
138         from: {height:height}
139     });
141     // Handler for the animation finishing.
142     animation.on('end', function() {
143         this.div.toggleClass('collapsed');
144         var collapsedimage = 't/collapsed'; // ltr mode
145         if (right_to_left()) {
146             collapsedimage = 't/collapsed_rtl';
147             } else {
148             collapsedimage = 't/collapsed';
149             }
150         if (this.div.hasClass('collapsed')) {
151             this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
152         } else {
153             this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
154         }
155     }, this);
157     // Hook up the event handler.
158     a.on('click', function(e, animation) {
159         e.preventDefault();
160         // Animate to the appropriate size.
161         if (animation.get('running')) {
162             animation.stop();
163         }
164         animation.set('reverse', this.div.hasClass('collapsed'));
165         // Update the user preference.
166         if (this.userpref) {
167             M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
168         }
169         animation.run();
170     }, this, animation);
171 };
173 /**
174  * The user preference that stores the state of this box.
175  * @property userpref
176  * @type String
177  */
178 M.util.CollapsibleRegion.prototype.userpref = null;
180 /**
181  * The key divs that make up this
182  * @property div
183  * @type Y.Node
184  */
185 M.util.CollapsibleRegion.prototype.div = null;
187 /**
188  * The key divs that make up this
189  * @property icon
190  * @type Y.Node
191  */
192 M.util.CollapsibleRegion.prototype.icon = null;
194 /**
195  * Makes a best effort to connect back to Moodle to update a user preference,
196  * however, there is no mechanism for finding out if the update succeeded.
197  *
198  * Before you can use this function in your JavsScript, you must have called
199  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
200  * the udpate is allowed, and how to safely clean and submitted values.
201  *
202  * @param String name the name of the setting to udpate.
203  * @param String the value to set it to.
204  */
205 M.util.set_user_preference = function(name, value) {
206     YUI().use('io', function(Y) {
207         var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
208                 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
210         // If we are a developer, ensure that failures are reported.
211         var cfg = {
212                 method: 'get',
213                 on: {}
214             };
215         if (M.cfg.developerdebug) {
216             cfg.on.failure = function(id, o, args) {
217                 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
218             }
219         }
221         // Make the request.
222         Y.io(url, cfg);
223     });
224 };
226 /**
227  * Prints a confirmation dialog in the style of DOM.confirm().
228  *
229  * @method show_confirm_dialog
230  * @param {EventFacade} e
231  * @param {Object} args
232  * @param {String} args.message The question to ask the user
233  * @param {Function} [args.callback] A callback to apply on confirmation.
234  * @param {Object} [args.scope] The scope to use when calling the callback.
235  * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
236  * @param {String} [args.cancellabel] The label to use on the cancel button.
237  * @param {String} [args.continuelabel] The label to use on the continue button.
238  */
239 M.util.show_confirm_dialog = function(e, args) {
240     var target = e.target;
241     if (e.preventDefault) {
242         e.preventDefault();
243     }
245     YUI().use('moodle-core-notification-confirm', function(Y) {
246         var confirmationDialogue = new M.core.confirm({
247             width: '300px',
248             center: true,
249             modal: true,
250             visible: false,
251             draggable: false,
252             title: M.util.get_string('confirmation', 'admin'),
253             noLabel: M.util.get_string('cancel', 'moodle'),
254             question: args.message
255         });
257         // The dialogue was submitted with a positive value indication.
258         confirmationDialogue.on('complete-yes', function(e) {
259             // Handle any callbacks.
260             if (args.callback) {
261                 if (!Y.Lang.isFunction(args.callback)) {
262                     Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
263                             'warn', 'M.util.show_confirm_dialog');
264                     return;
265                 }
267                 var scope = e.target;
268                 if (Y.Lang.isObject(args.scope)) {
269                     scope = args.scope;
270                 }
272                 var callbackargs = args.callbackargs || [];
273                 args.callback.apply(scope, callbackargs);
274                 return;
275             }
277             var targetancestor = null,
278                 targetform = null;
280             if (target.test('a')) {
281                 window.location = target.get('href');
283             } else if ((targetancestor = target.ancestor('a')) !== null) {
284                 window.location = targetancestor.get('href');
286             } else if (target.test('input') || target.test('button')) {
287                 targetform = target.ancestor('form', true);
288                 if (!targetform) {
289                     return;
290                 }
291                 if (target.get('name') && target.get('value')) {
292                     targetform.append('<input type="hidden" name="' + target.get('name') +
293                                     '" value="' + target.get('value') + '">');
294                     if (typeof M.core_formchangechecker !== 'undefined') {
295                         M.core_formchangechecker.set_form_submitted();
296                     }
297                 }
298                 targetform.submit();
300             } else if (target.test('form')) {
301                 if (typeof M.core_formchangechecker !== 'undefined') {
302                     M.core_formchangechecker.set_form_submitted();
303                 }
304                 target.submit();
306             } else {
307                 Y.log("Element of type " + target.get('tagName') +
308                         " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, BUTTON or FORM",
309                         'warn', 'javascript-static');
310             }
311         }, this);
313         if (args.cancellabel) {
314             confirmationDialogue.set('noLabel', args.cancellabel);
315         }
317         if (args.continuelabel) {
318             confirmationDialogue.set('yesLabel', args.continuelabel);
319         }
321         confirmationDialogue.render()
322                 .show();
323     });
324 };
326 /** Useful for full embedding of various stuff */
327 M.util.init_maximised_embed = function(Y, id) {
328     var obj = Y.one('#'+id);
329     if (!obj) {
330         return;
331     }
333     var get_htmlelement_size = function(el, prop) {
334         if (Y.Lang.isString(el)) {
335             el = Y.one('#' + el);
336         }
337         // Ensure element exists.
338         if (el) {
339             var val = el.getStyle(prop);
340             if (val == 'auto') {
341                 val = el.getComputedStyle(prop);
342             }
343             val = parseInt(val);
344             if (isNaN(val)) {
345                 return 0;
346             }
347             return val;
348         } else {
349             return 0;
350         }
351     };
353     var resize_object = function() {
354         obj.setStyle('display', 'none');
355         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
357         if (newwidth > 500) {
358             obj.setStyle('width', newwidth  + 'px');
359         } else {
360             obj.setStyle('width', '500px');
361         }
363         var headerheight = get_htmlelement_size('page-header', 'height');
364         var footerheight = get_htmlelement_size('page-footer', 'height');
365         var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
366         if (newheight < 400) {
367             newheight = 400;
368         }
369         obj.setStyle('height', newheight+'px');
370         obj.setStyle('display', '');
371     };
373     resize_object();
374     // fix layout if window resized too
375     Y.use('event-resize', function (Y) {
376         Y.on("windowresize", function() {
377             resize_object();
378         });
379     });
380 };
382 /**
383  * Breaks out all links to the top frame - used in frametop page layout.
384  */
385 M.util.init_frametop = function(Y) {
386     Y.all('a').each(function(node) {
387         node.set('target', '_top');
388     });
389     Y.all('form').each(function(node) {
390         node.set('target', '_top');
391     });
392 };
394 /**
395  * Finds all nodes that match the given CSS selector and attaches events to them
396  * so that they toggle a given classname when clicked.
397  *
398  * @param {YUI} Y
399  * @param {string} id An id containing elements to target
400  * @param {string} cssselector A selector to use to find targets
401  * @param {string} toggleclassname A classname to toggle
402  */
403 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
405     if (togglecssselector == '') {
406         togglecssselector = cssselector;
407     }
409     var node = Y.one('#'+id);
410     node.all(cssselector).each(function(n){
411         n.on('click', function(e){
412             e.stopPropagation();
413             if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
414                 if (this.test(togglecssselector)) {
415                     this.toggleClass(toggleclassname);
416                 } else {
417                     this.ancestor(togglecssselector).toggleClass(toggleclassname);
418             }
419             }
420         }, n);
421     });
422     // Attach this click event to the node rather than all selectors... will be much better
423     // for performance
424     node.on('click', function(e){
425         if (e.target.hasClass('addtoall')) {
426             this.all(togglecssselector).addClass(toggleclassname);
427         } else if (e.target.hasClass('removefromall')) {
428             this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
429         }
430     }, node);
431 };
433 /**
434  * Initialises a colour picker
435  *
436  * Designed to be used with admin_setting_configcolourpicker although could be used
437  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
438  * above or below the input (must have the same parent) and then call this with the
439  * id.
440  *
441  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
442  * contrib/blocks. For better docs refer to that.
443  *
444  * @param {YUI} Y
445  * @param {int} id
446  * @param {object} previewconf
447  */
448 M.util.init_colour_picker = function(Y, id, previewconf) {
449     /**
450      * We need node and event-mouseenter
451      */
452     Y.use('node', 'event-mouseenter', function(){
453         /**
454          * The colour picker object
455          */
456         var colourpicker = {
457             box : null,
458             input : null,
459             image : null,
460             preview : null,
461             current : null,
462             eventClick : null,
463             eventMouseEnter : null,
464             eventMouseLeave : null,
465             eventMouseMove : null,
466             width : 300,
467             height :  100,
468             factor : 5,
469             /**
470              * Initalises the colour picker by putting everything together and wiring the events
471              */
472             init : function() {
473                 this.input = Y.one('#'+id);
474                 this.box = this.input.ancestor().one('.admin_colourpicker');
475                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
476                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
477                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
478                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
479                 this.current = Y.Node.create('<div class="currentcolour"></div>');
480                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
481                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
483                 if (typeof(previewconf) === 'object' && previewconf !== null) {
484                     Y.one('#'+id+'_preview').on('click', function(e){
485                         if (Y.Lang.isString(previewconf.selector)) {
486                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
487                         } else {
488                             for (var i in previewconf.selector) {
489                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
490                             }
491                         }
492                     }, this);
493                 }
495                 this.eventClick = this.image.on('click', this.pickColour, this);
496                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
497             },
498             /**
499              * Starts to follow the mouse once it enter the image
500              */
501             startFollow : function(e) {
502                 this.eventMouseEnter.detach();
503                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
504                 this.eventMouseMove = this.image.on('mousemove', function(e){
505                     this.preview.setStyle('backgroundColor', this.determineColour(e));
506                 }, this);
507             },
508             /**
509              * Stops following the mouse
510              */
511             endFollow : function(e) {
512                 this.eventMouseMove.detach();
513                 this.eventMouseLeave.detach();
514                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
515             },
516             /**
517              * Picks the colour the was clicked on
518              */
519             pickColour : function(e) {
520                 var colour = this.determineColour(e);
521                 this.input.set('value', colour);
522                 this.current.setStyle('backgroundColor', colour);
523             },
524             /**
525              * Calculates the colour fromthe given co-ordinates
526              */
527             determineColour : function(e) {
528                 var eventx = Math.floor(e.pageX-e.target.getX());
529                 var eventy = Math.floor(e.pageY-e.target.getY());
531                 var imagewidth = this.width;
532                 var imageheight = this.height;
533                 var factor = this.factor;
534                 var colour = [255,0,0];
536                 var matrices = [
537                     [  0,  1,  0],
538                     [ -1,  0,  0],
539                     [  0,  0,  1],
540                     [  0, -1,  0],
541                     [  1,  0,  0],
542                     [  0,  0, -1]
543                 ];
545                 var matrixcount = matrices.length;
546                 var limit = Math.round(imagewidth/matrixcount);
547                 var heightbreak = Math.round(imageheight/2);
549                 for (var x = 0; x < imagewidth; x++) {
550                     var divisor = Math.floor(x / limit);
551                     var matrix = matrices[divisor];
553                     colour[0] += matrix[0]*factor;
554                     colour[1] += matrix[1]*factor;
555                     colour[2] += matrix[2]*factor;
557                     if (eventx==x) {
558                         break;
559                     }
560                 }
562                 var pixel = [colour[0], colour[1], colour[2]];
563                 if (eventy < heightbreak) {
564                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
565                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
566                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
567                 } else if (eventy > heightbreak) {
568                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
569                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
570                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
571                 }
573                 return this.convert_rgb_to_hex(pixel);
574             },
575             /**
576              * Converts an RGB value to Hex
577              */
578             convert_rgb_to_hex : function(rgb) {
579                 var hex = '#';
580                 var hexchars = "0123456789ABCDEF";
581                 for (var i=0; i<3; i++) {
582                     var number = Math.abs(rgb[i]);
583                     if (number == 0 || isNaN(number)) {
584                         hex += '00';
585                     } else {
586                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
587                     }
588                 }
589                 return hex;
590             }
591         };
592         /**
593          * Initialise the colour picker :) Hoorah
594          */
595         colourpicker.init();
596     });
597 };
599 M.util.init_block_hider = function(Y, config) {
600     Y.use('base', 'node', function(Y) {
601         M.util.block_hider = M.util.block_hider || (function(){
602             var blockhider = function() {
603                 blockhider.superclass.constructor.apply(this, arguments);
604             };
605             blockhider.prototype = {
606                 initializer : function(config) {
607                     this.set('block', '#'+this.get('id'));
608                     var b = this.get('block'),
609                         t = b.one('.title'),
610                         a = null,
611                         hide,
612                         show;
613                     if (t && (a = t.one('.block_action'))) {
614                         hide = Y.Node.create('<img />')
615                             .addClass('block-hider-hide')
616                             .setAttrs({
617                                 alt:        config.tooltipVisible,
618                                 src:        this.get('iconVisible'),
619                                 tabIndex:   0,
620                                 'title':    config.tooltipVisible
621                             });
622                         hide.on('keypress', this.updateStateKey, this, true);
623                         hide.on('click', this.updateState, this, true);
625                         show = Y.Node.create('<img />')
626                             .addClass('block-hider-show')
627                             .setAttrs({
628                                 alt:        config.tooltipHidden,
629                                 src:        this.get('iconHidden'),
630                                 tabIndex:   0,
631                                 'title':    config.tooltipHidden
632                             });
633                         show.on('keypress', this.updateStateKey, this, false);
634                         show.on('click', this.updateState, this, false);
636                         a.insert(show, 0).insert(hide, 0);
637                     }
638                 },
639                 updateState : function(e, hide) {
640                     M.util.set_user_preference(this.get('preference'), hide);
641                     if (hide) {
642                         this.get('block').addClass('hidden');
643                         this.get('block').one('.block-hider-show').focus();
644                     } else {
645                         this.get('block').removeClass('hidden');
646                         this.get('block').one('.block-hider-hide').focus();
647                     }
648                 },
649                 updateStateKey : function(e, hide) {
650                     if (e.keyCode == 13) { //allow hide/show via enter key
651                         this.updateState(this, hide);
652                     }
653                 }
654             };
655             Y.extend(blockhider, Y.Base, blockhider.prototype, {
656                 NAME : 'blockhider',
657                 ATTRS : {
658                     id : {},
659                     preference : {},
660                     iconVisible : {
661                         value : M.util.image_url('t/switch_minus', 'moodle')
662                     },
663                     iconHidden : {
664                         value : M.util.image_url('t/switch_plus', 'moodle')
665                     },
666                     block : {
667                         setter : function(node) {
668                             return Y.one(node);
669                         }
670                     }
671                 }
672             });
673             return blockhider;
674         })();
675         new M.util.block_hider(config);
676     });
677 };
679 /**
680  * @var pending_js - The keys are the list of all pending js actions.
681  * @type Object
682  */
683 M.util.pending_js = [];
684 M.util.complete_js = [];
686 /**
687  * Register any long running javascript code with a unique identifier.
688  * Should be followed with a call to js_complete with a matching
689  * idenfitier when the code is complete. May also be called with no arguments
690  * to test if there is any js calls pending. This is relied on by behat so that
691  * it can wait for all pending updates before interacting with a page.
692  * @param String uniqid - optional, if provided,
693  *                        registers this identifier until js_complete is called.
694  * @return boolean - True if there is any pending js.
695  */
696 M.util.js_pending = function(uniqid) {
697     if (uniqid !== false) {
698         M.util.pending_js.push(uniqid);
699     }
701     return M.util.pending_js.length;
702 };
704 // Start this asap.
705 M.util.js_pending('init');
707 /**
708  * Register listeners for Y.io start/end so we can wait for them in behat.
709  */
710 YUI.add('moodle-core-io', function(Y) {
711     Y.on('io:start', function(id) {
712         M.util.js_pending('io:' + id);
713     });
714     Y.on('io:end', function(id) {
715         M.util.js_complete('io:' + id);
716     });
717 }, '@VERSION@', {
718     condition: {
719         trigger: 'io-base',
720         when: 'after'
721     }
722 });
724 /**
725  * Unregister any long running javascript code by unique identifier.
726  * This function should form a matching pair with js_pending
727  *
728  * @param String uniqid - required, unregisters this identifier
729  * @return boolean - True if there is any pending js.
730  */
731 M.util.js_complete = function(uniqid) {
732     // Use the Y.Array.indexOf instead of the native because some older browsers do not support
733     // the native function. Y.Array polyfills the native function if it does not exist.
734     var index = Y.Array.indexOf(M.util.pending_js, uniqid);
735     if (index >= 0) {
736         M.util.complete_js.push(M.util.pending_js.splice(index, 1));
737     }
739     return M.util.pending_js.length;
740 };
742 /**
743  * Returns a string registered in advance for usage in JavaScript
744  *
745  * If you do not pass the third parameter, the function will just return
746  * the corresponding value from the M.str object. If the third parameter is
747  * provided, the function performs {$a} placeholder substitution in the
748  * same way as PHP get_string() in Moodle does.
749  *
750  * @param {String} identifier string identifier
751  * @param {String} component the component providing the string
752  * @param {Object|String} a optional variable to populate placeholder with
753  */
754 M.util.get_string = function(identifier, component, a) {
755     var stringvalue;
757     if (M.cfg.developerdebug) {
758         // creating new instance if YUI is not optimal but it seems to be better way then
759         // require the instance via the function API - note that it is used in rare cases
760         // for debugging only anyway
761         // To ensure we don't kill browser performance if hundreds of get_string requests
762         // are made we cache the instance we generate within the M.util namespace.
763         // We don't publicly define the variable so that it doesn't get abused.
764         if (typeof M.util.get_string_yui_instance === 'undefined') {
765             M.util.get_string_yui_instance = new YUI({ debug : true });
766         }
767         var Y = M.util.get_string_yui_instance;
768     }
770     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
771         stringvalue = '[[' + identifier + ',' + component + ']]';
772         if (M.cfg.developerdebug) {
773             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
774         }
775         return stringvalue;
776     }
778     stringvalue = M.str[component][identifier];
780     if (typeof a == 'undefined') {
781         // no placeholder substitution requested
782         return stringvalue;
783     }
785     if (typeof a == 'number' || typeof a == 'string') {
786         // replace all occurrences of {$a} with the placeholder value
787         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
788         return stringvalue;
789     }
791     if (typeof a == 'object') {
792         // replace {$a->key} placeholders
793         for (var key in a) {
794             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
795                 if (M.cfg.developerdebug) {
796                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
797                 }
798                 continue;
799             }
800             var search = '{$a->' + key + '}';
801             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
802             search = new RegExp(search, 'g');
803             stringvalue = stringvalue.replace(search, a[key]);
804         }
805         return stringvalue;
806     }
808     if (M.cfg.developerdebug) {
809         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
810     }
811     return stringvalue;
812 };
814 /**
815  * Set focus on username or password field of the login form
816  */
817 M.util.focus_login_form = function(Y) {
818     var username = Y.one('#username');
819     var password = Y.one('#password');
821     if (username == null || password == null) {
822         // something is wrong here
823         return;
824     }
826     var curElement = document.activeElement
827     if (curElement == 'undefined') {
828         // legacy browser - skip refocus protection
829     } else if (curElement.tagName == 'INPUT') {
830         // user was probably faster to focus something, do not mess with focus
831         return;
832     }
834     if (username.get('value') == '') {
835         username.focus();
836     } else {
837         password.focus();
838     }
841 /**
842  * Set focus on login error message
843  */
844 M.util.focus_login_error = function(Y) {
845     var errorlog = Y.one('#loginerrormessage');
847     if (errorlog) {
848         errorlog.focus();
849     }
851 /**
852  * Adds lightbox hidden element that covers the whole node.
853  *
854  * @param {YUI} Y
855  * @param {Node} the node lightbox should be added to
856  * @retun {Node} created lightbox node
857  */
858 M.util.add_lightbox = function(Y, node) {
859     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
861     // Check if lightbox is already there
862     if (node.one('.lightbox')) {
863         return node.one('.lightbox');
864     }
866     node.setStyle('position', 'relative');
867     var waiticon = Y.Node.create('<img />')
868     .setAttrs({
869         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
870     })
871     .setStyles({
872         'position' : 'relative',
873         'top' : '50%'
874     });
876     var lightbox = Y.Node.create('<div></div>')
877     .setStyles({
878         'opacity' : '.75',
879         'position' : 'absolute',
880         'width' : '100%',
881         'height' : '100%',
882         'top' : 0,
883         'left' : 0,
884         'backgroundColor' : 'white',
885         'textAlign' : 'center'
886     })
887     .setAttribute('class', 'lightbox')
888     .hide();
890     lightbox.appendChild(waiticon);
891     node.append(lightbox);
892     return lightbox;
895 /**
896  * Appends a hidden spinner element to the specified node.
897  *
898  * @param {YUI} Y
899  * @param {Node} the node the spinner should be added to
900  * @return {Node} created spinner node
901  */
902 M.util.add_spinner = function(Y, node) {
903     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
905     // Check if spinner is already there
906     if (node.one('.spinner')) {
907         return node.one('.spinner');
908     }
910     var spinner = Y.Node.create('<img />')
911         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
912         .addClass('spinner')
913         .addClass('iconsmall')
914         .hide();
916     node.append(spinner);
917     return spinner;
920 /**
921  * @deprecated since Moodle 3.3.
922  */
923 function checkall() {
924     throw new Error('checkall can not be used any more. Please use jQuery instead.');
927 /**
928  * @deprecated since Moodle 3.3.
929  */
930 function checknone() {
931     throw new Error('checknone can not be used any more. Please use jQuery instead.');
934 /**
935  * @deprecated since Moodle 3.3.
936  */
937 function select_all_in_element_with_id(id, checked) {
938     throw new Error('select_all_in_element_with_id can not be used any more. Please use jQuery instead.');
941 /**
942  * @deprecated since Moodle 3.3.
943  */
944 function select_all_in(elTagName, elClass, elId) {
945     throw new Error('select_all_in can not be used any more. Please use jQuery instead.');
948 /**
949  * @deprecated since Moodle 3.3.
950  */
951 function deselect_all_in(elTagName, elClass, elId) {
952     throw new Error('deselect_all_in can not be used any more. Please use jQuery instead.');
955 /**
956  * @deprecated since Moodle 3.3.
957  */
958 function confirm_if(expr, message) {
959     throw new Error('confirm_if can not be used any more.');
962 /**
963  * @deprecated since Moodle 3.3.
964  */
965 function findParentNode(el, elName, elClass, elId) {
966     throw new Error('findParentNode can not be used any more. Please use jQuery instead.');
969 function unmaskPassword(id) {
970     var pw = document.getElementById(id);
971     var chb = document.getElementById(id+'unmask');
973     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
974     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
975     // functionality won't work in IE8 or lower.
976     // This is a temporary fixed to allow other browsers to function properly.
977     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
978         if (chb.checked) {
979             pw.type = "text";
980         } else {
981             pw.type = "password";
982         }
983     } else {  //IE Browser version 8 or lower
984         try {
985             // first try IE way - it can not set name attribute later
986             if (chb.checked) {
987               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
988             } else {
989               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
990             }
991             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
992         } catch (e) {
993             var newpw = document.createElement('input');
994             newpw.setAttribute('autocomplete', 'off');
995             newpw.setAttribute('name', pw.name);
996             if (chb.checked) {
997               newpw.setAttribute('type', 'text');
998             } else {
999               newpw.setAttribute('type', 'password');
1000             }
1001             newpw.setAttribute('class', pw.getAttribute('class'));
1002         }
1003         newpw.id = pw.id;
1004         newpw.size = pw.size;
1005         newpw.onblur = pw.onblur;
1006         newpw.onchange = pw.onchange;
1007         newpw.value = pw.value;
1008         pw.parentNode.replaceChild(newpw, pw);
1009     }
1012 /**
1013  * @deprecated since Moodle 3.3.
1014  */
1015 function filterByParent(elCollection, parentFinder) {
1016     throw new Error('filterByParent can not be used any more. Please use jQuery instead.');
1019 /**
1020  * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
1021  */
1022 function fix_column_widths() {
1023     Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
1026 /**
1027  * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
1028  */
1029 function fix_column_width(colName) {
1030     Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
1034 /*
1035    Insert myValue at current cursor position
1036  */
1037 function insertAtCursor(myField, myValue) {
1038     // IE support
1039     if (document.selection) {
1040         myField.focus();
1041         sel = document.selection.createRange();
1042         sel.text = myValue;
1043     }
1044     // Mozilla/Netscape support
1045     else if (myField.selectionStart || myField.selectionStart == '0') {
1046         var startPos = myField.selectionStart;
1047         var endPos = myField.selectionEnd;
1048         myField.value = myField.value.substring(0, startPos)
1049             + myValue + myField.value.substring(endPos, myField.value.length);
1050     } else {
1051         myField.value += myValue;
1052     }
1055 /**
1056  * Increment a file name.
1057  *
1058  * @param string file name.
1059  * @param boolean ignoreextension do not extract the extension prior to appending the
1060  *                                suffix. Useful when incrementing folder names.
1061  * @return string the incremented file name.
1062  */
1063 function increment_filename(filename, ignoreextension) {
1064     var extension = '';
1065     var basename = filename;
1067     // Split the file name into the basename + extension.
1068     if (!ignoreextension) {
1069         var dotpos = filename.lastIndexOf('.');
1070         if (dotpos !== -1) {
1071             basename = filename.substr(0, dotpos);
1072             extension = filename.substr(dotpos, filename.length);
1073         }
1074     }
1076     // Look to see if the name already has (NN) at the end of it.
1077     var number = 0;
1078     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1079     if (hasnumber !== null) {
1080         // Note the current number & remove it from the basename.
1081         number = parseInt(hasnumber[2], 10);
1082         basename = hasnumber[1];
1083     }
1085     number++;
1086     var newname = basename + ' (' + number + ')' + extension;
1087     return newname;
1090 /**
1091  * Return whether we are in right to left mode or not.
1092  *
1093  * @return boolean
1094  */
1095 function right_to_left() {
1096     var body = Y.one('body');
1097     var rtl = false;
1098     if (body && body.hasClass('dir-rtl')) {
1099         rtl = true;
1100     }
1101     return rtl;
1104 function openpopup(event, args) {
1106     if (event) {
1107         if (event.preventDefault) {
1108             event.preventDefault();
1109         } else {
1110             event.returnValue = false;
1111         }
1112     }
1114     // Make sure the name argument is set and valid.
1115     var nameregex = /[^a-z0-9_]/i;
1116     if (typeof args.name !== 'string') {
1117         args.name = '_blank';
1118     } else if (args.name.match(nameregex)) {
1119         // Cleans window name because IE does not support funky ones.
1120         if (M.cfg.developerdebug) {
1121             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1122         }
1123         args.name = args.name.replace(nameregex, '_');
1124     }
1126     var fullurl = args.url;
1127     if (!args.url.match(/https?:\/\//)) {
1128         fullurl = M.cfg.wwwroot + args.url;
1129     }
1130     if (args.fullscreen) {
1131         args.options = args.options.
1132                 replace(/top=\d+/, 'top=0').
1133                 replace(/left=\d+/, 'left=0').
1134                 replace(/width=\d+/, 'width=' + screen.availWidth).
1135                 replace(/height=\d+/, 'height=' + screen.availHeight);
1136     }
1137     var windowobj = window.open(fullurl,args.name,args.options);
1138     if (!windowobj) {
1139         return true;
1140     }
1142     if (args.fullscreen) {
1143         // In some browser / OS combinations (E.g. Chrome on Windows), the
1144         // window initially opens slighly too big. The width and heigh options
1145         // seem to control the area inside the browser window, so what with
1146         // scroll-bars, etc. the actual window is bigger than the screen.
1147         // Therefore, we need to fix things up after the window is open.
1148         var hackcount = 100;
1149         var get_size_exactly_right = function() {
1150             windowobj.moveTo(0, 0);
1151             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1153             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1154             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1155             // about 50ms) after the window is open, then it actually behaves
1156             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1157             // check that the resize actually worked, and if not, repeatedly try
1158             // again after a short delay until it works (but with a limit of
1159             // hackcount repeats.
1160             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1161                 hackcount -= 1;
1162                 setTimeout(get_size_exactly_right, 10);
1163             }
1164         }
1165         setTimeout(get_size_exactly_right, 0);
1166     }
1167     windowobj.focus();
1169     return false;
1172 /** Close the current browser window. */
1173 function close_window(e) {
1174     if (e.preventDefault) {
1175         e.preventDefault();
1176     } else {
1177         e.returnValue = false;
1178     }
1179     window.close();
1182 /**
1183  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1184  * @param controlid the control id.
1185  */
1186 function focuscontrol(controlid) {
1187     var control = document.getElementById(controlid);
1188     if (control) {
1189         control.focus();
1190     }
1193 /**
1194  * Transfers keyboard focus to an HTML element based on the old style style of focus
1195  * This function should be removed as soon as it is no longer used
1196  */
1197 function old_onload_focus(formid, controlname) {
1198     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1199         document.forms[formid].elements[controlname].focus();
1200     }
1203 function build_querystring(obj) {
1204     return convert_object_to_string(obj, '&');
1207 function build_windowoptionsstring(obj) {
1208     return convert_object_to_string(obj, ',');
1211 function convert_object_to_string(obj, separator) {
1212     if (typeof obj !== 'object') {
1213         return null;
1214     }
1215     var list = [];
1216     for(var k in obj) {
1217         k = encodeURIComponent(k);
1218         var value = obj[k];
1219         if(obj[k] instanceof Array) {
1220             for(var i in value) {
1221                 list.push(k+'[]='+encodeURIComponent(value[i]));
1222             }
1223         } else {
1224             list.push(k+'='+encodeURIComponent(value));
1225         }
1226     }
1227     return list.join(separator);
1230 /**
1231  * @deprecated since Moodle 3.3.
1232  */
1233 function stripHTML(str) {
1234     throw new Error('stripHTML can not be used any more. Please use jQuery instead.');
1237 function updateProgressBar(id, percent, msg, estimate) {
1238     var event,
1239         el = document.getElementById(id),
1240         eventData = {};
1242     if (!el) {
1243         return;
1244     }
1246     eventData.message = msg;
1247     eventData.percent = percent;
1248     eventData.estimate = estimate;
1250     try {
1251         event = new CustomEvent('update', {
1252             bubbles: false,
1253             cancelable: true,
1254             detail: eventData
1255         });
1256     } catch (exception) {
1257         if (!(exception instanceof TypeError)) {
1258             throw exception;
1259         }
1260         event = document.createEvent('CustomEvent');
1261         event.initCustomEvent('update', false, true, eventData);
1262         event.prototype = window.Event.prototype;
1263     }
1265     el.dispatchEvent(event);
1268 M.util.help_popups = {
1269     setup : function(Y) {
1270         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1271     },
1272     open_popup : function(e) {
1273         // Prevent the default page action
1274         e.preventDefault();
1276         // Grab the anchor that was clicked
1277         var anchor = e.target.ancestor('a', true);
1278         var args = {
1279             'name'          : 'popup',
1280             'url'           : anchor.getAttribute('href'),
1281             'options'       : ''
1282         };
1283         var options = [
1284             'height=600',
1285             'width=800',
1286             'top=0',
1287             'left=0',
1288             'menubar=0',
1289             'location=0',
1290             'scrollbars',
1291             'resizable',
1292             'toolbar',
1293             'status',
1294             'directories=0',
1295             'fullscreen=0',
1296             'dependent'
1297         ]
1298         args.options = options.join(',');
1300         openpopup(e, args);
1301     }
1304 /**
1305  * Custom menu namespace
1306  */
1307 M.core_custom_menu = {
1308     /**
1309      * This method is used to initialise a custom menu given the id that belongs
1310      * to the custom menu's root node.
1311      *
1312      * @param {YUI} Y
1313      * @param {string} nodeid
1314      */
1315     init : function(Y, nodeid) {
1316         var node = Y.one('#'+nodeid);
1317         if (node) {
1318             Y.use('node-menunav', function(Y) {
1319                 // Get the node
1320                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1321                 node.removeClass('javascript-disabled');
1322                 // Initialise the menunav plugin
1323                 node.plug(Y.Plugin.NodeMenuNav);
1324             });
1325         }
1326     }
1327 };
1329 /**
1330  * Used to store form manipulation methods and enhancments
1331  */
1332 M.form = M.form || {};
1334 /**
1335  * Converts a nbsp indented select box into a multi drop down custom control much
1336  * like the custom menu. Can no longer be used.
1337  * @deprecated since Moodle 3.3
1338  */
1339 M.form.init_smartselect = function() {
1340     throw new Error('M.form.init_smartselect can not be used any more.');
1341 };
1343 /**
1344  * Initiates the listeners for skiplink interaction
1345  *
1346  * @param {YUI} Y
1347  */
1348 M.util.init_skiplink = function(Y) {
1349     Y.one(Y.config.doc.body).delegate('click', function(e) {
1350         e.preventDefault();
1351         e.stopPropagation();
1352         var node = Y.one(this.getAttribute('href'));
1353         node.setAttribute('tabindex', '-1');
1354         node.focus();
1355         return true;
1356     }, 'a.skip');
1357 };