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 * Close the current browser window, forcing the window/tab that opened this
1066 * popup to reload itself. */
1067 function close_window_reloading_opener() {
1068 if (window.opener) {
1069 window.opener.location.reload(1);
1071 // Intentionally, only try to close the window if there is some evidence we are in a popup.
1076 * Used in a couple of modules to hide navigation areas when using AJAX
1079 function show_item(itemid) {
1080 var item = document.getElementById(itemid);
1082 item.style.display = "";
1086 function destroy_item(itemid) {
1087 var item = document.getElementById(itemid);
1089 item.parentNode.removeChild(item);
1093 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1094 * @param controlid the control id.
1096 function focuscontrol(controlid) {
1097 var control = document.getElementById(controlid);
1104 * Transfers keyboard focus to an HTML element based on the old style style of focus
1105 * This function should be removed as soon as it is no longer used
1107 function old_onload_focus(formid, controlname) {
1108 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1109 document.forms[formid].elements[controlname].focus();
1113 function build_querystring(obj) {
1114 return convert_object_to_string(obj, '&');
1117 function build_windowoptionsstring(obj) {
1118 return convert_object_to_string(obj, ',');
1121 function convert_object_to_string(obj, separator) {
1122 if (typeof obj !== 'object') {
1127 k = encodeURIComponent(k);
1129 if(obj[k] instanceof Array) {
1130 for(var i in value) {
1131 list.push(k+'[]='+encodeURIComponent(value[i]));
1134 list.push(k+'='+encodeURIComponent(value));
1137 return list.join(separator);
1140 function stripHTML(str) {
1141 var re = /<\S[^><]*>/g;
1142 var ret = str.replace(re, "");
1146 Number.prototype.fixed=function(n){
1148 return round(Number(this)*pow(10,n))/pow(10,n);
1150 function update_progress_bar (id, width, pt, msg, es){
1152 var status = document.getElementById("status_"+id);
1153 var percent_indicator = document.getElementById("pt_"+id);
1154 var progress_bar = document.getElementById("progress_"+id);
1155 var time_es = document.getElementById("time_"+id);
1156 status.innerHTML = msg;
1157 percent_indicator.innerHTML = percent.fixed(2) + '%';
1158 if(percent == 100) {
1159 progress_bar.style.background = "green";
1160 time_es.style.display = "none";
1162 progress_bar.style.background = "#FFCC66";
1164 time_es.innerHTML = "";
1166 time_es.innerHTML = es.fixed(2)+" sec";
1167 time_es.style.display
1171 progress_bar.style.width = width + "px";
1175 function frame_breakout(e, properties) {
1176 this.setAttribute('target', properties.framename);
1180 // ===== Deprecated core Javascript functions for Moodle ====
1181 // DO NOT USE!!!!!!!
1182 // Do not put this stuff in separate file because it only adds extra load on servers!
1185 * Used in a couple of modules to hide navigation areas when using AJAX
1187 function hide_item(itemid) {
1188 // use class='hiddenifjs' instead
1189 var item = document.getElementById(itemid);
1191 item.style.display = "none";
1195 M.util.help_icon = {
1198 add : function(Y, properties) {
1200 properties.node = Y.one('#'+properties.id);
1201 if (properties.node) {
1202 properties.node.on('click', this.display, this, properties);
1205 display : function(event, args) {
1206 event.preventDefault();
1207 if (M.util.help_icon.instance === null) {
1208 var Y = M.util.help_icon.Y;
1209 Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
1210 var help_content_overlay = {
1215 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1216 // Create an overlay from markup
1217 this.overlay = new Y.Overlay({
1218 headerContent: closebtn,
1225 this.overlay.render(Y.one(document.body));
1227 closebtn.on('click', this.overlay.hide, this.overlay);
1229 var boundingBox = this.overlay.get("boundingBox");
1231 // Hide the menu if the user clicks outside of its content
1232 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1233 var oTarget = event.target;
1234 var menuButton = Y.one("#"+args.id);
1236 if (!oTarget.compareTo(menuButton) &&
1237 !menuButton.contains(oTarget) &&
1238 !oTarget.compareTo(boundingBox) &&
1239 !boundingBox.contains(oTarget)) {
1240 this.overlay.hide();
1244 Y.on("key", this.close, closebtn , "down:13", this);
1245 closebtn.on('click', this.close, this);
1248 close : function(e) {
1250 this.helplink.focus();
1251 this.overlay.hide();
1254 display : function(event, args) {
1255 this.helplink = args.node;
1256 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1257 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1259 var fullurl = args.url;
1260 if (!args.url.match(/https?:\/\//)) {
1261 fullurl = M.cfg.wwwroot + args.url;
1264 var ajaxurl = fullurl + '&ajax=1';
1270 success: function(id, o, node) {
1271 this.display_callback(o.responseText);
1273 failure: function(id, o, node) {
1274 var debuginfo = o.statusText;
1275 if (M.cfg.developerdebug) {
1276 o.statusText += ' (' + ajaxurl + ')';
1278 this.display_callback('bodyContent',debuginfo);
1284 this.overlay.show();
1286 Y.one('#closehelpbox').focus();
1289 display_callback : function(content) {
1290 this.overlay.set('bodyContent', content);
1293 hideContent : function() {
1295 help.overlay.hide();
1298 help_content_overlay.init();
1299 M.util.help_icon.instance = help_content_overlay;
1300 M.util.help_icon.instance.display(event, args);
1303 M.util.help_icon.instance.display(event, args);
1306 init : function(Y) {
1312 * Custom menu namespace
1314 M.core_custom_menu = {
1316 * This method is used to initialise a custom menu given the id that belongs
1317 * to the custom menu's root node.
1320 * @param {string} nodeid
1322 init : function(Y, nodeid) {
1323 var node = Y.one('#'+nodeid);
1325 Y.use('node-menunav', function(Y) {
1327 // Remove the javascript-disabled class.... obviously javascript is enabled.
1328 node.removeClass('javascript-disabled');
1329 // Initialise the menunav plugin
1330 node.plug(Y.Plugin.NodeMenuNav);
1337 * Used to store form manipulation methods and enhancments
1339 M.form = M.form || {};
1342 * Converts a nbsp indented select box into a multi drop down custom control much
1343 * like the custom menu. It also selectable categories on or off.
1345 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1348 * @param {string} id
1349 * @param {Array} options
1351 M.form.init_smartselect = function(Y, id, options) {
1352 if (!id.match(/^id_/)) {
1355 var select = Y.one('select#'+id);
1359 Y.use('event-delegate',function(){
1365 currentvalue : null,
1369 selectablecategories : true,
1377 init : function(Y, id, args, nodes) {
1378 if (typeof(args)=='object') {
1379 for (var i in this.cfg) {
1380 if (args[i] || args[i]===false) {
1381 this.cfg[i] = args[i];
1386 // Display a loading message first up
1387 this.nodes.select = nodes.select;
1389 this.currentvalue = this.nodes.select.get('selectedIndex');
1390 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1392 var options = Array();
1393 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1394 this.nodes.select.all('option').each(function(option, index) {
1395 var rawtext = option.get('innerHTML');
1396 var text = rawtext.replace(/^( )*/, '');
1397 if (rawtext === text) {
1398 text = rawtext.replace(/^(\s)*/, '');
1399 var depth = (rawtext.length - text.length ) + 1;
1401 var depth = ((rawtext.length - text.length )/12)+1;
1403 option.set('innerHTML', text);
1404 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1407 this.structure = [];
1408 var structcount = 0;
1409 for (var i in options) {
1412 this.structure.push(o);
1416 var current = this.structure[structcount-1];
1417 for (var j = 0; j < o.depth-1;j++) {
1418 if (current && current.children) {
1419 current = current.children[current.children.length-1];
1422 if (current && current.children) {
1423 current.children.push(o);
1428 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1429 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1430 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1431 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1433 if (this.cfg.mode == null) {
1434 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1435 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1436 this.cfg.mode = 'compact';
1438 this.cfg.mode = 'spanning';
1442 if (this.cfg.mode == 'compact') {
1443 this.nodes.menu.addClass('compactmenu');
1445 this.nodes.menu.addClass('spanningmenu');
1446 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1449 Y.one(document.body).append(this.nodes.menu);
1450 var pos = this.nodes.select.getXY();
1452 this.nodes.menu.setXY(pos);
1453 this.nodes.menu.on('click', this.handle_click, this);
1455 Y.one(window).on('resize', function(){
1456 var pos = this.nodes.select.getXY();
1458 this.nodes.menu.setXY(pos);
1461 generate_menu_content : function() {
1462 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1463 content += this.generate_submenu_content(this.structure[0], true);
1464 content += '</ul></div>';
1467 generate_submenu_content : function(item, rootelement) {
1468 this.submenucount++;
1470 if (item.children.length > 0) {
1472 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1473 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1474 content += '<div class="smartselect_menu_content">';
1476 content += '<li class="smartselect_submenuitem">';
1477 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1478 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1479 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1480 content += '<div class="smartselect_submenu_content">';
1483 for (var i in item.children) {
1484 content += this.generate_submenu_content(item.children[i],false);
1487 content += '</div>';
1488 content += '</div>';
1494 content += '<li class="smartselect_menuitem">';
1495 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1500 select : function(e) {
1503 this.currenttext = t.get('innerHTML');
1504 this.currentvalue = t.getAttribute('value');
1505 this.nodes.select.set('selectedIndex', this.currentvalue);
1508 handle_click : function(e) {
1509 var target = e.target;
1510 if (target.hasClass('smartselect_mask')) {
1512 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1514 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1515 this.show_sub_menu(e);
1518 show_menu : function(e) {
1520 var menu = e.target.ancestor().one('.smartselect_menu');
1521 menu.addClass('visible');
1522 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1524 show_sub_menu : function(e) {
1526 var target = e.target;
1527 if (!target.hasClass('smartselect_submenuitem')) {
1528 target = target.ancestor('.smartselect_submenuitem');
1530 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1531 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1534 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1535 target.one('.smartselect_submenu').addClass('visible');
1537 hide_menu : function() {
1538 this.nodes.menu.all('.visible').removeClass('visible');
1539 if (this.shownevent) {
1540 this.shownevent.detach();
1544 smartselect.init(Y, id, options, {select:select});
1548 M.util.init_flvflowplayer = function (id, playerpath, fileurl) {
1549 $f(id, playerpath, {
1563 M.util.init_mp3flowplayer = function (id, playerpath, audioplayerpath, fileurl, color) {
1565 $f(id, playerpath, {
1572 background: '#'+color['bgColour'],
1573 buttonColor: '#'+color['btnColour'],
1574 sliderColor: '#'+color['handleColour'],
1575 volumeSliderColor: '#'+color['handleColour'],
1576 volumeColor: '#'+color['trackColour'],
1577 durationColor: '#'+color['fontColour'],
1578 buttonOverColor: '#'+color['iconOverColour'],
1579 progressColor: '#'+color['handleColour']
1581 audio: {url: audioplayerpath}
1583 clip: {url: fileurl,
1590 M.util.init_mp3flowplayerplugin = function (id, playerpath, audioplayerpath, fileurl, color) {
1591 $f(id, playerpath, {
1600 background: '#'+color['bgColour'],
1601 buttonColor: '#'+color['btnColour'],
1602 sliderColor: '#'+color['handleColour'],
1603 volumeSliderColor: '#'+color['handleColour'],
1604 progressColor: '#'+color['handleColour'],
1605 volumeColor: '#'+color['trackColour'],
1606 buttonOverColor: '#'+color['iconOverColour']
1608 audio: {url: audioplayerpath}
1610 clip: {url: fileurl,