1 // Miscellaneous core Javascript functions for Moodle
2 // Global M object is initilised in inline javascript
5 * Add module to list of available modules that can be laoded from YUI.
6 * @param {Array} modules
8 M.yui.add_module = function(modules) {
9 for (var modname in modules) {
10 M.yui.loader.modules[modname] = modules[modname];
14 * The gallery version to use when loading YUI modules from the gallery.
15 * Will be changed every time when using local galleries.
17 M.yui.galleryversion = '2010.04.21-21-51';
20 * Various utility functions
22 M.util = M.util || {};
25 * Language strings - initialised from page footer.
30 * Returns url for images.
31 * @param {String} imagename
32 * @param {String} component
35 M.util.image_url = function(imagename, component) {
36 var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename;
38 if (M.cfg.themerev > 0) {
39 url = url + '&rev=' + M.cfg.themerev;
42 if (component && component != '' && component != 'moodle' && component != 'core') {
43 url = url + '&component=' + component;
49 M.util.in_array = function(item, array){
50 for( var i = 0; i<array.length; i++){
59 * Init a collapsible region, see print_collapsible_region in weblib.php
60 * @param {YUI} Y YUI3 instance with all libraries loaded
61 * @param {String} id the HTML id for the div.
62 * @param {String} userpref the user preference that records the state of this box. false if none.
63 * @param {String} strtooltip
65 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
66 Y.use('anim', function(Y) {
67 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
72 * Object to handle a collapsible region : instantiate and forget styled object
76 * @param {YUI} Y YUI3 instance with all libraries loaded
77 * @param {String} id The HTML id for the div.
78 * @param {String} userpref The user preference that records the state of this box. false if none.
79 * @param {String} strtooltip
81 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
82 // Record the pref name
83 this.userpref = userpref;
85 // Find the divs in the document.
86 this.div = Y.one('#'+id);
88 // Get the caption for the collapsible region
89 var caption = this.div.one('#'+id + '_caption');
90 caption.setAttribute('title', strtooltip);
93 var a = Y.Node.create('<a href="#"></a>');
94 // Create a local scoped lamba function to move nodes to a new link
95 var movenode = function(node){
99 // Apply the lamba function on each of the captions child nodes
100 caption.get('children').each(movenode, this);
103 // Get the height of the div at this point before we shrink it if required
104 var height = this.div.get('offsetHeight');
105 if (this.div.hasClass('collapsed')) {
106 // Add the correct image and record the YUI node created in the process
107 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
108 // Shrink the div as it is collapsed by default
109 this.div.setStyle('height', caption.get('offsetHeight')+'px');
111 // Add the correct image and record the YUI node created in the process
112 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
116 // Create the animation.
117 var animation = new Y.Anim({
120 easing: Y.Easing.easeBoth,
121 to: {height:caption.get('offsetHeight')},
122 from: {height:height}
125 // Handler for the animation finishing.
126 animation.on('end', function() {
127 this.div.toggleClass('collapsed');
128 if (this.div.hasClass('collapsed')) {
129 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
131 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
135 // Hook up the event handler.
136 a.on('click', function(e, animation) {
138 // Animate to the appropriate size.
139 if (animation.get('running')) {
142 animation.set('reverse', this.div.hasClass('collapsed'));
143 // Update the user preference.
145 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
152 * The user preference that stores the state of this box.
156 M.util.CollapsibleRegion.prototype.userpref = null;
159 * The key divs that make up this
163 M.util.CollapsibleRegion.prototype.div = null;
166 * The key divs that make up this
170 M.util.CollapsibleRegion.prototype.icon = null;
173 * Makes a best effort to connect back to Moodle to update a user preference,
174 * however, there is no mechanism for finding out if the update succeeded.
176 * Before you can use this function in your JavsScript, you must have called
177 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
178 * the udpate is allowed, and how to safely clean and submitted values.
180 * @param String name the name of the setting to udpate.
181 * @param String the value to set it to.
183 M.util.set_user_preference = function(name, value) {
184 YUI(M.yui.loader).use('io', function(Y) {
185 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
186 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
188 // If we are a developer, ensure that failures are reported.
193 if (M.cfg.developerdebug) {
194 cfg.on.failure = function(id, o, args) {
195 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
205 * Prints a confirmation dialog in the style of DOM.confirm().
206 * @param object event A YUI DOM event or null if launched manually
207 * @param string message The message to show in the dialog
208 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
209 * @param function fn A JS function to run if YES is clicked.
211 M.util.show_confirm_dialog = function(e, args) {
212 var target = e.target;
213 if (e.preventDefault) {
217 YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
218 var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
227 simpledialog.setHeader(M.str.admin.confirmation);
228 simpledialog.setBody(args.message);
229 simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
231 var handle_cancel = function() {
235 var handle_yes = function() {
239 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
241 if (Y.Lang.isFunction(args.callback)) {
242 callback = args.callback;
244 callback = eval('('+args.callback+')');
247 if (Y.Lang.isObject(args.scope)) {
253 if (args.callbackargs) {
254 callback.apply(sc, args.callbackargs);
261 var targetancestor = null,
264 if (target.test('a')) {
265 window.location = target.get('href');
266 } else if ((targetancestor = target.ancestor('a')) !== null) {
267 window.location = targetancestor.get('href');
268 } else if (target.test('input')) {
269 targetform = target.ancestor('form');
270 if (targetform && targetform.submit) {
273 } else if (M.cfg.developerdebug) {
274 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT");
278 if (!args.cancellabel) {
279 args.cancellabel = M.str.moodle.cancel;
281 if (!args.continuelabel) {
282 args.continuelabel = M.str.moodle.yes;
286 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
287 {text: args.continuelabel, handler: handle_yes}
290 simpledialog.cfg.queueProperty('buttons', buttons);
292 simpledialog.render(document.body);
297 /** Useful for full embedding of various stuff */
298 M.util.init_maximised_embed = function(Y, id) {
299 var obj = Y.one('#'+id);
304 var get_htmlelement_size = function(el, prop) {
305 if (Y.Lang.isString(el)) {
306 el = Y.one('#' + el);
308 var val = el.getStyle(prop);
310 val = el.getComputedStyle(prop);
312 return parseInt(val);
315 var resize_object = function() {
316 obj.setStyle('width', '0px');
317 obj.setStyle('height', '0px');
318 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
320 if (newwidth > 500) {
321 obj.setStyle('width', newwidth + 'px');
323 obj.setStyle('width', '500px');
326 var headerheight = get_htmlelement_size('page-header', 'height');
327 var footerheight = get_htmlelement_size('page-footer', 'height');
328 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
329 if (newheight < 400) {
332 obj.setStyle('height', newheight+'px');
336 // fix layout if window resized too
337 window.onresize = function() {
343 * Attach handler to single_select
345 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
346 Y.use('event-key', function() {
347 var select = Y.one('#'+selectid);
349 // Try to get the form by id
350 var form = Y.one('#'+formid) || (function(){
351 // Hmmm the form's id may have been overriden by an internal input
352 // with the name id which will KILL IE.
353 // We need to manually iterate at this point because if the case
354 // above is true YUI's ancestor method will also kill IE!
356 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
357 form = form.ancestor();
361 // Make sure we have the form
363 // Create a function to handle our change event
364 var processchange = function(e, lastindex) {
365 if ((nothing===false || select.get('value') != nothing) && lastindex != select.get('selectedIndex')) {
369 // Attach the change event to the keypress, blur, and click actions.
370 // We don't use the change event because IE fires it on every arrow up/down
371 // event.... usability
372 Y.on('key', processchange, select, 'press:13', form, select.get('selectedIndex'));
373 select.on('blur', processchange, form, select.get('selectedIndex'));
374 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
376 select.on('change', processchange, form, select.get('selectedIndex'));
378 select.on('click', processchange, form, select.get('selectedIndex'));
386 * Attach handler to url_select
388 M.util.init_url_select = function(Y, formid, selectid, nothing) {
389 YUI(M.yui.loader).use('node', function(Y) {
390 Y.on('change', function() {
391 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
392 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
400 * Breaks out all links to the top frame - used in frametop page layout.
402 M.util.init_frametop = function(Y) {
403 Y.all('a').each(function(node) {
404 node.set('target', '_top');
406 Y.all('form').each(function(node) {
407 node.set('target', '_top');
412 * Finds all nodes that match the given CSS selector and attaches events to them
413 * so that they toggle a given classname when clicked.
416 * @param {string} id An id containing elements to target
417 * @param {string} cssselector A selector to use to find targets
418 * @param {string} toggleclassname A classname to toggle
420 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
422 if (togglecssselector == '') {
423 togglecssselector = cssselector;
426 var node = Y.one('#'+id);
427 node.all(cssselector).each(function(n){
428 n.on('click', function(e){
430 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
431 if (this.test(togglecssselector)) {
432 this.toggleClass(toggleclassname);
434 this.ancestor(togglecssselector).toggleClass(toggleclassname);
439 // Attach this click event to the node rather than all selectors... will be much better
441 node.on('click', function(e){
442 if (e.target.hasClass('addtoall')) {
443 this.all(togglecssselector).addClass(toggleclassname);
444 } else if (e.target.hasClass('removefromall')) {
445 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
451 * Initialises a colour picker
453 * Designed to be used with admin_setting_configcolourpicker although could be used
454 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
455 * above or below the input (must have the same parent) and then call this with the
458 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
459 * contrib/blocks. For better docs refer to that.
463 * @param {object} previewconf
465 M.util.init_colour_picker = function(Y, id, previewconf) {
467 * We need node and event-mouseenter
469 Y.use('node', 'event-mouseenter', function(){
471 * The colour picker object
480 eventMouseEnter : null,
481 eventMouseLeave : null,
482 eventMouseMove : null,
487 * Initalises the colour picker by putting everything together and wiring the events
490 this.input = Y.one('#'+id);
491 this.box = this.input.ancestor().one('.admin_colourpicker');
492 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
493 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
494 this.preview = Y.Node.create('<div class="previewcolour"></div>');
495 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
496 this.current = Y.Node.create('<div class="currentcolour"></div>');
497 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
498 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
500 if (typeof(previewconf) === 'object' && previewconf !== null) {
501 Y.one('#'+id+'_preview').on('click', function(e){
502 if (Y.Lang.isString(previewconf.selector)) {
503 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
505 for (var i in previewconf.selector) {
506 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
512 this.eventClick = this.image.on('click', this.pickColour, this);
513 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
516 * Starts to follow the mouse once it enter the image
518 startFollow : function(e) {
519 this.eventMouseEnter.detach();
520 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
521 this.eventMouseMove = this.image.on('mousemove', function(e){
522 this.preview.setStyle('backgroundColor', this.determineColour(e));
526 * Stops following the mouse
528 endFollow : function(e) {
529 this.eventMouseMove.detach();
530 this.eventMouseLeave.detach();
531 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
534 * Picks the colour the was clicked on
536 pickColour : function(e) {
537 var colour = this.determineColour(e);
538 this.input.set('value', colour);
539 this.current.setStyle('backgroundColor', colour);
542 * Calculates the colour fromthe given co-ordinates
544 determineColour : function(e) {
545 var eventx = Math.floor(e.pageX-e.target.getX());
546 var eventy = Math.floor(e.pageY-e.target.getY());
548 var imagewidth = this.width;
549 var imageheight = this.height;
550 var factor = this.factor;
551 var colour = [255,0,0];
562 var matrixcount = matrices.length;
563 var limit = Math.round(imagewidth/matrixcount);
564 var heightbreak = Math.round(imageheight/2);
566 for (var x = 0; x < imagewidth; x++) {
567 var divisor = Math.floor(x / limit);
568 var matrix = matrices[divisor];
570 colour[0] += matrix[0]*factor;
571 colour[1] += matrix[1]*factor;
572 colour[2] += matrix[2]*factor;
579 var pixel = [colour[0], colour[1], colour[2]];
580 if (eventy < heightbreak) {
581 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
582 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
583 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
584 } else if (eventy > heightbreak) {
585 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
586 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
587 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
590 return this.convert_rgb_to_hex(pixel);
593 * Converts an RGB value to Hex
595 convert_rgb_to_hex : function(rgb) {
597 var hexchars = "0123456789ABCDEF";
598 for (var i=0; i<3; i++) {
599 var number = Math.abs(rgb[i]);
600 if (number == 0 || isNaN(number)) {
603 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
610 * Initialise the colour picker :) Hoorah
616 M.util.init_block_hider = function(Y, config) {
617 Y.use('base', 'node', function(Y) {
618 M.util.block_hider = M.util.block_hider || (function(){
619 var blockhider = function() {
620 blockhider.superclass.constructor.apply(this, arguments);
622 blockhider.prototype = {
623 initializer : function(config) {
624 this.set('block', '#'+this.get('id'));
625 var b = this.get('block'),
628 if (t && (a = t.one('.block_action'))) {
629 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
630 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
631 hide.on('keypress', this.updateStateKey, this, true);
632 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
633 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
634 show.on('keypress', this.updateStateKey, this, false);
635 a.insert(show, 0).insert(hide, 0);
638 updateState : function(e, hide) {
639 M.util.set_user_preference(this.get('preference'), hide);
641 this.get('block').addClass('hidden');
643 this.get('block').removeClass('hidden');
646 updateStateKey : function(e, hide) {
647 if (e.keyCode == 13) { //allow hide/show via enter key
648 this.updateState(this, hide);
652 Y.extend(blockhider, Y.Base, blockhider.prototype, {
658 value : M.util.image_url('t/switch_minus', 'moodle')
661 value : M.util.image_url('t/switch_plus', 'moodle')
664 setter : function(node) {
672 new M.util.block_hider(config);
677 * Returns a string registered in advance for usage in JavaScript
679 * If you do not pass the third parameter, the function will just return
680 * the corresponding value from the M.str object. If the third parameter is
681 * provided, the function performs {$a} placeholder substitution in the
682 * same way as PHP get_string() in Moodle does.
684 * @param {String} identifier string identifier
685 * @param {String} component the component providing the string
686 * @param {Object|String} a optional variable to populate placeholder with
688 M.util.get_string = function(identifier, component, a) {
691 if (M.cfg.developerdebug) {
692 // creating new instance if YUI is not optimal but it seems to be better way then
693 // require the instance via the function API - note that it is used in rare cases
694 // for debugging only anyway
695 var Y = new YUI({ debug : true });
698 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
699 stringvalue = '[[' + identifier + ',' + component + ']]';
700 if (M.cfg.developerdebug) {
701 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
706 stringvalue = M.str[component][identifier];
708 if (typeof a == 'undefined') {
709 // no placeholder substitution requested
713 if (typeof a == 'number' || typeof a == 'string') {
714 // replace all occurrences of {$a} with the placeholder value
715 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
719 if (typeof a == 'object') {
720 // replace {$a->key} placeholders
722 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
723 if (M.cfg.developerdebug) {
724 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
728 var search = '{$a->' + key + '}';
729 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
730 search = new RegExp(search, 'g');
731 stringvalue = stringvalue.replace(search, a[key]);
736 if (M.cfg.developerdebug) {
737 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
743 * Set focus on username or password field of the login form
745 M.util.focus_login_form = function(Y) {
746 var username = Y.one('#username');
747 var password = Y.one('#password');
749 if (username == null || password == null) {
750 // something is wrong here
754 var curElement = document.activeElement
755 if (curElement == 'undefined') {
756 // legacy browser - skip refocus protection
757 } else if (curElement.tagName == 'INPUT') {
758 // user was probably faster to focus something, do not mess with focus
762 if (username.get('value') == '') {
770 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
772 function checkall() {
773 var inputs = document.getElementsByTagName('input');
774 for (var i = 0; i < inputs.length; i++) {
775 if (inputs[i].type == 'checkbox') {
776 inputs[i].checked = true;
781 function checknone() {
782 var inputs = document.getElementsByTagName('input');
783 for (var i = 0; i < inputs.length; i++) {
784 if (inputs[i].type == 'checkbox') {
785 inputs[i].checked = false;
791 * Either check, or uncheck, all checkboxes inside the element with id is
792 * @param id the id of the container
793 * @param checked the new state, either '' or 'checked'.
795 function select_all_in_element_with_id(id, checked) {
796 var container = document.getElementById(id);
800 var inputs = container.getElementsByTagName('input');
801 for (var i = 0; i < inputs.length; ++i) {
802 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
803 inputs[i].checked = checked;
808 function select_all_in(elTagName, elClass, elId) {
809 var inputs = document.getElementsByTagName('input');
810 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
811 for(var i = 0; i < inputs.length; ++i) {
812 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
813 inputs[i].checked = 'checked';
818 function deselect_all_in(elTagName, elClass, elId) {
819 var inputs = document.getElementsByTagName('INPUT');
820 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
821 for(var i = 0; i < inputs.length; ++i) {
822 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
823 inputs[i].checked = '';
828 function confirm_if(expr, message) {
832 return confirm(message);
837 findParentNode (start, elementName, elementClass, elementID)
839 Travels up the DOM hierarchy to find a parent element with the
840 specified tag name, class, and id. All conditions must be met,
841 but any can be ommitted. Returns the BODY element if no match
844 function findParentNode(el, elName, elClass, elId) {
845 while (el.nodeName.toUpperCase() != 'BODY') {
846 if ((!elName || el.nodeName.toUpperCase() == elName) &&
847 (!elClass || el.className.indexOf(elClass) != -1) &&
848 (!elId || el.id == elId)) {
856 findChildNode (start, elementName, elementClass, elementID)
858 Travels down the DOM hierarchy to find all child elements with the
859 specified tag name, class, and id. All conditions must be met,
860 but any can be ommitted.
861 Doesn't examine children of matches.
863 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
864 var children = new Array();
865 for (var i = 0; i < start.childNodes.length; i++) {
866 var classfound = false;
867 var child = start.childNodes[i];
868 if((child.nodeType == 1) &&//element node type
869 (elementClass && (typeof(child.className)=='string'))) {
870 var childClasses = child.className.split(/\s+/);
871 for (var childClassIndex in childClasses) {
872 if (childClasses[childClassIndex]==elementClass) {
878 if(child.nodeType == 1) { //element node type
879 if ( (!tagName || child.nodeName == tagName) &&
880 (!elementClass || classfound)&&
881 (!elementID || child.id == elementID) &&
882 (!elementName || child.name == elementName))
884 children = children.concat(child);
886 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
893 function unmaskPassword(id) {
894 var pw = document.getElementById(id);
895 var chb = document.getElementById(id+'unmask');
898 // first try IE way - it can not set name attribute later
900 var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
902 var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
904 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
906 var newpw = document.createElement('input');
907 newpw.setAttribute('name', pw.name);
909 newpw.setAttribute('type', 'text');
911 newpw.setAttribute('type', 'password');
913 newpw.setAttribute('class', pw.getAttribute('class'));
916 newpw.size = pw.size;
917 newpw.onblur = pw.onblur;
918 newpw.onchange = pw.onchange;
919 newpw.value = pw.value;
920 pw.parentNode.replaceChild(newpw, pw);
923 function filterByParent(elCollection, parentFinder) {
924 var filteredCollection = [];
925 for (var i = 0; i < elCollection.length; ++i) {
926 var findParent = parentFinder(elCollection[i]);
927 if (findParent.nodeName.toUpperCase != 'BODY') {
928 filteredCollection.push(elCollection[i]);
931 return filteredCollection;
935 All this is here just so that IE gets to handle oversized blocks
936 in a visually pleasing manner. It does a browser detect. So sue me.
939 function fix_column_widths() {
940 var agt = navigator.userAgent.toLowerCase();
941 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
942 fix_column_width('left-column');
943 fix_column_width('right-column');
947 function fix_column_width(colName) {
948 if(column = document.getElementById(colName)) {
949 if(!column.offsetWidth) {
950 setTimeout("fix_column_width('" + colName + "')", 20);
955 var nodes = column.childNodes;
957 for(i = 0; i < nodes.length; ++i) {
958 if(nodes[i].className.indexOf("block") != -1 ) {
959 if(width < nodes[i].offsetWidth) {
960 width = nodes[i].offsetWidth;
965 for(i = 0; i < nodes.length; ++i) {
966 if(nodes[i].className.indexOf("block") != -1 ) {
967 nodes[i].style.width = width + 'px';
975 Insert myValue at current cursor position
977 function insertAtCursor(myField, myValue) {
979 if (document.selection) {
981 sel = document.selection.createRange();
984 // Mozilla/Netscape support
985 else if (myField.selectionStart || myField.selectionStart == '0') {
986 var startPos = myField.selectionStart;
987 var endPos = myField.selectionEnd;
988 myField.value = myField.value.substring(0, startPos)
989 + myValue + myField.value.substring(endPos, myField.value.length);
991 myField.value += myValue;
997 Call instead of setting window.onload directly or setting body onload=.
998 Adds your function to a chain of functions rather than overwriting anything
1001 function addonload(fn) {
1002 var oldhandler=window.onload;
1003 window.onload=function() {
1004 if(oldhandler) oldhandler();
1009 * Replacement for getElementsByClassName in browsers that aren't cool enough
1011 * Relying on the built-in getElementsByClassName is far, far faster than
1014 * Note: the third argument used to be an object with odd behaviour. It now
1015 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1016 * mimicked if you pass an object.
1018 * @param {Node} oElm The top-level node for searching. To search a whole
1019 * document, use `document`.
1020 * @param {String} strTagName filter by tag names
1021 * @param {String} name same as HTML5 spec
1023 function getElementsByClassName(oElm, strTagName, name) {
1024 // for backwards compatibility
1025 if(typeof name == "object") {
1026 var names = new Array();
1027 for(var i=0; i<name.length; i++) names.push(names[i]);
1028 name = names.join('');
1030 // use native implementation if possible
1031 if (oElm.getElementsByClassName && Array.filter) {
1032 if (strTagName == '*') {
1033 return oElm.getElementsByClassName(name);
1035 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1036 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1040 // native implementation unavailable, fall back to slow method
1041 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1042 var arrReturnElements = new Array();
1043 var arrRegExpClassNames = new Array();
1044 var names = name.split(' ');
1045 for(var i=0; i<names.length; i++) {
1046 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1050 for(var j=0; j<arrElements.length; j++) {
1051 oElement = arrElements[j];
1053 for(var k=0; k<arrRegExpClassNames.length; k++) {
1054 if(!arrRegExpClassNames[k].test(oElement.className)) {
1055 bMatchesAll = false;
1060 arrReturnElements.push(oElement);
1063 return (arrReturnElements)
1066 function openpopup(event, args) {
1069 if (event.preventDefault) {
1070 event.preventDefault();
1072 event.returnValue = false;
1076 var fullurl = args.url;
1077 if (!args.url.match(/https?:\/\//)) {
1078 fullurl = M.cfg.wwwroot + args.url;
1080 var windowobj = window.open(fullurl,args.name,args.options);
1084 if (args.fullscreen) {
1085 windowobj.moveTo(0,0);
1086 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1093 /** Close the current browser window. */
1094 function close_window(e) {
1095 if (e.preventDefault) {
1098 e.returnValue = false;
1104 * Used in a couple of modules to hide navigation areas when using AJAX
1107 function show_item(itemid) {
1108 var item = document.getElementById(itemid);
1110 item.style.display = "";
1114 function destroy_item(itemid) {
1115 var item = document.getElementById(itemid);
1117 item.parentNode.removeChild(item);
1121 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1122 * @param controlid the control id.
1124 function focuscontrol(controlid) {
1125 var control = document.getElementById(controlid);
1132 * Transfers keyboard focus to an HTML element based on the old style style of focus
1133 * This function should be removed as soon as it is no longer used
1135 function old_onload_focus(formid, controlname) {
1136 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1137 document.forms[formid].elements[controlname].focus();
1141 function build_querystring(obj) {
1142 return convert_object_to_string(obj, '&');
1145 function build_windowoptionsstring(obj) {
1146 return convert_object_to_string(obj, ',');
1149 function convert_object_to_string(obj, separator) {
1150 if (typeof obj !== 'object') {
1155 k = encodeURIComponent(k);
1157 if(obj[k] instanceof Array) {
1158 for(var i in value) {
1159 list.push(k+'[]='+encodeURIComponent(value[i]));
1162 list.push(k+'='+encodeURIComponent(value));
1165 return list.join(separator);
1168 function stripHTML(str) {
1169 var re = /<\S[^><]*>/g;
1170 var ret = str.replace(re, "");
1174 Number.prototype.fixed=function(n){
1176 return round(Number(this)*pow(10,n))/pow(10,n);
1178 function update_progress_bar (id, width, pt, msg, es){
1180 var status = document.getElementById("status_"+id);
1181 var percent_indicator = document.getElementById("pt_"+id);
1182 var progress_bar = document.getElementById("progress_"+id);
1183 var time_es = document.getElementById("time_"+id);
1184 status.innerHTML = msg;
1185 percent_indicator.innerHTML = percent.fixed(2) + '%';
1186 if(percent == 100) {
1187 progress_bar.style.background = "green";
1188 time_es.style.display = "none";
1190 progress_bar.style.background = "#FFCC66";
1192 time_es.innerHTML = "";
1194 time_es.innerHTML = es.fixed(2)+" sec";
1195 time_es.style.display
1199 progress_bar.style.width = width + "px";
1203 function frame_breakout(e, properties) {
1204 this.setAttribute('target', properties.framename);
1208 // ===== Deprecated core Javascript functions for Moodle ====
1209 // DO NOT USE!!!!!!!
1210 // Do not put this stuff in separate file because it only adds extra load on servers!
1213 * Used in a couple of modules to hide navigation areas when using AJAX
1215 function hide_item(itemid) {
1216 // use class='hiddenifjs' instead
1217 var item = document.getElementById(itemid);
1219 item.style.display = "none";
1223 M.util.help_icon = {
1226 add : function(Y, properties) {
1228 properties.node = Y.one('#'+properties.id);
1229 if (properties.node) {
1230 properties.node.on('click', this.display, this, properties);
1233 display : function(event, args) {
1234 event.preventDefault();
1235 if (M.util.help_icon.instance === null) {
1236 var Y = M.util.help_icon.Y;
1237 Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
1238 var help_content_overlay = {
1243 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1244 // Create an overlay from markup
1245 this.overlay = new Y.Overlay({
1246 headerContent: closebtn,
1253 this.overlay.render(Y.one(document.body));
1255 closebtn.on('click', this.overlay.hide, this.overlay);
1257 var boundingBox = this.overlay.get("boundingBox");
1259 // Hide the menu if the user clicks outside of its content
1260 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1261 var oTarget = event.target;
1262 var menuButton = Y.one("#"+args.id);
1264 if (!oTarget.compareTo(menuButton) &&
1265 !menuButton.contains(oTarget) &&
1266 !oTarget.compareTo(boundingBox) &&
1267 !boundingBox.contains(oTarget)) {
1268 this.overlay.hide();
1272 Y.on("key", this.close, closebtn , "down:13", this);
1273 closebtn.on('click', this.close, this);
1276 close : function(e) {
1278 this.helplink.focus();
1279 this.overlay.hide();
1282 display : function(event, args) {
1283 this.helplink = args.node;
1284 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1285 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1287 var fullurl = args.url;
1288 if (!args.url.match(/https?:\/\//)) {
1289 fullurl = M.cfg.wwwroot + args.url;
1292 var ajaxurl = fullurl + '&ajax=1';
1298 success: function(id, o, node) {
1299 this.display_callback(o.responseText);
1301 failure: function(id, o, node) {
1302 var debuginfo = o.statusText;
1303 if (M.cfg.developerdebug) {
1304 o.statusText += ' (' + ajaxurl + ')';
1306 this.display_callback('bodyContent',debuginfo);
1312 this.overlay.show();
1314 Y.one('#closehelpbox').focus();
1317 display_callback : function(content) {
1318 this.overlay.set('bodyContent', content);
1321 hideContent : function() {
1323 help.overlay.hide();
1326 help_content_overlay.init();
1327 M.util.help_icon.instance = help_content_overlay;
1328 M.util.help_icon.instance.display(event, args);
1331 M.util.help_icon.instance.display(event, args);
1334 init : function(Y) {
1340 * Custom menu namespace
1342 M.core_custom_menu = {
1344 * This method is used to initialise a custom menu given the id that belongs
1345 * to the custom menu's root node.
1348 * @param {string} nodeid
1350 init : function(Y, nodeid) {
1351 var node = Y.one('#'+nodeid);
1353 Y.use('node-menunav', function(Y) {
1355 // Remove the javascript-disabled class.... obviously javascript is enabled.
1356 node.removeClass('javascript-disabled');
1357 // Initialise the menunav plugin
1358 node.plug(Y.Plugin.NodeMenuNav);
1365 * Used to store form manipulation methods and enhancments
1367 M.form = M.form || {};
1370 * Converts a nbsp indented select box into a multi drop down custom control much
1371 * like the custom menu. It also selectable categories on or off.
1373 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1376 * @param {string} id
1377 * @param {Array} options
1379 M.form.init_smartselect = function(Y, id, options) {
1380 if (!id.match(/^id_/)) {
1383 var select = Y.one('select#'+id);
1387 Y.use('event-delegate',function(){
1393 currentvalue : null,
1397 selectablecategories : true,
1405 init : function(Y, id, args, nodes) {
1406 if (typeof(args)=='object') {
1407 for (var i in this.cfg) {
1408 if (args[i] || args[i]===false) {
1409 this.cfg[i] = args[i];
1414 // Display a loading message first up
1415 this.nodes.select = nodes.select;
1417 this.currentvalue = this.nodes.select.get('selectedIndex');
1418 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1420 var options = Array();
1421 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1422 this.nodes.select.all('option').each(function(option, index) {
1423 var rawtext = option.get('innerHTML');
1424 var text = rawtext.replace(/^( )*/, '');
1425 if (rawtext === text) {
1426 text = rawtext.replace(/^(\s)*/, '');
1427 var depth = (rawtext.length - text.length ) + 1;
1429 var depth = ((rawtext.length - text.length )/12)+1;
1431 option.set('innerHTML', text);
1432 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1435 this.structure = [];
1436 var structcount = 0;
1437 for (var i in options) {
1440 this.structure.push(o);
1444 var current = this.structure[structcount-1];
1445 for (var j = 0; j < o.depth-1;j++) {
1446 if (current && current.children) {
1447 current = current.children[current.children.length-1];
1450 if (current && current.children) {
1451 current.children.push(o);
1456 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1457 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1458 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1459 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1461 if (this.cfg.mode == null) {
1462 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1463 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1464 this.cfg.mode = 'compact';
1466 this.cfg.mode = 'spanning';
1470 if (this.cfg.mode == 'compact') {
1471 this.nodes.menu.addClass('compactmenu');
1473 this.nodes.menu.addClass('spanningmenu');
1474 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1477 Y.one(document.body).append(this.nodes.menu);
1478 var pos = this.nodes.select.getXY();
1480 this.nodes.menu.setXY(pos);
1481 this.nodes.menu.on('click', this.handle_click, this);
1483 Y.one(window).on('resize', function(){
1484 var pos = this.nodes.select.getXY();
1486 this.nodes.menu.setXY(pos);
1489 generate_menu_content : function() {
1490 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1491 content += this.generate_submenu_content(this.structure[0], true);
1492 content += '</ul></div>';
1495 generate_submenu_content : function(item, rootelement) {
1496 this.submenucount++;
1498 if (item.children.length > 0) {
1500 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1501 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1502 content += '<div class="smartselect_menu_content">';
1504 content += '<li class="smartselect_submenuitem">';
1505 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1506 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1507 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1508 content += '<div class="smartselect_submenu_content">';
1511 for (var i in item.children) {
1512 content += this.generate_submenu_content(item.children[i],false);
1515 content += '</div>';
1516 content += '</div>';
1522 content += '<li class="smartselect_menuitem">';
1523 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1528 select : function(e) {
1531 this.currenttext = t.get('innerHTML');
1532 this.currentvalue = t.getAttribute('value');
1533 this.nodes.select.set('selectedIndex', this.currentvalue);
1536 handle_click : function(e) {
1537 var target = e.target;
1538 if (target.hasClass('smartselect_mask')) {
1540 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1542 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1543 this.show_sub_menu(e);
1546 show_menu : function(e) {
1548 var menu = e.target.ancestor().one('.smartselect_menu');
1549 menu.addClass('visible');
1550 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1552 show_sub_menu : function(e) {
1554 var target = e.target;
1555 if (!target.hasClass('smartselect_submenuitem')) {
1556 target = target.ancestor('.smartselect_submenuitem');
1558 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1559 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1562 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1563 target.one('.smartselect_submenu').addClass('visible');
1565 hide_menu : function() {
1566 this.nodes.menu.all('.visible').removeClass('visible');
1567 if (this.shownevent) {
1568 this.shownevent.detach();
1572 smartselect.init(Y, id, options, {select:select});
1576 /** List of flv players to be loaded */
1577 M.util.video_players = [];
1578 /** List of mp3 players to be loaded */
1579 M.util.audio_players = [];
1583 * @param id element id
1584 * @param fileurl media url
1587 * @param autosize true means detect size from media
1589 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1590 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1599 M.util.add_audio_player = function (id, fileurl, small) {
1600 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1604 * Initialise all audio and video player, must be called from page footer.
1606 M.util.load_flowplayer = function() {
1607 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1610 if (typeof(flowplayer) == 'undefined') {
1613 var embed_function = function() {
1614 if (loaded || typeof(flowplayer) == 'undefined') {
1622 /* TODO: add CSS color overrides for the flv flow player */
1624 for(var i=0; i<M.util.video_players.length; i++) {
1625 var video = M.util.video_players[i];
1626 if (video.width > 0 && video.height > 0) {
1627 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1629 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1631 flowplayer(video.id, src, {
1632 plugins: {controls: controls},
1634 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1635 onMetaData: function(clip) {
1636 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1637 clip.mvideo.resized = true;
1638 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1639 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1640 // bad luck, we have to guess - we may not get metadata at all
1641 var width = clip.width;
1642 var height = clip.height;
1644 var width = clip.metaData.width;
1645 var height = clip.metaData.height;
1647 var minwidth = 300; // controls are messed up in smaller objects
1648 if (width < minwidth) {
1649 height = (height * minwidth) / width;
1653 var object = this._api();
1654 object.width = width;
1655 object.height = height;
1661 if (M.util.audio_players.length == 0) {
1674 backgroundGradient: [0.5,0,0.3]
1678 for (var j=0; j < document.styleSheets.length; j++) {
1679 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1680 var allrules = document.styleSheets[j].rules;
1681 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1682 var allrules = document.styleSheets[j].cssRules;
1687 for(var i=0; i<allrules.length; i++) {
1689 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1690 if (typeof(allrules[i].cssText) != 'undefined') {
1691 rule = allrules[i].style.cssText;
1692 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1693 rule = allrules[i].style.cssText;
1695 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1696 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1697 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1698 controls[colprop] = rule;
1705 for(i=0; i<M.util.audio_players.length; i++) {
1706 var audio = M.util.audio_players[i];
1708 controls.controlall = false;
1709 controls.height = 15;
1710 controls.time = false;
1712 controls.controlall = true;
1713 controls.height = 25;
1714 controls.time = true;
1716 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1717 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1718 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1723 if (M.cfg.jsrev == -10) {
1724 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1726 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1728 var fileref = document.createElement('script');
1729 fileref.setAttribute('type','text/javascript');
1730 fileref.setAttribute('src', jsurl);
1731 fileref.onload = embed_function;
1732 fileref.onreadystatechange = embed_function;
1733 document.getElementsByTagName('head')[0].appendChild(fileref);