1 // Miscellaneous core Javascript functions for Moodle
2 // Global M object is initilised in inline javascript
5 * Add module to list of available modules that can be laoded from YUI.
6 * @param {Array} modules
8 M.yui.add_module = function(modules) {
9 for (var modname in modules) {
10 M.yui.loader.modules[modname] = modules[modname];
14 * The gallery version to use when loading YUI modules from the gallery.
15 * Will be changed every time when using local galleries.
17 M.yui.galleryversion = '2010.04.21-21-51';
20 * Various utility functions
22 M.util = M.util || {};
25 * Language strings - initialised from page footer.
30 * Returns url for images.
31 * @param {String} imagename
32 * @param {String} component
35 M.util.image_url = function(imagename, component) {
36 var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename;
38 if (M.cfg.themerev > 0) {
39 url = url + '&rev=' + M.cfg.themerev;
42 if (component && component != '' && component != 'moodle' && component != 'core') {
43 url = url + '&component=' + component;
49 M.util.create_UFO_object = function (eid, FO) {
53 M.util.in_array = function(item, array){
54 for( var i = 0; i<array.length; i++){
63 * Init a collapsible region, see print_collapsible_region in weblib.php
64 * @param {YUI} Y YUI3 instance with all libraries loaded
65 * @param {String} id the HTML id for the div.
66 * @param {String} userpref the user preference that records the state of this box. false if none.
67 * @param {String} strtooltip
69 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
70 Y.use('anim', function(Y) {
71 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
76 * Object to handle a collapsible region : instantiate and forget styled object
80 * @param {YUI} Y YUI3 instance with all libraries loaded
81 * @param {String} id The HTML id for the div.
82 * @param {String} userpref The user preference that records the state of this box. false if none.
83 * @param {String} strtooltip
85 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
86 // Record the pref name
87 this.userpref = userpref;
89 // Find the divs in the document.
90 this.div = Y.one('#'+id);
92 // Get the caption for the collapsible region
93 var caption = this.div.one('#'+id + '_caption');
94 caption.setAttribute('title', strtooltip);
97 var a = Y.Node.create('<a href="#"></a>');
98 // Create a local scoped lamba function to move nodes to a new link
99 var movenode = function(node){
103 // Apply the lamba function on each of the captions child nodes
104 caption.get('children').each(movenode, this);
107 // Get the height of the div at this point before we shrink it if required
108 var height = this.div.get('offsetHeight');
109 if (this.div.hasClass('collapsed')) {
110 // Add the correct image and record the YUI node created in the process
111 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
112 // Shrink the div as it is collapsed by default
113 this.div.setStyle('height', caption.get('offsetHeight')+'px');
115 // Add the correct image and record the YUI node created in the process
116 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
120 // Create the animation.
121 var animation = new Y.Anim({
124 easing: Y.Easing.easeBoth,
125 to: {height:caption.get('offsetHeight')},
126 from: {height:height}
129 // Handler for the animation finishing.
130 animation.on('end', function() {
131 this.div.toggleClass('collapsed');
132 if (this.div.hasClass('collapsed')) {
133 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
135 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
139 // Hook up the event handler.
140 a.on('click', function(e, animation) {
142 // Animate to the appropriate size.
143 if (animation.get('running')) {
146 animation.set('reverse', this.div.hasClass('collapsed'));
147 // Update the user preference.
149 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
156 * The user preference that stores the state of this box.
160 M.util.CollapsibleRegion.prototype.userpref = null;
163 * The key divs that make up this
167 M.util.CollapsibleRegion.prototype.div = null;
170 * The key divs that make up this
174 M.util.CollapsibleRegion.prototype.icon = null;
177 * Makes a best effort to connect back to Moodle to update a user preference,
178 * however, there is no mechanism for finding out if the update succeeded.
180 * Before you can use this function in your JavsScript, you must have called
181 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
182 * the udpate is allowed, and how to safely clean and submitted values.
184 * @param String name the name of the setting to udpate.
185 * @param String the value to set it to.
187 M.util.set_user_preference = function(name, value) {
188 YUI(M.yui.loader).use('io', function(Y) {
189 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
190 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
192 // If we are a developer, ensure that failures are reported.
197 if (M.cfg.developerdebug) {
198 cfg.on.failure = function(id, o, args) {
199 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
209 * Prints a confirmation dialog in the style of DOM.confirm().
210 * @param object event A YUI DOM event or null if launched manually
211 * @param string message The message to show in the dialog
212 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
213 * @param function fn A JS function to run if YES is clicked.
215 M.util.show_confirm_dialog = function(e, args) {
216 var target = e.target;
217 if (e.preventDefault) {
221 YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
222 var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
231 simpledialog.setHeader(M.str.admin.confirmation);
232 simpledialog.setBody(args.message);
233 simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
235 var handle_cancel = function() {
239 var handle_yes = function() {
243 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
245 if (Y.Lang.isFunction(args.callback)) {
246 callback = args.callback;
248 callback = eval('('+args.callback+')');
251 if (Y.Lang.isObject(args.scope)) {
257 if (args.callbackargs) {
258 callback.apply(sc, args.callbackargs);
265 var targetancestor = null,
268 if (target.test('a')) {
269 window.location = target.get('href');
270 } else if ((targetancestor = target.ancestor('a')) !== null) {
271 window.location = targetancestor.get('href');
272 } else if (target.test('input')) {
273 targetform = target.ancestor('form');
274 if (targetform && targetform.submit) {
277 } else if (M.cfg.developerdebug) {
278 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT");
282 var buttons = [ {text: M.str.moodle.cancel, handler: handle_cancel, isDefault: true},
283 {text: M.str.moodle.yes, handler: handle_yes} ];
285 simpledialog.cfg.queueProperty('buttons', buttons);
287 simpledialog.render(document.body);
292 /** Useful for full embedding of various stuff */
293 M.util.init_maximised_embed = function(Y, id) {
294 var obj = Y.one('#'+id);
300 var get_htmlelement_size = function(el, prop) {
301 if (Y.Lang.isString(el)) {
302 el = Y.one('#' + el);
304 var val = el.getStyle(prop);
306 val = el.getComputedStyle(prop);
308 return parseInt(val);
311 var resize_object = function() {
312 obj.setStyle('width', '0px');
313 obj.setStyle('height', '0px');
314 var newwidth = get_htmlelement_size('maincontent', 'width') - 15;
316 if (newwidth > 600) {
317 obj.setStyle('width', newwidth + 'px');
319 obj.setStyle('width', '600px');
322 var headerheight = get_htmlelement_size('page-header', 'height');
323 var footerheight = get_htmlelement_size('page-footer', 'height');
324 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 20;
325 if (newheight < 400) {
328 obj.setStyle('height', newheight+'px');
332 // fix layout if window resized too
333 window.onresize = function() {
339 * Attach handler to single_select
341 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
342 Y.use('event-key', function() {
343 var select = Y.one('#'+selectid);
345 // Try to get the form by id
346 var form = Y.one('#'+formid) || (function(){
347 // Hmmm the form's id may have been overriden by an internal input
348 // with the name id which will KILL IE.
349 // We need to manually iterate at this point because if the case
350 // above is true YUI's ancestor method will also kill IE!
352 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
353 form = form.ancestor();
357 // Make sure we have the form
359 // Create a function to handle our change event
360 var processchange = function(e, lastindex) {
361 if ((nothing===false || select.get('value') != nothing) && lastindex != select.get('selectedIndex')) {
365 // Attach the change event to the keypress, blur, and click actions.
366 // We don't use the change event because IE fires it on every arrow up/down
367 // event.... usability
368 Y.on('key', processchange, select, 'press:13', form, select.get('selectedIndex'));
369 select.on('blur', processchange, form, select.get('selectedIndex'));
370 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
372 select.on('change', processchange, form, select.get('selectedIndex'));
374 select.on('click', processchange, form, select.get('selectedIndex'));
382 * Attach handler to url_select
384 M.util.init_url_select = function(Y, formid, selectid, nothing) {
385 YUI(M.yui.loader).use('node', function(Y) {
386 Y.on('change', function() {
387 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
388 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
396 * Breaks out all links to the top frame - used in frametop page layout.
398 M.util.init_frametop = function(Y) {
399 Y.all('a').each(function(node) {
400 node.set('target', '_top');
402 Y.all('form').each(function(node) {
403 node.set('target', '_top');
408 * Finds all nodes that match the given CSS selector and attaches events to them
409 * so that they toggle a given classname when clicked.
412 * @param {string} id An id containing elements to target
413 * @param {string} cssselector A selector to use to find targets
414 * @param {string} toggleclassname A classname to toggle
416 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
418 if (togglecssselector == '') {
419 togglecssselector = cssselector;
422 var node = Y.one('#'+id);
423 node.all(cssselector).each(function(n){
424 n.on('click', function(e){
426 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
427 if (this.test(togglecssselector)) {
428 this.toggleClass(toggleclassname);
430 this.ancestor(togglecssselector).toggleClass(toggleclassname);
435 // Attach this click event to the node rather than all selectors... will be much better
437 node.on('click', function(e){
438 if (e.target.hasClass('addtoall')) {
439 this.all(togglecssselector).addClass(toggleclassname);
440 } else if (e.target.hasClass('removefromall')) {
441 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
447 * Initialises a colour picker
449 * Designed to be used with admin_setting_configcolourpicker although could be used
450 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
451 * above or below the input (must have the same parent) and then call this with the
454 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
455 * contrib/blocks. For better docs refer to that.
459 * @param {object} previewconf
461 M.util.init_colour_picker = function(Y, id, previewconf) {
463 * We need node and event-mouseenter
465 Y.use('node', 'event-mouseenter', function(){
467 * The colour picker object
476 eventMouseEnter : null,
477 eventMouseLeave : null,
478 eventMouseMove : null,
483 * Initalises the colour picker by putting everything together and wiring the events
486 this.input = Y.one('#'+id);
487 this.box = this.input.ancestor().one('.admin_colourpicker');
488 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
489 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
490 this.preview = Y.Node.create('<div class="previewcolour"></div>');
491 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
492 this.current = Y.Node.create('<div class="currentcolour"></div>');
493 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
494 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
496 if (typeof(previewconf) === 'object' && previewconf !== null) {
497 Y.one('#'+id+'_preview').on('click', function(e){
498 if (Y.Lang.isString(previewconf.selector)) {
499 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
501 for (var i in previewconf.selector) {
502 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
508 this.eventClick = this.image.on('click', this.pickColour, this);
509 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
512 * Starts to follow the mouse once it enter the image
514 startFollow : function(e) {
515 this.eventMouseEnter.detach();
516 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
517 this.eventMouseMove = this.image.on('mousemove', function(e){
518 this.preview.setStyle('backgroundColor', this.determineColour(e));
522 * Stops following the mouse
524 endFollow : function(e) {
525 this.eventMouseMove.detach();
526 this.eventMouseLeave.detach();
527 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
530 * Picks the colour the was clicked on
532 pickColour : function(e) {
533 var colour = this.determineColour(e);
534 this.input.set('value', colour);
535 this.current.setStyle('backgroundColor', colour);
538 * Calculates the colour fromthe given co-ordinates
540 determineColour : function(e) {
541 var eventx = Math.floor(e.pageX-e.target.getX());
542 var eventy = Math.floor(e.pageY-e.target.getY());
544 var imagewidth = this.width;
545 var imageheight = this.height;
546 var factor = this.factor;
547 var colour = [255,0,0];
558 var matrixcount = matrices.length;
559 var limit = Math.round(imagewidth/matrixcount);
560 var heightbreak = Math.round(imageheight/2);
562 for (var x = 0; x < imagewidth; x++) {
563 var divisor = Math.floor(x / limit);
564 var matrix = matrices[divisor];
566 colour[0] += matrix[0]*factor;
567 colour[1] += matrix[1]*factor;
568 colour[2] += matrix[2]*factor;
575 var pixel = [colour[0], colour[1], colour[2]];
576 if (eventy < heightbreak) {
577 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
578 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
579 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
580 } else if (eventy > heightbreak) {
581 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
582 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
583 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
586 return this.convert_rgb_to_hex(pixel);
589 * Converts an RGB value to Hex
591 convert_rgb_to_hex : function(rgb) {
593 var hexchars = "0123456789ABCDEF";
594 for (var i=0; i<3; i++) {
595 var number = Math.abs(rgb[i]);
596 if (number == 0 || isNaN(number)) {
599 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
606 * Initialise the colour picker :) Hoorah
612 M.util.init_block_hider = function(Y, config) {
613 Y.use('base', 'node', function(Y) {
614 M.util.block_hider = M.util.block_hider || (function(){
615 var blockhider = function() {
616 blockhider.superclass.constructor.apply(this, arguments);
618 blockhider.prototype = {
619 initializer : function(config) {
620 this.set('block', '#'+this.get('id'));
621 var b = this.get('block'),
624 if (t && (a = t.one('.block_action'))) {
625 var hide = Y.Node.create('<img class="block-hider-hide" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
626 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
627 var show = Y.Node.create('<img class="block-hider-show" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
628 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
629 a.insert(show, 0).insert(hide, 0);
632 updateState : function(e, hide) {
633 M.util.set_user_preference(this.get('preference'), hide);
635 this.get('block').addClass('hidden');
637 this.get('block').removeClass('hidden');
641 Y.extend(blockhider, Y.Base, blockhider.prototype, {
647 value : M.util.image_url('t/switch_minus', 'moodle')
650 value : M.util.image_url('t/switch_plus', 'moodle')
653 setter : function(node) {
661 new M.util.block_hider(config);
666 * Returns a string registered in advance for usage in JavaScript
668 * If you do not pass the third parameter, the function will just return
669 * the corresponding value from the M.str object. If the third parameter is
670 * provided, the function performs {$a} placeholder substitution in the
671 * same way as PHP get_string() in Moodle does.
673 * @param {String} identifier string identifier
674 * @param {String} component the component providing the string
675 * @param {Object|String} a optional variable to populate placeholder with
677 M.util.get_string = function(identifier, component, a) {
680 if (M.cfg.developerdebug) {
681 // creating new instance if YUI is not optimal but it seems to be better way then
682 // require the instance via the function API - note that it is used in rare cases
683 // for debugging only anyway
684 var Y = new YUI({ debug : true });
687 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
688 stringvalue = '[[' + identifier + ',' + component + ']]';
689 if (M.cfg.developerdebug) {
690 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
695 stringvalue = M.str[component][identifier];
697 if (typeof a == 'undefined') {
698 // no placeholder substitution requested
702 if (typeof a == 'number' || typeof a == 'string') {
703 // replace all occurrences of {$a} with the placeholder value
704 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
708 if (typeof a == 'object') {
709 // replace {$a->key} placeholders
711 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
712 if (M.cfg.developerdebug) {
713 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
717 var search = '{$a->' + key + '}';
718 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
719 search = new RegExp(search, 'g');
720 stringvalue = stringvalue.replace(search, a[key]);
725 if (M.cfg.developerdebug) {
726 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
731 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
733 function checkall() {
734 var inputs = document.getElementsByTagName('input');
735 for (var i = 0; i < inputs.length; i++) {
736 if (inputs[i].type == 'checkbox') {
737 inputs[i].checked = true;
742 function checknone() {
743 var inputs = document.getElementsByTagName('input');
744 for (var i = 0; i < inputs.length; i++) {
745 if (inputs[i].type == 'checkbox') {
746 inputs[i].checked = false;
752 * Either check, or uncheck, all checkboxes inside the element with id is
753 * @param id the id of the container
754 * @param checked the new state, either '' or 'checked'.
756 function select_all_in_element_with_id(id, checked) {
757 var container = document.getElementById(id);
761 var inputs = container.getElementsByTagName('input');
762 for (var i = 0; i < inputs.length; ++i) {
763 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
764 inputs[i].checked = checked;
769 function select_all_in(elTagName, elClass, elId) {
770 var inputs = document.getElementsByTagName('input');
771 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
772 for(var i = 0; i < inputs.length; ++i) {
773 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
774 inputs[i].checked = 'checked';
779 function deselect_all_in(elTagName, elClass, elId) {
780 var inputs = document.getElementsByTagName('INPUT');
781 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
782 for(var i = 0; i < inputs.length; ++i) {
783 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
784 inputs[i].checked = '';
789 function confirm_if(expr, message) {
793 return confirm(message);
798 findParentNode (start, elementName, elementClass, elementID)
800 Travels up the DOM hierarchy to find a parent element with the
801 specified tag name, class, and id. All conditions must be met,
802 but any can be ommitted. Returns the BODY element if no match
805 function findParentNode(el, elName, elClass, elId) {
806 while (el.nodeName.toUpperCase() != 'BODY') {
807 if ((!elName || el.nodeName.toUpperCase() == elName) &&
808 (!elClass || el.className.indexOf(elClass) != -1) &&
809 (!elId || el.id == elId)) {
817 findChildNode (start, elementName, elementClass, elementID)
819 Travels down the DOM hierarchy to find all child elements with the
820 specified tag name, class, and id. All conditions must be met,
821 but any can be ommitted.
822 Doesn't examine children of matches.
824 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
825 var children = new Array();
826 for (var i = 0; i < start.childNodes.length; i++) {
827 var classfound = false;
828 var child = start.childNodes[i];
829 if((child.nodeType == 1) &&//element node type
830 (elementClass && (typeof(child.className)=='string'))) {
831 var childClasses = child.className.split(/\s+/);
832 for (var childClassIndex in childClasses) {
833 if (childClasses[childClassIndex]==elementClass) {
839 if(child.nodeType == 1) { //element node type
840 if ( (!tagName || child.nodeName == tagName) &&
841 (!elementClass || classfound)&&
842 (!elementID || child.id == elementID) &&
843 (!elementName || child.name == elementName))
845 children = children.concat(child);
847 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
854 function unmaskPassword(id) {
855 var pw = document.getElementById(id);
856 var chb = document.getElementById(id+'unmask');
859 // first try IE way - it can not set name attribute later
861 var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
863 var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
865 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
867 var newpw = document.createElement('input');
868 newpw.setAttribute('name', pw.name);
870 newpw.setAttribute('type', 'text');
872 newpw.setAttribute('type', 'password');
874 newpw.setAttribute('class', pw.getAttribute('class'));
877 newpw.size = pw.size;
878 newpw.onblur = pw.onblur;
879 newpw.onchange = pw.onchange;
880 newpw.value = pw.value;
881 pw.parentNode.replaceChild(newpw, pw);
884 function filterByParent(elCollection, parentFinder) {
885 var filteredCollection = [];
886 for (var i = 0; i < elCollection.length; ++i) {
887 var findParent = parentFinder(elCollection[i]);
888 if (findParent.nodeName.toUpperCase != 'BODY') {
889 filteredCollection.push(elCollection[i]);
892 return filteredCollection;
896 All this is here just so that IE gets to handle oversized blocks
897 in a visually pleasing manner. It does a browser detect. So sue me.
900 function fix_column_widths() {
901 var agt = navigator.userAgent.toLowerCase();
902 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
903 fix_column_width('left-column');
904 fix_column_width('right-column');
908 function fix_column_width(colName) {
909 if(column = document.getElementById(colName)) {
910 if(!column.offsetWidth) {
911 setTimeout("fix_column_width('" + colName + "')", 20);
916 var nodes = column.childNodes;
918 for(i = 0; i < nodes.length; ++i) {
919 if(nodes[i].className.indexOf("block") != -1 ) {
920 if(width < nodes[i].offsetWidth) {
921 width = nodes[i].offsetWidth;
926 for(i = 0; i < nodes.length; ++i) {
927 if(nodes[i].className.indexOf("block") != -1 ) {
928 nodes[i].style.width = width + 'px';
936 Insert myValue at current cursor position
938 function insertAtCursor(myField, myValue) {
940 if (document.selection) {
942 sel = document.selection.createRange();
945 // Mozilla/Netscape support
946 else if (myField.selectionStart || myField.selectionStart == '0') {
947 var startPos = myField.selectionStart;
948 var endPos = myField.selectionEnd;
949 myField.value = myField.value.substring(0, startPos)
950 + myValue + myField.value.substring(endPos, myField.value.length);
952 myField.value += myValue;
958 Call instead of setting window.onload directly or setting body onload=.
959 Adds your function to a chain of functions rather than overwriting anything
962 function addonload(fn) {
963 var oldhandler=window.onload;
964 window.onload=function() {
965 if(oldhandler) oldhandler();
970 * Replacement for getElementsByClassName in browsers that aren't cool enough
972 * Relying on the built-in getElementsByClassName is far, far faster than
975 * Note: the third argument used to be an object with odd behaviour. It now
976 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
977 * mimicked if you pass an object.
979 * @param {Node} oElm The top-level node for searching. To search a whole
980 * document, use `document`.
981 * @param {String} strTagName filter by tag names
982 * @param {String} name same as HTML5 spec
984 function getElementsByClassName(oElm, strTagName, name) {
985 // for backwards compatibility
986 if(typeof name == "object") {
987 var names = new Array();
988 for(var i=0; i<name.length; i++) names.push(names[i]);
989 name = names.join('');
991 // use native implementation if possible
992 if (oElm.getElementsByClassName && Array.filter) {
993 if (strTagName == '*') {
994 return oElm.getElementsByClassName(name);
996 return Array.filter(oElm.getElementsByClassName(name), function(el) {
997 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1001 // native implementation unavailable, fall back to slow method
1002 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1003 var arrReturnElements = new Array();
1004 var arrRegExpClassNames = new Array();
1005 var names = name.split(' ');
1006 for(var i=0; i<names.length; i++) {
1007 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1011 for(var j=0; j<arrElements.length; j++) {
1012 oElement = arrElements[j];
1014 for(var k=0; k<arrRegExpClassNames.length; k++) {
1015 if(!arrRegExpClassNames[k].test(oElement.className)) {
1016 bMatchesAll = false;
1021 arrReturnElements.push(oElement);
1024 return (arrReturnElements)
1027 function openpopup(event, args) {
1030 if (event.preventDefault) {
1031 event.preventDefault();
1033 event.returnValue = false;
1037 var fullurl = args.url;
1038 if (!args.url.match(/https?:\/\//)) {
1039 fullurl = M.cfg.wwwroot + args.url;
1041 var windowobj = window.open(fullurl,args.name,args.options);
1045 if (args.fullscreen) {
1046 windowobj.moveTo(0,0);
1047 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1054 /** Close the current browser window. */
1055 function close_window(e) {
1056 if (e.preventDefault) {
1059 e.returnValue = false;
1065 * Used in a couple of modules to hide navigation areas when using AJAX
1068 function show_item(itemid) {
1069 var item = document.getElementById(itemid);
1071 item.style.display = "";
1075 function destroy_item(itemid) {
1076 var item = document.getElementById(itemid);
1078 item.parentNode.removeChild(item);
1082 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1083 * @param controlid the control id.
1085 function focuscontrol(controlid) {
1086 var control = document.getElementById(controlid);
1093 * Transfers keyboard focus to an HTML element based on the old style style of focus
1094 * This function should be removed as soon as it is no longer used
1096 function old_onload_focus(formid, controlname) {
1097 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1098 document.forms[formid].elements[controlname].focus();
1102 function build_querystring(obj) {
1103 return convert_object_to_string(obj, '&');
1106 function build_windowoptionsstring(obj) {
1107 return convert_object_to_string(obj, ',');
1110 function convert_object_to_string(obj, separator) {
1111 if (typeof obj !== 'object') {
1116 k = encodeURIComponent(k);
1118 if(obj[k] instanceof Array) {
1119 for(var i in value) {
1120 list.push(k+'[]='+encodeURIComponent(value[i]));
1123 list.push(k+'='+encodeURIComponent(value));
1126 return list.join(separator);
1129 function stripHTML(str) {
1130 var re = /<\S[^><]*>/g;
1131 var ret = str.replace(re, "");
1135 Number.prototype.fixed=function(n){
1137 return round(Number(this)*pow(10,n))/pow(10,n);
1139 function update_progress_bar (id, width, pt, msg, es){
1141 var status = document.getElementById("status_"+id);
1142 var percent_indicator = document.getElementById("pt_"+id);
1143 var progress_bar = document.getElementById("progress_"+id);
1144 var time_es = document.getElementById("time_"+id);
1145 status.innerHTML = msg;
1146 percent_indicator.innerHTML = percent.fixed(2) + '%';
1147 if(percent == 100) {
1148 progress_bar.style.background = "green";
1149 time_es.style.display = "none";
1151 progress_bar.style.background = "#FFCC66";
1153 time_es.innerHTML = "";
1155 time_es.innerHTML = es.fixed(2)+" sec";
1156 time_es.style.display
1160 progress_bar.style.width = width + "px";
1164 function frame_breakout(e, properties) {
1165 this.setAttribute('target', properties.framename);
1169 // ===== Deprecated core Javascript functions for Moodle ====
1170 // DO NOT USE!!!!!!!
1171 // Do not put this stuff in separate file because it only adds extra load on servers!
1174 * Used in a couple of modules to hide navigation areas when using AJAX
1176 function hide_item(itemid) {
1177 // use class='hiddenifjs' instead
1178 var item = document.getElementById(itemid);
1180 item.style.display = "none";
1184 M.util.help_icon = {
1187 add : function(Y, properties) {
1189 properties.node = Y.one('#'+properties.id);
1190 if (properties.node) {
1191 properties.node.on('click', this.display, this, properties);
1194 display : function(event, args) {
1195 event.preventDefault();
1196 if (M.util.help_icon.instance === null) {
1197 var Y = M.util.help_icon.Y;
1198 Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
1199 var help_content_overlay = {
1204 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1205 // Create an overlay from markup
1206 this.overlay = new Y.Overlay({
1207 headerContent: closebtn,
1214 this.overlay.render(Y.one(document.body));
1216 closebtn.on('click', this.overlay.hide, this.overlay);
1218 var boundingBox = this.overlay.get("boundingBox");
1220 // Hide the menu if the user clicks outside of its content
1221 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1222 var oTarget = event.target;
1223 var menuButton = Y.one("#"+args.id);
1225 if (!oTarget.compareTo(menuButton) &&
1226 !menuButton.contains(oTarget) &&
1227 !oTarget.compareTo(boundingBox) &&
1228 !boundingBox.contains(oTarget)) {
1229 this.overlay.hide();
1233 Y.on("key", this.close, closebtn , "down:13", this);
1234 closebtn.on('click', this.close, this);
1237 close : function(e) {
1239 this.helplink.focus();
1240 this.overlay.hide();
1243 display : function(event, args) {
1244 this.helplink = args.node;
1245 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1246 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1248 var fullurl = args.url;
1249 if (!args.url.match(/https?:\/\//)) {
1250 fullurl = M.cfg.wwwroot + args.url;
1253 var ajaxurl = fullurl + '&ajax=1';
1259 success: function(id, o, node) {
1260 this.display_callback(o.responseText);
1262 failure: function(id, o, node) {
1263 var debuginfo = o.statusText;
1264 if (M.cfg.developerdebug) {
1265 o.statusText += ' (' + ajaxurl + ')';
1267 this.display_callback('bodyContent',debuginfo);
1273 this.overlay.show();
1275 Y.one('#closehelpbox').focus();
1278 display_callback : function(content) {
1279 this.overlay.set('bodyContent', content);
1282 hideContent : function() {
1284 help.overlay.hide();
1287 help_content_overlay.init();
1288 M.util.help_icon.instance = help_content_overlay;
1289 M.util.help_icon.instance.display(event, args);
1292 M.util.help_icon.instance.display(event, args);
1295 init : function(Y) {
1301 * Custom menu namespace
1303 M.core_custom_menu = {
1305 * This method is used to initialise a custom menu given the id that belongs
1306 * to the custom menu's root node.
1309 * @param {string} nodeid
1311 init : function(Y, nodeid) {
1312 var node = Y.one('#'+nodeid);
1314 Y.use('node-menunav', function(Y) {
1316 // Remove the javascript-disabled class.... obviously javascript is enabled.
1317 node.removeClass('javascript-disabled');
1318 // Initialise the menunav plugin
1319 node.plug(Y.Plugin.NodeMenuNav);
1326 * Used to store form manipulation methods and enhancments
1328 M.form = M.form || {};
1331 * Converts a nbsp indented select box into a multi drop down custom control much
1332 * like the custom menu. It also selectable categories on or off.
1334 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1337 * @param {string} id
1338 * @param {Array} options
1340 M.form.init_smartselect = function(Y, id, options) {
1341 if (!id.match(/^id_/)) {
1344 var select = Y.one('select#'+id);
1348 Y.use('event-delegate',function(){
1354 currentvalue : null,
1358 selectablecategories : true,
1366 init : function(Y, id, args, nodes) {
1367 if (typeof(args)=='object') {
1368 for (var i in this.cfg) {
1369 if (args[i] || args[i]===false) {
1370 this.cfg[i] = args[i];
1375 // Display a loading message first up
1376 this.nodes.select = nodes.select;
1378 this.currentvalue = this.nodes.select.get('selectedIndex');
1379 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1381 var options = Array();
1382 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1383 this.nodes.select.all('option').each(function(option, index) {
1384 var rawtext = option.get('innerHTML');
1385 var text = rawtext.replace(/^( )*/, '');
1386 if (rawtext === text) {
1387 text = rawtext.replace(/^(\s)*/, '');
1388 var depth = (rawtext.length - text.length ) + 1;
1390 var depth = ((rawtext.length - text.length )/12)+1;
1392 option.set('innerHTML', text);
1393 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1396 this.structure = [];
1397 var structcount = 0;
1398 for (var i in options) {
1401 this.structure.push(o);
1405 var current = this.structure[structcount-1];
1406 for (var j = 0; j < o.depth-1;j++) {
1407 if (current && current.children) {
1408 current = current.children[current.children.length-1];
1411 if (current && current.children) {
1412 current.children.push(o);
1417 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1418 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1419 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1420 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1422 if (this.cfg.mode == null) {
1423 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1424 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1425 this.cfg.mode = 'compact';
1427 this.cfg.mode = 'spanning';
1431 if (this.cfg.mode == 'compact') {
1432 this.nodes.menu.addClass('compactmenu');
1434 this.nodes.menu.addClass('spanningmenu');
1435 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1438 Y.one(document.body).append(this.nodes.menu);
1439 var pos = this.nodes.select.getXY();
1441 this.nodes.menu.setXY(pos);
1442 this.nodes.menu.on('click', this.handle_click, this);
1444 Y.one(window).on('resize', function(){
1445 var pos = this.nodes.select.getXY();
1447 this.nodes.menu.setXY(pos);
1450 generate_menu_content : function() {
1451 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1452 content += this.generate_submenu_content(this.structure[0], true);
1453 content += '</ul></div>';
1456 generate_submenu_content : function(item, rootelement) {
1457 this.submenucount++;
1459 if (item.children.length > 0) {
1461 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1462 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1463 content += '<div class="smartselect_menu_content">';
1465 content += '<li class="smartselect_submenuitem">';
1466 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1467 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1468 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1469 content += '<div class="smartselect_submenu_content">';
1472 for (var i in item.children) {
1473 content += this.generate_submenu_content(item.children[i],false);
1476 content += '</div>';
1477 content += '</div>';
1483 content += '<li class="smartselect_menuitem">';
1484 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1489 select : function(e) {
1492 this.currenttext = t.get('innerHTML');
1493 this.currentvalue = t.getAttribute('value');
1494 this.nodes.select.set('selectedIndex', this.currentvalue);
1497 handle_click : function(e) {
1498 var target = e.target;
1499 if (target.hasClass('smartselect_mask')) {
1501 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1503 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1504 this.show_sub_menu(e);
1507 show_menu : function(e) {
1509 var menu = e.target.ancestor().one('.smartselect_menu');
1510 menu.addClass('visible');
1511 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1513 show_sub_menu : function(e) {
1515 var target = e.target;
1516 if (!target.hasClass('smartselect_submenuitem')) {
1517 target = target.ancestor('.smartselect_submenuitem');
1519 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1520 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1523 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1524 target.one('.smartselect_submenu').addClass('visible');
1526 hide_menu : function() {
1527 this.nodes.menu.all('.visible').removeClass('visible');
1528 if (this.shownevent) {
1529 this.shownevent.detach();
1533 smartselect.init(Y, id, options, {select:select});
1537 M.util.init_flvflowplayer = function (id, playerpath, fileurl) {
1538 $f(id, playerpath, {
1552 M.util.init_mp3flowplayer = function (id, playerpath, audioplayerpath, fileurl, color) {
1554 $f(id, playerpath, {
1561 background: '#'+color['bgColour'],
1562 buttonColor: '#'+color['btnColour'],
1563 sliderColor: '#'+color['handleColour'],
1564 volumeSliderColor: '#'+color['handleColour'],
1565 volumeColor: '#'+color['trackColour'],
1566 durationColor: '#'+color['fontColour'],
1567 buttonOverColor: '#'+color['iconOverColour'],
1568 progressColor: '#'+color['handleColour']
1570 audio: {url: audioplayerpath}
1572 clip: {url: fileurl,
1579 M.util.init_mp3flowplayerplugin = function (id, playerpath, audioplayerpath, fileurl, color) {
1580 $f(id, playerpath, {
1589 background: '#'+color['bgColour'],
1590 buttonColor: '#'+color['btnColour'],
1591 sliderColor: '#'+color['handleColour'],
1592 volumeSliderColor: '#'+color['handleColour'],
1593 progressColor: '#'+color['handleColour'],
1594 volumeColor: '#'+color['trackColour'],
1595 buttonOverColor: '#'+color['iconOverColour']
1597 audio: {url: audioplayerpath}
1599 clip: {url: fileurl,