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";
1233 function frame_breakout(e, properties) {
1234 this.setAttribute('target', properties.framename);
1238 // ===== Deprecated core Javascript functions for Moodle ====
1239 // DO NOT USE!!!!!!!
1240 // Do not put this stuff in separate file because it only adds extra load on servers!
1243 * Used in a couple of modules to hide navigation areas when using AJAX
1245 function hide_item(itemid) {
1246 // use class='hiddenifjs' instead
1247 var item = document.getElementById(itemid);
1249 item.style.display = "none";
1253 M.util.help_icon = {
1256 add : function(Y, properties) {
1258 properties.node = Y.one('#'+properties.id);
1259 if (properties.node) {
1260 properties.node.on('click', this.display, this, properties);
1263 display : function(event, args) {
1264 event.preventDefault();
1265 if (M.util.help_icon.instance === null) {
1266 var Y = M.util.help_icon.Y;
1267 Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
1268 var help_content_overlay = {
1273 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1274 // Create an overlay from markup
1275 this.overlay = new Y.Overlay({
1276 headerContent: closebtn,
1283 this.overlay.render(Y.one(document.body));
1285 closebtn.on('click', this.overlay.hide, this.overlay);
1287 var boundingBox = this.overlay.get("boundingBox");
1289 // Hide the menu if the user clicks outside of its content
1290 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1291 var oTarget = event.target;
1292 var menuButton = Y.one("#"+args.id);
1294 if (!oTarget.compareTo(menuButton) &&
1295 !menuButton.contains(oTarget) &&
1296 !oTarget.compareTo(boundingBox) &&
1297 !boundingBox.contains(oTarget)) {
1298 this.overlay.hide();
1302 Y.on("key", this.close, closebtn , "down:13", this);
1303 closebtn.on('click', this.close, this);
1306 close : function(e) {
1308 this.helplink.focus();
1309 this.overlay.hide();
1312 display : function(event, args) {
1313 this.helplink = args.node;
1314 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1315 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1317 var fullurl = args.url;
1318 if (!args.url.match(/https?:\/\//)) {
1319 fullurl = M.cfg.wwwroot + args.url;
1322 var ajaxurl = fullurl + '&ajax=1';
1328 success: function(id, o, node) {
1329 this.display_callback(o.responseText);
1331 failure: function(id, o, node) {
1332 var debuginfo = o.statusText;
1333 if (M.cfg.developerdebug) {
1334 o.statusText += ' (' + ajaxurl + ')';
1336 this.display_callback('bodyContent',debuginfo);
1342 this.overlay.show();
1344 Y.one('#closehelpbox').focus();
1347 display_callback : function(content) {
1348 this.overlay.set('bodyContent', content);
1351 hideContent : function() {
1353 help.overlay.hide();
1356 help_content_overlay.init();
1357 M.util.help_icon.instance = help_content_overlay;
1358 M.util.help_icon.instance.display(event, args);
1361 M.util.help_icon.instance.display(event, args);
1364 init : function(Y) {
1370 * Custom menu namespace
1372 M.core_custom_menu = {
1374 * This method is used to initialise a custom menu given the id that belongs
1375 * to the custom menu's root node.
1378 * @param {string} nodeid
1380 init : function(Y, nodeid) {
1381 var node = Y.one('#'+nodeid);
1383 Y.use('node-menunav', function(Y) {
1385 // Remove the javascript-disabled class.... obviously javascript is enabled.
1386 node.removeClass('javascript-disabled');
1387 // Initialise the menunav plugin
1388 node.plug(Y.Plugin.NodeMenuNav);
1395 * Used to store form manipulation methods and enhancments
1397 M.form = M.form || {};
1400 * Converts a nbsp indented select box into a multi drop down custom control much
1401 * like the custom menu. It also selectable categories on or off.
1403 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1406 * @param {string} id
1407 * @param {Array} options
1409 M.form.init_smartselect = function(Y, id, options) {
1410 if (!id.match(/^id_/)) {
1413 var select = Y.one('select#'+id);
1417 Y.use('event-delegate',function(){
1423 currentvalue : null,
1427 selectablecategories : true,
1435 init : function(Y, id, args, nodes) {
1436 if (typeof(args)=='object') {
1437 for (var i in this.cfg) {
1438 if (args[i] || args[i]===false) {
1439 this.cfg[i] = args[i];
1444 // Display a loading message first up
1445 this.nodes.select = nodes.select;
1447 this.currentvalue = this.nodes.select.get('selectedIndex');
1448 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1450 var options = Array();
1451 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1452 this.nodes.select.all('option').each(function(option, index) {
1453 var rawtext = option.get('innerHTML');
1454 var text = rawtext.replace(/^( )*/, '');
1455 if (rawtext === text) {
1456 text = rawtext.replace(/^(\s)*/, '');
1457 var depth = (rawtext.length - text.length ) + 1;
1459 var depth = ((rawtext.length - text.length )/12)+1;
1461 option.set('innerHTML', text);
1462 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1465 this.structure = [];
1466 var structcount = 0;
1467 for (var i in options) {
1470 this.structure.push(o);
1474 var current = this.structure[structcount-1];
1475 for (var j = 0; j < o.depth-1;j++) {
1476 if (current && current.children) {
1477 current = current.children[current.children.length-1];
1480 if (current && current.children) {
1481 current.children.push(o);
1486 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1487 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1488 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1489 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1491 if (this.cfg.mode == null) {
1492 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1493 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1494 this.cfg.mode = 'compact';
1496 this.cfg.mode = 'spanning';
1500 if (this.cfg.mode == 'compact') {
1501 this.nodes.menu.addClass('compactmenu');
1503 this.nodes.menu.addClass('spanningmenu');
1504 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1507 Y.one(document.body).append(this.nodes.menu);
1508 var pos = this.nodes.select.getXY();
1510 this.nodes.menu.setXY(pos);
1511 this.nodes.menu.on('click', this.handle_click, this);
1513 Y.one(window).on('resize', function(){
1514 var pos = this.nodes.select.getXY();
1516 this.nodes.menu.setXY(pos);
1519 generate_menu_content : function() {
1520 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1521 content += this.generate_submenu_content(this.structure[0], true);
1522 content += '</ul></div>';
1525 generate_submenu_content : function(item, rootelement) {
1526 this.submenucount++;
1528 if (item.children.length > 0) {
1530 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1531 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1532 content += '<div class="smartselect_menu_content">';
1534 content += '<li class="smartselect_submenuitem">';
1535 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1536 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1537 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1538 content += '<div class="smartselect_submenu_content">';
1541 for (var i in item.children) {
1542 content += this.generate_submenu_content(item.children[i],false);
1545 content += '</div>';
1546 content += '</div>';
1552 content += '<li class="smartselect_menuitem">';
1553 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1558 select : function(e) {
1561 this.currenttext = t.get('innerHTML');
1562 this.currentvalue = t.getAttribute('value');
1563 this.nodes.select.set('selectedIndex', this.currentvalue);
1566 handle_click : function(e) {
1567 var target = e.target;
1568 if (target.hasClass('smartselect_mask')) {
1570 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1572 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1573 this.show_sub_menu(e);
1576 show_menu : function(e) {
1578 var menu = e.target.ancestor().one('.smartselect_menu');
1579 menu.addClass('visible');
1580 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1582 show_sub_menu : function(e) {
1584 var target = e.target;
1585 if (!target.hasClass('smartselect_submenuitem')) {
1586 target = target.ancestor('.smartselect_submenuitem');
1588 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1589 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1592 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1593 target.one('.smartselect_submenu').addClass('visible');
1595 hide_menu : function() {
1596 this.nodes.menu.all('.visible').removeClass('visible');
1597 if (this.shownevent) {
1598 this.shownevent.detach();
1602 smartselect.init(Y, id, options, {select:select});
1606 /** List of flv players to be loaded */
1607 M.util.video_players = [];
1608 /** List of mp3 players to be loaded */
1609 M.util.audio_players = [];
1613 * @param id element id
1614 * @param fileurl media url
1617 * @param autosize true means detect size from media
1619 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1620 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1629 M.util.add_audio_player = function (id, fileurl, small) {
1630 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1634 * Initialise all audio and video player, must be called from page footer.
1636 M.util.load_flowplayer = function() {
1637 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1640 if (typeof(flowplayer) == 'undefined') {
1643 var embed_function = function() {
1644 if (loaded || typeof(flowplayer) == 'undefined') {
1652 /* TODO: add CSS color overrides for the flv flow player */
1654 for(var i=0; i<M.util.video_players.length; i++) {
1655 var video = M.util.video_players[i];
1656 if (video.width > 0 && video.height > 0) {
1657 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1659 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1661 flowplayer(video.id, src, {
1662 plugins: {controls: controls},
1664 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1665 onMetaData: function(clip) {
1666 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1667 clip.mvideo.resized = true;
1668 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1669 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1670 // bad luck, we have to guess - we may not get metadata at all
1671 var width = clip.width;
1672 var height = clip.height;
1674 var width = clip.metaData.width;
1675 var height = clip.metaData.height;
1677 var minwidth = 300; // controls are messed up in smaller objects
1678 if (width < minwidth) {
1679 height = (height * minwidth) / width;
1683 var object = this._api();
1684 object.width = width;
1685 object.height = height;
1691 if (M.util.audio_players.length == 0) {
1704 backgroundGradient: [0.5,0,0.3]
1708 for (var j=0; j < document.styleSheets.length; j++) {
1709 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1710 var allrules = document.styleSheets[j].rules;
1711 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1712 var allrules = document.styleSheets[j].cssRules;
1717 for(var i=0; i<allrules.length; i++) {
1719 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1720 if (typeof(allrules[i].cssText) != 'undefined') {
1721 rule = allrules[i].cssText;
1722 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1723 rule = allrules[i].style.cssText;
1725 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1726 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1727 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1728 controls[colprop] = rule;
1735 for(i=0; i<M.util.audio_players.length; i++) {
1736 var audio = M.util.audio_players[i];
1738 controls.controlall = false;
1739 controls.height = 15;
1740 controls.time = false;
1742 controls.controlall = true;
1743 controls.height = 25;
1744 controls.time = true;
1746 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1747 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1748 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1753 if (M.cfg.jsrev == -10) {
1754 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1756 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1758 var fileref = document.createElement('script');
1759 fileref.setAttribute('type','text/javascript');
1760 fileref.setAttribute('src', jsurl);
1761 fileref.onload = embed_function;
1762 fileref.onreadystatechange = embed_function;
1763 document.getElementsByTagName('head')[0].appendChild(fileref);