Merge branch 'MDL-82530-spelling' of https://github.com/leonstr/moodle
[moodle.git] / lib / javascript-static.js
blobbca7adc7ee090086f9ce8eddad69cecfbb82fab2
1 /* eslint-disable camelcase */
2 // Miscellaneous core Javascript functions for Moodle
3 // Global M object is initilised in inline javascript
5 /**
6  * Add module to list of available modules that can be loaded from YUI.
7  * @param {Array} modules
8  */
9 M.yui.add_module = function(modules) {
10     for (var modname in modules) {
11         YUI_config.modules[modname] = modules[modname];
12     }
13     // Ensure thaat the YUI_config is applied to the main YUI instance.
14     Y.applyConfig(YUI_config);
16 /**
17  * The gallery version to use when loading YUI modules from the gallery.
18  * Will be changed every time when using local galleries.
19  */
20 M.yui.galleryversion = '2010.04.21-21-51';
22 /**
23  * Various utility functions
24  */
25 M.util = M.util || {};
27 /**
28  * Language strings - initialised from page footer.
29  */
30 M.str = M.str || {};
32 /**
33  * Returns url for images.
34  * @param {String} imagename
35  * @param {String} component
36  * @return {String}
37  */
38 M.util.image_url = function(imagename, component) {
40     if (!component || component == '' || component == 'moodle' || component == 'core') {
41         component = 'core';
42     }
44     var url = M.cfg.wwwroot + '/theme/image.php';
45     if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
46         if (!M.cfg.svgicons) {
47             url += '/_s';
48         }
49         url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
50     } else {
51         url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
52         if (!M.cfg.svgicons) {
53             url += '&svg=0';
54         }
55     }
57     return url;
60 M.util.in_array = function(item, array) {
61     return array.indexOf(item) !== -1;
64 /**
65  * Init a collapsible region, see print_collapsible_region in weblib.php
66  * @param {YUI} Y YUI3 instance with all libraries loaded
67  * @param {String} id the HTML id for the div.
68  * @param {String} userpref the user preference that records the state of this box. false if none.
69  * @param {String} strtooltip
70  */
71 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
72     Y.use('anim', function(Y) {
73         new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
74     });
77 /**
78  * Object to handle a collapsible region : instantiate and forget styled object
79  *
80  * @class
81  * @constructor
82  * @param {YUI} Y YUI3 instance with all libraries loaded
83  * @param {String} id The HTML id for the div.
84  * @param {String} userpref The user preference that records the state of this box. false if none.
85  * @param {String} strtooltip
86  */
87 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
88     // Record the pref name
89     this.userpref = userpref;
91     // Find the divs in the document.
92     this.div = Y.one('#'+id);
94     // Get the caption for the collapsible region
95     var caption = this.div.one('#'+id + '_caption');
97     // Create a link
98     var a = Y.Node.create('<a href="#"></a>');
99     a.setAttribute('title', strtooltip);
101     // Get all the nodes from caption, remove them and append them to <a>
102     while (caption.hasChildNodes()) {
103         child = caption.get('firstChild');
104         child.remove();
105         a.append(child);
106     }
107     caption.append(a);
109     // Get the height of the div at this point before we shrink it if required
110     var height = this.div.get('offsetHeight');
111     var collapsedimage = 't/collapsed'; // ltr mode
112     if (right_to_left()) {
113         collapsedimage = 't/collapsed_rtl';
114     } else {
115         collapsedimage = 't/collapsed';
116     }
117     if (this.div.hasClass('collapsed')) {
118         // Add the correct image and record the YUI node created in the process
119         this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
120         // Shrink the div as it is collapsed by default
121         this.div.setStyle('height', caption.get('offsetHeight')+'px');
122     } else {
123         // Add the correct image and record the YUI node created in the process
124         this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
125     }
126     a.append(this.icon);
128     // Create the animation.
129     var animation = new Y.Anim({
130         node: this.div,
131         duration: 0.3,
132         easing: Y.Easing.easeBoth,
133         to: {height:caption.get('offsetHeight')},
134         from: {height:height}
135     });
137     animation.on('start', () => M.util.js_pending('CollapsibleRegion'));
138     animation.on('resume', () => M.util.js_pending('CollapsibleRegion'));
139     animation.on('pause', () => M.util.js_complete('CollapsibleRegion'));
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         }
156         M.util.js_complete('CollapsibleRegion');
157     }, this);
159     // Hook up the event handler.
160     a.on('click', function(e, animation) {
161         e.preventDefault();
162         // Animate to the appropriate size.
163         if (animation.get('running')) {
164             animation.stop();
165         }
166         animation.set('reverse', this.div.hasClass('collapsed'));
167         // Update the user preference.
168         if (this.userpref) {
169             require(['core_user/repository'], function(UserRepository) {
170                 UserRepository.setUserPreference(this.userpref, !this.div.hasClass('collapsed'));
171             }.bind(this));
172         }
173         animation.run();
174     }, this, animation);
178  * The user preference that stores the state of this box.
179  * @property userpref
180  * @type String
181  */
182 M.util.CollapsibleRegion.prototype.userpref = null;
185  * The key divs that make up this
186  * @property div
187  * @type Y.Node
188  */
189 M.util.CollapsibleRegion.prototype.div = null;
192  * The key divs that make up this
193  * @property icon
194  * @type Y.Node
195  */
196 M.util.CollapsibleRegion.prototype.icon = null;
199  * Makes a best effort to connect back to Moodle to update a user preference,
200  * however, there is no mechanism for finding out if the update succeeded.
202  * Before you can use this function in your JavsScript, you must have called
203  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
204  * the udpate is allowed, and how to safely clean and submitted values.
206  * @param {String} name the name of the setting to update.
207  * @param {String} value the value to set it to.
209  * @deprecated since Moodle 4.3.
210  */
211 M.util.set_user_preference = function(name, value) {
212     Y.log('M.util.set_user_preference is deprecated. Please use the "core_user/repository" module instead.', 'warn');
214     require(['core_user/repository'], function(UserRepository) {
215         UserRepository.setUserPreference(name, value);
216     });
220  * Prints a confirmation dialog in the style of DOM.confirm().
222  * @method show_confirm_dialog
223  * @param {EventFacade} e
224  * @param {Object} args
225  * @param {String} args.message The question to ask the user
226  * @param {Function} [args.callback] A callback to apply on confirmation.
227  * @param {Object} [args.scope] The scope to use when calling the callback.
228  * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
229  * @param {String} [args.cancellabel] The label to use on the cancel button.
230  * @param {String} [args.continuelabel] The label to use on the continue button.
231  */
232 M.util.show_confirm_dialog = (e, {
233     message,
234     continuelabel,
235     callback = null,
236     scope = null,
237     callbackargs = [],
238 } = {}) => {
239     if (e.preventDefault) {
240         e.preventDefault();
241     }
243     require(
244         ['core/notification', 'core/str', 'core_form/changechecker', 'core/normalise'],
245         function(Notification, Str, FormChangeChecker, Normalise) {
247             if (scope === null && e.target) {
248                 // Fall back to the event target if no scope provided.
249                 scope = e.target;
250             }
252             Notification.saveCancelPromise(
253                 Str.get_string('confirmation', 'admin'),
254                 message,
255                 continuelabel || Str.get_string('yes', 'moodle'),
256             )
257             .then(() => {
258                 if (callback) {
259                     callback.apply(scope, callbackargs);
260                     return;
261                 }
263                 if (!e.target) {
264                     window.console.error(
265                         `M.util.show_confirm_dialog: No target found for event`,
266                         e
267                     );
268                     return;
269                 }
271                 const target = Normalise.getElement(e.target);
273                 if (target.closest('a')) {
274                     window.location = target.closest('a').getAttribute('href');
275                     return;
276                 } else if (target.closest('input') || target.closest('button')) {
277                     const form = target.closest('form');
278                     const hiddenValue = document.createElement('input');
279                     hiddenValue.setAttribute('type', 'hidden');
280                     hiddenValue.setAttribute('name', target.getAttribute('name'));
281                     hiddenValue.setAttribute('value', target.getAttribute('value'));
282                     form.appendChild(hiddenValue);
283                     FormChangeChecker.markFormAsDirty(form);
284                     form.submit();
285                     return;
286                 } else if (target.closest('form')) {
287                     const form = target.closest('form');
288                     FormChangeChecker.markFormAsDirty(form);
289                     form.submit();
290                     return;
291                 }
292                 window.console.error(
293                     `Element of type ${target.tagName} is not supported by M.util.show_confirm_dialog.`
294                 );
296                 return;
297             })
298             .catch(() => {
299                 // User cancelled.
300                 return;
301             });
302         }
303     );
306 /** Useful for full embedding of various stuff */
307 M.util.init_maximised_embed = function(Y, id) {
308     var obj = Y.one('#'+id);
309     if (!obj) {
310         return;
311     }
313     var get_htmlelement_size = function(el, prop) {
314         if (Y.Lang.isString(el)) {
315             el = Y.one('#' + el);
316         }
317         // Ensure element exists.
318         if (el) {
319             var val = el.getStyle(prop);
320             if (val == 'auto') {
321                 val = el.getComputedStyle(prop);
322             }
323             val = parseInt(val);
324             if (isNaN(val)) {
325                 return 0;
326             }
327             return val;
328         } else {
329             return 0;
330         }
331     };
333     var resize_object = function() {
334         obj.setStyle('display', 'none');
335         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
337         if (newwidth > 500) {
338             obj.setStyle('width', newwidth  + 'px');
339         } else {
340             obj.setStyle('width', '500px');
341         }
343         var headerheight = get_htmlelement_size('page-header', 'height');
344         var footerheight = get_htmlelement_size('page-footer', 'height');
345         var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
346         if (newheight < 400) {
347             newheight = 400;
348         }
349         obj.setStyle('height', newheight+'px');
350         obj.setStyle('display', '');
351     };
353     resize_object();
354     // fix layout if window resized too
355     Y.use('event-resize', function (Y) {
356         Y.on("windowresize", function() {
357             resize_object();
358         });
359     });
363  * Breaks out all links to the top frame - used in frametop page layout.
364  */
365 M.util.init_frametop = function(Y) {
366     Y.all('a').each(function(node) {
367         node.set('target', '_top');
368     });
369     Y.all('form').each(function(node) {
370         node.set('target', '_top');
371     });
375  * @deprecated since Moodle 3.3
376  */
377 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
378     throw new Error('M.util.init_toggle_class_on_click can not be used any more. Please use jQuery instead.');
382  * Initialises a colour picker
384  * Designed to be used with admin_setting_configcolourpicker although could be used
385  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
386  * above or below the input (must have the same parent) and then call this with the
387  * id.
389  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
390  * contrib/blocks. For better docs refer to that.
392  * @param {YUI} Y
393  * @param {int} id
394  * @param {object} previewconf
395  */
396 M.util.init_colour_picker = function(Y, id, previewconf) {
397     /**
398      * We need node and event-mouseenter
399      */
400     Y.use('node', 'event-mouseenter', function(){
401         /**
402          * The colour picker object
403          */
404         var colourpicker = {
405             box : null,
406             input : null,
407             image : null,
408             preview : null,
409             current : null,
410             eventClick : null,
411             eventMouseEnter : null,
412             eventMouseLeave : null,
413             eventMouseMove : null,
414             width : 300,
415             height :  100,
416             factor : 5,
417             /**
418              * Initalises the colour picker by putting everything together and wiring the events
419              */
420             init : function() {
421                 this.input = Y.one('#'+id);
422                 this.box = this.input.ancestor().one('.admin_colourpicker');
423                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
424                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
425                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
426                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
427                 this.current = Y.Node.create('<div class="currentcolour"></div>');
428                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
429                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
431                 if (typeof(previewconf) === 'object' && previewconf !== null) {
432                     Y.one('#'+id+'_preview').on('click', function(e){
433                         if (Y.Lang.isString(previewconf.selector)) {
434                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
435                         } else {
436                             for (var i in previewconf.selector) {
437                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
438                             }
439                         }
440                     }, this);
441                 }
443                 this.eventClick = this.image.on('click', this.pickColour, this);
444                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
445             },
446             /**
447              * Starts to follow the mouse once it enter the image
448              */
449             startFollow : function(e) {
450                 this.eventMouseEnter.detach();
451                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
452                 this.eventMouseMove = this.image.on('mousemove', function(e){
453                     this.preview.setStyle('backgroundColor', this.determineColour(e));
454                 }, this);
455             },
456             /**
457              * Stops following the mouse
458              */
459             endFollow : function(e) {
460                 this.eventMouseMove.detach();
461                 this.eventMouseLeave.detach();
462                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
463             },
464             /**
465              * Picks the colour the was clicked on
466              */
467             pickColour : function(e) {
468                 var colour = this.determineColour(e);
469                 this.input.set('value', colour);
470                 this.current.setStyle('backgroundColor', colour);
471             },
472             /**
473              * Calculates the colour fromthe given co-ordinates
474              */
475             determineColour : function(e) {
476                 var eventx = Math.floor(e.pageX-e.target.getX());
477                 var eventy = Math.floor(e.pageY-e.target.getY());
479                 var imagewidth = this.width;
480                 var imageheight = this.height;
481                 var factor = this.factor;
482                 var colour = [255,0,0];
484                 var matrices = [
485                     [  0,  1,  0],
486                     [ -1,  0,  0],
487                     [  0,  0,  1],
488                     [  0, -1,  0],
489                     [  1,  0,  0],
490                     [  0,  0, -1]
491                 ];
493                 var matrixcount = matrices.length;
494                 var limit = Math.round(imagewidth/matrixcount);
495                 var heightbreak = Math.round(imageheight/2);
497                 for (var x = 0; x < imagewidth; x++) {
498                     var divisor = Math.floor(x / limit);
499                     var matrix = matrices[divisor];
501                     colour[0] += matrix[0]*factor;
502                     colour[1] += matrix[1]*factor;
503                     colour[2] += matrix[2]*factor;
505                     if (eventx==x) {
506                         break;
507                     }
508                 }
510                 var pixel = [colour[0], colour[1], colour[2]];
511                 if (eventy < heightbreak) {
512                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
513                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
514                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
515                 } else if (eventy > heightbreak) {
516                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
517                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
518                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
519                 }
521                 return this.convert_rgb_to_hex(pixel);
522             },
523             /**
524              * Converts an RGB value to Hex
525              */
526             convert_rgb_to_hex : function(rgb) {
527                 var hex = '#';
528                 var hexchars = "0123456789ABCDEF";
529                 for (var i=0; i<3; i++) {
530                     var number = Math.abs(rgb[i]);
531                     if (number == 0 || isNaN(number)) {
532                         hex += '00';
533                     } else {
534                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
535                     }
536                 }
537                 return hex;
538             }
539         };
540         /**
541          * Initialise the colour picker :) Hoorah
542          */
543         colourpicker.init();
544     });
547 M.util.init_block_hider = function(Y, config) {
548     Y.use('base', 'node', function(Y) {
549         M.util.block_hider = M.util.block_hider || (function(){
550             var blockhider = function() {
551                 blockhider.superclass.constructor.apply(this, arguments);
552             };
553             blockhider.prototype = {
554                 initializer : function(config) {
555                     this.set('block', '#'+this.get('id'));
556                     var b = this.get('block'),
557                         t = b.one('.title'),
558                         a = null,
559                         hide,
560                         show;
561                     if (t && (a = t.one('.block_action'))) {
562                         hide = Y.Node.create('<img />')
563                             .addClass('block-hider-hide')
564                             .setAttrs({
565                                 alt:        config.tooltipVisible,
566                                 src:        this.get('iconVisible'),
567                                 tabIndex:   0,
568                                 'title':    config.tooltipVisible
569                             });
570                         hide.on('keypress', this.updateStateKey, this, true);
571                         hide.on('click', this.updateState, this, true);
573                         show = Y.Node.create('<img />')
574                             .addClass('block-hider-show')
575                             .setAttrs({
576                                 alt:        config.tooltipHidden,
577                                 src:        this.get('iconHidden'),
578                                 tabIndex:   0,
579                                 'title':    config.tooltipHidden
580                             });
581                         show.on('keypress', this.updateStateKey, this, false);
582                         show.on('click', this.updateState, this, false);
584                         a.insert(show, 0).insert(hide, 0);
585                     }
586                 },
587                 updateState : function(e, hide) {
588                     require(['core_user/repository'], function(UserRepository) {
589                         UserRepository.setUserPreference(this.get('preference'), hide);
590                     }.bind(this));
591                     if (hide) {
592                         this.get('block').addClass('hidden');
593                         this.get('block').one('.block-hider-show').focus();
594                     } else {
595                         this.get('block').removeClass('hidden');
596                         this.get('block').one('.block-hider-hide').focus();
597                     }
598                 },
599                 updateStateKey : function(e, hide) {
600                     if (e.keyCode == 13) { //allow hide/show via enter key
601                         this.updateState(this, hide);
602                     }
603                 }
604             };
605             Y.extend(blockhider, Y.Base, blockhider.prototype, {
606                 NAME : 'blockhider',
607                 ATTRS : {
608                     id : {},
609                     preference : {},
610                     iconVisible : {
611                         value : M.util.image_url('t/switch_minus', 'moodle')
612                     },
613                     iconHidden : {
614                         value : M.util.image_url('t/switch_plus', 'moodle')
615                     },
616                     block : {
617                         setter : function(node) {
618                             return Y.one(node);
619                         }
620                     }
621                 }
622             });
623             return blockhider;
624         })();
625         new M.util.block_hider(config);
626     });
630  * @var pending_js - The keys are the list of all pending js actions.
631  * @type Object
632  */
633 M.util.pending_js = [];
634 M.util.complete_js = [];
637  * Register any long running javascript code with a unique identifier.
638  * This is used to ensure that Behat steps do not continue with interactions until the page finishes loading.
640  * All calls to M.util.js_pending _must_ be followed by a subsequent call to M.util.js_complete with the same exact
641  * uniqid.
643  * This function may also be called with no arguments to test if there is any js calls pending.
645  * The uniqid specified may be any Object, including Number, String, or actual Object; however please note that the
646  * paired js_complete function performs a strict search for the key specified. As such, if using an Object, the exact
647  * Object must be passed into both functions.
649  * @param   {Mixed}     uniqid Register long-running code against the supplied identifier
650  * @return  {Number}    Number of pending items
651  */
652 M.util.js_pending = function(uniqid) {
653     if (typeof uniqid !== 'undefined') {
654         M.util.pending_js.push(uniqid);
655     }
657     return M.util.pending_js.length;
660 // Start this asap.
661 M.util.js_pending('init');
664  * Register listeners for Y.io start/end so we can wait for them in behat.
665  */
666 YUI.add('moodle-core-io', function(Y) {
667     Y.on('io:start', function(id) {
668         M.util.js_pending('io:' + id);
669     });
670     Y.on('io:end', function(id) {
671         M.util.js_complete('io:' + id);
672     });
673 }, '@VERSION@', {
674     condition: {
675         trigger: 'io-base',
676         when: 'after'
677     }
681  * Unregister some long running javascript code using the unique identifier specified in M.util.js_pending.
683  * This function must be matched with an identical call to M.util.js_pending.
685  * @param   {Mixed}     uniqid Register long-running code against the supplied identifier
686  * @return  {Number}    Number of pending items remaining after removing this item
687  */
688 M.util.js_complete = function(uniqid) {
689     const index = M.util.pending_js.indexOf(uniqid);
690     if (index >= 0) {
691         M.util.complete_js.push(M.util.pending_js.splice(index, 1)[0]);
692     } else {
693         window.console.log("Unable to locate key for js_complete call", uniqid);
694     }
696     return M.util.pending_js.length;
700  * Returns a string registered in advance for usage in JavaScript
702  * If you do not pass the third parameter, the function will just return
703  * the corresponding value from the M.str object. If the third parameter is
704  * provided, the function performs {$a} placeholder substitution in the
705  * same way as PHP get_string() in Moodle does.
707  * @param {String} identifier string identifier
708  * @param {String} component the component providing the string
709  * @param {Object|String} [a] optional variable to populate placeholder with
710  */
711 M.util.get_string = function(identifier, component, a) {
712     var stringvalue;
714     if (M.cfg.developerdebug) {
715         // creating new instance if YUI is not optimal but it seems to be better way then
716         // require the instance via the function API - note that it is used in rare cases
717         // for debugging only anyway
718         // To ensure we don't kill browser performance if hundreds of get_string requests
719         // are made we cache the instance we generate within the M.util namespace.
720         // We don't publicly define the variable so that it doesn't get abused.
721         if (typeof M.util.get_string_yui_instance === 'undefined') {
722             M.util.get_string_yui_instance = new YUI({ debug : true });
723         }
724         var Y = M.util.get_string_yui_instance;
725     }
727     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
728         stringvalue = '[[' + identifier + ',' + component + ']]';
729         if (M.cfg.developerdebug) {
730             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
731         }
732         return stringvalue;
733     }
735     stringvalue = M.str[component][identifier];
737     if (typeof a == 'undefined') {
738         // no placeholder substitution requested
739         return stringvalue;
740     }
742     if (typeof a == 'number' || typeof a == 'string') {
743         // replace all occurrences of {$a} with the placeholder value
744         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
745         return stringvalue;
746     }
748     if (typeof a == 'object') {
749         // replace {$a->key} placeholders
750         for (var key in a) {
751             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
752                 if (M.cfg.developerdebug) {
753                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
754                 }
755                 continue;
756             }
757             var search = '{$a->' + key + '}';
758             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
759             search = new RegExp(search, 'g');
760             stringvalue = stringvalue.replace(search, a[key]);
761         }
762         return stringvalue;
763     }
765     if (M.cfg.developerdebug) {
766         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
767     }
768     return stringvalue;
772  * Set focus on username or password field of the login form.
773  * @deprecated since Moodle 3.3.
774  */
775 M.util.focus_login_form = function(Y) {
776     Y.log('M.util.focus_login_form no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
780  * Set focus on login error message.
781  * @deprecated since Moodle 3.3.
782  */
783 M.util.focus_login_error = function(Y) {
784     Y.log('M.util.focus_login_error no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
788  * Adds lightbox hidden element that covers the whole node.
790  * @param {YUI} Y
791  * @param {Node} the node lightbox should be added to
792  * @retun {Node} created lightbox node
793  */
794 M.util.add_lightbox = function(Y, node) {
795     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
797     // Check if lightbox is already there
798     if (node.one('.lightbox')) {
799         return node.one('.lightbox');
800     }
802     node.setStyle('position', 'relative');
803     var waiticon = Y.Node.create('<img />')
804     .setAttrs({
805         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
806     })
807     .setStyles({
808         'position' : 'relative',
809         'top' : '50%'
810     });
812     var lightbox = Y.Node.create('<div></div>')
813     .setStyles({
814         'opacity' : '.75',
815         'position' : 'absolute',
816         'width' : '100%',
817         'height' : '100%',
818         'top' : 0,
819         'left' : 0,
820         'backgroundColor' : 'white',
821         'textAlign' : 'center'
822     })
823     .setAttribute('class', 'lightbox')
824     .hide();
826     lightbox.appendChild(waiticon);
827     node.append(lightbox);
828     return lightbox;
832  * Appends a hidden spinner element to the specified node.
834  * @param {YUI} Y
835  * @param {Node} the node the spinner should be added to
836  * @return {Node} created spinner node
837  */
838 M.util.add_spinner = function(Y, node) {
839     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
841     // Check if spinner is already there
842     if (node.one('.spinner')) {
843         return node.one('.spinner');
844     }
846     var spinner = Y.Node.create('<img />')
847         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
848         .addClass('spinner')
849         .addClass('iconsmall')
850         .hide();
852     node.append(spinner);
853     return spinner;
857  * @deprecated since Moodle 3.3.
858  */
859 function checkall() {
860     throw new Error('checkall can not be used any more. Please use jQuery instead.');
864  * @deprecated since Moodle 3.3.
865  */
866 function checknone() {
867     throw new Error('checknone can not be used any more. Please use jQuery instead.');
871  * @deprecated since Moodle 3.3.
872  */
873 function select_all_in_element_with_id(id, checked) {
874     throw new Error('select_all_in_element_with_id can not be used any more. Please use jQuery instead.');
878  * @deprecated since Moodle 3.3.
879  */
880 function select_all_in(elTagName, elClass, elId) {
881     throw new Error('select_all_in can not be used any more. Please use jQuery instead.');
885  * @deprecated since Moodle 3.3.
886  */
887 function deselect_all_in(elTagName, elClass, elId) {
888     throw new Error('deselect_all_in can not be used any more. Please use jQuery instead.');
892  * @deprecated since Moodle 3.3.
893  */
894 function confirm_if(expr, message) {
895     throw new Error('confirm_if can not be used any more.');
899  * @deprecated since Moodle 3.3.
900  */
901 function findParentNode(el, elName, elClass, elId) {
902     throw new Error('findParentNode can not be used any more. Please use jQuery instead.');
905 function unmaskPassword(id) {
906     var pw = document.getElementById(id);
907     var chb = document.getElementById(id+'unmask');
909     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
910     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
911     // functionality won't work in IE8 or lower.
912     // This is a temporary fixed to allow other browsers to function properly.
913     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
914         if (chb.checked) {
915             pw.type = "text";
916         } else {
917             pw.type = "password";
918         }
919     } else {  //IE Browser version 8 or lower
920         try {
921             // first try IE way - it can not set name attribute later
922             if (chb.checked) {
923               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
924             } else {
925               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
926             }
927             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
928         } catch (e) {
929             var newpw = document.createElement('input');
930             newpw.setAttribute('autocomplete', 'off');
931             newpw.setAttribute('name', pw.name);
932             if (chb.checked) {
933               newpw.setAttribute('type', 'text');
934             } else {
935               newpw.setAttribute('type', 'password');
936             }
937             newpw.setAttribute('class', pw.getAttribute('class'));
938         }
939         newpw.id = pw.id;
940         newpw.size = pw.size;
941         newpw.onblur = pw.onblur;
942         newpw.onchange = pw.onchange;
943         newpw.value = pw.value;
944         pw.parentNode.replaceChild(newpw, pw);
945     }
949  * @deprecated since Moodle 3.3.
950  */
951 function filterByParent(elCollection, parentFinder) {
952     throw new Error('filterByParent can not be used any more. Please use jQuery instead.');
956  * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
957  */
958 function fix_column_widths() {
959     Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
963  * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
964  */
965 function fix_column_width(colName) {
966     Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
971    Insert myValue at current cursor position
972  */
973 function insertAtCursor(myField, myValue) {
974     // IE support
975     if (document.selection) {
976         myField.focus();
977         sel = document.selection.createRange();
978         sel.text = myValue;
979     }
980     // Mozilla/Netscape support
981     else if (myField.selectionStart || myField.selectionStart == '0') {
982         var startPos = myField.selectionStart;
983         var endPos = myField.selectionEnd;
984         myField.value = myField.value.substring(0, startPos)
985             + myValue + myField.value.substring(endPos, myField.value.length);
986     } else {
987         myField.value += myValue;
988     }
992  * Increment a file name.
994  * @param string file name.
995  * @param boolean ignoreextension do not extract the extension prior to appending the
996  *                                suffix. Useful when incrementing folder names.
997  * @return string the incremented file name.
998  */
999 function increment_filename(filename, ignoreextension) {
1000     var extension = '';
1001     var basename = filename;
1003     // Split the file name into the basename + extension.
1004     if (!ignoreextension) {
1005         var dotpos = filename.lastIndexOf('.');
1006         if (dotpos !== -1) {
1007             basename = filename.substr(0, dotpos);
1008             extension = filename.substr(dotpos, filename.length);
1009         }
1010     }
1012     // Look to see if the name already has (NN) at the end of it.
1013     var number = 0;
1014     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1015     if (hasnumber !== null) {
1016         // Note the current number & remove it from the basename.
1017         number = parseInt(hasnumber[2], 10);
1018         basename = hasnumber[1];
1019     }
1021     number++;
1022     var newname = basename + ' (' + number + ')' + extension;
1023     return newname;
1027  * Return whether we are in right to left mode or not.
1029  * @return boolean
1030  */
1031 function right_to_left() {
1032     var body = Y.one('body');
1033     var rtl = false;
1034     if (body && body.hasClass('dir-rtl')) {
1035         rtl = true;
1036     }
1037     return rtl;
1040 function openpopup(event, args) {
1042     if (event) {
1043         if (event.preventDefault) {
1044             event.preventDefault();
1045         } else {
1046             event.returnValue = false;
1047         }
1048     }
1050     // Make sure the name argument is set and valid.
1051     var nameregex = /[^a-z0-9_]/i;
1052     if (typeof args.name !== 'string') {
1053         args.name = '_blank';
1054     } else if (args.name.match(nameregex)) {
1055         // Cleans window name because IE does not support funky ones.
1056         if (M.cfg.developerdebug) {
1057             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1058         }
1059         args.name = args.name.replace(nameregex, '_');
1060     }
1062     var fullurl = args.url;
1063     if (!args.url.match(/https?:\/\//)) {
1064         fullurl = M.cfg.wwwroot + args.url;
1065     }
1066     if (args.fullscreen) {
1067         args.options = args.options.
1068                 replace(/top=\d+/, 'top=0').
1069                 replace(/left=\d+/, 'left=0').
1070                 replace(/width=\d+/, 'width=' + screen.availWidth).
1071                 replace(/height=\d+/, 'height=' + screen.availHeight);
1072     }
1073     var windowobj = window.open(fullurl,args.name,args.options);
1074     if (!windowobj) {
1075         return true;
1076     }
1078     if (args.fullscreen) {
1079         // In some browser / OS combinations (E.g. Chrome on Windows), the
1080         // window initially opens slighly too big. The width and heigh options
1081         // seem to control the area inside the browser window, so what with
1082         // scroll-bars, etc. the actual window is bigger than the screen.
1083         // Therefore, we need to fix things up after the window is open.
1084         var hackcount = 100;
1085         var get_size_exactly_right = function() {
1086             windowobj.moveTo(0, 0);
1087             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1089             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1090             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1091             // about 50ms) after the window is open, then it actually behaves
1092             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1093             // check that the resize actually worked, and if not, repeatedly try
1094             // again after a short delay until it works (but with a limit of
1095             // hackcount repeats.
1096             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1097                 hackcount -= 1;
1098                 setTimeout(get_size_exactly_right, 10);
1099             }
1100         }
1101         setTimeout(get_size_exactly_right, 0);
1102     }
1103     windowobj.focus();
1105     return false;
1108 /** Close the current browser window. */
1109 function close_window(e) {
1110     if (e.preventDefault) {
1111         e.preventDefault();
1112     } else {
1113         e.returnValue = false;
1114     }
1115     window.close();
1119  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1120  * @param controlid the control id.
1121  */
1122 function focuscontrol(controlid) {
1123     var control = document.getElementById(controlid);
1124     if (control) {
1125         control.focus();
1126     }
1130  * Transfers keyboard focus to an HTML element based on the old style style of focus
1131  * This function should be removed as soon as it is no longer used
1132  */
1133 function old_onload_focus(formid, controlname) {
1134     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1135         document.forms[formid].elements[controlname].focus();
1136     }
1139 function build_querystring(obj) {
1140     return convert_object_to_string(obj, '&');
1143 function build_windowoptionsstring(obj) {
1144     return convert_object_to_string(obj, ',');
1147 function convert_object_to_string(obj, separator) {
1148     if (typeof obj !== 'object') {
1149         return null;
1150     }
1151     var list = [];
1152     for(var k in obj) {
1153         k = encodeURIComponent(k);
1154         var value = obj[k];
1155         if(obj[k] instanceof Array) {
1156             for(var i in value) {
1157                 list.push(k+'[]='+encodeURIComponent(value[i]));
1158             }
1159         } else {
1160             list.push(k+'='+encodeURIComponent(value));
1161         }
1162     }
1163     return list.join(separator);
1167  * @deprecated since Moodle 3.3.
1168  */
1169 function stripHTML(str) {
1170     throw new Error('stripHTML can not be used any more. Please use jQuery instead.');
1173 // eslint-disable-next-line no-unused-vars
1174 function updateProgressBar(id, percent, msg, estimate, error) {
1175     var event,
1176         el = document.getElementById(id),
1177         eventData = {};
1179     if (!el) {
1180         return;
1181     }
1183     eventData.message = msg;
1184     eventData.percent = percent;
1185     eventData.estimate = estimate;
1186     eventData.error = error;
1188     try {
1189         event = new CustomEvent('update', {
1190             bubbles: false,
1191             cancelable: true,
1192             detail: eventData
1193         });
1194     } catch (exception) {
1195         if (!(exception instanceof TypeError)) {
1196             throw exception;
1197         }
1198         event = document.createEvent('CustomEvent');
1199         event.initCustomEvent('update', false, true, eventData);
1200         event.prototype = window.Event.prototype;
1201     }
1203     el.dispatchEvent(event);
1206 M.util.help_popups = {
1207     setup : function(Y) {
1208         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1209     },
1210     open_popup : function(e) {
1211         // Prevent the default page action
1212         e.preventDefault();
1214         // Grab the anchor that was clicked
1215         var anchor = e.target.ancestor('a', true);
1216         var args = {
1217             'name'          : 'popup',
1218             'url'           : anchor.getAttribute('href'),
1219             'options'       : ''
1220         };
1221         var options = [
1222             'height=600',
1223             'width=800',
1224             'top=0',
1225             'left=0',
1226             'menubar=0',
1227             'location=0',
1228             'scrollbars',
1229             'resizable',
1230             'toolbar',
1231             'status',
1232             'directories=0',
1233             'fullscreen=0',
1234             'dependent'
1235         ]
1236         args.options = options.join(',');
1238         openpopup(e, args);
1239     }
1243  * Custom menu namespace
1244  */
1245 M.core_custom_menu = {
1246     /**
1247      * This method is used to initialise a custom menu given the id that belongs
1248      * to the custom menu's root node.
1249      *
1250      * @param {YUI} Y
1251      * @param {string} nodeid
1252      */
1253     init : function(Y, nodeid) {
1254         var node = Y.one('#'+nodeid);
1255         if (node) {
1256             Y.use('node-menunav', function(Y) {
1257                 // Get the node
1258                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1259                 node.removeClass('javascript-disabled');
1260                 // Initialise the menunav plugin
1261                 node.plug(Y.Plugin.NodeMenuNav);
1262             });
1263         }
1264     }
1268  * Used to store form manipulation methods and enhancments
1269  */
1270 M.form = M.form || {};
1273  * Converts a nbsp indented select box into a multi drop down custom control much
1274  * like the custom menu. Can no longer be used.
1275  * @deprecated since Moodle 3.3
1276  */
1277 M.form.init_smartselect = function() {
1278     throw new Error('M.form.init_smartselect can not be used any more.');
1282  * Initiates the listeners for skiplink interaction
1284  * @param {YUI} Y
1285  */
1286 M.util.init_skiplink = function(Y) {
1287     Y.one(Y.config.doc.body).delegate('click', function(e) {
1288         e.preventDefault();
1289         e.stopPropagation();
1290         var node = Y.one(this.getAttribute('href'));
1291         node.setAttribute('tabindex', '-1');
1292         node.focus();
1293         return true;
1294     }, 'a.skip');