MDL-81717 h5p: Improve robustness content type fetching
[moodle.git] / lib / javascript-static.js
blob9782cfa4996661e70882fe18cf138e0cbbed3e49
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     // Handler for the animation finishing.
138     animation.on('end', function() {
139         this.div.toggleClass('collapsed');
140         var collapsedimage = 't/collapsed'; // ltr mode
141         if (right_to_left()) {
142             collapsedimage = 't/collapsed_rtl';
143             } else {
144             collapsedimage = 't/collapsed';
145             }
146         if (this.div.hasClass('collapsed')) {
147             this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
148         } else {
149             this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
150         }
151     }, this);
153     // Hook up the event handler.
154     a.on('click', function(e, animation) {
155         e.preventDefault();
156         // Animate to the appropriate size.
157         if (animation.get('running')) {
158             animation.stop();
159         }
160         animation.set('reverse', this.div.hasClass('collapsed'));
161         // Update the user preference.
162         if (this.userpref) {
163             require(['core_user/repository'], function(UserRepository) {
164                 UserRepository.setUserPreference(this.userpref, !this.div.hasClass('collapsed'));
165             }.bind(this));
166         }
167         animation.run();
168     }, this, animation);
172  * The user preference that stores the state of this box.
173  * @property userpref
174  * @type String
175  */
176 M.util.CollapsibleRegion.prototype.userpref = null;
179  * The key divs that make up this
180  * @property div
181  * @type Y.Node
182  */
183 M.util.CollapsibleRegion.prototype.div = null;
186  * The key divs that make up this
187  * @property icon
188  * @type Y.Node
189  */
190 M.util.CollapsibleRegion.prototype.icon = null;
193  * Makes a best effort to connect back to Moodle to update a user preference,
194  * however, there is no mechanism for finding out if the update succeeded.
196  * Before you can use this function in your JavsScript, you must have called
197  * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
198  * the udpate is allowed, and how to safely clean and submitted values.
200  * @param {String} name the name of the setting to update.
201  * @param {String} value the value to set it to.
203  * @deprecated since Moodle 4.3.
204  */
205 M.util.set_user_preference = function(name, value) {
206     Y.log('M.util.set_user_preference is deprecated. Please use the "core_user/repository" module instead.', 'warn');
208     require(['core_user/repository'], function(UserRepository) {
209         UserRepository.setUserPreference(name, value);
210     });
214  * Prints a confirmation dialog in the style of DOM.confirm().
216  * @method show_confirm_dialog
217  * @param {EventFacade} e
218  * @param {Object} args
219  * @param {String} args.message The question to ask the user
220  * @param {Function} [args.callback] A callback to apply on confirmation.
221  * @param {Object} [args.scope] The scope to use when calling the callback.
222  * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
223  * @param {String} [args.cancellabel] The label to use on the cancel button.
224  * @param {String} [args.continuelabel] The label to use on the continue button.
225  */
226 M.util.show_confirm_dialog = (e, {
227     message,
228     continuelabel,
229     callback = null,
230     scope = null,
231     callbackargs = [],
232 } = {}) => {
233     if (e.preventDefault) {
234         e.preventDefault();
235     }
237     require(
238         ['core/notification', 'core/str', 'core_form/changechecker', 'core/normalise'],
239         function(Notification, Str, FormChangeChecker, Normalise) {
241             if (scope === null && e.target) {
242                 // Fall back to the event target if no scope provided.
243                 scope = e.target;
244             }
246             Notification.saveCancelPromise(
247                 Str.get_string('confirmation', 'admin'),
248                 message,
249                 continuelabel || Str.get_string('yes', 'moodle'),
250             )
251             .then(() => {
252                 if (callback) {
253                     callback.apply(scope, callbackargs);
254                     return;
255                 }
257                 if (!e.target) {
258                     window.console.error(
259                         `M.util.show_confirm_dialog: No target found for event`,
260                         e
261                     );
262                     return;
263                 }
265                 const target = Normalise.getElement(e.target);
267                 if (target.closest('a')) {
268                     window.location = target.closest('a').getAttribute('href');
269                     return;
270                 } else if (target.closest('input') || target.closest('button')) {
271                     const form = target.closest('form');
272                     const hiddenValue = document.createElement('input');
273                     hiddenValue.setAttribute('type', 'hidden');
274                     hiddenValue.setAttribute('name', target.getAttribute('name'));
275                     hiddenValue.setAttribute('value', target.getAttribute('value'));
276                     form.appendChild(hiddenValue);
277                     FormChangeChecker.markFormAsDirty(form);
278                     form.submit();
279                     return;
280                 } else if (target.closest('form')) {
281                     const form = target.closest('form');
282                     FormChangeChecker.markFormAsDirty(form);
283                     form.submit();
284                     return;
285                 }
286                 window.console.error(
287                     `Element of type ${target.tagName} is not supported by M.util.show_confirm_dialog.`
288                 );
290                 return;
291             })
292             .catch(() => {
293                 // User cancelled.
294                 return;
295             });
296         }
297     );
300 /** Useful for full embedding of various stuff */
301 M.util.init_maximised_embed = function(Y, id) {
302     var obj = Y.one('#'+id);
303     if (!obj) {
304         return;
305     }
307     var get_htmlelement_size = function(el, prop) {
308         if (Y.Lang.isString(el)) {
309             el = Y.one('#' + el);
310         }
311         // Ensure element exists.
312         if (el) {
313             var val = el.getStyle(prop);
314             if (val == 'auto') {
315                 val = el.getComputedStyle(prop);
316             }
317             val = parseInt(val);
318             if (isNaN(val)) {
319                 return 0;
320             }
321             return val;
322         } else {
323             return 0;
324         }
325     };
327     var resize_object = function() {
328         obj.setStyle('display', 'none');
329         var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
331         if (newwidth > 500) {
332             obj.setStyle('width', newwidth  + 'px');
333         } else {
334             obj.setStyle('width', '500px');
335         }
337         var headerheight = get_htmlelement_size('page-header', 'height');
338         var footerheight = get_htmlelement_size('page-footer', 'height');
339         var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
340         if (newheight < 400) {
341             newheight = 400;
342         }
343         obj.setStyle('height', newheight+'px');
344         obj.setStyle('display', '');
345     };
347     resize_object();
348     // fix layout if window resized too
349     Y.use('event-resize', function (Y) {
350         Y.on("windowresize", function() {
351             resize_object();
352         });
353     });
357  * Breaks out all links to the top frame - used in frametop page layout.
358  */
359 M.util.init_frametop = function(Y) {
360     Y.all('a').each(function(node) {
361         node.set('target', '_top');
362     });
363     Y.all('form').each(function(node) {
364         node.set('target', '_top');
365     });
369  * @deprecated since Moodle 3.3
370  */
371 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
372     throw new Error('M.util.init_toggle_class_on_click can not be used any more. Please use jQuery instead.');
376  * Initialises a colour picker
378  * Designed to be used with admin_setting_configcolourpicker although could be used
379  * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
380  * above or below the input (must have the same parent) and then call this with the
381  * id.
383  * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
384  * contrib/blocks. For better docs refer to that.
386  * @param {YUI} Y
387  * @param {int} id
388  * @param {object} previewconf
389  */
390 M.util.init_colour_picker = function(Y, id, previewconf) {
391     /**
392      * We need node and event-mouseenter
393      */
394     Y.use('node', 'event-mouseenter', function(){
395         /**
396          * The colour picker object
397          */
398         var colourpicker = {
399             box : null,
400             input : null,
401             image : null,
402             preview : null,
403             current : null,
404             eventClick : null,
405             eventMouseEnter : null,
406             eventMouseLeave : null,
407             eventMouseMove : null,
408             width : 300,
409             height :  100,
410             factor : 5,
411             /**
412              * Initalises the colour picker by putting everything together and wiring the events
413              */
414             init : function() {
415                 this.input = Y.one('#'+id);
416                 this.box = this.input.ancestor().one('.admin_colourpicker');
417                 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
418                 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
419                 this.preview = Y.Node.create('<div class="previewcolour"></div>');
420                 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
421                 this.current = Y.Node.create('<div class="currentcolour"></div>');
422                 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
423                 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
425                 if (typeof(previewconf) === 'object' && previewconf !== null) {
426                     Y.one('#'+id+'_preview').on('click', function(e){
427                         if (Y.Lang.isString(previewconf.selector)) {
428                             Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
429                         } else {
430                             for (var i in previewconf.selector) {
431                                 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
432                             }
433                         }
434                     }, this);
435                 }
437                 this.eventClick = this.image.on('click', this.pickColour, this);
438                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
439             },
440             /**
441              * Starts to follow the mouse once it enter the image
442              */
443             startFollow : function(e) {
444                 this.eventMouseEnter.detach();
445                 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
446                 this.eventMouseMove = this.image.on('mousemove', function(e){
447                     this.preview.setStyle('backgroundColor', this.determineColour(e));
448                 }, this);
449             },
450             /**
451              * Stops following the mouse
452              */
453             endFollow : function(e) {
454                 this.eventMouseMove.detach();
455                 this.eventMouseLeave.detach();
456                 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
457             },
458             /**
459              * Picks the colour the was clicked on
460              */
461             pickColour : function(e) {
462                 var colour = this.determineColour(e);
463                 this.input.set('value', colour);
464                 this.current.setStyle('backgroundColor', colour);
465             },
466             /**
467              * Calculates the colour fromthe given co-ordinates
468              */
469             determineColour : function(e) {
470                 var eventx = Math.floor(e.pageX-e.target.getX());
471                 var eventy = Math.floor(e.pageY-e.target.getY());
473                 var imagewidth = this.width;
474                 var imageheight = this.height;
475                 var factor = this.factor;
476                 var colour = [255,0,0];
478                 var matrices = [
479                     [  0,  1,  0],
480                     [ -1,  0,  0],
481                     [  0,  0,  1],
482                     [  0, -1,  0],
483                     [  1,  0,  0],
484                     [  0,  0, -1]
485                 ];
487                 var matrixcount = matrices.length;
488                 var limit = Math.round(imagewidth/matrixcount);
489                 var heightbreak = Math.round(imageheight/2);
491                 for (var x = 0; x < imagewidth; x++) {
492                     var divisor = Math.floor(x / limit);
493                     var matrix = matrices[divisor];
495                     colour[0] += matrix[0]*factor;
496                     colour[1] += matrix[1]*factor;
497                     colour[2] += matrix[2]*factor;
499                     if (eventx==x) {
500                         break;
501                     }
502                 }
504                 var pixel = [colour[0], colour[1], colour[2]];
505                 if (eventy < heightbreak) {
506                     pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
507                     pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
508                     pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
509                 } else if (eventy > heightbreak) {
510                     pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
511                     pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
512                     pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
513                 }
515                 return this.convert_rgb_to_hex(pixel);
516             },
517             /**
518              * Converts an RGB value to Hex
519              */
520             convert_rgb_to_hex : function(rgb) {
521                 var hex = '#';
522                 var hexchars = "0123456789ABCDEF";
523                 for (var i=0; i<3; i++) {
524                     var number = Math.abs(rgb[i]);
525                     if (number == 0 || isNaN(number)) {
526                         hex += '00';
527                     } else {
528                         hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
529                     }
530                 }
531                 return hex;
532             }
533         };
534         /**
535          * Initialise the colour picker :) Hoorah
536          */
537         colourpicker.init();
538     });
541 M.util.init_block_hider = function(Y, config) {
542     Y.use('base', 'node', function(Y) {
543         M.util.block_hider = M.util.block_hider || (function(){
544             var blockhider = function() {
545                 blockhider.superclass.constructor.apply(this, arguments);
546             };
547             blockhider.prototype = {
548                 initializer : function(config) {
549                     this.set('block', '#'+this.get('id'));
550                     var b = this.get('block'),
551                         t = b.one('.title'),
552                         a = null,
553                         hide,
554                         show;
555                     if (t && (a = t.one('.block_action'))) {
556                         hide = Y.Node.create('<img />')
557                             .addClass('block-hider-hide')
558                             .setAttrs({
559                                 alt:        config.tooltipVisible,
560                                 src:        this.get('iconVisible'),
561                                 tabIndex:   0,
562                                 'title':    config.tooltipVisible
563                             });
564                         hide.on('keypress', this.updateStateKey, this, true);
565                         hide.on('click', this.updateState, this, true);
567                         show = Y.Node.create('<img />')
568                             .addClass('block-hider-show')
569                             .setAttrs({
570                                 alt:        config.tooltipHidden,
571                                 src:        this.get('iconHidden'),
572                                 tabIndex:   0,
573                                 'title':    config.tooltipHidden
574                             });
575                         show.on('keypress', this.updateStateKey, this, false);
576                         show.on('click', this.updateState, this, false);
578                         a.insert(show, 0).insert(hide, 0);
579                     }
580                 },
581                 updateState : function(e, hide) {
582                     require(['core_user/repository'], function(UserRepository) {
583                         UserRepository.setUserPreference(this.get('preference'), hide);
584                     }.bind(this));
585                     if (hide) {
586                         this.get('block').addClass('hidden');
587                         this.get('block').one('.block-hider-show').focus();
588                     } else {
589                         this.get('block').removeClass('hidden');
590                         this.get('block').one('.block-hider-hide').focus();
591                     }
592                 },
593                 updateStateKey : function(e, hide) {
594                     if (e.keyCode == 13) { //allow hide/show via enter key
595                         this.updateState(this, hide);
596                     }
597                 }
598             };
599             Y.extend(blockhider, Y.Base, blockhider.prototype, {
600                 NAME : 'blockhider',
601                 ATTRS : {
602                     id : {},
603                     preference : {},
604                     iconVisible : {
605                         value : M.util.image_url('t/switch_minus', 'moodle')
606                     },
607                     iconHidden : {
608                         value : M.util.image_url('t/switch_plus', 'moodle')
609                     },
610                     block : {
611                         setter : function(node) {
612                             return Y.one(node);
613                         }
614                     }
615                 }
616             });
617             return blockhider;
618         })();
619         new M.util.block_hider(config);
620     });
624  * @var pending_js - The keys are the list of all pending js actions.
625  * @type Object
626  */
627 M.util.pending_js = [];
628 M.util.complete_js = [];
631  * Register any long running javascript code with a unique identifier.
632  * This is used to ensure that Behat steps do not continue with interactions until the page finishes loading.
634  * All calls to M.util.js_pending _must_ be followed by a subsequent call to M.util.js_complete with the same exact
635  * uniqid.
637  * This function may also be called with no arguments to test if there is any js calls pending.
639  * The uniqid specified may be any Object, including Number, String, or actual Object; however please note that the
640  * paired js_complete function performs a strict search for the key specified. As such, if using an Object, the exact
641  * Object must be passed into both functions.
643  * @param   {Mixed}     uniqid Register long-running code against the supplied identifier
644  * @return  {Number}    Number of pending items
645  */
646 M.util.js_pending = function(uniqid) {
647     if (typeof uniqid !== 'undefined') {
648         M.util.pending_js.push(uniqid);
649     }
651     return M.util.pending_js.length;
654 // Start this asap.
655 M.util.js_pending('init');
658  * Register listeners for Y.io start/end so we can wait for them in behat.
659  */
660 YUI.add('moodle-core-io', function(Y) {
661     Y.on('io:start', function(id) {
662         M.util.js_pending('io:' + id);
663     });
664     Y.on('io:end', function(id) {
665         M.util.js_complete('io:' + id);
666     });
667 }, '@VERSION@', {
668     condition: {
669         trigger: 'io-base',
670         when: 'after'
671     }
675  * Unregister some long running javascript code using the unique identifier specified in M.util.js_pending.
677  * This function must be matched with an identical call to M.util.js_pending.
679  * @param   {Mixed}     uniqid Register long-running code against the supplied identifier
680  * @return  {Number}    Number of pending items remaining after removing this item
681  */
682 M.util.js_complete = function(uniqid) {
683     const index = M.util.pending_js.indexOf(uniqid);
684     if (index >= 0) {
685         M.util.complete_js.push(M.util.pending_js.splice(index, 1)[0]);
686     } else {
687         window.console.log("Unable to locate key for js_complete call", uniqid);
688     }
690     return M.util.pending_js.length;
694  * Returns a string registered in advance for usage in JavaScript
696  * If you do not pass the third parameter, the function will just return
697  * the corresponding value from the M.str object. If the third parameter is
698  * provided, the function performs {$a} placeholder substitution in the
699  * same way as PHP get_string() in Moodle does.
701  * @param {String} identifier string identifier
702  * @param {String} component the component providing the string
703  * @param {Object|String} [a] optional variable to populate placeholder with
704  */
705 M.util.get_string = function(identifier, component, a) {
706     var stringvalue;
708     if (M.cfg.developerdebug) {
709         // creating new instance if YUI is not optimal but it seems to be better way then
710         // require the instance via the function API - note that it is used in rare cases
711         // for debugging only anyway
712         // To ensure we don't kill browser performance if hundreds of get_string requests
713         // are made we cache the instance we generate within the M.util namespace.
714         // We don't publicly define the variable so that it doesn't get abused.
715         if (typeof M.util.get_string_yui_instance === 'undefined') {
716             M.util.get_string_yui_instance = new YUI({ debug : true });
717         }
718         var Y = M.util.get_string_yui_instance;
719     }
721     if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
722         stringvalue = '[[' + identifier + ',' + component + ']]';
723         if (M.cfg.developerdebug) {
724             Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
725         }
726         return stringvalue;
727     }
729     stringvalue = M.str[component][identifier];
731     if (typeof a == 'undefined') {
732         // no placeholder substitution requested
733         return stringvalue;
734     }
736     if (typeof a == 'number' || typeof a == 'string') {
737         // replace all occurrences of {$a} with the placeholder value
738         stringvalue = stringvalue.replace(/\{\$a\}/g, a);
739         return stringvalue;
740     }
742     if (typeof a == 'object') {
743         // replace {$a->key} placeholders
744         for (var key in a) {
745             if (typeof a[key] != 'number' && typeof a[key] != 'string') {
746                 if (M.cfg.developerdebug) {
747                     Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
748                 }
749                 continue;
750             }
751             var search = '{$a->' + key + '}';
752             search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
753             search = new RegExp(search, 'g');
754             stringvalue = stringvalue.replace(search, a[key]);
755         }
756         return stringvalue;
757     }
759     if (M.cfg.developerdebug) {
760         Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
761     }
762     return stringvalue;
766  * Set focus on username or password field of the login form.
767  * @deprecated since Moodle 3.3.
768  */
769 M.util.focus_login_form = function(Y) {
770     Y.log('M.util.focus_login_form no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
774  * Set focus on login error message.
775  * @deprecated since Moodle 3.3.
776  */
777 M.util.focus_login_error = function(Y) {
778     Y.log('M.util.focus_login_error no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
782  * Adds lightbox hidden element that covers the whole node.
784  * @param {YUI} Y
785  * @param {Node} the node lightbox should be added to
786  * @retun {Node} created lightbox node
787  */
788 M.util.add_lightbox = function(Y, node) {
789     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
791     // Check if lightbox is already there
792     if (node.one('.lightbox')) {
793         return node.one('.lightbox');
794     }
796     node.setStyle('position', 'relative');
797     var waiticon = Y.Node.create('<img />')
798     .setAttrs({
799         'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
800     })
801     .setStyles({
802         'position' : 'relative',
803         'top' : '50%'
804     });
806     var lightbox = Y.Node.create('<div></div>')
807     .setStyles({
808         'opacity' : '.75',
809         'position' : 'absolute',
810         'width' : '100%',
811         'height' : '100%',
812         'top' : 0,
813         'left' : 0,
814         'backgroundColor' : 'white',
815         'textAlign' : 'center'
816     })
817     .setAttribute('class', 'lightbox')
818     .hide();
820     lightbox.appendChild(waiticon);
821     node.append(lightbox);
822     return lightbox;
826  * Appends a hidden spinner element to the specified node.
828  * @param {YUI} Y
829  * @param {Node} the node the spinner should be added to
830  * @return {Node} created spinner node
831  */
832 M.util.add_spinner = function(Y, node) {
833     var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
835     // Check if spinner is already there
836     if (node.one('.spinner')) {
837         return node.one('.spinner');
838     }
840     var spinner = Y.Node.create('<img />')
841         .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
842         .addClass('spinner')
843         .addClass('iconsmall')
844         .hide();
846     node.append(spinner);
847     return spinner;
851  * @deprecated since Moodle 3.3.
852  */
853 function checkall() {
854     throw new Error('checkall can not be used any more. Please use jQuery instead.');
858  * @deprecated since Moodle 3.3.
859  */
860 function checknone() {
861     throw new Error('checknone can not be used any more. Please use jQuery instead.');
865  * @deprecated since Moodle 3.3.
866  */
867 function select_all_in_element_with_id(id, checked) {
868     throw new Error('select_all_in_element_with_id can not be used any more. Please use jQuery instead.');
872  * @deprecated since Moodle 3.3.
873  */
874 function select_all_in(elTagName, elClass, elId) {
875     throw new Error('select_all_in can not be used any more. Please use jQuery instead.');
879  * @deprecated since Moodle 3.3.
880  */
881 function deselect_all_in(elTagName, elClass, elId) {
882     throw new Error('deselect_all_in can not be used any more. Please use jQuery instead.');
886  * @deprecated since Moodle 3.3.
887  */
888 function confirm_if(expr, message) {
889     throw new Error('confirm_if can not be used any more.');
893  * @deprecated since Moodle 3.3.
894  */
895 function findParentNode(el, elName, elClass, elId) {
896     throw new Error('findParentNode can not be used any more. Please use jQuery instead.');
899 function unmaskPassword(id) {
900     var pw = document.getElementById(id);
901     var chb = document.getElementById(id+'unmask');
903     // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
904     // Replacing existing child with a new one, removed all yui properties for the node.  Therefore, this
905     // functionality won't work in IE8 or lower.
906     // This is a temporary fixed to allow other browsers to function properly.
907     if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
908         if (chb.checked) {
909             pw.type = "text";
910         } else {
911             pw.type = "password";
912         }
913     } else {  //IE Browser version 8 or lower
914         try {
915             // first try IE way - it can not set name attribute later
916             if (chb.checked) {
917               var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
918             } else {
919               var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
920             }
921             newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
922         } catch (e) {
923             var newpw = document.createElement('input');
924             newpw.setAttribute('autocomplete', 'off');
925             newpw.setAttribute('name', pw.name);
926             if (chb.checked) {
927               newpw.setAttribute('type', 'text');
928             } else {
929               newpw.setAttribute('type', 'password');
930             }
931             newpw.setAttribute('class', pw.getAttribute('class'));
932         }
933         newpw.id = pw.id;
934         newpw.size = pw.size;
935         newpw.onblur = pw.onblur;
936         newpw.onchange = pw.onchange;
937         newpw.value = pw.value;
938         pw.parentNode.replaceChild(newpw, pw);
939     }
943  * @deprecated since Moodle 3.3.
944  */
945 function filterByParent(elCollection, parentFinder) {
946     throw new Error('filterByParent can not be used any more. Please use jQuery instead.');
950  * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
951  */
952 function fix_column_widths() {
953     Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
957  * @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
958  */
959 function fix_column_width(colName) {
960     Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
965    Insert myValue at current cursor position
966  */
967 function insertAtCursor(myField, myValue) {
968     // IE support
969     if (document.selection) {
970         myField.focus();
971         sel = document.selection.createRange();
972         sel.text = myValue;
973     }
974     // Mozilla/Netscape support
975     else if (myField.selectionStart || myField.selectionStart == '0') {
976         var startPos = myField.selectionStart;
977         var endPos = myField.selectionEnd;
978         myField.value = myField.value.substring(0, startPos)
979             + myValue + myField.value.substring(endPos, myField.value.length);
980     } else {
981         myField.value += myValue;
982     }
986  * Increment a file name.
988  * @param string file name.
989  * @param boolean ignoreextension do not extract the extension prior to appending the
990  *                                suffix. Useful when incrementing folder names.
991  * @return string the incremented file name.
992  */
993 function increment_filename(filename, ignoreextension) {
994     var extension = '';
995     var basename = filename;
997     // Split the file name into the basename + extension.
998     if (!ignoreextension) {
999         var dotpos = filename.lastIndexOf('.');
1000         if (dotpos !== -1) {
1001             basename = filename.substr(0, dotpos);
1002             extension = filename.substr(dotpos, filename.length);
1003         }
1004     }
1006     // Look to see if the name already has (NN) at the end of it.
1007     var number = 0;
1008     var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1009     if (hasnumber !== null) {
1010         // Note the current number & remove it from the basename.
1011         number = parseInt(hasnumber[2], 10);
1012         basename = hasnumber[1];
1013     }
1015     number++;
1016     var newname = basename + ' (' + number + ')' + extension;
1017     return newname;
1021  * Return whether we are in right to left mode or not.
1023  * @return boolean
1024  */
1025 function right_to_left() {
1026     var body = Y.one('body');
1027     var rtl = false;
1028     if (body && body.hasClass('dir-rtl')) {
1029         rtl = true;
1030     }
1031     return rtl;
1034 function openpopup(event, args) {
1036     if (event) {
1037         if (event.preventDefault) {
1038             event.preventDefault();
1039         } else {
1040             event.returnValue = false;
1041         }
1042     }
1044     // Make sure the name argument is set and valid.
1045     var nameregex = /[^a-z0-9_]/i;
1046     if (typeof args.name !== 'string') {
1047         args.name = '_blank';
1048     } else if (args.name.match(nameregex)) {
1049         // Cleans window name because IE does not support funky ones.
1050         if (M.cfg.developerdebug) {
1051             alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1052         }
1053         args.name = args.name.replace(nameregex, '_');
1054     }
1056     var fullurl = args.url;
1057     if (!args.url.match(/https?:\/\//)) {
1058         fullurl = M.cfg.wwwroot + args.url;
1059     }
1060     if (args.fullscreen) {
1061         args.options = args.options.
1062                 replace(/top=\d+/, 'top=0').
1063                 replace(/left=\d+/, 'left=0').
1064                 replace(/width=\d+/, 'width=' + screen.availWidth).
1065                 replace(/height=\d+/, 'height=' + screen.availHeight);
1066     }
1067     var windowobj = window.open(fullurl,args.name,args.options);
1068     if (!windowobj) {
1069         return true;
1070     }
1072     if (args.fullscreen) {
1073         // In some browser / OS combinations (E.g. Chrome on Windows), the
1074         // window initially opens slighly too big. The width and heigh options
1075         // seem to control the area inside the browser window, so what with
1076         // scroll-bars, etc. the actual window is bigger than the screen.
1077         // Therefore, we need to fix things up after the window is open.
1078         var hackcount = 100;
1079         var get_size_exactly_right = function() {
1080             windowobj.moveTo(0, 0);
1081             windowobj.resizeTo(screen.availWidth, screen.availHeight);
1083             // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1084             // something like windowobj.resizeTo(1280, 1024) too soon (up to
1085             // about 50ms) after the window is open, then it actually behaves
1086             // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1087             // check that the resize actually worked, and if not, repeatedly try
1088             // again after a short delay until it works (but with a limit of
1089             // hackcount repeats.
1090             if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1091                 hackcount -= 1;
1092                 setTimeout(get_size_exactly_right, 10);
1093             }
1094         }
1095         setTimeout(get_size_exactly_right, 0);
1096     }
1097     windowobj.focus();
1099     return false;
1102 /** Close the current browser window. */
1103 function close_window(e) {
1104     if (e.preventDefault) {
1105         e.preventDefault();
1106     } else {
1107         e.returnValue = false;
1108     }
1109     window.close();
1113  * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1114  * @param controlid the control id.
1115  */
1116 function focuscontrol(controlid) {
1117     var control = document.getElementById(controlid);
1118     if (control) {
1119         control.focus();
1120     }
1124  * Transfers keyboard focus to an HTML element based on the old style style of focus
1125  * This function should be removed as soon as it is no longer used
1126  */
1127 function old_onload_focus(formid, controlname) {
1128     if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1129         document.forms[formid].elements[controlname].focus();
1130     }
1133 function build_querystring(obj) {
1134     return convert_object_to_string(obj, '&');
1137 function build_windowoptionsstring(obj) {
1138     return convert_object_to_string(obj, ',');
1141 function convert_object_to_string(obj, separator) {
1142     if (typeof obj !== 'object') {
1143         return null;
1144     }
1145     var list = [];
1146     for(var k in obj) {
1147         k = encodeURIComponent(k);
1148         var value = obj[k];
1149         if(obj[k] instanceof Array) {
1150             for(var i in value) {
1151                 list.push(k+'[]='+encodeURIComponent(value[i]));
1152             }
1153         } else {
1154             list.push(k+'='+encodeURIComponent(value));
1155         }
1156     }
1157     return list.join(separator);
1161  * @deprecated since Moodle 3.3.
1162  */
1163 function stripHTML(str) {
1164     throw new Error('stripHTML can not be used any more. Please use jQuery instead.');
1167 function updateProgressBar(id, percent, msg, estimate) {
1168     var event,
1169         el = document.getElementById(id),
1170         eventData = {};
1172     if (!el) {
1173         return;
1174     }
1176     eventData.message = msg;
1177     eventData.percent = percent;
1178     eventData.estimate = estimate;
1180     try {
1181         event = new CustomEvent('update', {
1182             bubbles: false,
1183             cancelable: true,
1184             detail: eventData
1185         });
1186     } catch (exception) {
1187         if (!(exception instanceof TypeError)) {
1188             throw exception;
1189         }
1190         event = document.createEvent('CustomEvent');
1191         event.initCustomEvent('update', false, true, eventData);
1192         event.prototype = window.Event.prototype;
1193     }
1195     el.dispatchEvent(event);
1198 M.util.help_popups = {
1199     setup : function(Y) {
1200         Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1201     },
1202     open_popup : function(e) {
1203         // Prevent the default page action
1204         e.preventDefault();
1206         // Grab the anchor that was clicked
1207         var anchor = e.target.ancestor('a', true);
1208         var args = {
1209             'name'          : 'popup',
1210             'url'           : anchor.getAttribute('href'),
1211             'options'       : ''
1212         };
1213         var options = [
1214             'height=600',
1215             'width=800',
1216             'top=0',
1217             'left=0',
1218             'menubar=0',
1219             'location=0',
1220             'scrollbars',
1221             'resizable',
1222             'toolbar',
1223             'status',
1224             'directories=0',
1225             'fullscreen=0',
1226             'dependent'
1227         ]
1228         args.options = options.join(',');
1230         openpopup(e, args);
1231     }
1235  * Custom menu namespace
1236  */
1237 M.core_custom_menu = {
1238     /**
1239      * This method is used to initialise a custom menu given the id that belongs
1240      * to the custom menu's root node.
1241      *
1242      * @param {YUI} Y
1243      * @param {string} nodeid
1244      */
1245     init : function(Y, nodeid) {
1246         var node = Y.one('#'+nodeid);
1247         if (node) {
1248             Y.use('node-menunav', function(Y) {
1249                 // Get the node
1250                 // Remove the javascript-disabled class.... obviously javascript is enabled.
1251                 node.removeClass('javascript-disabled');
1252                 // Initialise the menunav plugin
1253                 node.plug(Y.Plugin.NodeMenuNav);
1254             });
1255         }
1256     }
1260  * Used to store form manipulation methods and enhancments
1261  */
1262 M.form = M.form || {};
1265  * Converts a nbsp indented select box into a multi drop down custom control much
1266  * like the custom menu. Can no longer be used.
1267  * @deprecated since Moodle 3.3
1268  */
1269 M.form.init_smartselect = function() {
1270     throw new Error('M.form.init_smartselect can not be used any more.');
1274  * Initiates the listeners for skiplink interaction
1276  * @param {YUI} Y
1277  */
1278 M.util.init_skiplink = function(Y) {
1279     Y.one(Y.config.doc.body).delegate('click', function(e) {
1280         e.preventDefault();
1281         e.stopPropagation();
1282         var node = Y.one(this.getAttribute('href'));
1283         node.setAttribute('tabindex', '-1');
1284         node.focus();
1285         return true;
1286     }, 'a.skip');