1 /* eslint-disable camelcase */
2 // Miscellaneous core Javascript functions for Moodle
3 // Global M object is initilised in inline javascript
6 * Add module to list of available modules that can be loaded from YUI.
7 * @param {Array} modules
9 M.yui.add_module = function(modules) {
10 for (var modname in modules) {
11 YUI_config.modules[modname] = modules[modname];
13 // Ensure thaat the YUI_config is applied to the main YUI instance.
14 Y.applyConfig(YUI_config);
17 * The gallery version to use when loading YUI modules from the gallery.
18 * Will be changed every time when using local galleries.
20 M.yui.galleryversion = '2010.04.21-21-51';
23 * Various utility functions
25 M.util = M.util || {};
28 * Language strings - initialised from page footer.
33 * Returns url for images.
34 * @param {String} imagename
35 * @param {String} component
38 M.util.image_url = function(imagename, component) {
40 if (!component || component == '' || component == 'moodle' || component == 'core') {
44 var url = M.cfg.wwwroot + '/theme/image.php';
45 if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
46 if (!M.cfg.svgicons) {
49 url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
51 url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
52 if (!M.cfg.svgicons) {
60 M.util.in_array = function(item, array) {
61 return array.indexOf(item) !== -1;
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
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);
78 * Object to handle a collapsible region : instantiate and forget styled object
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
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');
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');
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';
115 collapsedimage = 't/collapsed';
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');
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="" />');
128 // Create the animation.
129 var animation = new Y.Anim({
132 easing: Y.Easing.easeBoth,
133 to: {height:caption.get('offsetHeight')},
134 from: {height:height}
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';
144 collapsedimage = 't/collapsed';
146 if (this.div.hasClass('collapsed')) {
147 this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
149 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
153 // Hook up the event handler.
154 a.on('click', function(e, animation) {
156 // Animate to the appropriate size.
157 if (animation.get('running')) {
160 animation.set('reverse', this.div.hasClass('collapsed'));
161 // Update the user preference.
163 require(['core_user/repository'], function(UserRepository) {
164 UserRepository.setUserPreference(this.userpref, !this.div.hasClass('collapsed'));
172 * The user preference that stores the state of this box.
176 M.util.CollapsibleRegion.prototype.userpref = null;
179 * The key divs that make up this
183 M.util.CollapsibleRegion.prototype.div = null;
186 * The key divs that make up this
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.
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);
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.
226 M.util.show_confirm_dialog = (e, {
233 if (e.preventDefault) {
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.
246 Notification.saveCancelPromise(
247 Str.get_string('confirmation', 'admin'),
249 continuelabel || Str.get_string('yes', 'moodle'),
253 callback.apply(scope, callbackargs);
258 window.console.error(
259 `M.util.show_confirm_dialog: No target found for event`,
265 const target = Normalise.getElement(e.target);
267 if (target.closest('a')) {
268 window.location = target.closest('a').getAttribute('href');
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);
280 } else if (target.closest('form')) {
281 const form = target.closest('form');
282 FormChangeChecker.markFormAsDirty(form);
286 window.console.error(
287 `Element of type ${target.tagName} is not supported by M.util.show_confirm_dialog.`
300 /** Useful for full embedding of various stuff */
301 M.util.init_maximised_embed = function(Y, id) {
302 var obj = Y.one('#'+id);
307 var get_htmlelement_size = function(el, prop) {
308 if (Y.Lang.isString(el)) {
309 el = Y.one('#' + el);
311 // Ensure element exists.
313 var val = el.getStyle(prop);
315 val = el.getComputedStyle(prop);
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');
334 obj.setStyle('width', '500px');
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) {
343 obj.setStyle('height', newheight+'px');
344 obj.setStyle('display', '');
348 // fix layout if window resized too
349 Y.use('event-resize', function (Y) {
350 Y.on("windowresize", function() {
357 * Breaks out all links to the top frame - used in frametop page layout.
359 M.util.init_frametop = function(Y) {
360 Y.all('a').each(function(node) {
361 node.set('target', '_top');
363 Y.all('form').each(function(node) {
364 node.set('target', '_top');
369 * @deprecated since Moodle 3.3
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
383 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
384 * contrib/blocks. For better docs refer to that.
388 * @param {object} previewconf
390 M.util.init_colour_picker = function(Y, id, previewconf) {
392 * We need node and event-mouseenter
394 Y.use('node', 'event-mouseenter', function(){
396 * The colour picker object
405 eventMouseEnter : null,
406 eventMouseLeave : null,
407 eventMouseMove : null,
412 * Initalises the colour picker by putting everything together and wiring the events
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'));
430 for (var i in previewconf.selector) {
431 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
437 this.eventClick = this.image.on('click', this.pickColour, this);
438 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
441 * Starts to follow the mouse once it enter the image
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));
451 * Stops following the mouse
453 endFollow : function(e) {
454 this.eventMouseMove.detach();
455 this.eventMouseLeave.detach();
456 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
459 * Picks the colour the was clicked on
461 pickColour : function(e) {
462 var colour = this.determineColour(e);
463 this.input.set('value', colour);
464 this.current.setStyle('backgroundColor', colour);
467 * Calculates the colour fromthe given co-ordinates
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];
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;
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));
515 return this.convert_rgb_to_hex(pixel);
518 * Converts an RGB value to Hex
520 convert_rgb_to_hex : function(rgb) {
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)) {
528 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
535 * Initialise the colour picker :) Hoorah
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);
547 blockhider.prototype = {
548 initializer : function(config) {
549 this.set('block', '#'+this.get('id'));
550 var b = this.get('block'),
555 if (t && (a = t.one('.block_action'))) {
556 hide = Y.Node.create('<img />')
557 .addClass('block-hider-hide')
559 alt: config.tooltipVisible,
560 src: this.get('iconVisible'),
562 'title': config.tooltipVisible
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')
570 alt: config.tooltipHidden,
571 src: this.get('iconHidden'),
573 'title': config.tooltipHidden
575 show.on('keypress', this.updateStateKey, this, false);
576 show.on('click', this.updateState, this, false);
578 a.insert(show, 0).insert(hide, 0);
581 updateState : function(e, hide) {
582 require(['core_user/repository'], function(UserRepository) {
583 UserRepository.setUserPreference(this.get('preference'), hide);
586 this.get('block').addClass('hidden');
587 this.get('block').one('.block-hider-show').focus();
589 this.get('block').removeClass('hidden');
590 this.get('block').one('.block-hider-hide').focus();
593 updateStateKey : function(e, hide) {
594 if (e.keyCode == 13) { //allow hide/show via enter key
595 this.updateState(this, hide);
599 Y.extend(blockhider, Y.Base, blockhider.prototype, {
605 value : M.util.image_url('t/switch_minus', 'moodle')
608 value : M.util.image_url('t/switch_plus', 'moodle')
611 setter : function(node) {
619 new M.util.block_hider(config);
624 * @var pending_js - The keys are the list of all pending js actions.
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
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
646 M.util.js_pending = function(uniqid) {
647 if (typeof uniqid !== 'undefined') {
648 M.util.pending_js.push(uniqid);
651 return M.util.pending_js.length;
655 M.util.js_pending('init');
658 * Register listeners for Y.io start/end so we can wait for them in behat.
660 YUI.add('moodle-core-io', function(Y) {
661 Y.on('io:start', function(id) {
662 M.util.js_pending('io:' + id);
664 Y.on('io:end', function(id) {
665 M.util.js_complete('io:' + id);
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
682 M.util.js_complete = function(uniqid) {
683 const index = M.util.pending_js.indexOf(uniqid);
685 M.util.complete_js.push(M.util.pending_js.splice(index, 1)[0]);
687 window.console.log("Unable to locate key for js_complete call", uniqid);
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
705 M.util.get_string = function(identifier, component, a) {
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 });
718 var Y = M.util.get_string_yui_instance;
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');
729 stringvalue = M.str[component][identifier];
731 if (typeof a == 'undefined') {
732 // no placeholder substitution requested
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);
742 if (typeof a == 'object') {
743 // replace {$a->key} placeholders
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');
751 var search = '{$a->' + key + '}';
752 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
753 search = new RegExp(search, 'g');
754 stringvalue = stringvalue.replace(search, a[key]);
759 if (M.cfg.developerdebug) {
760 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
766 * Set focus on username or password field of the login form.
767 * @deprecated since Moodle 3.3.
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.
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.
785 * @param {Node} the node lightbox should be added to
786 * @retun {Node} created lightbox node
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');
796 node.setStyle('position', 'relative');
797 var waiticon = Y.Node.create('<img />')
799 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
802 'position' : 'relative',
806 var lightbox = Y.Node.create('<div></div>')
809 'position' : 'absolute',
814 'backgroundColor' : 'white',
815 'textAlign' : 'center'
817 .setAttribute('class', 'lightbox')
820 lightbox.appendChild(waiticon);
821 node.append(lightbox);
826 * Appends a hidden spinner element to the specified node.
829 * @param {Node} the node the spinner should be added to
830 * @return {Node} created spinner node
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');
840 var spinner = Y.Node.create('<img />')
841 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
843 .addClass('iconsmall')
846 node.append(spinner);
851 * @deprecated since Moodle 3.3.
853 function checkall() {
854 throw new Error('checkall can not be used any more. Please use jQuery instead.');
858 * @deprecated since Moodle 3.3.
860 function checknone() {
861 throw new Error('checknone can not be used any more. Please use jQuery instead.');
865 * @deprecated since Moodle 3.3.
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.
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.
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.
888 function confirm_if(expr, message) {
889 throw new Error('confirm_if can not be used any more.');
893 * @deprecated since Moodle 3.3.
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) {
911 pw.type = "password";
913 } else { //IE Browser version 8 or lower
915 // first try IE way - it can not set name attribute later
917 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
919 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
921 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
923 var newpw = document.createElement('input');
924 newpw.setAttribute('autocomplete', 'off');
925 newpw.setAttribute('name', pw.name);
927 newpw.setAttribute('type', 'text');
929 newpw.setAttribute('type', 'password');
931 newpw.setAttribute('class', pw.getAttribute('class'));
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);
943 * @deprecated since Moodle 3.3.
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.
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.
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
967 function insertAtCursor(myField, myValue) {
969 if (document.selection) {
971 sel = document.selection.createRange();
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);
981 myField.value += myValue;
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.
993 function increment_filename(filename, ignoreextension) {
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);
1006 // Look to see if the name already has (NN) at the end of it.
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];
1016 var newname = basename + ' (' + number + ')' + extension;
1021 * Return whether we are in right to left mode or not.
1025 function right_to_left() {
1026 var body = Y.one('body');
1028 if (body && body.hasClass('dir-rtl')) {
1034 function openpopup(event, args) {
1037 if (event.preventDefault) {
1038 event.preventDefault();
1040 event.returnValue = false;
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);
1053 args.name = args.name.replace(nameregex, '_');
1056 var fullurl = args.url;
1057 if (!args.url.match(/https?:\/\//)) {
1058 fullurl = M.cfg.wwwroot + args.url;
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);
1067 var windowobj = window.open(fullurl,args.name,args.options);
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)) {
1092 setTimeout(get_size_exactly_right, 10);
1095 setTimeout(get_size_exactly_right, 0);
1102 /** Close the current browser window. */
1103 function close_window(e) {
1104 if (e.preventDefault) {
1107 e.returnValue = false;
1113 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1114 * @param controlid the control id.
1116 function focuscontrol(controlid) {
1117 var control = document.getElementById(controlid);
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
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();
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') {
1147 k = encodeURIComponent(k);
1149 if(obj[k] instanceof Array) {
1150 for(var i in value) {
1151 list.push(k+'[]='+encodeURIComponent(value[i]));
1154 list.push(k+'='+encodeURIComponent(value));
1157 return list.join(separator);
1161 * @deprecated since Moodle 3.3.
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) {
1169 el = document.getElementById(id),
1176 eventData.message = msg;
1177 eventData.percent = percent;
1178 eventData.estimate = estimate;
1181 event = new CustomEvent('update', {
1186 } catch (exception) {
1187 if (!(exception instanceof TypeError)) {
1190 event = document.createEvent('CustomEvent');
1191 event.initCustomEvent('update', false, true, eventData);
1192 event.prototype = window.Event.prototype;
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);
1202 open_popup : function(e) {
1203 // Prevent the default page action
1206 // Grab the anchor that was clicked
1207 var anchor = e.target.ancestor('a', true);
1210 'url' : anchor.getAttribute('href'),
1228 args.options = options.join(',');
1235 * Custom menu namespace
1237 M.core_custom_menu = {
1239 * This method is used to initialise a custom menu given the id that belongs
1240 * to the custom menu's root node.
1243 * @param {string} nodeid
1245 init : function(Y, nodeid) {
1246 var node = Y.one('#'+nodeid);
1248 Y.use('node-menunav', function(Y) {
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);
1260 * Used to store form manipulation methods and enhancments
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
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
1278 M.util.init_skiplink = function(Y) {
1279 Y.one(Y.config.doc.body).delegate('click', function(e) {
1281 e.stopPropagation();
1282 var node = Y.one(this.getAttribute('href'));
1283 node.setAttribute('tabindex', '-1');