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.in_array = function(item, array){
50 for( var i = 0; i<array.length; i++){
59 * Init a collapsible region, see print_collapsible_region in weblib.php
60 * @param {YUI} Y YUI3 instance with all libraries loaded
61 * @param {String} id the HTML id for the div.
62 * @param {String} userpref the user preference that records the state of this box. false if none.
63 * @param {String} strtooltip
65 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
66 Y.use('anim', function(Y) {
67 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
72 * Object to handle a collapsible region : instantiate and forget styled object
76 * @param {YUI} Y YUI3 instance with all libraries loaded
77 * @param {String} id The HTML id for the div.
78 * @param {String} userpref The user preference that records the state of this box. false if none.
79 * @param {String} strtooltip
81 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
82 // Record the pref name
83 this.userpref = userpref;
85 // Find the divs in the document.
86 this.div = Y.one('#'+id);
88 // Get the caption for the collapsible region
89 var caption = this.div.one('#'+id + '_caption');
90 caption.setAttribute('title', strtooltip);
93 var a = Y.Node.create('<a href="#"></a>');
94 // Create a local scoped lamba function to move nodes to a new link
95 var movenode = function(node){
99 // Apply the lamba function on each of the captions child nodes
100 caption.get('children').each(movenode, this);
103 // Get the height of the div at this point before we shrink it if required
104 var height = this.div.get('offsetHeight');
105 if (this.div.hasClass('collapsed')) {
106 // Add the correct image and record the YUI node created in the process
107 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
108 // Shrink the div as it is collapsed by default
109 this.div.setStyle('height', caption.get('offsetHeight')+'px');
111 // Add the correct image and record the YUI node created in the process
112 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
116 // Create the animation.
117 var animation = new Y.Anim({
120 easing: Y.Easing.easeBoth,
121 to: {height:caption.get('offsetHeight')},
122 from: {height:height}
125 // Handler for the animation finishing.
126 animation.on('end', function() {
127 this.div.toggleClass('collapsed');
128 if (this.div.hasClass('collapsed')) {
129 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
131 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
135 // Hook up the event handler.
136 a.on('click', function(e, animation) {
138 // Animate to the appropriate size.
139 if (animation.get('running')) {
142 animation.set('reverse', this.div.hasClass('collapsed'));
143 // Update the user preference.
145 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
152 * The user preference that stores the state of this box.
156 M.util.CollapsibleRegion.prototype.userpref = null;
159 * The key divs that make up this
163 M.util.CollapsibleRegion.prototype.div = null;
166 * The key divs that make up this
170 M.util.CollapsibleRegion.prototype.icon = null;
173 * Makes a best effort to connect back to Moodle to update a user preference,
174 * however, there is no mechanism for finding out if the update succeeded.
176 * Before you can use this function in your JavsScript, you must have called
177 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
178 * the udpate is allowed, and how to safely clean and submitted values.
180 * @param String name the name of the setting to udpate.
181 * @param String the value to set it to.
183 M.util.set_user_preference = function(name, value) {
184 YUI(M.yui.loader).use('io', function(Y) {
185 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
186 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
188 // If we are a developer, ensure that failures are reported.
193 if (M.cfg.developerdebug) {
194 cfg.on.failure = function(id, o, args) {
195 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
205 * Prints a confirmation dialog in the style of DOM.confirm().
206 * @param object event A YUI DOM event or null if launched manually
207 * @param string message The message to show in the dialog
208 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
209 * @param function fn A JS function to run if YES is clicked.
211 M.util.show_confirm_dialog = function(e, args) {
212 var target = e.target;
213 if (e.preventDefault) {
217 YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
218 var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
227 simpledialog.setHeader(M.str.admin.confirmation);
228 simpledialog.setBody(args.message);
229 simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
231 var handle_cancel = function() {
235 var handle_yes = function() {
239 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
241 if (Y.Lang.isFunction(args.callback)) {
242 callback = args.callback;
244 callback = eval('('+args.callback+')');
247 if (Y.Lang.isObject(args.scope)) {
253 if (args.callbackargs) {
254 callback.apply(sc, args.callbackargs);
261 var targetancestor = null,
264 if (target.test('a')) {
265 window.location = target.get('href');
266 } else if ((targetancestor = target.ancestor('a')) !== null) {
267 window.location = targetancestor.get('href');
268 } else if (target.test('input')) {
269 targetform = target.ancestor('form');
270 if (targetform && targetform.submit) {
273 } else if (M.cfg.developerdebug) {
274 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT");
278 if (!args.cancellabel) {
279 args.cancellabel = M.str.moodle.cancel;
281 if (!args.continuelabel) {
282 args.continuelabel = M.str.moodle.yes;
286 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
287 {text: args.continuelabel, handler: handle_yes}
290 simpledialog.cfg.queueProperty('buttons', buttons);
292 simpledialog.render(document.body);
297 /** Useful for full embedding of various stuff */
298 M.util.init_maximised_embed = function(Y, id) {
299 var obj = Y.one('#'+id);
304 var get_htmlelement_size = function(el, prop) {
305 if (Y.Lang.isString(el)) {
306 el = Y.one('#' + el);
308 var val = el.getStyle(prop);
310 val = el.getComputedStyle(prop);
312 return parseInt(val);
315 var resize_object = function() {
316 obj.setStyle('width', '0px');
317 obj.setStyle('height', '0px');
318 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
320 if (newwidth > 500) {
321 obj.setStyle('width', newwidth + 'px');
323 obj.setStyle('width', '500px');
326 var headerheight = get_htmlelement_size('page-header', 'height');
327 var footerheight = get_htmlelement_size('page-footer', 'height');
328 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
329 if (newheight < 400) {
332 obj.setStyle('height', newheight+'px');
336 // fix layout if window resized too
337 window.onresize = function() {
343 * Attach handler to single_select
345 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
346 Y.use('event-key', function() {
347 var select = Y.one('#'+selectid);
349 // Try to get the form by id
350 var form = Y.one('#'+formid) || (function(){
351 // Hmmm the form's id may have been overriden by an internal input
352 // with the name id which will KILL IE.
353 // We need to manually iterate at this point because if the case
354 // above is true YUI's ancestor method will also kill IE!
356 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
357 form = form.ancestor();
361 // Make sure we have the form
363 // Create a function to handle our change event
364 var processchange = function(e, lastindex) {
365 if ((nothing===false || select.get('value') != nothing) && lastindex != select.get('selectedIndex')) {
369 // Attach the change event to the keypress, blur, and click actions.
370 // We don't use the change event because IE fires it on every arrow up/down
371 // event.... usability
372 Y.on('key', processchange, select, 'press:13', form, select.get('selectedIndex'));
373 select.on('blur', processchange, form, select.get('selectedIndex'));
374 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
376 select.on('change', processchange, form, select.get('selectedIndex'));
378 select.on('click', processchange, form, select.get('selectedIndex'));
386 * Attach handler to url_select
388 M.util.init_url_select = function(Y, formid, selectid, nothing) {
389 YUI(M.yui.loader).use('node', function(Y) {
390 Y.on('change', function() {
391 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
392 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
400 * Breaks out all links to the top frame - used in frametop page layout.
402 M.util.init_frametop = function(Y) {
403 Y.all('a').each(function(node) {
404 node.set('target', '_top');
406 Y.all('form').each(function(node) {
407 node.set('target', '_top');
412 * Finds all nodes that match the given CSS selector and attaches events to them
413 * so that they toggle a given classname when clicked.
416 * @param {string} id An id containing elements to target
417 * @param {string} cssselector A selector to use to find targets
418 * @param {string} toggleclassname A classname to toggle
420 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
422 if (togglecssselector == '') {
423 togglecssselector = cssselector;
426 var node = Y.one('#'+id);
427 node.all(cssselector).each(function(n){
428 n.on('click', function(e){
430 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
431 if (this.test(togglecssselector)) {
432 this.toggleClass(toggleclassname);
434 this.ancestor(togglecssselector).toggleClass(toggleclassname);
439 // Attach this click event to the node rather than all selectors... will be much better
441 node.on('click', function(e){
442 if (e.target.hasClass('addtoall')) {
443 this.all(togglecssselector).addClass(toggleclassname);
444 } else if (e.target.hasClass('removefromall')) {
445 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
451 * Initialises a colour picker
453 * Designed to be used with admin_setting_configcolourpicker although could be used
454 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
455 * above or below the input (must have the same parent) and then call this with the
458 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
459 * contrib/blocks. For better docs refer to that.
463 * @param {object} previewconf
465 M.util.init_colour_picker = function(Y, id, previewconf) {
467 * We need node and event-mouseenter
469 Y.use('node', 'event-mouseenter', function(){
471 * The colour picker object
480 eventMouseEnter : null,
481 eventMouseLeave : null,
482 eventMouseMove : null,
487 * Initalises the colour picker by putting everything together and wiring the events
490 this.input = Y.one('#'+id);
491 this.box = this.input.ancestor().one('.admin_colourpicker');
492 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
493 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
494 this.preview = Y.Node.create('<div class="previewcolour"></div>');
495 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
496 this.current = Y.Node.create('<div class="currentcolour"></div>');
497 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
498 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
500 if (typeof(previewconf) === 'object' && previewconf !== null) {
501 Y.one('#'+id+'_preview').on('click', function(e){
502 if (Y.Lang.isString(previewconf.selector)) {
503 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
505 for (var i in previewconf.selector) {
506 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
512 this.eventClick = this.image.on('click', this.pickColour, this);
513 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
516 * Starts to follow the mouse once it enter the image
518 startFollow : function(e) {
519 this.eventMouseEnter.detach();
520 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
521 this.eventMouseMove = this.image.on('mousemove', function(e){
522 this.preview.setStyle('backgroundColor', this.determineColour(e));
526 * Stops following the mouse
528 endFollow : function(e) {
529 this.eventMouseMove.detach();
530 this.eventMouseLeave.detach();
531 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
534 * Picks the colour the was clicked on
536 pickColour : function(e) {
537 var colour = this.determineColour(e);
538 this.input.set('value', colour);
539 this.current.setStyle('backgroundColor', colour);
542 * Calculates the colour fromthe given co-ordinates
544 determineColour : function(e) {
545 var eventx = Math.floor(e.pageX-e.target.getX());
546 var eventy = Math.floor(e.pageY-e.target.getY());
548 var imagewidth = this.width;
549 var imageheight = this.height;
550 var factor = this.factor;
551 var colour = [255,0,0];
562 var matrixcount = matrices.length;
563 var limit = Math.round(imagewidth/matrixcount);
564 var heightbreak = Math.round(imageheight/2);
566 for (var x = 0; x < imagewidth; x++) {
567 var divisor = Math.floor(x / limit);
568 var matrix = matrices[divisor];
570 colour[0] += matrix[0]*factor;
571 colour[1] += matrix[1]*factor;
572 colour[2] += matrix[2]*factor;
579 var pixel = [colour[0], colour[1], colour[2]];
580 if (eventy < heightbreak) {
581 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
582 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
583 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
584 } else if (eventy > heightbreak) {
585 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
586 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
587 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
590 return this.convert_rgb_to_hex(pixel);
593 * Converts an RGB value to Hex
595 convert_rgb_to_hex : function(rgb) {
597 var hexchars = "0123456789ABCDEF";
598 for (var i=0; i<3; i++) {
599 var number = Math.abs(rgb[i]);
600 if (number == 0 || isNaN(number)) {
603 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
610 * Initialise the colour picker :) Hoorah
616 M.util.init_block_hider = function(Y, config) {
617 Y.use('base', 'node', function(Y) {
618 M.util.block_hider = M.util.block_hider || (function(){
619 var blockhider = function() {
620 blockhider.superclass.constructor.apply(this, arguments);
622 blockhider.prototype = {
623 initializer : function(config) {
624 this.set('block', '#'+this.get('id'));
625 var b = this.get('block'),
628 if (t && (a = t.one('.block_action'))) {
629 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
630 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
631 hide.on('keypress', this.updateStateKey, this, true);
632 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
633 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
634 show.on('keypress', this.updateStateKey, this, false);
635 a.insert(show, 0).insert(hide, 0);
638 updateState : function(e, hide) {
639 M.util.set_user_preference(this.get('preference'), hide);
641 this.get('block').addClass('hidden');
643 this.get('block').removeClass('hidden');
646 updateStateKey : function(e, hide) {
647 if (e.keyCode == 13) { //allow hide/show via enter key
648 this.updateState(this, hide);
652 Y.extend(blockhider, Y.Base, blockhider.prototype, {
658 value : M.util.image_url('t/switch_minus', 'moodle')
661 value : M.util.image_url('t/switch_plus', 'moodle')
664 setter : function(node) {
672 new M.util.block_hider(config);
677 * Returns a string registered in advance for usage in JavaScript
679 * If you do not pass the third parameter, the function will just return
680 * the corresponding value from the M.str object. If the third parameter is
681 * provided, the function performs {$a} placeholder substitution in the
682 * same way as PHP get_string() in Moodle does.
684 * @param {String} identifier string identifier
685 * @param {String} component the component providing the string
686 * @param {Object|String} a optional variable to populate placeholder with
688 M.util.get_string = function(identifier, component, a) {
691 if (M.cfg.developerdebug) {
692 // creating new instance if YUI is not optimal but it seems to be better way then
693 // require the instance via the function API - note that it is used in rare cases
694 // for debugging only anyway
695 var Y = new YUI({ debug : true });
698 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
699 stringvalue = '[[' + identifier + ',' + component + ']]';
700 if (M.cfg.developerdebug) {
701 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
706 stringvalue = M.str[component][identifier];
708 if (typeof a == 'undefined') {
709 // no placeholder substitution requested
713 if (typeof a == 'number' || typeof a == 'string') {
714 // replace all occurrences of {$a} with the placeholder value
715 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
719 if (typeof a == 'object') {
720 // replace {$a->key} placeholders
722 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
723 if (M.cfg.developerdebug) {
724 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
728 var search = '{$a->' + key + '}';
729 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
730 search = new RegExp(search, 'g');
731 stringvalue = stringvalue.replace(search, a[key]);
736 if (M.cfg.developerdebug) {
737 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
742 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
744 function checkall() {
745 var inputs = document.getElementsByTagName('input');
746 for (var i = 0; i < inputs.length; i++) {
747 if (inputs[i].type == 'checkbox') {
748 inputs[i].checked = true;
753 function checknone() {
754 var inputs = document.getElementsByTagName('input');
755 for (var i = 0; i < inputs.length; i++) {
756 if (inputs[i].type == 'checkbox') {
757 inputs[i].checked = false;
763 * Either check, or uncheck, all checkboxes inside the element with id is
764 * @param id the id of the container
765 * @param checked the new state, either '' or 'checked'.
767 function select_all_in_element_with_id(id, checked) {
768 var container = document.getElementById(id);
772 var inputs = container.getElementsByTagName('input');
773 for (var i = 0; i < inputs.length; ++i) {
774 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
775 inputs[i].checked = checked;
780 function select_all_in(elTagName, elClass, elId) {
781 var inputs = document.getElementsByTagName('input');
782 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
783 for(var i = 0; i < inputs.length; ++i) {
784 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
785 inputs[i].checked = 'checked';
790 function deselect_all_in(elTagName, elClass, elId) {
791 var inputs = document.getElementsByTagName('INPUT');
792 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
793 for(var i = 0; i < inputs.length; ++i) {
794 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
795 inputs[i].checked = '';
800 function confirm_if(expr, message) {
804 return confirm(message);
809 findParentNode (start, elementName, elementClass, elementID)
811 Travels up the DOM hierarchy to find a parent element with the
812 specified tag name, class, and id. All conditions must be met,
813 but any can be ommitted. Returns the BODY element if no match
816 function findParentNode(el, elName, elClass, elId) {
817 while (el.nodeName.toUpperCase() != 'BODY') {
818 if ((!elName || el.nodeName.toUpperCase() == elName) &&
819 (!elClass || el.className.indexOf(elClass) != -1) &&
820 (!elId || el.id == elId)) {
828 findChildNode (start, elementName, elementClass, elementID)
830 Travels down the DOM hierarchy to find all child elements with the
831 specified tag name, class, and id. All conditions must be met,
832 but any can be ommitted.
833 Doesn't examine children of matches.
835 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
836 var children = new Array();
837 for (var i = 0; i < start.childNodes.length; i++) {
838 var classfound = false;
839 var child = start.childNodes[i];
840 if((child.nodeType == 1) &&//element node type
841 (elementClass && (typeof(child.className)=='string'))) {
842 var childClasses = child.className.split(/\s+/);
843 for (var childClassIndex in childClasses) {
844 if (childClasses[childClassIndex]==elementClass) {
850 if(child.nodeType == 1) { //element node type
851 if ( (!tagName || child.nodeName == tagName) &&
852 (!elementClass || classfound)&&
853 (!elementID || child.id == elementID) &&
854 (!elementName || child.name == elementName))
856 children = children.concat(child);
858 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
865 function unmaskPassword(id) {
866 var pw = document.getElementById(id);
867 var chb = document.getElementById(id+'unmask');
870 // first try IE way - it can not set name attribute later
872 var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
874 var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
876 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
878 var newpw = document.createElement('input');
879 newpw.setAttribute('name', pw.name);
881 newpw.setAttribute('type', 'text');
883 newpw.setAttribute('type', 'password');
885 newpw.setAttribute('class', pw.getAttribute('class'));
888 newpw.size = pw.size;
889 newpw.onblur = pw.onblur;
890 newpw.onchange = pw.onchange;
891 newpw.value = pw.value;
892 pw.parentNode.replaceChild(newpw, pw);
895 function filterByParent(elCollection, parentFinder) {
896 var filteredCollection = [];
897 for (var i = 0; i < elCollection.length; ++i) {
898 var findParent = parentFinder(elCollection[i]);
899 if (findParent.nodeName.toUpperCase != 'BODY') {
900 filteredCollection.push(elCollection[i]);
903 return filteredCollection;
907 All this is here just so that IE gets to handle oversized blocks
908 in a visually pleasing manner. It does a browser detect. So sue me.
911 function fix_column_widths() {
912 var agt = navigator.userAgent.toLowerCase();
913 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
914 fix_column_width('left-column');
915 fix_column_width('right-column');
919 function fix_column_width(colName) {
920 if(column = document.getElementById(colName)) {
921 if(!column.offsetWidth) {
922 setTimeout("fix_column_width('" + colName + "')", 20);
927 var nodes = column.childNodes;
929 for(i = 0; i < nodes.length; ++i) {
930 if(nodes[i].className.indexOf("block") != -1 ) {
931 if(width < nodes[i].offsetWidth) {
932 width = nodes[i].offsetWidth;
937 for(i = 0; i < nodes.length; ++i) {
938 if(nodes[i].className.indexOf("block") != -1 ) {
939 nodes[i].style.width = width + 'px';
947 Insert myValue at current cursor position
949 function insertAtCursor(myField, myValue) {
951 if (document.selection) {
953 sel = document.selection.createRange();
956 // Mozilla/Netscape support
957 else if (myField.selectionStart || myField.selectionStart == '0') {
958 var startPos = myField.selectionStart;
959 var endPos = myField.selectionEnd;
960 myField.value = myField.value.substring(0, startPos)
961 + myValue + myField.value.substring(endPos, myField.value.length);
963 myField.value += myValue;
969 Call instead of setting window.onload directly or setting body onload=.
970 Adds your function to a chain of functions rather than overwriting anything
973 function addonload(fn) {
974 var oldhandler=window.onload;
975 window.onload=function() {
976 if(oldhandler) oldhandler();
981 * Replacement for getElementsByClassName in browsers that aren't cool enough
983 * Relying on the built-in getElementsByClassName is far, far faster than
986 * Note: the third argument used to be an object with odd behaviour. It now
987 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
988 * mimicked if you pass an object.
990 * @param {Node} oElm The top-level node for searching. To search a whole
991 * document, use `document`.
992 * @param {String} strTagName filter by tag names
993 * @param {String} name same as HTML5 spec
995 function getElementsByClassName(oElm, strTagName, name) {
996 // for backwards compatibility
997 if(typeof name == "object") {
998 var names = new Array();
999 for(var i=0; i<name.length; i++) names.push(names[i]);
1000 name = names.join('');
1002 // use native implementation if possible
1003 if (oElm.getElementsByClassName && Array.filter) {
1004 if (strTagName == '*') {
1005 return oElm.getElementsByClassName(name);
1007 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1008 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1012 // native implementation unavailable, fall back to slow method
1013 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1014 var arrReturnElements = new Array();
1015 var arrRegExpClassNames = new Array();
1016 var names = name.split(' ');
1017 for(var i=0; i<names.length; i++) {
1018 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1022 for(var j=0; j<arrElements.length; j++) {
1023 oElement = arrElements[j];
1025 for(var k=0; k<arrRegExpClassNames.length; k++) {
1026 if(!arrRegExpClassNames[k].test(oElement.className)) {
1027 bMatchesAll = false;
1032 arrReturnElements.push(oElement);
1035 return (arrReturnElements)
1038 function openpopup(event, args) {
1041 if (event.preventDefault) {
1042 event.preventDefault();
1044 event.returnValue = false;
1048 var fullurl = args.url;
1049 if (!args.url.match(/https?:\/\//)) {
1050 fullurl = M.cfg.wwwroot + args.url;
1052 var windowobj = window.open(fullurl,args.name,args.options);
1056 if (args.fullscreen) {
1057 windowobj.moveTo(0,0);
1058 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1065 /** Close the current browser window. */
1066 function close_window(e) {
1067 if (e.preventDefault) {
1070 e.returnValue = false;
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 /** List of flv players to be loaded */
1549 M.util.video_players = [];
1550 /** List of mp3 players to be loaded */
1551 M.util.audio_players = [];
1555 * @param id element id
1556 * @param fileurl media url
1559 * @param autosize true means detect size from media
1561 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1562 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1571 M.util.add_audio_player = function (id, fileurl, small) {
1572 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1576 * Initialise all audio and video player, must be called from page footer.
1578 M.util.load_flowplayer = function() {
1579 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1582 if (typeof(flowplayer) == 'undefined') {
1585 var embed_function = function() {
1586 if (loaded || typeof(flowplayer) == 'undefined') {
1594 /* TODO: add CSS color overrides for the flv flow player */
1596 for(var i=0; i<M.util.video_players.length; i++) {
1597 var video = M.util.video_players[i];
1598 if (video.width > 0 && video.height > 0) {
1599 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1601 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1603 flowplayer(video.id, src, {
1604 plugins: {controls: controls},
1606 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1607 onMetaData: function(clip) {
1608 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1609 clip.mvideo.resized = true;
1610 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1611 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1612 // bad luck, we have to guess - we may not get metadata at all
1613 var width = clip.width;
1614 var height = clip.height;
1616 var width = clip.metaData.width;
1617 var height = clip.metaData.height;
1619 var minwidth = 300; // controls are messed up in smaller objects
1620 if (width < minwidth) {
1621 height = (height * minwidth) / width;
1625 var object = this._api();
1626 object.width = width;
1627 object.height = height;
1633 if (M.util.audio_players.length == 0) {
1646 backgroundGradient: [0.5,0,0.3]
1650 for (var j=0; j < document.styleSheets.length; j++) {
1651 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1652 var allrules = document.styleSheets[j].rules;
1653 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1654 var allrules = document.styleSheets[j].cssRules;
1659 for(var i=0; i<allrules.length; i++) {
1661 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1662 if (typeof(allrules[i].cssText) != 'undefined') {
1663 rule = allrules[i].style.cssText;
1664 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1665 rule = allrules[i].style.cssText;
1667 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1668 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1669 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1670 controls[colprop] = rule;
1677 for(i=0; i<M.util.audio_players.length; i++) {
1678 var audio = M.util.audio_players[i];
1680 controls.controlall = false;
1681 controls.height = 15;
1682 controls.time = false;
1684 controls.controlall = true;
1685 controls.height = 25;
1686 controls.time = true;
1688 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1689 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1690 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1695 if (M.cfg.jsrev == -10) {
1696 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1698 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1700 var fileref = document.createElement('script');
1701 fileref.setAttribute('type','text/javascript');
1702 fileref.setAttribute('src', jsurl);
1703 fileref.onload = embed_function;
1704 fileref.onreadystatechange = embed_function;
1705 document.getElementsByTagName('head')[0].appendChild(fileref);