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 if (inputs[i].disabled || inputs[i].readOnly) {
802 inputs[i].checked = true;
807 function checknone() {
808 var inputs = document.getElementsByTagName('input');
809 for (var i = 0; i < inputs.length; i++) {
810 if (inputs[i].type == 'checkbox') {
811 if (inputs[i].disabled || inputs[i].readOnly) {
814 inputs[i].checked = false;
820 * Either check, or uncheck, all checkboxes inside the element with id is
821 * @param id the id of the container
822 * @param checked the new state, either '' or 'checked'.
824 function select_all_in_element_with_id(id, checked) {
825 var container = document.getElementById(id);
829 var inputs = container.getElementsByTagName('input');
830 for (var i = 0; i < inputs.length; ++i) {
831 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
832 inputs[i].checked = checked;
837 function select_all_in(elTagName, elClass, elId) {
838 var inputs = document.getElementsByTagName('input');
839 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
840 for(var i = 0; i < inputs.length; ++i) {
841 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
842 inputs[i].checked = 'checked';
847 function deselect_all_in(elTagName, elClass, elId) {
848 var inputs = document.getElementsByTagName('INPUT');
849 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
850 for(var i = 0; i < inputs.length; ++i) {
851 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
852 inputs[i].checked = '';
857 function confirm_if(expr, message) {
861 return confirm(message);
866 findParentNode (start, elementName, elementClass, elementID)
868 Travels up the DOM hierarchy to find a parent element with the
869 specified tag name, class, and id. All conditions must be met,
870 but any can be ommitted. Returns the BODY element if no match
873 function findParentNode(el, elName, elClass, elId) {
874 while (el.nodeName.toUpperCase() != 'BODY') {
875 if ((!elName || el.nodeName.toUpperCase() == elName) &&
876 (!elClass || el.className.indexOf(elClass) != -1) &&
877 (!elId || el.id == elId)) {
885 findChildNode (start, elementName, elementClass, elementID)
887 Travels down the DOM hierarchy to find all child elements with the
888 specified tag name, class, and id. All conditions must be met,
889 but any can be ommitted.
890 Doesn't examine children of matches.
892 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
893 var children = new Array();
894 for (var i = 0; i < start.childNodes.length; i++) {
895 var classfound = false;
896 var child = start.childNodes[i];
897 if((child.nodeType == 1) &&//element node type
898 (elementClass && (typeof(child.className)=='string'))) {
899 var childClasses = child.className.split(/\s+/);
900 for (var childClassIndex in childClasses) {
901 if (childClasses[childClassIndex]==elementClass) {
907 if(child.nodeType == 1) { //element node type
908 if ( (!tagName || child.nodeName == tagName) &&
909 (!elementClass || classfound)&&
910 (!elementID || child.id == elementID) &&
911 (!elementName || child.name == elementName))
913 children = children.concat(child);
915 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
922 function unmaskPassword(id) {
923 var pw = document.getElementById(id);
924 var chb = document.getElementById(id+'unmask');
927 // first try IE way - it can not set name attribute later
929 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
931 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
933 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
935 var newpw = document.createElement('input');
936 newpw.setAttribute('autocomplete', 'off');
937 newpw.setAttribute('name', pw.name);
939 newpw.setAttribute('type', 'text');
941 newpw.setAttribute('type', 'password');
943 newpw.setAttribute('class', pw.getAttribute('class'));
946 newpw.size = pw.size;
947 newpw.onblur = pw.onblur;
948 newpw.onchange = pw.onchange;
949 newpw.value = pw.value;
950 pw.parentNode.replaceChild(newpw, pw);
953 function filterByParent(elCollection, parentFinder) {
954 var filteredCollection = [];
955 for (var i = 0; i < elCollection.length; ++i) {
956 var findParent = parentFinder(elCollection[i]);
957 if (findParent.nodeName.toUpperCase() != 'BODY') {
958 filteredCollection.push(elCollection[i]);
961 return filteredCollection;
965 All this is here just so that IE gets to handle oversized blocks
966 in a visually pleasing manner. It does a browser detect. So sue me.
969 function fix_column_widths() {
970 var agt = navigator.userAgent.toLowerCase();
971 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
972 fix_column_width('left-column');
973 fix_column_width('right-column');
977 function fix_column_width(colName) {
978 if(column = document.getElementById(colName)) {
979 if(!column.offsetWidth) {
980 setTimeout("fix_column_width('" + colName + "')", 20);
985 var nodes = column.childNodes;
987 for(i = 0; i < nodes.length; ++i) {
988 if(nodes[i].className.indexOf("block") != -1 ) {
989 if(width < nodes[i].offsetWidth) {
990 width = nodes[i].offsetWidth;
995 for(i = 0; i < nodes.length; ++i) {
996 if(nodes[i].className.indexOf("block") != -1 ) {
997 nodes[i].style.width = width + 'px';
1005 Insert myValue at current cursor position
1007 function insertAtCursor(myField, myValue) {
1009 if (document.selection) {
1011 sel = document.selection.createRange();
1014 // Mozilla/Netscape support
1015 else if (myField.selectionStart || myField.selectionStart == '0') {
1016 var startPos = myField.selectionStart;
1017 var endPos = myField.selectionEnd;
1018 myField.value = myField.value.substring(0, startPos)
1019 + myValue + myField.value.substring(endPos, myField.value.length);
1021 myField.value += myValue;
1027 Call instead of setting window.onload directly or setting body onload=.
1028 Adds your function to a chain of functions rather than overwriting anything
1031 function addonload(fn) {
1032 var oldhandler=window.onload;
1033 window.onload=function() {
1034 if(oldhandler) oldhandler();
1039 * Replacement for getElementsByClassName in browsers that aren't cool enough
1041 * Relying on the built-in getElementsByClassName is far, far faster than
1044 * Note: the third argument used to be an object with odd behaviour. It now
1045 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1046 * mimicked if you pass an object.
1048 * @param {Node} oElm The top-level node for searching. To search a whole
1049 * document, use `document`.
1050 * @param {String} strTagName filter by tag names
1051 * @param {String} name same as HTML5 spec
1053 function getElementsByClassName(oElm, strTagName, name) {
1054 // for backwards compatibility
1055 if(typeof name == "object") {
1056 var names = new Array();
1057 for(var i=0; i<name.length; i++) names.push(names[i]);
1058 name = names.join('');
1060 // use native implementation if possible
1061 if (oElm.getElementsByClassName && Array.filter) {
1062 if (strTagName == '*') {
1063 return oElm.getElementsByClassName(name);
1065 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1066 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1070 // native implementation unavailable, fall back to slow method
1071 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1072 var arrReturnElements = new Array();
1073 var arrRegExpClassNames = new Array();
1074 var names = name.split(' ');
1075 for(var i=0; i<names.length; i++) {
1076 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1080 for(var j=0; j<arrElements.length; j++) {
1081 oElement = arrElements[j];
1083 for(var k=0; k<arrRegExpClassNames.length; k++) {
1084 if(!arrRegExpClassNames[k].test(oElement.className)) {
1085 bMatchesAll = false;
1090 arrReturnElements.push(oElement);
1093 return (arrReturnElements)
1096 function openpopup(event, args) {
1099 if (event.preventDefault) {
1100 event.preventDefault();
1102 event.returnValue = false;
1106 var fullurl = args.url;
1107 if (!args.url.match(/https?:\/\//)) {
1108 fullurl = M.cfg.wwwroot + args.url;
1110 var windowobj = window.open(fullurl,args.name,args.options);
1114 if (args.fullscreen) {
1115 windowobj.moveTo(0,0);
1116 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1123 /** Close the current browser window. */
1124 function close_window(e) {
1125 if (e.preventDefault) {
1128 e.returnValue = false;
1134 * Used in a couple of modules to hide navigation areas when using AJAX
1137 function show_item(itemid) {
1138 var item = document.getElementById(itemid);
1140 item.style.display = "";
1144 function destroy_item(itemid) {
1145 var item = document.getElementById(itemid);
1147 item.parentNode.removeChild(item);
1151 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1152 * @param controlid the control id.
1154 function focuscontrol(controlid) {
1155 var control = document.getElementById(controlid);
1162 * Transfers keyboard focus to an HTML element based on the old style style of focus
1163 * This function should be removed as soon as it is no longer used
1165 function old_onload_focus(formid, controlname) {
1166 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1167 document.forms[formid].elements[controlname].focus();
1171 function build_querystring(obj) {
1172 return convert_object_to_string(obj, '&');
1175 function build_windowoptionsstring(obj) {
1176 return convert_object_to_string(obj, ',');
1179 function convert_object_to_string(obj, separator) {
1180 if (typeof obj !== 'object') {
1185 k = encodeURIComponent(k);
1187 if(obj[k] instanceof Array) {
1188 for(var i in value) {
1189 list.push(k+'[]='+encodeURIComponent(value[i]));
1192 list.push(k+'='+encodeURIComponent(value));
1195 return list.join(separator);
1198 function stripHTML(str) {
1199 var re = /<\S[^><]*>/g;
1200 var ret = str.replace(re, "");
1204 Number.prototype.fixed=function(n){
1206 return round(Number(this)*pow(10,n))/pow(10,n);
1208 function update_progress_bar (id, width, pt, msg, es){
1210 var status = document.getElementById("status_"+id);
1211 var percent_indicator = document.getElementById("pt_"+id);
1212 var progress_bar = document.getElementById("progress_"+id);
1213 var time_es = document.getElementById("time_"+id);
1214 status.innerHTML = msg;
1215 percent_indicator.innerHTML = percent.fixed(2) + '%';
1216 if(percent == 100) {
1217 progress_bar.style.background = "green";
1218 time_es.style.display = "none";
1220 progress_bar.style.background = "#FFCC66";
1222 time_es.innerHTML = "";
1224 time_es.innerHTML = es.fixed(2)+" sec";
1225 time_es.style.display
1229 progress_bar.style.width = width + "px";
1234 // ===== Deprecated core Javascript functions for Moodle ====
1235 // DO NOT USE!!!!!!!
1236 // Do not put this stuff in separate file because it only adds extra load on servers!
1239 * Used in a couple of modules to hide navigation areas when using AJAX
1241 function hide_item(itemid) {
1242 // use class='hiddenifjs' instead
1243 var item = document.getElementById(itemid);
1245 item.style.display = "none";
1249 M.util.help_icon = {
1252 add : function(Y, properties) {
1254 properties.node = Y.one('#'+properties.id);
1255 if (properties.node) {
1256 properties.node.on('click', this.display, this, properties);
1259 display : function(event, args) {
1260 event.preventDefault();
1261 if (M.util.help_icon.instance === null) {
1262 var Y = M.util.help_icon.Y;
1263 Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1264 var help_content_overlay = {
1269 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1270 // Create an overlay from markup
1271 this.overlay = new Y.Overlay({
1272 headerContent: closebtn,
1279 this.overlay.render(Y.one(document.body));
1281 closebtn.on('click', this.overlay.hide, this.overlay);
1283 var boundingBox = this.overlay.get("boundingBox");
1285 // Hide the menu if the user clicks outside of its content
1286 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1287 var oTarget = event.target;
1288 var menuButton = Y.one("#"+args.id);
1290 if (!oTarget.compareTo(menuButton) &&
1291 !menuButton.contains(oTarget) &&
1292 !oTarget.compareTo(boundingBox) &&
1293 !boundingBox.contains(oTarget)) {
1294 this.overlay.hide();
1298 Y.on("key", this.close, closebtn , "down:13", this);
1299 closebtn.on('click', this.close, this);
1302 close : function(e) {
1304 this.helplink.focus();
1305 this.overlay.hide();
1308 display : function(event, args) {
1309 this.helplink = args.node;
1310 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1311 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1313 var fullurl = args.url;
1314 if (!args.url.match(/https?:\/\//)) {
1315 fullurl = M.cfg.wwwroot + args.url;
1318 var ajaxurl = fullurl + '&ajax=1';
1324 success: function(id, o, node) {
1325 this.display_callback(o.responseText);
1327 failure: function(id, o, node) {
1328 var debuginfo = o.statusText;
1329 if (M.cfg.developerdebug) {
1330 o.statusText += ' (' + ajaxurl + ')';
1332 this.display_callback('bodyContent',debuginfo);
1338 this.overlay.show();
1340 Y.one('#closehelpbox').focus();
1343 display_callback : function(content) {
1344 this.overlay.set('bodyContent', content);
1347 hideContent : function() {
1349 help.overlay.hide();
1352 help_content_overlay.init();
1353 M.util.help_icon.instance = help_content_overlay;
1354 M.util.help_icon.instance.display(event, args);
1357 M.util.help_icon.instance.display(event, args);
1360 init : function(Y) {
1366 * Custom menu namespace
1368 M.core_custom_menu = {
1370 * This method is used to initialise a custom menu given the id that belongs
1371 * to the custom menu's root node.
1374 * @param {string} nodeid
1376 init : function(Y, nodeid) {
1377 var node = Y.one('#'+nodeid);
1379 Y.use('node-menunav', function(Y) {
1381 // Remove the javascript-disabled class.... obviously javascript is enabled.
1382 node.removeClass('javascript-disabled');
1383 // Initialise the menunav plugin
1384 node.plug(Y.Plugin.NodeMenuNav);
1391 * Used to store form manipulation methods and enhancments
1393 M.form = M.form || {};
1396 * Converts a nbsp indented select box into a multi drop down custom control much
1397 * like the custom menu. It also selectable categories on or off.
1399 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1402 * @param {string} id
1403 * @param {Array} options
1405 M.form.init_smartselect = function(Y, id, options) {
1406 if (!id.match(/^id_/)) {
1409 var select = Y.one('select#'+id);
1413 Y.use('event-delegate',function(){
1419 currentvalue : null,
1423 selectablecategories : true,
1431 init : function(Y, id, args, nodes) {
1432 if (typeof(args)=='object') {
1433 for (var i in this.cfg) {
1434 if (args[i] || args[i]===false) {
1435 this.cfg[i] = args[i];
1440 // Display a loading message first up
1441 this.nodes.select = nodes.select;
1443 this.currentvalue = this.nodes.select.get('selectedIndex');
1444 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1446 var options = Array();
1447 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1448 this.nodes.select.all('option').each(function(option, index) {
1449 var rawtext = option.get('innerHTML');
1450 var text = rawtext.replace(/^( )*/, '');
1451 if (rawtext === text) {
1452 text = rawtext.replace(/^(\s)*/, '');
1453 var depth = (rawtext.length - text.length ) + 1;
1455 var depth = ((rawtext.length - text.length )/12)+1;
1457 option.set('innerHTML', text);
1458 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1461 this.structure = [];
1462 var structcount = 0;
1463 for (var i in options) {
1466 this.structure.push(o);
1470 var current = this.structure[structcount-1];
1471 for (var j = 0; j < o.depth-1;j++) {
1472 if (current && current.children) {
1473 current = current.children[current.children.length-1];
1476 if (current && current.children) {
1477 current.children.push(o);
1482 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1483 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1484 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1485 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1487 if (this.cfg.mode == null) {
1488 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1489 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1490 this.cfg.mode = 'compact';
1492 this.cfg.mode = 'spanning';
1496 if (this.cfg.mode == 'compact') {
1497 this.nodes.menu.addClass('compactmenu');
1499 this.nodes.menu.addClass('spanningmenu');
1500 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1503 Y.one(document.body).append(this.nodes.menu);
1504 var pos = this.nodes.select.getXY();
1506 this.nodes.menu.setXY(pos);
1507 this.nodes.menu.on('click', this.handle_click, this);
1509 Y.one(window).on('resize', function(){
1510 var pos = this.nodes.select.getXY();
1512 this.nodes.menu.setXY(pos);
1515 generate_menu_content : function() {
1516 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1517 content += this.generate_submenu_content(this.structure[0], true);
1518 content += '</ul></div>';
1521 generate_submenu_content : function(item, rootelement) {
1522 this.submenucount++;
1524 if (item.children.length > 0) {
1526 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1527 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1528 content += '<div class="smartselect_menu_content">';
1530 content += '<li class="smartselect_submenuitem">';
1531 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1532 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1533 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1534 content += '<div class="smartselect_submenu_content">';
1537 for (var i in item.children) {
1538 content += this.generate_submenu_content(item.children[i],false);
1541 content += '</div>';
1542 content += '</div>';
1548 content += '<li class="smartselect_menuitem">';
1549 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1554 select : function(e) {
1557 this.currenttext = t.get('innerHTML');
1558 this.currentvalue = t.getAttribute('value');
1559 this.nodes.select.set('selectedIndex', this.currentvalue);
1562 handle_click : function(e) {
1563 var target = e.target;
1564 if (target.hasClass('smartselect_mask')) {
1566 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1568 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1569 this.show_sub_menu(e);
1572 show_menu : function(e) {
1574 var menu = e.target.ancestor().one('.smartselect_menu');
1575 menu.addClass('visible');
1576 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1578 show_sub_menu : function(e) {
1580 var target = e.target;
1581 if (!target.hasClass('smartselect_submenuitem')) {
1582 target = target.ancestor('.smartselect_submenuitem');
1584 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1585 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1588 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1589 target.one('.smartselect_submenu').addClass('visible');
1591 hide_menu : function() {
1592 this.nodes.menu.all('.visible').removeClass('visible');
1593 if (this.shownevent) {
1594 this.shownevent.detach();
1598 smartselect.init(Y, id, options, {select:select});
1602 /** List of flv players to be loaded */
1603 M.util.video_players = [];
1604 /** List of mp3 players to be loaded */
1605 M.util.audio_players = [];
1609 * @param id element id
1610 * @param fileurl media url
1613 * @param autosize true means detect size from media
1615 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1616 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1625 M.util.add_audio_player = function (id, fileurl, small) {
1626 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1630 * Initialise all audio and video player, must be called from page footer.
1632 M.util.load_flowplayer = function() {
1633 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1636 if (typeof(flowplayer) == 'undefined') {
1639 var embed_function = function() {
1640 if (loaded || typeof(flowplayer) == 'undefined') {
1648 /* TODO: add CSS color overrides for the flv flow player */
1650 for(var i=0; i<M.util.video_players.length; i++) {
1651 var video = M.util.video_players[i];
1652 if (video.width > 0 && video.height > 0) {
1653 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1655 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1657 flowplayer(video.id, src, {
1658 plugins: {controls: controls},
1660 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1661 onMetaData: function(clip) {
1662 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1663 clip.mvideo.resized = true;
1664 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1665 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1666 // bad luck, we have to guess - we may not get metadata at all
1667 var width = clip.width;
1668 var height = clip.height;
1670 var width = clip.metaData.width;
1671 var height = clip.metaData.height;
1673 var minwidth = 300; // controls are messed up in smaller objects
1674 if (width < minwidth) {
1675 height = (height * minwidth) / width;
1679 var object = this._api();
1680 object.width = width;
1681 object.height = height;
1687 if (M.util.audio_players.length == 0) {
1700 backgroundGradient: [0.5,0,0.3]
1704 for (var j=0; j < document.styleSheets.length; j++) {
1705 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1706 var allrules = document.styleSheets[j].rules;
1707 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1708 var allrules = document.styleSheets[j].cssRules;
1713 if (!allrules) continue;
1714 for(var i=0; i<allrules.length; i++) {
1716 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1717 if (typeof(allrules[i].cssText) != 'undefined') {
1718 rule = allrules[i].cssText;
1719 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1720 rule = allrules[i].style.cssText;
1722 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1723 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1724 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1725 controls[colprop] = rule;
1732 for(i=0; i<M.util.audio_players.length; i++) {
1733 var audio = M.util.audio_players[i];
1735 controls.controlall = false;
1736 controls.height = 15;
1737 controls.time = false;
1739 controls.controlall = true;
1740 controls.height = 25;
1741 controls.time = true;
1743 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1744 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1745 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1750 if (M.cfg.jsrev == -10) {
1751 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1753 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1755 var fileref = document.createElement('script');
1756 fileref.setAttribute('type','text/javascript');
1757 fileref.setAttribute('src', jsurl);
1758 fileref.onload = embed_function;
1759 fileref.onreadystatechange = embed_function;
1760 document.getElementsByTagName('head')[0].appendChild(fileref);