// Miscellaneous core Javascript functions for Moodle // Global M object is initilised in inline javascript /** * Add module to list of available modules that can be laoded from YUI. * @param {Array} modules */ M.yui.add_module = function(modules) { for (var modname in modules) { M.yui.loader.modules[modname] = modules[modname]; } }; /** * 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) { var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename; if (M.cfg.themerev > 0) { url = url + '&rev=' + M.cfg.themerev; } if (component && component != '' && component != 'moodle' && component != 'core') { url = url + '&component=' + component; } return url; }; M.util.create_UFO_object = function (eid, FO) { UFO.create(FO, eid); } M.util.in_array = function(item, array){ for( var i = 0; i'); // Create a local scoped lamba function to move nodes to a new link var movenode = function(node){ node.remove(); a.append(node); }; // Apply the lamba function on each of the captions child nodes caption.get('children').each(movenode, this); caption.append(a); // Get the height of the div at this point before we shrink it if required var height = this.div.get('offsetHeight'); if (this.div.hasClass('collapsed')) { // Add the correct image and record the YUI node created in the process this.icon = Y.Node.create(''); // 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(''); } 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'); if (this.div.hasClass('collapsed')) { this.icon.set('src', M.util.image_url('t/collapsed', '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(M.yui.loader).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(). * @param object event A YUI DOM event or null if launched manually * @param string message The message to show in the dialog * @param string url The URL to forward to if YES is clicked. Disabled if fn is given * @param function fn A JS function to run if YES is clicked. */ M.util.show_confirm_dialog = function(e, args) { var target = e.target; if (e.preventDefault) { e.preventDefault(); } YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) { var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog', {width: '300px', fixedcenter: true, modal: true, visible: false, draggable: false } ); simpledialog.setHeader(M.str.admin.confirmation); simpledialog.setBody(args.message); simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN); var handle_cancel = function() { simpledialog.hide(); }; var handle_yes = function() { simpledialog.hide(); if (args.callback) { // args comes from PHP, so callback will be a string, needs to be evaluated by JS var callback = null; if (Y.Lang.isFunction(args.callback)) { callback = args.callback; } else { callback = eval('('+args.callback+')'); } if (Y.Lang.isObject(args.scope)) { var sc = args.scope; } else { var sc = e.target; } if (args.callbackargs) { callback.apply(sc, args.callbackargs); } else { callback.apply(sc); } return; } if (target.get('tagName').toLowerCase() == 'a') { window.location = target.get('href'); } else if (target.get('tagName').toLowerCase() == 'input') { var parentelement = target.get('parentNode'); while (parentelement.get('tagName').toLowerCase() != 'form' && parentelement.get('tagName').toLowerCase() != 'body') { parentelement = parentelement.get('parentNode'); } if (parentelement.get('tagName').toLowerCase() == 'form') { parentelement.submit(); } } else if (M.cfg.developerdebug) { alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT"); } }; var buttons = [ {text: M.str.moodle.cancel, handler: handle_cancel, isDefault: true}, {text: M.str.moodle.yes, handler: handle_yes} ]; simpledialog.cfg.queueProperty('buttons', buttons); simpledialog.render(document.body); simpledialog.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); } var val = el.getStyle(prop); if (val == 'auto') { val = el.getComputedStyle(prop); } return parseInt(val); }; var resize_object = function() { obj.setStyle('width', '0px'); obj.setStyle('height', '0px'); var newwidth = get_htmlelement_size('content', 'width') - 15; if (newwidth > 600) { obj.setStyle('width', newwidth + 'px'); } else { obj.setStyle('width', '600px'); } var pageheight = get_htmlelement_size('page', 'height'); var objheight = get_htmlelement_size(obj, 'height'); var newheight = objheight + parseInt(obj.get('winHeight')) - pageheight - 30; if (newheight > 400) { obj.setStyle('height', newheight + 'px'); } else { obj.setStyle('height', '400px'); } }; resize_object(); // fix layout if window resized too window.onresize = function() { resize_object(); }; }; /** * Attach handler to single_select */ M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) { Y.use('event-key', function() { var select = Y.one('#'+selectid); if (select) { // Try to get the form by id var form = Y.one('#'+formid) || (function(){ // Hmmm the form's id may have been overriden by an internal input // with the name id which will KILL IE. // We need to manually iterate at this point because if the case // above is true YUI's ancestor method will also kill IE! var form = select; while (form && form.get('nodeName').toUpperCase() !== 'FORM') { form = form.ancestor(); } return form; })(); // Make sure we have the form if (form) { // Create a function to handle our change event var processchange = function(e, lastindex) { if ((nothing===false || select.get('value') != nothing) && lastindex != select.get('selectedIndex')) { this.submit(); } } // Attach the change event to the keypress, blur, and click actions. // We don't use the change event because IE fires it on every arrow up/down // event.... usability Y.on('key', processchange, select, 'press:13', form, select.get('selectedIndex')); select.on('blur', processchange, form, select.get('selectedIndex')); select.on('click', processchange, form, select.get('selectedIndex')); } } }); }; /** * Attach handler to url_select */ M.util.init_url_select = function(Y, formid, selectid, nothing) { YUI(M.yui.loader).use('node', function(Y) { Y.on('change', function() { if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) { window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value'); } }, '#'+selectid); }); }; /** * 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'); }); }; /** * Finds all nodes that match the given CSS selector and attaches events to them * so that they toggle a given classname when clicked. * * @param {YUI} Y * @param {string} id An id containing elements to target * @param {string} cssselector A selector to use to find targets * @param {string} toggleclassname A classname to toggle */ M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname) { var node = Y.one('#'+id); node.all(cssselector).each(function(node){ node.on('click', function(e){ e.stopPropagation(); if (e.target.get('nodeName')!='A' && e.target.get('nodeName')!='IMG') { this.toggleClass(toggleclassname); } }, node); }); // Attach this click event to the node rather than all selectors... will be much better // for performance node.on('click', function(e){ if (e.target.hasClass('addtoall')) { node.all(cssselector).addClass(toggleclassname); } else if (e.target.hasClass('removefromall')) { node.all(cssselector+'.'+toggleclassname).removeClass(toggleclassname); } }, node); } /** * 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(''); this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle')); this.preview = Y.Node.create('
'); this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value')); this.current = Y.Node.create('
'); 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){ Y.one(previewconf.selector).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(); }); } //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code === function popupchecker(msg) { var testwindow = window.open('', '', 'width=1,height=1,left=0,top=0,scrollbars=no'); if (!testwindow) { alert(msg); } else { testwindow.close(); } } function checkall() { var inputs = document.getElementsByTagName('input'); for (var i = 0; i < inputs.length; i++) { if (inputs[i].type == 'checkbox') { inputs[i].checked = true; } } } function checknone() { var inputs = document.getElementsByTagName('input'); for (var i = 0; i < inputs.length; i++) { if (inputs[i].type == 'checkbox') { inputs[i].checked = false; } } } function lockoptions(formid, master, subitems) { // Subitems is an array of names of sub items. // Optionally, each item in subitems may have a // companion hidden item in the form with the // same name but prefixed by "h". var form = document.forms[formid]; if (eval("form."+master+".checked")) { for (i=0; i