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');
267 } else if ((targetancestor = target.ancestor('a')) !== null) {
268 window.location = targetancestor.get('href');
270 } else if (target.test('input')) {
271 targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
272 // We cannot use target.ancestor('form') on the previous line
273 // because of http://yuilibrary.com/projects/yui3/ticket/2531561
277 if (target.get('name') && target.get('value')) {
278 targetform.append('<input type="hidden" name="' + target.get('name') +
279 '" value="' + target.get('value') + '">');
283 } else if (target.get('tagName').toLowerCase() == 'form') {
284 // We cannot use target.test('form') on the previous line because of
285 // http://yuilibrary.com/projects/yui3/ticket/2531561
288 } else if (M.cfg.developerdebug) {
289 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
293 if (!args.cancellabel) {
294 args.cancellabel = M.str.moodle.cancel;
296 if (!args.continuelabel) {
297 args.continuelabel = M.str.moodle.yes;
301 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
302 {text: args.continuelabel, handler: handle_yes}
305 simpledialog.cfg.queueProperty('buttons', buttons);
307 simpledialog.render(document.body);
312 /** Useful for full embedding of various stuff */
313 M.util.init_maximised_embed = function(Y, id) {
314 var obj = Y.one('#'+id);
319 var get_htmlelement_size = function(el, prop) {
320 if (Y.Lang.isString(el)) {
321 el = Y.one('#' + el);
323 var val = el.getStyle(prop);
325 val = el.getComputedStyle(prop);
327 return parseInt(val);
330 var resize_object = function() {
331 obj.setStyle('width', '0px');
332 obj.setStyle('height', '0px');
333 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
335 if (newwidth > 500) {
336 obj.setStyle('width', newwidth + 'px');
338 obj.setStyle('width', '500px');
341 var headerheight = get_htmlelement_size('page-header', 'height');
342 var footerheight = get_htmlelement_size('page-footer', 'height');
343 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
344 if (newheight < 400) {
347 obj.setStyle('height', newheight+'px');
351 // fix layout if window resized too
352 window.onresize = function() {
358 * Attach handler to single_select
360 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
361 Y.use('event-key', function() {
362 var select = Y.one('#'+selectid);
364 // Try to get the form by id
365 var form = Y.one('#'+formid) || (function(){
366 // Hmmm the form's id may have been overriden by an internal input
367 // with the name id which will KILL IE.
368 // We need to manually iterate at this point because if the case
369 // above is true YUI's ancestor method will also kill IE!
371 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
372 form = form.ancestor();
376 // Make sure we have the form
378 // Create a function to handle our change event
379 var processchange = function(e, paramobject) {
380 if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
381 //prevent event bubbling and detach handlers to prevent multiple submissions caused by double clicking
383 paramobject.eventkeypress.detach();
384 paramobject.eventblur.detach();
385 paramobject.eventchangeorblur.detach();
390 // Attach the change event to the keypress, blur, and click actions.
391 // We don't use the change event because IE fires it on every arrow up/down
392 // event.... usability
393 var paramobject = new Object();
394 paramobject.lastindex = select.get('selectedIndex');
395 paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
396 paramobject.eventblur = select.on('blur', processchange, form, paramobject);
397 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
399 paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
401 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
409 * Attach handler to url_select
411 M.util.init_url_select = function(Y, formid, selectid, nothing) {
412 YUI(M.yui.loader).use('node', function(Y) {
413 Y.on('change', function() {
414 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
415 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
423 * Breaks out all links to the top frame - used in frametop page layout.
425 M.util.init_frametop = function(Y) {
426 Y.all('a').each(function(node) {
427 node.set('target', '_top');
429 Y.all('form').each(function(node) {
430 node.set('target', '_top');
435 * Finds all nodes that match the given CSS selector and attaches events to them
436 * so that they toggle a given classname when clicked.
439 * @param {string} id An id containing elements to target
440 * @param {string} cssselector A selector to use to find targets
441 * @param {string} toggleclassname A classname to toggle
443 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
445 if (togglecssselector == '') {
446 togglecssselector = cssselector;
449 var node = Y.one('#'+id);
450 node.all(cssselector).each(function(n){
451 n.on('click', function(e){
453 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
454 if (this.test(togglecssselector)) {
455 this.toggleClass(toggleclassname);
457 this.ancestor(togglecssselector).toggleClass(toggleclassname);
462 // Attach this click event to the node rather than all selectors... will be much better
464 node.on('click', function(e){
465 if (e.target.hasClass('addtoall')) {
466 this.all(togglecssselector).addClass(toggleclassname);
467 } else if (e.target.hasClass('removefromall')) {
468 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
474 * Initialises a colour picker
476 * Designed to be used with admin_setting_configcolourpicker although could be used
477 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
478 * above or below the input (must have the same parent) and then call this with the
481 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
482 * contrib/blocks. For better docs refer to that.
486 * @param {object} previewconf
488 M.util.init_colour_picker = function(Y, id, previewconf) {
490 * We need node and event-mouseenter
492 Y.use('node', 'event-mouseenter', function(){
494 * The colour picker object
503 eventMouseEnter : null,
504 eventMouseLeave : null,
505 eventMouseMove : null,
510 * Initalises the colour picker by putting everything together and wiring the events
513 this.input = Y.one('#'+id);
514 this.box = this.input.ancestor().one('.admin_colourpicker');
515 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
516 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
517 this.preview = Y.Node.create('<div class="previewcolour"></div>');
518 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
519 this.current = Y.Node.create('<div class="currentcolour"></div>');
520 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
521 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
523 if (typeof(previewconf) === 'object' && previewconf !== null) {
524 Y.one('#'+id+'_preview').on('click', function(e){
525 if (Y.Lang.isString(previewconf.selector)) {
526 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
528 for (var i in previewconf.selector) {
529 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
535 this.eventClick = this.image.on('click', this.pickColour, this);
536 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
539 * Starts to follow the mouse once it enter the image
541 startFollow : function(e) {
542 this.eventMouseEnter.detach();
543 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
544 this.eventMouseMove = this.image.on('mousemove', function(e){
545 this.preview.setStyle('backgroundColor', this.determineColour(e));
549 * Stops following the mouse
551 endFollow : function(e) {
552 this.eventMouseMove.detach();
553 this.eventMouseLeave.detach();
554 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
557 * Picks the colour the was clicked on
559 pickColour : function(e) {
560 var colour = this.determineColour(e);
561 this.input.set('value', colour);
562 this.current.setStyle('backgroundColor', colour);
565 * Calculates the colour fromthe given co-ordinates
567 determineColour : function(e) {
568 var eventx = Math.floor(e.pageX-e.target.getX());
569 var eventy = Math.floor(e.pageY-e.target.getY());
571 var imagewidth = this.width;
572 var imageheight = this.height;
573 var factor = this.factor;
574 var colour = [255,0,0];
585 var matrixcount = matrices.length;
586 var limit = Math.round(imagewidth/matrixcount);
587 var heightbreak = Math.round(imageheight/2);
589 for (var x = 0; x < imagewidth; x++) {
590 var divisor = Math.floor(x / limit);
591 var matrix = matrices[divisor];
593 colour[0] += matrix[0]*factor;
594 colour[1] += matrix[1]*factor;
595 colour[2] += matrix[2]*factor;
602 var pixel = [colour[0], colour[1], colour[2]];
603 if (eventy < heightbreak) {
604 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
605 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
606 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
607 } else if (eventy > heightbreak) {
608 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
609 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
610 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
613 return this.convert_rgb_to_hex(pixel);
616 * Converts an RGB value to Hex
618 convert_rgb_to_hex : function(rgb) {
620 var hexchars = "0123456789ABCDEF";
621 for (var i=0; i<3; i++) {
622 var number = Math.abs(rgb[i]);
623 if (number == 0 || isNaN(number)) {
626 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
633 * Initialise the colour picker :) Hoorah
639 M.util.init_block_hider = function(Y, config) {
640 Y.use('base', 'node', function(Y) {
641 M.util.block_hider = M.util.block_hider || (function(){
642 var blockhider = function() {
643 blockhider.superclass.constructor.apply(this, arguments);
645 blockhider.prototype = {
646 initializer : function(config) {
647 this.set('block', '#'+this.get('id'));
648 var b = this.get('block'),
651 if (t && (a = t.one('.block_action'))) {
652 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
653 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
654 hide.on('keypress', this.updateStateKey, this, true);
655 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
656 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
657 show.on('keypress', this.updateStateKey, this, false);
658 a.insert(show, 0).insert(hide, 0);
661 updateState : function(e, hide) {
662 M.util.set_user_preference(this.get('preference'), hide);
664 this.get('block').addClass('hidden');
666 this.get('block').removeClass('hidden');
669 updateStateKey : function(e, hide) {
670 if (e.keyCode == 13) { //allow hide/show via enter key
671 this.updateState(this, hide);
675 Y.extend(blockhider, Y.Base, blockhider.prototype, {
681 value : M.util.image_url('t/switch_minus', 'moodle')
684 value : M.util.image_url('t/switch_plus', 'moodle')
687 setter : function(node) {
695 new M.util.block_hider(config);
700 * Returns a string registered in advance for usage in JavaScript
702 * If you do not pass the third parameter, the function will just return
703 * the corresponding value from the M.str object. If the third parameter is
704 * provided, the function performs {$a} placeholder substitution in the
705 * same way as PHP get_string() in Moodle does.
707 * @param {String} identifier string identifier
708 * @param {String} component the component providing the string
709 * @param {Object|String} a optional variable to populate placeholder with
711 M.util.get_string = function(identifier, component, a) {
714 if (M.cfg.developerdebug) {
715 // creating new instance if YUI is not optimal but it seems to be better way then
716 // require the instance via the function API - note that it is used in rare cases
717 // for debugging only anyway
718 var Y = new YUI({ debug : true });
721 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
722 stringvalue = '[[' + identifier + ',' + component + ']]';
723 if (M.cfg.developerdebug) {
724 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
729 stringvalue = M.str[component][identifier];
731 if (typeof a == 'undefined') {
732 // no placeholder substitution requested
736 if (typeof a == 'number' || typeof a == 'string') {
737 // replace all occurrences of {$a} with the placeholder value
738 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
742 if (typeof a == 'object') {
743 // replace {$a->key} placeholders
745 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
746 if (M.cfg.developerdebug) {
747 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
751 var search = '{$a->' + key + '}';
752 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
753 search = new RegExp(search, 'g');
754 stringvalue = stringvalue.replace(search, a[key]);
759 if (M.cfg.developerdebug) {
760 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
766 * Set focus on username or password field of the login form
768 M.util.focus_login_form = function(Y) {
769 var username = Y.one('#username');
770 var password = Y.one('#password');
772 if (username == null || password == null) {
773 // something is wrong here
777 var curElement = document.activeElement
778 if (curElement == 'undefined') {
779 // legacy browser - skip refocus protection
780 } else if (curElement.tagName == 'INPUT') {
781 // user was probably faster to focus something, do not mess with focus
785 if (username.get('value') == '') {
793 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
795 function checkall() {
796 var inputs = document.getElementsByTagName('input');
797 for (var i = 0; i < inputs.length; i++) {
798 if (inputs[i].type == 'checkbox') {
799 inputs[i].checked = true;
804 function checknone() {
805 var inputs = document.getElementsByTagName('input');
806 for (var i = 0; i < inputs.length; i++) {
807 if (inputs[i].type == 'checkbox') {
808 inputs[i].checked = false;
814 * Either check, or uncheck, all checkboxes inside the element with id is
815 * @param id the id of the container
816 * @param checked the new state, either '' or 'checked'.
818 function select_all_in_element_with_id(id, checked) {
819 var container = document.getElementById(id);
823 var inputs = container.getElementsByTagName('input');
824 for (var i = 0; i < inputs.length; ++i) {
825 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
826 inputs[i].checked = checked;
831 function select_all_in(elTagName, elClass, elId) {
832 var inputs = document.getElementsByTagName('input');
833 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
834 for(var i = 0; i < inputs.length; ++i) {
835 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
836 inputs[i].checked = 'checked';
841 function deselect_all_in(elTagName, elClass, elId) {
842 var inputs = document.getElementsByTagName('INPUT');
843 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
844 for(var i = 0; i < inputs.length; ++i) {
845 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
846 inputs[i].checked = '';
851 function confirm_if(expr, message) {
855 return confirm(message);
860 findParentNode (start, elementName, elementClass, elementID)
862 Travels up the DOM hierarchy to find a parent element with the
863 specified tag name, class, and id. All conditions must be met,
864 but any can be ommitted. Returns the BODY element if no match
867 function findParentNode(el, elName, elClass, elId) {
868 while (el.nodeName.toUpperCase() != 'BODY') {
869 if ((!elName || el.nodeName.toUpperCase() == elName) &&
870 (!elClass || el.className.indexOf(elClass) != -1) &&
871 (!elId || el.id == elId)) {
879 findChildNode (start, elementName, elementClass, elementID)
881 Travels down the DOM hierarchy to find all child elements with the
882 specified tag name, class, and id. All conditions must be met,
883 but any can be ommitted.
884 Doesn't examine children of matches.
886 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
887 var children = new Array();
888 for (var i = 0; i < start.childNodes.length; i++) {
889 var classfound = false;
890 var child = start.childNodes[i];
891 if((child.nodeType == 1) &&//element node type
892 (elementClass && (typeof(child.className)=='string'))) {
893 var childClasses = child.className.split(/\s+/);
894 for (var childClassIndex in childClasses) {
895 if (childClasses[childClassIndex]==elementClass) {
901 if(child.nodeType == 1) { //element node type
902 if ( (!tagName || child.nodeName == tagName) &&
903 (!elementClass || classfound)&&
904 (!elementID || child.id == elementID) &&
905 (!elementName || child.name == elementName))
907 children = children.concat(child);
909 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
916 function unmaskPassword(id) {
917 var pw = document.getElementById(id);
918 var chb = document.getElementById(id+'unmask');
921 // first try IE way - it can not set name attribute later
923 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
925 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
927 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
929 var newpw = document.createElement('input');
930 newpw.setAttribute('autocomplete', 'off');
931 newpw.setAttribute('name', pw.name);
933 newpw.setAttribute('type', 'text');
935 newpw.setAttribute('type', 'password');
937 newpw.setAttribute('class', pw.getAttribute('class'));
940 newpw.size = pw.size;
941 newpw.onblur = pw.onblur;
942 newpw.onchange = pw.onchange;
943 newpw.value = pw.value;
944 pw.parentNode.replaceChild(newpw, pw);
947 function filterByParent(elCollection, parentFinder) {
948 var filteredCollection = [];
949 for (var i = 0; i < elCollection.length; ++i) {
950 var findParent = parentFinder(elCollection[i]);
951 if (findParent.nodeName.toUpperCase() != 'BODY') {
952 filteredCollection.push(elCollection[i]);
955 return filteredCollection;
959 All this is here just so that IE gets to handle oversized blocks
960 in a visually pleasing manner. It does a browser detect. So sue me.
963 function fix_column_widths() {
964 var agt = navigator.userAgent.toLowerCase();
965 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
966 fix_column_width('left-column');
967 fix_column_width('right-column');
971 function fix_column_width(colName) {
972 if(column = document.getElementById(colName)) {
973 if(!column.offsetWidth) {
974 setTimeout("fix_column_width('" + colName + "')", 20);
979 var nodes = column.childNodes;
981 for(i = 0; i < nodes.length; ++i) {
982 if(nodes[i].className.indexOf("block") != -1 ) {
983 if(width < nodes[i].offsetWidth) {
984 width = nodes[i].offsetWidth;
989 for(i = 0; i < nodes.length; ++i) {
990 if(nodes[i].className.indexOf("block") != -1 ) {
991 nodes[i].style.width = width + 'px';
999 Insert myValue at current cursor position
1001 function insertAtCursor(myField, myValue) {
1003 if (document.selection) {
1005 sel = document.selection.createRange();
1008 // Mozilla/Netscape support
1009 else if (myField.selectionStart || myField.selectionStart == '0') {
1010 var startPos = myField.selectionStart;
1011 var endPos = myField.selectionEnd;
1012 myField.value = myField.value.substring(0, startPos)
1013 + myValue + myField.value.substring(endPos, myField.value.length);
1015 myField.value += myValue;
1021 Call instead of setting window.onload directly or setting body onload=.
1022 Adds your function to a chain of functions rather than overwriting anything
1025 function addonload(fn) {
1026 var oldhandler=window.onload;
1027 window.onload=function() {
1028 if(oldhandler) oldhandler();
1033 * Replacement for getElementsByClassName in browsers that aren't cool enough
1035 * Relying on the built-in getElementsByClassName is far, far faster than
1038 * Note: the third argument used to be an object with odd behaviour. It now
1039 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1040 * mimicked if you pass an object.
1042 * @param {Node} oElm The top-level node for searching. To search a whole
1043 * document, use `document`.
1044 * @param {String} strTagName filter by tag names
1045 * @param {String} name same as HTML5 spec
1047 function getElementsByClassName(oElm, strTagName, name) {
1048 // for backwards compatibility
1049 if(typeof name == "object") {
1050 var names = new Array();
1051 for(var i=0; i<name.length; i++) names.push(names[i]);
1052 name = names.join('');
1054 // use native implementation if possible
1055 if (oElm.getElementsByClassName && Array.filter) {
1056 if (strTagName == '*') {
1057 return oElm.getElementsByClassName(name);
1059 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1060 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1064 // native implementation unavailable, fall back to slow method
1065 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1066 var arrReturnElements = new Array();
1067 var arrRegExpClassNames = new Array();
1068 var names = name.split(' ');
1069 for(var i=0; i<names.length; i++) {
1070 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1074 for(var j=0; j<arrElements.length; j++) {
1075 oElement = arrElements[j];
1077 for(var k=0; k<arrRegExpClassNames.length; k++) {
1078 if(!arrRegExpClassNames[k].test(oElement.className)) {
1079 bMatchesAll = false;
1084 arrReturnElements.push(oElement);
1087 return (arrReturnElements)
1090 function openpopup(event, args) {
1093 if (event.preventDefault) {
1094 event.preventDefault();
1096 event.returnValue = false;
1100 var fullurl = args.url;
1101 if (!args.url.match(/https?:\/\//)) {
1102 fullurl = M.cfg.wwwroot + args.url;
1104 var windowobj = window.open(fullurl,args.name,args.options);
1108 if (args.fullscreen) {
1109 windowobj.moveTo(0,0);
1110 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1117 /** Close the current browser window. */
1118 function close_window(e) {
1119 if (e.preventDefault) {
1122 e.returnValue = false;
1128 * Used in a couple of modules to hide navigation areas when using AJAX
1131 function show_item(itemid) {
1132 var item = document.getElementById(itemid);
1134 item.style.display = "";
1138 function destroy_item(itemid) {
1139 var item = document.getElementById(itemid);
1141 item.parentNode.removeChild(item);
1145 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1146 * @param controlid the control id.
1148 function focuscontrol(controlid) {
1149 var control = document.getElementById(controlid);
1156 * Transfers keyboard focus to an HTML element based on the old style style of focus
1157 * This function should be removed as soon as it is no longer used
1159 function old_onload_focus(formid, controlname) {
1160 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1161 document.forms[formid].elements[controlname].focus();
1165 function build_querystring(obj) {
1166 return convert_object_to_string(obj, '&');
1169 function build_windowoptionsstring(obj) {
1170 return convert_object_to_string(obj, ',');
1173 function convert_object_to_string(obj, separator) {
1174 if (typeof obj !== 'object') {
1179 k = encodeURIComponent(k);
1181 if(obj[k] instanceof Array) {
1182 for(var i in value) {
1183 list.push(k+'[]='+encodeURIComponent(value[i]));
1186 list.push(k+'='+encodeURIComponent(value));
1189 return list.join(separator);
1192 function stripHTML(str) {
1193 var re = /<\S[^><]*>/g;
1194 var ret = str.replace(re, "");
1198 Number.prototype.fixed=function(n){
1200 return round(Number(this)*pow(10,n))/pow(10,n);
1202 function update_progress_bar (id, width, pt, msg, es){
1204 var status = document.getElementById("status_"+id);
1205 var percent_indicator = document.getElementById("pt_"+id);
1206 var progress_bar = document.getElementById("progress_"+id);
1207 var time_es = document.getElementById("time_"+id);
1208 status.innerHTML = msg;
1209 percent_indicator.innerHTML = percent.fixed(2) + '%';
1210 if(percent == 100) {
1211 progress_bar.style.background = "green";
1212 time_es.style.display = "none";
1214 progress_bar.style.background = "#FFCC66";
1216 time_es.innerHTML = "";
1218 time_es.innerHTML = es.fixed(2)+" sec";
1219 time_es.style.display
1223 progress_bar.style.width = width + "px";
1228 // ===== Deprecated core Javascript functions for Moodle ====
1229 // DO NOT USE!!!!!!!
1230 // Do not put this stuff in separate file because it only adds extra load on servers!
1233 * Used in a couple of modules to hide navigation areas when using AJAX
1235 function hide_item(itemid) {
1236 // use class='hiddenifjs' instead
1237 var item = document.getElementById(itemid);
1239 item.style.display = "none";
1243 M.util.help_icon = {
1246 add : function(Y, properties) {
1248 properties.node = Y.one('#'+properties.id);
1249 if (properties.node) {
1250 properties.node.on('click', this.display, this, properties);
1253 display : function(event, args) {
1254 event.preventDefault();
1255 if (M.util.help_icon.instance === null) {
1256 var Y = M.util.help_icon.Y;
1257 Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1258 var help_content_overlay = {
1263 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1264 // Create an overlay from markup
1265 this.overlay = new Y.Overlay({
1266 headerContent: closebtn,
1273 this.overlay.render(Y.one(document.body));
1275 closebtn.on('click', this.overlay.hide, this.overlay);
1277 var boundingBox = this.overlay.get("boundingBox");
1279 // Hide the menu if the user clicks outside of its content
1280 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1281 var oTarget = event.target;
1282 var menuButton = Y.one("#"+args.id);
1284 if (!oTarget.compareTo(menuButton) &&
1285 !menuButton.contains(oTarget) &&
1286 !oTarget.compareTo(boundingBox) &&
1287 !boundingBox.contains(oTarget)) {
1288 this.overlay.hide();
1292 Y.on("key", this.close, closebtn , "down:13", this);
1293 closebtn.on('click', this.close, this);
1296 close : function(e) {
1298 this.helplink.focus();
1299 this.overlay.hide();
1302 display : function(event, args) {
1303 this.helplink = args.node;
1304 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1305 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1307 var fullurl = args.url;
1308 if (!args.url.match(/https?:\/\//)) {
1309 fullurl = M.cfg.wwwroot + args.url;
1312 var ajaxurl = fullurl + '&ajax=1';
1318 success: function(id, o, node) {
1319 this.display_callback(o.responseText);
1321 failure: function(id, o, node) {
1322 var debuginfo = o.statusText;
1323 if (M.cfg.developerdebug) {
1324 o.statusText += ' (' + ajaxurl + ')';
1326 this.display_callback('bodyContent',debuginfo);
1332 this.overlay.show();
1334 Y.one('#closehelpbox').focus();
1337 display_callback : function(content) {
1338 this.overlay.set('bodyContent', content);
1341 hideContent : function() {
1343 help.overlay.hide();
1346 help_content_overlay.init();
1347 M.util.help_icon.instance = help_content_overlay;
1348 M.util.help_icon.instance.display(event, args);
1351 M.util.help_icon.instance.display(event, args);
1354 init : function(Y) {
1360 * Custom menu namespace
1362 M.core_custom_menu = {
1364 * This method is used to initialise a custom menu given the id that belongs
1365 * to the custom menu's root node.
1368 * @param {string} nodeid
1370 init : function(Y, nodeid) {
1371 var node = Y.one('#'+nodeid);
1373 Y.use('node-menunav', function(Y) {
1375 // Remove the javascript-disabled class.... obviously javascript is enabled.
1376 node.removeClass('javascript-disabled');
1377 // Initialise the menunav plugin
1378 node.plug(Y.Plugin.NodeMenuNav);
1385 * Used to store form manipulation methods and enhancments
1387 M.form = M.form || {};
1390 * Converts a nbsp indented select box into a multi drop down custom control much
1391 * like the custom menu. It also selectable categories on or off.
1393 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1396 * @param {string} id
1397 * @param {Array} options
1399 M.form.init_smartselect = function(Y, id, options) {
1400 if (!id.match(/^id_/)) {
1403 var select = Y.one('select#'+id);
1407 Y.use('event-delegate',function(){
1413 currentvalue : null,
1417 selectablecategories : true,
1425 init : function(Y, id, args, nodes) {
1426 if (typeof(args)=='object') {
1427 for (var i in this.cfg) {
1428 if (args[i] || args[i]===false) {
1429 this.cfg[i] = args[i];
1434 // Display a loading message first up
1435 this.nodes.select = nodes.select;
1437 this.currentvalue = this.nodes.select.get('selectedIndex');
1438 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1440 var options = Array();
1441 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1442 this.nodes.select.all('option').each(function(option, index) {
1443 var rawtext = option.get('innerHTML');
1444 var text = rawtext.replace(/^( )*/, '');
1445 if (rawtext === text) {
1446 text = rawtext.replace(/^(\s)*/, '');
1447 var depth = (rawtext.length - text.length ) + 1;
1449 var depth = ((rawtext.length - text.length )/12)+1;
1451 option.set('innerHTML', text);
1452 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1455 this.structure = [];
1456 var structcount = 0;
1457 for (var i in options) {
1460 this.structure.push(o);
1464 var current = this.structure[structcount-1];
1465 for (var j = 0; j < o.depth-1;j++) {
1466 if (current && current.children) {
1467 current = current.children[current.children.length-1];
1470 if (current && current.children) {
1471 current.children.push(o);
1476 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1477 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1478 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1479 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1481 if (this.cfg.mode == null) {
1482 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1483 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1484 this.cfg.mode = 'compact';
1486 this.cfg.mode = 'spanning';
1490 if (this.cfg.mode == 'compact') {
1491 this.nodes.menu.addClass('compactmenu');
1493 this.nodes.menu.addClass('spanningmenu');
1494 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1497 Y.one(document.body).append(this.nodes.menu);
1498 var pos = this.nodes.select.getXY();
1500 this.nodes.menu.setXY(pos);
1501 this.nodes.menu.on('click', this.handle_click, this);
1503 Y.one(window).on('resize', function(){
1504 var pos = this.nodes.select.getXY();
1506 this.nodes.menu.setXY(pos);
1509 generate_menu_content : function() {
1510 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1511 content += this.generate_submenu_content(this.structure[0], true);
1512 content += '</ul></div>';
1515 generate_submenu_content : function(item, rootelement) {
1516 this.submenucount++;
1518 if (item.children.length > 0) {
1520 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1521 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1522 content += '<div class="smartselect_menu_content">';
1524 content += '<li class="smartselect_submenuitem">';
1525 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1526 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1527 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1528 content += '<div class="smartselect_submenu_content">';
1531 for (var i in item.children) {
1532 content += this.generate_submenu_content(item.children[i],false);
1535 content += '</div>';
1536 content += '</div>';
1542 content += '<li class="smartselect_menuitem">';
1543 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1548 select : function(e) {
1551 this.currenttext = t.get('innerHTML');
1552 this.currentvalue = t.getAttribute('value');
1553 this.nodes.select.set('selectedIndex', this.currentvalue);
1556 handle_click : function(e) {
1557 var target = e.target;
1558 if (target.hasClass('smartselect_mask')) {
1560 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1562 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1563 this.show_sub_menu(e);
1566 show_menu : function(e) {
1568 var menu = e.target.ancestor().one('.smartselect_menu');
1569 menu.addClass('visible');
1570 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1572 show_sub_menu : function(e) {
1574 var target = e.target;
1575 if (!target.hasClass('smartselect_submenuitem')) {
1576 target = target.ancestor('.smartselect_submenuitem');
1578 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1579 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1582 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1583 target.one('.smartselect_submenu').addClass('visible');
1585 hide_menu : function() {
1586 this.nodes.menu.all('.visible').removeClass('visible');
1587 if (this.shownevent) {
1588 this.shownevent.detach();
1592 smartselect.init(Y, id, options, {select:select});
1596 /** List of flv players to be loaded */
1597 M.util.video_players = [];
1598 /** List of mp3 players to be loaded */
1599 M.util.audio_players = [];
1603 * @param id element id
1604 * @param fileurl media url
1607 * @param autosize true means detect size from media
1609 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1610 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1619 M.util.add_audio_player = function (id, fileurl, small) {
1620 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1624 * Initialise all audio and video player, must be called from page footer.
1626 M.util.load_flowplayer = function() {
1627 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1630 if (typeof(flowplayer) == 'undefined') {
1633 var embed_function = function() {
1634 if (loaded || typeof(flowplayer) == 'undefined') {
1642 /* TODO: add CSS color overrides for the flv flow player */
1644 for(var i=0; i<M.util.video_players.length; i++) {
1645 var video = M.util.video_players[i];
1646 if (video.width > 0 && video.height > 0) {
1647 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1649 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1651 flowplayer(video.id, src, {
1652 plugins: {controls: controls},
1654 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1655 onMetaData: function(clip) {
1656 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1657 clip.mvideo.resized = true;
1658 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1659 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1660 // bad luck, we have to guess - we may not get metadata at all
1661 var width = clip.width;
1662 var height = clip.height;
1664 var width = clip.metaData.width;
1665 var height = clip.metaData.height;
1667 var minwidth = 300; // controls are messed up in smaller objects
1668 if (width < minwidth) {
1669 height = (height * minwidth) / width;
1673 var object = this._api();
1674 object.width = width;
1675 object.height = height;
1681 if (M.util.audio_players.length == 0) {
1694 backgroundGradient: [0.5,0,0.3]
1698 for (var j=0; j < document.styleSheets.length; j++) {
1699 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1700 var allrules = document.styleSheets[j].rules;
1701 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1702 var allrules = document.styleSheets[j].cssRules;
1707 for(var i=0; i<allrules.length; i++) {
1709 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1710 if (typeof(allrules[i].cssText) != 'undefined') {
1711 rule = allrules[i].cssText;
1712 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1713 rule = allrules[i].style.cssText;
1715 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1716 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1717 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1718 controls[colprop] = rule;
1725 for(i=0; i<M.util.audio_players.length; i++) {
1726 var audio = M.util.audio_players[i];
1728 controls.controlall = false;
1729 controls.height = 15;
1730 controls.time = false;
1732 controls.controlall = true;
1733 controls.height = 25;
1734 controls.time = true;
1736 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1737 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1738 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1743 if (M.cfg.jsrev == -10) {
1744 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1746 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1748 var fileref = document.createElement('script');
1749 fileref.setAttribute('type','text/javascript');
1750 fileref.setAttribute('src', jsurl);
1751 fileref.onload = embed_function;
1752 fileref.onreadystatechange = embed_function;
1753 document.getElementsByTagName('head')[0].appendChild(fileref);