You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1313 lines
43 KiB
1313 lines
43 KiB
// Miscellaneous core Javascript functions for Moodle
|
|
// Global M object is initilised in inline javascript
|
|
|
|
/**
|
|
* Add module to list of available modules that can be loaded from YUI.
|
|
* @param {Array} modules
|
|
*/
|
|
M.yui.add_module = function(modules) {
|
|
for (var modname in modules) {
|
|
YUI_config.modules[modname] = modules[modname];
|
|
}
|
|
// Ensure thaat the YUI_config is applied to the main YUI instance.
|
|
Y.applyConfig(YUI_config);
|
|
};
|
|
/**
|
|
* The gallery version to use when loading YUI modules from the gallery.
|
|
* Will be changed every time when using local galleries.
|
|
*/
|
|
M.yui.galleryversion = '2010.04.21-21-51';
|
|
|
|
/**
|
|
* Various utility functions
|
|
*/
|
|
M.util = M.util || {};
|
|
|
|
/**
|
|
* Language strings - initialised from page footer.
|
|
*/
|
|
M.str = M.str || {};
|
|
|
|
/**
|
|
* Returns url for images.
|
|
* @param {String} imagename
|
|
* @param {String} component
|
|
* @return {String}
|
|
*/
|
|
M.util.image_url = function(imagename, component) {
|
|
|
|
if (!component || component == '' || component == 'moodle' || component == 'core') {
|
|
component = 'core';
|
|
}
|
|
|
|
var url = M.cfg.wwwroot + '/theme/image.php';
|
|
if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
|
|
if (!M.cfg.svgicons) {
|
|
url += '/_s';
|
|
}
|
|
url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
|
|
} else {
|
|
url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
|
|
if (!M.cfg.svgicons) {
|
|
url += '&svg=0';
|
|
}
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
M.util.in_array = function(item, array){
|
|
for( var i = 0; i<array.length; i++){
|
|
if(item==array[i]){
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Init a collapsible region, see print_collapsible_region in weblib.php
|
|
* @param {YUI} Y YUI3 instance with all libraries loaded
|
|
* @param {String} id the HTML id for the div.
|
|
* @param {String} userpref the user preference that records the state of this box. false if none.
|
|
* @param {String} strtooltip
|
|
*/
|
|
M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
|
|
Y.use('anim', function(Y) {
|
|
new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Object to handle a collapsible region : instantiate and forget styled object
|
|
*
|
|
* @class
|
|
* @constructor
|
|
* @param {YUI} Y YUI3 instance with all libraries loaded
|
|
* @param {String} id The HTML id for the div.
|
|
* @param {String} userpref The user preference that records the state of this box. false if none.
|
|
* @param {String} strtooltip
|
|
*/
|
|
M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
|
|
// Record the pref name
|
|
this.userpref = userpref;
|
|
|
|
// Find the divs in the document.
|
|
this.div = Y.one('#'+id);
|
|
|
|
// Get the caption for the collapsible region
|
|
var caption = this.div.one('#'+id + '_caption');
|
|
|
|
// Create a link
|
|
var a = Y.Node.create('<a href="#"></a>');
|
|
a.setAttribute('title', strtooltip);
|
|
|
|
// Get all the nodes from caption, remove them and append them to <a>
|
|
while (caption.hasChildNodes()) {
|
|
child = caption.get('firstChild');
|
|
child.remove();
|
|
a.append(child);
|
|
}
|
|
caption.append(a);
|
|
|
|
// Get the height of the div at this point before we shrink it if required
|
|
var height = this.div.get('offsetHeight');
|
|
var collapsedimage = 't/collapsed'; // ltr mode
|
|
if (right_to_left()) {
|
|
collapsedimage = 't/collapsed_rtl';
|
|
} else {
|
|
collapsedimage = 't/collapsed';
|
|
}
|
|
if (this.div.hasClass('collapsed')) {
|
|
// Add the correct image and record the YUI node created in the process
|
|
this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
|
|
// Shrink the div as it is collapsed by default
|
|
this.div.setStyle('height', caption.get('offsetHeight')+'px');
|
|
} else {
|
|
// Add the correct image and record the YUI node created in the process
|
|
this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
|
|
}
|
|
a.append(this.icon);
|
|
|
|
// Create the animation.
|
|
var animation = new Y.Anim({
|
|
node: this.div,
|
|
duration: 0.3,
|
|
easing: Y.Easing.easeBoth,
|
|
to: {height:caption.get('offsetHeight')},
|
|
from: {height:height}
|
|
});
|
|
|
|
// Handler for the animation finishing.
|
|
animation.on('end', function() {
|
|
this.div.toggleClass('collapsed');
|
|
var collapsedimage = 't/collapsed'; // ltr mode
|
|
if (right_to_left()) {
|
|
collapsedimage = 't/collapsed_rtl';
|
|
} else {
|
|
collapsedimage = 't/collapsed';
|
|
}
|
|
if (this.div.hasClass('collapsed')) {
|
|
this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
|
|
} else {
|
|
this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
|
|
}
|
|
}, this);
|
|
|
|
// Hook up the event handler.
|
|
a.on('click', function(e, animation) {
|
|
e.preventDefault();
|
|
// Animate to the appropriate size.
|
|
if (animation.get('running')) {
|
|
animation.stop();
|
|
}
|
|
animation.set('reverse', this.div.hasClass('collapsed'));
|
|
// Update the user preference.
|
|
if (this.userpref) {
|
|
M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
|
|
}
|
|
animation.run();
|
|
}, this, animation);
|
|
};
|
|
|
|
/**
|
|
* The user preference that stores the state of this box.
|
|
* @property userpref
|
|
* @type String
|
|
*/
|
|
M.util.CollapsibleRegion.prototype.userpref = null;
|
|
|
|
/**
|
|
* The key divs that make up this
|
|
* @property div
|
|
* @type Y.Node
|
|
*/
|
|
M.util.CollapsibleRegion.prototype.div = null;
|
|
|
|
/**
|
|
* The key divs that make up this
|
|
* @property icon
|
|
* @type Y.Node
|
|
*/
|
|
M.util.CollapsibleRegion.prototype.icon = null;
|
|
|
|
/**
|
|
* Makes a best effort to connect back to Moodle to update a user preference,
|
|
* however, there is no mechanism for finding out if the update succeeded.
|
|
*
|
|
* Before you can use this function in your JavsScript, you must have called
|
|
* user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
|
|
* the udpate is allowed, and how to safely clean and submitted values.
|
|
*
|
|
* @param String name the name of the setting to udpate.
|
|
* @param String the value to set it to.
|
|
*/
|
|
M.util.set_user_preference = function(name, value) {
|
|
YUI().use('io', function(Y) {
|
|
var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
|
|
M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
|
|
|
|
// If we are a developer, ensure that failures are reported.
|
|
var cfg = {
|
|
method: 'get',
|
|
on: {}
|
|
};
|
|
if (M.cfg.developerdebug) {
|
|
cfg.on.failure = function(id, o, args) {
|
|
alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
|
|
}
|
|
}
|
|
|
|
// Make the request.
|
|
Y.io(url, cfg);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Prints a confirmation dialog in the style of DOM.confirm().
|
|
*
|
|
* @method show_confirm_dialog
|
|
* @param {EventFacade} e
|
|
* @param {Object} args
|
|
* @param {String} args.message The question to ask the user
|
|
* @param {Function} [args.callback] A callback to apply on confirmation.
|
|
* @param {Object} [args.scope] The scope to use when calling the callback.
|
|
* @param {Object} [args.callbackargs] Any arguments to pass to the callback.
|
|
* @param {String} [args.cancellabel] The label to use on the cancel button.
|
|
* @param {String} [args.continuelabel] The label to use on the continue button.
|
|
*/
|
|
M.util.show_confirm_dialog = function(e, args) {
|
|
var target = e.target;
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
}
|
|
|
|
YUI().use('moodle-core-notification-confirm', function(Y) {
|
|
var confirmationDialogue = new M.core.confirm({
|
|
width: '300px',
|
|
center: true,
|
|
modal: true,
|
|
visible: false,
|
|
draggable: false,
|
|
title: M.util.get_string('confirmation', 'admin'),
|
|
noLabel: M.util.get_string('cancel', 'moodle'),
|
|
question: args.message
|
|
});
|
|
|
|
// The dialogue was submitted with a positive value indication.
|
|
confirmationDialogue.on('complete-yes', function(e) {
|
|
// Handle any callbacks.
|
|
if (args.callback) {
|
|
if (!Y.Lang.isFunction(args.callback)) {
|
|
Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
|
|
'warn', 'M.util.show_confirm_dialog');
|
|
return;
|
|
}
|
|
|
|
var scope = e.target;
|
|
if (Y.Lang.isObject(args.scope)) {
|
|
scope = args.scope;
|
|
}
|
|
|
|
var callbackargs = args.callbackargs || [];
|
|
args.callback.apply(scope, callbackargs);
|
|
return;
|
|
}
|
|
|
|
var targetancestor = null,
|
|
targetform = null;
|
|
|
|
if (target.test('a')) {
|
|
window.location = target.get('href');
|
|
|
|
} else if ((targetancestor = target.ancestor('a')) !== null) {
|
|
window.location = targetancestor.get('href');
|
|
|
|
} else if (target.test('input') || target.test('button')) {
|
|
targetform = target.ancestor('form', true);
|
|
if (!targetform) {
|
|
return;
|
|
}
|
|
if (target.get('name') && target.get('value')) {
|
|
targetform.append('<input type="hidden" name="' + target.get('name') +
|
|
'" value="' + target.get('value') + '">');
|
|
if (typeof M.core_formchangechecker !== 'undefined') {
|
|
M.core_formchangechecker.set_form_submitted();
|
|
}
|
|
}
|
|
targetform.submit();
|
|
|
|
} else if (target.test('form')) {
|
|
if (typeof M.core_formchangechecker !== 'undefined') {
|
|
M.core_formchangechecker.set_form_submitted();
|
|
}
|
|
target.submit();
|
|
|
|
} else {
|
|
Y.log("Element of type " + target.get('tagName') +
|
|
" is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, BUTTON or FORM",
|
|
'warn', 'javascript-static');
|
|
}
|
|
}, this);
|
|
|
|
if (args.cancellabel) {
|
|
confirmationDialogue.set('noLabel', args.cancellabel);
|
|
}
|
|
|
|
if (args.continuelabel) {
|
|
confirmationDialogue.set('yesLabel', args.continuelabel);
|
|
}
|
|
|
|
confirmationDialogue.render()
|
|
.show();
|
|
});
|
|
};
|
|
|
|
/** Useful for full embedding of various stuff */
|
|
M.util.init_maximised_embed = function(Y, id) {
|
|
var obj = Y.one('#'+id);
|
|
if (!obj) {
|
|
return;
|
|
}
|
|
|
|
var get_htmlelement_size = function(el, prop) {
|
|
if (Y.Lang.isString(el)) {
|
|
el = Y.one('#' + el);
|
|
}
|
|
// Ensure element exists.
|
|
if (el) {
|
|
var val = el.getStyle(prop);
|
|
if (val == 'auto') {
|
|
val = el.getComputedStyle(prop);
|
|
}
|
|
val = parseInt(val);
|
|
if (isNaN(val)) {
|
|
return 0;
|
|
}
|
|
return val;
|
|
} else {
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
var resize_object = function() {
|
|
obj.setStyle('display', 'none');
|
|
var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
|
|
|
|
if (newwidth > 500) {
|
|
obj.setStyle('width', newwidth + 'px');
|
|
} else {
|
|
obj.setStyle('width', '500px');
|
|
}
|
|
|
|
var headerheight = get_htmlelement_size('page-header', 'height');
|
|
var footerheight = get_htmlelement_size('page-footer', 'height');
|
|
var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
|
|
if (newheight < 400) {
|
|
newheight = 400;
|
|
}
|
|
obj.setStyle('height', newheight+'px');
|
|
obj.setStyle('display', '');
|
|
};
|
|
|
|
resize_object();
|
|
// fix layout if window resized too
|
|
Y.use('event-resize', function (Y) {
|
|
Y.on("windowresize", function() {
|
|
resize_object();
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Breaks out all links to the top frame - used in frametop page layout.
|
|
*/
|
|
M.util.init_frametop = function(Y) {
|
|
Y.all('a').each(function(node) {
|
|
node.set('target', '_top');
|
|
});
|
|
Y.all('form').each(function(node) {
|
|
node.set('target', '_top');
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3
|
|
*/
|
|
M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
|
|
throw new Error('M.util.init_toggle_class_on_click can not be used any more. Please use jQuery instead.');
|
|
};
|
|
|
|
/**
|
|
* Initialises a colour picker
|
|
*
|
|
* Designed to be used with admin_setting_configcolourpicker although could be used
|
|
* anywhere, just give a text input an id and insert a div with the class admin_colourpicker
|
|
* above or below the input (must have the same parent) and then call this with the
|
|
* id.
|
|
*
|
|
* This code was mostly taken from my [Sam Hemelryk] css theme tool available in
|
|
* contrib/blocks. For better docs refer to that.
|
|
*
|
|
* @param {YUI} Y
|
|
* @param {int} id
|
|
* @param {object} previewconf
|
|
*/
|
|
M.util.init_colour_picker = function(Y, id, previewconf) {
|
|
/**
|
|
* We need node and event-mouseenter
|
|
*/
|
|
Y.use('node', 'event-mouseenter', function(){
|
|
/**
|
|
* The colour picker object
|
|
*/
|
|
var colourpicker = {
|
|
box : null,
|
|
input : null,
|
|
image : null,
|
|
preview : null,
|
|
current : null,
|
|
eventClick : null,
|
|
eventMouseEnter : null,
|
|
eventMouseLeave : null,
|
|
eventMouseMove : null,
|
|
width : 300,
|
|
height : 100,
|
|
factor : 5,
|
|
/**
|
|
* Initalises the colour picker by putting everything together and wiring the events
|
|
*/
|
|
init : function() {
|
|
this.input = Y.one('#'+id);
|
|
this.box = this.input.ancestor().one('.admin_colourpicker');
|
|
this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
|
|
this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
|
|
this.preview = Y.Node.create('<div class="previewcolour"></div>');
|
|
this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
|
|
this.current = Y.Node.create('<div class="currentcolour"></div>');
|
|
this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
|
|
this.box.setContent('').append(this.image).append(this.preview).append(this.current);
|
|
|
|
if (typeof(previewconf) === 'object' && previewconf !== null) {
|
|
Y.one('#'+id+'_preview').on('click', function(e){
|
|
if (Y.Lang.isString(previewconf.selector)) {
|
|
Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
|
|
} else {
|
|
for (var i in previewconf.selector) {
|
|
Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
|
|
}
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
this.eventClick = this.image.on('click', this.pickColour, this);
|
|
this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
|
|
},
|
|
/**
|
|
* Starts to follow the mouse once it enter the image
|
|
*/
|
|
startFollow : function(e) {
|
|
this.eventMouseEnter.detach();
|
|
this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
|
|
this.eventMouseMove = this.image.on('mousemove', function(e){
|
|
this.preview.setStyle('backgroundColor', this.determineColour(e));
|
|
}, this);
|
|
},
|
|
/**
|
|
* Stops following the mouse
|
|
*/
|
|
endFollow : function(e) {
|
|
this.eventMouseMove.detach();
|
|
this.eventMouseLeave.detach();
|
|
this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
|
|
},
|
|
/**
|
|
* Picks the colour the was clicked on
|
|
*/
|
|
pickColour : function(e) {
|
|
var colour = this.determineColour(e);
|
|
this.input.set('value', colour);
|
|
this.current.setStyle('backgroundColor', colour);
|
|
},
|
|
/**
|
|
* Calculates the colour fromthe given co-ordinates
|
|
*/
|
|
determineColour : function(e) {
|
|
var eventx = Math.floor(e.pageX-e.target.getX());
|
|
var eventy = Math.floor(e.pageY-e.target.getY());
|
|
|
|
var imagewidth = this.width;
|
|
var imageheight = this.height;
|
|
var factor = this.factor;
|
|
var colour = [255,0,0];
|
|
|
|
var matrices = [
|
|
[ 0, 1, 0],
|
|
[ -1, 0, 0],
|
|
[ 0, 0, 1],
|
|
[ 0, -1, 0],
|
|
[ 1, 0, 0],
|
|
[ 0, 0, -1]
|
|
];
|
|
|
|
var matrixcount = matrices.length;
|
|
var limit = Math.round(imagewidth/matrixcount);
|
|
var heightbreak = Math.round(imageheight/2);
|
|
|
|
for (var x = 0; x < imagewidth; x++) {
|
|
var divisor = Math.floor(x / limit);
|
|
var matrix = matrices[divisor];
|
|
|
|
colour[0] += matrix[0]*factor;
|
|
colour[1] += matrix[1]*factor;
|
|
colour[2] += matrix[2]*factor;
|
|
|
|
if (eventx==x) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
var pixel = [colour[0], colour[1], colour[2]];
|
|
if (eventy < heightbreak) {
|
|
pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
|
|
pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
|
|
pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
|
|
} else if (eventy > heightbreak) {
|
|
pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
|
|
pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
|
|
pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
|
|
}
|
|
|
|
return this.convert_rgb_to_hex(pixel);
|
|
},
|
|
/**
|
|
* Converts an RGB value to Hex
|
|
*/
|
|
convert_rgb_to_hex : function(rgb) {
|
|
var hex = '#';
|
|
var hexchars = "0123456789ABCDEF";
|
|
for (var i=0; i<3; i++) {
|
|
var number = Math.abs(rgb[i]);
|
|
if (number == 0 || isNaN(number)) {
|
|
hex += '00';
|
|
} else {
|
|
hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
|
|
}
|
|
}
|
|
return hex;
|
|
}
|
|
};
|
|
/**
|
|
* Initialise the colour picker :) Hoorah
|
|
*/
|
|
colourpicker.init();
|
|
});
|
|
};
|
|
|
|
M.util.init_block_hider = function(Y, config) {
|
|
Y.use('base', 'node', function(Y) {
|
|
M.util.block_hider = M.util.block_hider || (function(){
|
|
var blockhider = function() {
|
|
blockhider.superclass.constructor.apply(this, arguments);
|
|
};
|
|
blockhider.prototype = {
|
|
initializer : function(config) {
|
|
this.set('block', '#'+this.get('id'));
|
|
var b = this.get('block'),
|
|
t = b.one('.title'),
|
|
a = null,
|
|
hide,
|
|
show;
|
|
if (t && (a = t.one('.block_action'))) {
|
|
hide = Y.Node.create('<img />')
|
|
.addClass('block-hider-hide')
|
|
.setAttrs({
|
|
alt: config.tooltipVisible,
|
|
src: this.get('iconVisible'),
|
|
tabIndex: 0,
|
|
'title': config.tooltipVisible
|
|
});
|
|
hide.on('keypress', this.updateStateKey, this, true);
|
|
hide.on('click', this.updateState, this, true);
|
|
|
|
show = Y.Node.create('<img />')
|
|
.addClass('block-hider-show')
|
|
.setAttrs({
|
|
alt: config.tooltipHidden,
|
|
src: this.get('iconHidden'),
|
|
tabIndex: 0,
|
|
'title': config.tooltipHidden
|
|
});
|
|
show.on('keypress', this.updateStateKey, this, false);
|
|
show.on('click', this.updateState, this, false);
|
|
|
|
a.insert(show, 0).insert(hide, 0);
|
|
}
|
|
},
|
|
updateState : function(e, hide) {
|
|
M.util.set_user_preference(this.get('preference'), hide);
|
|
if (hide) {
|
|
this.get('block').addClass('hidden');
|
|
this.get('block').one('.block-hider-show').focus();
|
|
} else {
|
|
this.get('block').removeClass('hidden');
|
|
this.get('block').one('.block-hider-hide').focus();
|
|
}
|
|
},
|
|
updateStateKey : function(e, hide) {
|
|
if (e.keyCode == 13) { //allow hide/show via enter key
|
|
this.updateState(this, hide);
|
|
}
|
|
}
|
|
};
|
|
Y.extend(blockhider, Y.Base, blockhider.prototype, {
|
|
NAME : 'blockhider',
|
|
ATTRS : {
|
|
id : {},
|
|
preference : {},
|
|
iconVisible : {
|
|
value : M.util.image_url('t/switch_minus', 'moodle')
|
|
},
|
|
iconHidden : {
|
|
value : M.util.image_url('t/switch_plus', 'moodle')
|
|
},
|
|
block : {
|
|
setter : function(node) {
|
|
return Y.one(node);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return blockhider;
|
|
})();
|
|
new M.util.block_hider(config);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* @var pending_js - The keys are the list of all pending js actions.
|
|
* @type Object
|
|
*/
|
|
M.util.pending_js = [];
|
|
M.util.complete_js = [];
|
|
|
|
/**
|
|
* Register any long running javascript code with a unique identifier.
|
|
* This is used to ensure that Behat steps do not continue with interactions until the page finishes loading.
|
|
*
|
|
* All calls to M.util.js_pending _must_ be followed by a subsequent call to M.util.js_complete with the same exact
|
|
* uniqid.
|
|
*
|
|
* This function may also be called with no arguments to test if there is any js calls pending.
|
|
*
|
|
* The uniqid specified may be any Object, including Number, String, or actual Object; however please note that the
|
|
* paired js_complete function performs a strict search for the key specified. As such, if using an Object, the exact
|
|
* Object must be passed into both functions.
|
|
*
|
|
* @param {Mixed} uniqid Register long-running code against the supplied identifier
|
|
* @return {Number} Number of pending items
|
|
*/
|
|
M.util.js_pending = function(uniqid) {
|
|
if (typeof uniqid !== 'undefined') {
|
|
M.util.pending_js.push(uniqid);
|
|
}
|
|
|
|
return M.util.pending_js.length;
|
|
};
|
|
|
|
// Start this asap.
|
|
M.util.js_pending('init');
|
|
|
|
/**
|
|
* Register listeners for Y.io start/end so we can wait for them in behat.
|
|
*/
|
|
YUI.add('moodle-core-io', function(Y) {
|
|
Y.on('io:start', function(id) {
|
|
M.util.js_pending('io:' + id);
|
|
});
|
|
Y.on('io:end', function(id) {
|
|
M.util.js_complete('io:' + id);
|
|
});
|
|
}, '@VERSION@', {
|
|
condition: {
|
|
trigger: 'io-base',
|
|
when: 'after'
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Unregister some long running javascript code using the unique identifier specified in M.util.js_pending.
|
|
*
|
|
* This function must be matched with an identical call to M.util.js_pending.
|
|
*
|
|
* @param {Mixed} uniqid Register long-running code against the supplied identifier
|
|
* @return {Number} Number of pending items remaining after removing this item
|
|
*/
|
|
M.util.js_complete = function(uniqid) {
|
|
// Use the Y.Array.indexOf instead of the native because some older browsers do not support
|
|
// the native function. Y.Array polyfills the native function if it does not exist.
|
|
var index = Y.Array.indexOf(M.util.pending_js, uniqid);
|
|
if (index >= 0) {
|
|
M.util.complete_js.push(M.util.pending_js.splice(index, 1));
|
|
} else {
|
|
window.console.log("Unable to locate key for js_complete call", uniqid);
|
|
}
|
|
|
|
return M.util.pending_js.length;
|
|
};
|
|
|
|
/**
|
|
* Returns a string registered in advance for usage in JavaScript
|
|
*
|
|
* If you do not pass the third parameter, the function will just return
|
|
* the corresponding value from the M.str object. If the third parameter is
|
|
* provided, the function performs {$a} placeholder substitution in the
|
|
* same way as PHP get_string() in Moodle does.
|
|
*
|
|
* @param {String} identifier string identifier
|
|
* @param {String} component the component providing the string
|
|
* @param {Object|String} a optional variable to populate placeholder with
|
|
*/
|
|
M.util.get_string = function(identifier, component, a) {
|
|
var stringvalue;
|
|
|
|
if (M.cfg.developerdebug) {
|
|
// creating new instance if YUI is not optimal but it seems to be better way then
|
|
// require the instance via the function API - note that it is used in rare cases
|
|
// for debugging only anyway
|
|
// To ensure we don't kill browser performance if hundreds of get_string requests
|
|
// are made we cache the instance we generate within the M.util namespace.
|
|
// We don't publicly define the variable so that it doesn't get abused.
|
|
if (typeof M.util.get_string_yui_instance === 'undefined') {
|
|
M.util.get_string_yui_instance = new YUI({ debug : true });
|
|
}
|
|
var Y = M.util.get_string_yui_instance;
|
|
}
|
|
|
|
if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
|
|
stringvalue = '[[' + identifier + ',' + component + ']]';
|
|
if (M.cfg.developerdebug) {
|
|
Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
|
|
}
|
|
return stringvalue;
|
|
}
|
|
|
|
stringvalue = M.str[component][identifier];
|
|
|
|
if (typeof a == 'undefined') {
|
|
// no placeholder substitution requested
|
|
return stringvalue;
|
|
}
|
|
|
|
if (typeof a == 'number' || typeof a == 'string') {
|
|
// replace all occurrences of {$a} with the placeholder value
|
|
stringvalue = stringvalue.replace(/\{\$a\}/g, a);
|
|
return stringvalue;
|
|
}
|
|
|
|
if (typeof a == 'object') {
|
|
// replace {$a->key} placeholders
|
|
for (var key in a) {
|
|
if (typeof a[key] != 'number' && typeof a[key] != 'string') {
|
|
if (M.cfg.developerdebug) {
|
|
Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
|
|
}
|
|
continue;
|
|
}
|
|
var search = '{$a->' + key + '}';
|
|
search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
|
search = new RegExp(search, 'g');
|
|
stringvalue = stringvalue.replace(search, a[key]);
|
|
}
|
|
return stringvalue;
|
|
}
|
|
|
|
if (M.cfg.developerdebug) {
|
|
Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
|
|
}
|
|
return stringvalue;
|
|
};
|
|
|
|
/**
|
|
* Set focus on username or password field of the login form.
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
M.util.focus_login_form = function(Y) {
|
|
Y.log('M.util.focus_login_form no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
|
|
};
|
|
|
|
/**
|
|
* Set focus on login error message.
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
M.util.focus_login_error = function(Y) {
|
|
Y.log('M.util.focus_login_error no longer does anything. Please use jquery instead.', 'warn', 'javascript-static.js');
|
|
};
|
|
|
|
/**
|
|
* Adds lightbox hidden element that covers the whole node.
|
|
*
|
|
* @param {YUI} Y
|
|
* @param {Node} the node lightbox should be added to
|
|
* @retun {Node} created lightbox node
|
|
*/
|
|
M.util.add_lightbox = function(Y, node) {
|
|
var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
|
|
|
|
// Check if lightbox is already there
|
|
if (node.one('.lightbox')) {
|
|
return node.one('.lightbox');
|
|
}
|
|
|
|
node.setStyle('position', 'relative');
|
|
var waiticon = Y.Node.create('<img />')
|
|
.setAttrs({
|
|
'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
|
|
})
|
|
.setStyles({
|
|
'position' : 'relative',
|
|
'top' : '50%'
|
|
});
|
|
|
|
var lightbox = Y.Node.create('<div></div>')
|
|
.setStyles({
|
|
'opacity' : '.75',
|
|
'position' : 'absolute',
|
|
'width' : '100%',
|
|
'height' : '100%',
|
|
'top' : 0,
|
|
'left' : 0,
|
|
'backgroundColor' : 'white',
|
|
'textAlign' : 'center'
|
|
})
|
|
.setAttribute('class', 'lightbox')
|
|
.hide();
|
|
|
|
lightbox.appendChild(waiticon);
|
|
node.append(lightbox);
|
|
return lightbox;
|
|
}
|
|
|
|
/**
|
|
* Appends a hidden spinner element to the specified node.
|
|
*
|
|
* @param {YUI} Y
|
|
* @param {Node} the node the spinner should be added to
|
|
* @return {Node} created spinner node
|
|
*/
|
|
M.util.add_spinner = function(Y, node) {
|
|
var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
|
|
|
|
// Check if spinner is already there
|
|
if (node.one('.spinner')) {
|
|
return node.one('.spinner');
|
|
}
|
|
|
|
var spinner = Y.Node.create('<img />')
|
|
.setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
|
|
.addClass('spinner')
|
|
.addClass('iconsmall')
|
|
.hide();
|
|
|
|
node.append(spinner);
|
|
return spinner;
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function checkall() {
|
|
throw new Error('checkall can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function checknone() {
|
|
throw new Error('checknone can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function select_all_in_element_with_id(id, checked) {
|
|
throw new Error('select_all_in_element_with_id can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function select_all_in(elTagName, elClass, elId) {
|
|
throw new Error('select_all_in can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function deselect_all_in(elTagName, elClass, elId) {
|
|
throw new Error('deselect_all_in can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function confirm_if(expr, message) {
|
|
throw new Error('confirm_if can not be used any more.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function findParentNode(el, elName, elClass, elId) {
|
|
throw new Error('findParentNode can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
function unmaskPassword(id) {
|
|
var pw = document.getElementById(id);
|
|
var chb = document.getElementById(id+'unmask');
|
|
|
|
// MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
|
|
// Replacing existing child with a new one, removed all yui properties for the node. Therefore, this
|
|
// functionality won't work in IE8 or lower.
|
|
// This is a temporary fixed to allow other browsers to function properly.
|
|
if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
|
|
if (chb.checked) {
|
|
pw.type = "text";
|
|
} else {
|
|
pw.type = "password";
|
|
}
|
|
} else { //IE Browser version 8 or lower
|
|
try {
|
|
// first try IE way - it can not set name attribute later
|
|
if (chb.checked) {
|
|
var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
|
|
} else {
|
|
var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
|
|
}
|
|
newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
|
|
} catch (e) {
|
|
var newpw = document.createElement('input');
|
|
newpw.setAttribute('autocomplete', 'off');
|
|
newpw.setAttribute('name', pw.name);
|
|
if (chb.checked) {
|
|
newpw.setAttribute('type', 'text');
|
|
} else {
|
|
newpw.setAttribute('type', 'password');
|
|
}
|
|
newpw.setAttribute('class', pw.getAttribute('class'));
|
|
}
|
|
newpw.id = pw.id;
|
|
newpw.size = pw.size;
|
|
newpw.onblur = pw.onblur;
|
|
newpw.onchange = pw.onchange;
|
|
newpw.value = pw.value;
|
|
pw.parentNode.replaceChild(newpw, pw);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function filterByParent(elCollection, parentFinder) {
|
|
throw new Error('filterByParent can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
|
|
*/
|
|
function fix_column_widths() {
|
|
Y.log('fix_column_widths() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3, but shouldn't be used in earlier versions either.
|
|
*/
|
|
function fix_column_width(colName) {
|
|
Y.log('fix_column_width() no longer does anything. Please remove it from your code.', 'warn', 'javascript-static.js');
|
|
}
|
|
|
|
|
|
/*
|
|
Insert myValue at current cursor position
|
|
*/
|
|
function insertAtCursor(myField, myValue) {
|
|
// IE support
|
|
if (document.selection) {
|
|
myField.focus();
|
|
sel = document.selection.createRange();
|
|
sel.text = myValue;
|
|
}
|
|
// Mozilla/Netscape support
|
|
else if (myField.selectionStart || myField.selectionStart == '0') {
|
|
var startPos = myField.selectionStart;
|
|
var endPos = myField.selectionEnd;
|
|
myField.value = myField.value.substring(0, startPos)
|
|
+ myValue + myField.value.substring(endPos, myField.value.length);
|
|
} else {
|
|
myField.value += myValue;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Increment a file name.
|
|
*
|
|
* @param string file name.
|
|
* @param boolean ignoreextension do not extract the extension prior to appending the
|
|
* suffix. Useful when incrementing folder names.
|
|
* @return string the incremented file name.
|
|
*/
|
|
function increment_filename(filename, ignoreextension) {
|
|
var extension = '';
|
|
var basename = filename;
|
|
|
|
// Split the file name into the basename + extension.
|
|
if (!ignoreextension) {
|
|
var dotpos = filename.lastIndexOf('.');
|
|
if (dotpos !== -1) {
|
|
basename = filename.substr(0, dotpos);
|
|
extension = filename.substr(dotpos, filename.length);
|
|
}
|
|
}
|
|
|
|
// Look to see if the name already has (NN) at the end of it.
|
|
var number = 0;
|
|
var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
|
|
if (hasnumber !== null) {
|
|
// Note the current number & remove it from the basename.
|
|
number = parseInt(hasnumber[2], 10);
|
|
basename = hasnumber[1];
|
|
}
|
|
|
|
number++;
|
|
var newname = basename + ' (' + number + ')' + extension;
|
|
return newname;
|
|
}
|
|
|
|
/**
|
|
* Return whether we are in right to left mode or not.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
function right_to_left() {
|
|
var body = Y.one('body');
|
|
var rtl = false;
|
|
if (body && body.hasClass('dir-rtl')) {
|
|
rtl = true;
|
|
}
|
|
return rtl;
|
|
}
|
|
|
|
function openpopup(event, args) {
|
|
|
|
if (event) {
|
|
if (event.preventDefault) {
|
|
event.preventDefault();
|
|
} else {
|
|
event.returnValue = false;
|
|
}
|
|
}
|
|
|
|
// Make sure the name argument is set and valid.
|
|
var nameregex = /[^a-z0-9_]/i;
|
|
if (typeof args.name !== 'string') {
|
|
args.name = '_blank';
|
|
} else if (args.name.match(nameregex)) {
|
|
// Cleans window name because IE does not support funky ones.
|
|
if (M.cfg.developerdebug) {
|
|
alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
|
|
}
|
|
args.name = args.name.replace(nameregex, '_');
|
|
}
|
|
|
|
var fullurl = args.url;
|
|
if (!args.url.match(/https?:\/\//)) {
|
|
fullurl = M.cfg.wwwroot + args.url;
|
|
}
|
|
if (args.fullscreen) {
|
|
args.options = args.options.
|
|
replace(/top=\d+/, 'top=0').
|
|
replace(/left=\d+/, 'left=0').
|
|
replace(/width=\d+/, 'width=' + screen.availWidth).
|
|
replace(/height=\d+/, 'height=' + screen.availHeight);
|
|
}
|
|
var windowobj = window.open(fullurl,args.name,args.options);
|
|
if (!windowobj) {
|
|
return true;
|
|
}
|
|
|
|
if (args.fullscreen) {
|
|
// In some browser / OS combinations (E.g. Chrome on Windows), the
|
|
// window initially opens slighly too big. The width and heigh options
|
|
// seem to control the area inside the browser window, so what with
|
|
// scroll-bars, etc. the actual window is bigger than the screen.
|
|
// Therefore, we need to fix things up after the window is open.
|
|
var hackcount = 100;
|
|
var get_size_exactly_right = function() {
|
|
windowobj.moveTo(0, 0);
|
|
windowobj.resizeTo(screen.availWidth, screen.availHeight);
|
|
|
|
// Unfortunately, it seems that in Chrome on Ubuntu, if you call
|
|
// something like windowobj.resizeTo(1280, 1024) too soon (up to
|
|
// about 50ms) after the window is open, then it actually behaves
|
|
// as if you called windowobj.resizeTo(0, 0). Therefore, we need to
|
|
// check that the resize actually worked, and if not, repeatedly try
|
|
// again after a short delay until it works (but with a limit of
|
|
// hackcount repeats.
|
|
if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
|
|
hackcount -= 1;
|
|
setTimeout(get_size_exactly_right, 10);
|
|
}
|
|
}
|
|
setTimeout(get_size_exactly_right, 0);
|
|
}
|
|
windowobj.focus();
|
|
|
|
return false;
|
|
}
|
|
|
|
/** Close the current browser window. */
|
|
function close_window(e) {
|
|
if (e.preventDefault) {
|
|
e.preventDefault();
|
|
} else {
|
|
e.returnValue = false;
|
|
}
|
|
window.close();
|
|
}
|
|
|
|
/**
|
|
* Tranfer keyboard focus to the HTML element with the given id, if it exists.
|
|
* @param controlid the control id.
|
|
*/
|
|
function focuscontrol(controlid) {
|
|
var control = document.getElementById(controlid);
|
|
if (control) {
|
|
control.focus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Transfers keyboard focus to an HTML element based on the old style style of focus
|
|
* This function should be removed as soon as it is no longer used
|
|
*/
|
|
function old_onload_focus(formid, controlname) {
|
|
if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
|
|
document.forms[formid].elements[controlname].focus();
|
|
}
|
|
}
|
|
|
|
function build_querystring(obj) {
|
|
return convert_object_to_string(obj, '&');
|
|
}
|
|
|
|
function build_windowoptionsstring(obj) {
|
|
return convert_object_to_string(obj, ',');
|
|
}
|
|
|
|
function convert_object_to_string(obj, separator) {
|
|
if (typeof obj !== 'object') {
|
|
return null;
|
|
}
|
|
var list = [];
|
|
for(var k in obj) {
|
|
k = encodeURIComponent(k);
|
|
var value = obj[k];
|
|
if(obj[k] instanceof Array) {
|
|
for(var i in value) {
|
|
list.push(k+'[]='+encodeURIComponent(value[i]));
|
|
}
|
|
} else {
|
|
list.push(k+'='+encodeURIComponent(value));
|
|
}
|
|
}
|
|
return list.join(separator);
|
|
}
|
|
|
|
/**
|
|
* @deprecated since Moodle 3.3.
|
|
*/
|
|
function stripHTML(str) {
|
|
throw new Error('stripHTML can not be used any more. Please use jQuery instead.');
|
|
}
|
|
|
|
function updateProgressBar(id, percent, msg, estimate) {
|
|
var event,
|
|
el = document.getElementById(id),
|
|
eventData = {};
|
|
|
|
if (!el) {
|
|
return;
|
|
}
|
|
|
|
eventData.message = msg;
|
|
eventData.percent = percent;
|
|
eventData.estimate = estimate;
|
|
|
|
try {
|
|
event = new CustomEvent('update', {
|
|
bubbles: false,
|
|
cancelable: true,
|
|
detail: eventData
|
|
});
|
|
} catch (exception) {
|
|
if (!(exception instanceof TypeError)) {
|
|
throw exception;
|
|
}
|
|
event = document.createEvent('CustomEvent');
|
|
event.initCustomEvent('update', false, true, eventData);
|
|
event.prototype = window.Event.prototype;
|
|
}
|
|
|
|
el.dispatchEvent(event);
|
|
}
|
|
|
|
M.util.help_popups = {
|
|
setup : function(Y) {
|
|
Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
|
|
},
|
|
open_popup : function(e) {
|
|
// Prevent the default page action
|
|
e.preventDefault();
|
|
|
|
// Grab the anchor that was clicked
|
|
var anchor = e.target.ancestor('a', true);
|
|
var args = {
|
|
'name' : 'popup',
|
|
'url' : anchor.getAttribute('href'),
|
|
'options' : ''
|
|
};
|
|
var options = [
|
|
'height=600',
|
|
'width=800',
|
|
'top=0',
|
|
'left=0',
|
|
'menubar=0',
|
|
'location=0',
|
|
'scrollbars',
|
|
'resizable',
|
|
'toolbar',
|
|
'status',
|
|
'directories=0',
|
|
'fullscreen=0',
|
|
'dependent'
|
|
]
|
|
args.options = options.join(',');
|
|
|
|
openpopup(e, args);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Custom menu namespace
|
|
*/
|
|
M.core_custom_menu = {
|
|
/**
|
|
* This method is used to initialise a custom menu given the id that belongs
|
|
* to the custom menu's root node.
|
|
*
|
|
* @param {YUI} Y
|
|
* @param {string} nodeid
|
|
*/
|
|
init : function(Y, nodeid) {
|
|
var node = Y.one('#'+nodeid);
|
|
if (node) {
|
|
Y.use('node-menunav', function(Y) {
|
|
// Get the node
|
|
// Remove the javascript-disabled class.... obviously javascript is enabled.
|
|
node.removeClass('javascript-disabled');
|
|
// Initialise the menunav plugin
|
|
node.plug(Y.Plugin.NodeMenuNav);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Used to store form manipulation methods and enhancments
|
|
*/
|
|
M.form = M.form || {};
|
|
|
|
/**
|
|
* Converts a nbsp indented select box into a multi drop down custom control much
|
|
* like the custom menu. Can no longer be used.
|
|
* @deprecated since Moodle 3.3
|
|
*/
|
|
M.form.init_smartselect = function() {
|
|
throw new Error('M.form.init_smartselect can not be used any more.');
|
|
};
|
|
|
|
/**
|
|
* Initiates the listeners for skiplink interaction
|
|
*
|
|
* @param {YUI} Y
|
|
*/
|
|
M.util.init_skiplink = function(Y) {
|
|
Y.one(Y.config.doc.body).delegate('click', function(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var node = Y.one(this.getAttribute('href'));
|
|
node.setAttribute('tabindex', '-1');
|
|
node.focus();
|
|
return true;
|
|
}, 'a.skip');
|
|
};
|
|
|