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) {
37 if (!component || component == '' || component == 'moodle' || component == 'core') {
41 if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
42 var url = M.cfg.wwwroot + '/theme/image.php/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
44 var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
50 M.util.in_array = function(item, array){
51 for( var i = 0; i<array.length; i++){
60 * Init a collapsible region, see print_collapsible_region in weblib.php
61 * @param {YUI} Y YUI3 instance with all libraries loaded
62 * @param {String} id the HTML id for the div.
63 * @param {String} userpref the user preference that records the state of this box. false if none.
64 * @param {String} strtooltip
66 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
67 Y.use('anim', function(Y) {
68 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
73 * Object to handle a collapsible region : instantiate and forget styled object
77 * @param {YUI} Y YUI3 instance with all libraries loaded
78 * @param {String} id The HTML id for the div.
79 * @param {String} userpref The user preference that records the state of this box. false if none.
80 * @param {String} strtooltip
82 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
83 // Record the pref name
84 this.userpref = userpref;
86 // Find the divs in the document.
87 this.div = Y.one('#'+id);
89 // Get the caption for the collapsible region
90 var caption = this.div.one('#'+id + '_caption');
91 caption.setAttribute('title', strtooltip);
94 var a = Y.Node.create('<a href="#"></a>');
95 // Create a local scoped lamba function to move nodes to a new link
96 var movenode = function(node){
100 // Apply the lamba function on each of the captions child nodes
101 caption.get('children').each(movenode, this);
104 // Get the height of the div at this point before we shrink it if required
105 var height = this.div.get('offsetHeight');
106 if (this.div.hasClass('collapsed')) {
107 // Add the correct image and record the YUI node created in the process
108 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
109 // Shrink the div as it is collapsed by default
110 this.div.setStyle('height', caption.get('offsetHeight')+'px');
112 // Add the correct image and record the YUI node created in the process
113 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
117 // Create the animation.
118 var animation = new Y.Anim({
121 easing: Y.Easing.easeBoth,
122 to: {height:caption.get('offsetHeight')},
123 from: {height:height}
126 // Handler for the animation finishing.
127 animation.on('end', function() {
128 this.div.toggleClass('collapsed');
129 if (this.div.hasClass('collapsed')) {
130 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
132 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
136 // Hook up the event handler.
137 a.on('click', function(e, animation) {
139 // Animate to the appropriate size.
140 if (animation.get('running')) {
143 animation.set('reverse', this.div.hasClass('collapsed'));
144 // Update the user preference.
146 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
153 * The user preference that stores the state of this box.
157 M.util.CollapsibleRegion.prototype.userpref = null;
160 * The key divs that make up this
164 M.util.CollapsibleRegion.prototype.div = null;
167 * The key divs that make up this
171 M.util.CollapsibleRegion.prototype.icon = null;
174 * Makes a best effort to connect back to Moodle to update a user preference,
175 * however, there is no mechanism for finding out if the update succeeded.
177 * Before you can use this function in your JavsScript, you must have called
178 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
179 * the udpate is allowed, and how to safely clean and submitted values.
181 * @param String name the name of the setting to udpate.
182 * @param String the value to set it to.
184 M.util.set_user_preference = function(name, value) {
185 YUI(M.yui.loader).use('io', function(Y) {
186 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
187 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
189 // If we are a developer, ensure that failures are reported.
194 if (M.cfg.developerdebug) {
195 cfg.on.failure = function(id, o, args) {
196 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
206 * Prints a confirmation dialog in the style of DOM.confirm().
207 * @param object event A YUI DOM event or null if launched manually
208 * @param string message The message to show in the dialog
209 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
210 * @param function fn A JS function to run if YES is clicked.
212 M.util.show_confirm_dialog = function(e, args) {
213 var target = e.target;
214 if (e.preventDefault) {
218 YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
219 var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
228 simpledialog.setHeader(M.str.admin.confirmation);
229 simpledialog.setBody(args.message);
230 simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
232 var handle_cancel = function() {
236 var handle_yes = function() {
240 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
242 if (Y.Lang.isFunction(args.callback)) {
243 callback = args.callback;
245 callback = eval('('+args.callback+')');
248 if (Y.Lang.isObject(args.scope)) {
254 if (args.callbackargs) {
255 callback.apply(sc, args.callbackargs);
262 var targetancestor = null,
265 if (target.test('a')) {
266 window.location = target.get('href');
268 } else if ((targetancestor = target.ancestor('a')) !== null) {
269 window.location = targetancestor.get('href');
271 } else if (target.test('input')) {
272 targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
273 // We cannot use target.ancestor('form') on the previous line
274 // because of http://yuilibrary.com/projects/yui3/ticket/2531561
278 if (target.get('name') && target.get('value')) {
279 targetform.append('<input type="hidden" name="' + target.get('name') +
280 '" value="' + target.get('value') + '">');
284 } else if (target.get('tagName').toLowerCase() == 'form') {
285 // We cannot use target.test('form') on the previous line because of
286 // http://yuilibrary.com/projects/yui3/ticket/2531561
289 } else if (M.cfg.developerdebug) {
290 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
294 if (!args.cancellabel) {
295 args.cancellabel = M.str.moodle.cancel;
297 if (!args.continuelabel) {
298 args.continuelabel = M.str.moodle.yes;
302 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
303 {text: args.continuelabel, handler: handle_yes}
306 simpledialog.cfg.queueProperty('buttons', buttons);
308 simpledialog.render(document.body);
313 /** Useful for full embedding of various stuff */
314 M.util.init_maximised_embed = function(Y, id) {
315 var obj = Y.one('#'+id);
320 var get_htmlelement_size = function(el, prop) {
321 if (Y.Lang.isString(el)) {
322 el = Y.one('#' + el);
324 var val = el.getStyle(prop);
326 val = el.getComputedStyle(prop);
328 return parseInt(val);
331 var resize_object = function() {
332 obj.setStyle('width', '0px');
333 obj.setStyle('height', '0px');
334 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
336 if (newwidth > 500) {
337 obj.setStyle('width', newwidth + 'px');
339 obj.setStyle('width', '500px');
342 var headerheight = get_htmlelement_size('page-header', 'height');
343 var footerheight = get_htmlelement_size('page-footer', 'height');
344 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
345 if (newheight < 400) {
348 obj.setStyle('height', newheight+'px');
352 // fix layout if window resized too
353 window.onresize = function() {
359 * Attach handler to single_select
361 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
362 Y.use('event-key', function() {
363 var select = Y.one('#'+selectid);
365 // Try to get the form by id
366 var form = Y.one('#'+formid) || (function(){
367 // Hmmm the form's id may have been overriden by an internal input
368 // with the name id which will KILL IE.
369 // We need to manually iterate at this point because if the case
370 // above is true YUI's ancestor method will also kill IE!
372 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
373 form = form.ancestor();
377 // Make sure we have the form
379 // Create a function to handle our change event
380 var processchange = function(e, paramobject) {
381 if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
382 //prevent event bubbling and detach handlers to prevent multiple submissions caused by double clicking
384 paramobject.eventkeypress.detach();
385 paramobject.eventblur.detach();
386 paramobject.eventchangeorblur.detach();
391 // Attach the change event to the keypress, blur, and click actions.
392 // We don't use the change event because IE fires it on every arrow up/down
393 // event.... usability
394 var paramobject = new Object();
395 paramobject.lastindex = select.get('selectedIndex');
396 paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
397 paramobject.eventblur = select.on('blur', processchange, form, paramobject);
398 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
400 paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
402 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
410 * Attach handler to url_select
412 M.util.init_url_select = function(Y, formid, selectid, nothing) {
413 YUI(M.yui.loader).use('node', function(Y) {
414 Y.on('change', function() {
415 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
416 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
424 * Breaks out all links to the top frame - used in frametop page layout.
426 M.util.init_frametop = function(Y) {
427 Y.all('a').each(function(node) {
428 node.set('target', '_top');
430 Y.all('form').each(function(node) {
431 node.set('target', '_top');
436 * Finds all nodes that match the given CSS selector and attaches events to them
437 * so that they toggle a given classname when clicked.
440 * @param {string} id An id containing elements to target
441 * @param {string} cssselector A selector to use to find targets
442 * @param {string} toggleclassname A classname to toggle
444 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
446 if (togglecssselector == '') {
447 togglecssselector = cssselector;
450 var node = Y.one('#'+id);
451 node.all(cssselector).each(function(n){
452 n.on('click', function(e){
454 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
455 if (this.test(togglecssselector)) {
456 this.toggleClass(toggleclassname);
458 this.ancestor(togglecssselector).toggleClass(toggleclassname);
463 // Attach this click event to the node rather than all selectors... will be much better
465 node.on('click', function(e){
466 if (e.target.hasClass('addtoall')) {
467 this.all(togglecssselector).addClass(toggleclassname);
468 } else if (e.target.hasClass('removefromall')) {
469 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
475 * Initialises a colour picker
477 * Designed to be used with admin_setting_configcolourpicker although could be used
478 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
479 * above or below the input (must have the same parent) and then call this with the
482 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
483 * contrib/blocks. For better docs refer to that.
487 * @param {object} previewconf
489 M.util.init_colour_picker = function(Y, id, previewconf) {
491 * We need node and event-mouseenter
493 Y.use('node', 'event-mouseenter', function(){
495 * The colour picker object
504 eventMouseEnter : null,
505 eventMouseLeave : null,
506 eventMouseMove : null,
511 * Initalises the colour picker by putting everything together and wiring the events
514 this.input = Y.one('#'+id);
515 this.box = this.input.ancestor().one('.admin_colourpicker');
516 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
517 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
518 this.preview = Y.Node.create('<div class="previewcolour"></div>');
519 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
520 this.current = Y.Node.create('<div class="currentcolour"></div>');
521 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
522 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
524 if (typeof(previewconf) === 'object' && previewconf !== null) {
525 Y.one('#'+id+'_preview').on('click', function(e){
526 if (Y.Lang.isString(previewconf.selector)) {
527 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
529 for (var i in previewconf.selector) {
530 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
536 this.eventClick = this.image.on('click', this.pickColour, this);
537 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
540 * Starts to follow the mouse once it enter the image
542 startFollow : function(e) {
543 this.eventMouseEnter.detach();
544 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
545 this.eventMouseMove = this.image.on('mousemove', function(e){
546 this.preview.setStyle('backgroundColor', this.determineColour(e));
550 * Stops following the mouse
552 endFollow : function(e) {
553 this.eventMouseMove.detach();
554 this.eventMouseLeave.detach();
555 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
558 * Picks the colour the was clicked on
560 pickColour : function(e) {
561 var colour = this.determineColour(e);
562 this.input.set('value', colour);
563 this.current.setStyle('backgroundColor', colour);
566 * Calculates the colour fromthe given co-ordinates
568 determineColour : function(e) {
569 var eventx = Math.floor(e.pageX-e.target.getX());
570 var eventy = Math.floor(e.pageY-e.target.getY());
572 var imagewidth = this.width;
573 var imageheight = this.height;
574 var factor = this.factor;
575 var colour = [255,0,0];
586 var matrixcount = matrices.length;
587 var limit = Math.round(imagewidth/matrixcount);
588 var heightbreak = Math.round(imageheight/2);
590 for (var x = 0; x < imagewidth; x++) {
591 var divisor = Math.floor(x / limit);
592 var matrix = matrices[divisor];
594 colour[0] += matrix[0]*factor;
595 colour[1] += matrix[1]*factor;
596 colour[2] += matrix[2]*factor;
603 var pixel = [colour[0], colour[1], colour[2]];
604 if (eventy < heightbreak) {
605 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
606 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
607 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
608 } else if (eventy > heightbreak) {
609 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
610 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
611 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
614 return this.convert_rgb_to_hex(pixel);
617 * Converts an RGB value to Hex
619 convert_rgb_to_hex : function(rgb) {
621 var hexchars = "0123456789ABCDEF";
622 for (var i=0; i<3; i++) {
623 var number = Math.abs(rgb[i]);
624 if (number == 0 || isNaN(number)) {
627 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
634 * Initialise the colour picker :) Hoorah
640 M.util.init_block_hider = function(Y, config) {
641 Y.use('base', 'node', function(Y) {
642 M.util.block_hider = M.util.block_hider || (function(){
643 var blockhider = function() {
644 blockhider.superclass.constructor.apply(this, arguments);
646 blockhider.prototype = {
647 initializer : function(config) {
648 this.set('block', '#'+this.get('id'));
649 var b = this.get('block'),
652 if (t && (a = t.one('.block_action'))) {
653 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
654 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
655 hide.on('keypress', this.updateStateKey, this, true);
656 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
657 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
658 show.on('keypress', this.updateStateKey, this, false);
659 a.insert(show, 0).insert(hide, 0);
662 updateState : function(e, hide) {
663 M.util.set_user_preference(this.get('preference'), hide);
665 this.get('block').addClass('hidden');
667 this.get('block').removeClass('hidden');
670 updateStateKey : function(e, hide) {
671 if (e.keyCode == 13) { //allow hide/show via enter key
672 this.updateState(this, hide);
676 Y.extend(blockhider, Y.Base, blockhider.prototype, {
682 value : M.util.image_url('t/switch_minus', 'moodle')
685 value : M.util.image_url('t/switch_plus', 'moodle')
688 setter : function(node) {
696 new M.util.block_hider(config);
701 * Returns a string registered in advance for usage in JavaScript
703 * If you do not pass the third parameter, the function will just return
704 * the corresponding value from the M.str object. If the third parameter is
705 * provided, the function performs {$a} placeholder substitution in the
706 * same way as PHP get_string() in Moodle does.
708 * @param {String} identifier string identifier
709 * @param {String} component the component providing the string
710 * @param {Object|String} a optional variable to populate placeholder with
712 M.util.get_string = function(identifier, component, a) {
715 if (M.cfg.developerdebug) {
716 // creating new instance if YUI is not optimal but it seems to be better way then
717 // require the instance via the function API - note that it is used in rare cases
718 // for debugging only anyway
719 var Y = new YUI({ debug : true });
722 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
723 stringvalue = '[[' + identifier + ',' + component + ']]';
724 if (M.cfg.developerdebug) {
725 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
730 stringvalue = M.str[component][identifier];
732 if (typeof a == 'undefined') {
733 // no placeholder substitution requested
737 if (typeof a == 'number' || typeof a == 'string') {
738 // replace all occurrences of {$a} with the placeholder value
739 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
743 if (typeof a == 'object') {
744 // replace {$a->key} placeholders
746 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
747 if (M.cfg.developerdebug) {
748 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
752 var search = '{$a->' + key + '}';
753 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
754 search = new RegExp(search, 'g');
755 stringvalue = stringvalue.replace(search, a[key]);
760 if (M.cfg.developerdebug) {
761 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
767 * Set focus on username or password field of the login form
769 M.util.focus_login_form = function(Y) {
770 var username = Y.one('#username');
771 var password = Y.one('#password');
773 if (username == null || password == null) {
774 // something is wrong here
778 var curElement = document.activeElement
779 if (curElement == 'undefined') {
780 // legacy browser - skip refocus protection
781 } else if (curElement.tagName == 'INPUT') {
782 // user was probably faster to focus something, do not mess with focus
786 if (username.get('value') == '') {
794 * Adds lightbox hidden element that covers the whole node.
797 * @param {Node} the node lightbox should be added to
798 * @retun {Node} created lightbox node
800 M.util.add_lightbox = function(Y, node) {
801 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
803 // Check if lightbox is already there
804 if (node.one('.lightbox')) {
805 return node.one('.lightbox');
808 node.setStyle('position', 'relative');
809 var waiticon = Y.Node.create('<img />')
811 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
814 'position' : 'relative',
818 var lightbox = Y.Node.create('<div></div>')
821 'position' : 'absolute',
826 'backgroundColor' : 'white',
827 'text-align' : 'center'
829 .setAttribute('class', 'lightbox')
832 lightbox.appendChild(waiticon);
833 node.append(lightbox);
838 * Appends a hidden spinner element to the specified node.
841 * @param {Node} the node the spinner should be added to
842 * @return {Node} created spinner node
844 M.util.add_spinner = function(Y, node) {
845 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
847 // Check if spinner is already there
848 if (node.one('.spinner')) {
849 return node.one('.spinner');
852 var spinner = Y.Node.create('<img />')
853 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
855 .addClass('iconsmall')
858 node.append(spinner);
862 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
864 function checkall() {
865 var inputs = document.getElementsByTagName('input');
866 for (var i = 0; i < inputs.length; i++) {
867 if (inputs[i].type == 'checkbox') {
868 if (inputs[i].disabled || inputs[i].readOnly) {
871 inputs[i].checked = true;
876 function checknone() {
877 var inputs = document.getElementsByTagName('input');
878 for (var i = 0; i < inputs.length; i++) {
879 if (inputs[i].type == 'checkbox') {
880 if (inputs[i].disabled || inputs[i].readOnly) {
883 inputs[i].checked = false;
889 * Either check, or uncheck, all checkboxes inside the element with id is
890 * @param id the id of the container
891 * @param checked the new state, either '' or 'checked'.
893 function select_all_in_element_with_id(id, checked) {
894 var container = document.getElementById(id);
898 var inputs = container.getElementsByTagName('input');
899 for (var i = 0; i < inputs.length; ++i) {
900 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
901 inputs[i].checked = checked;
906 function select_all_in(elTagName, elClass, elId) {
907 var inputs = document.getElementsByTagName('input');
908 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
909 for(var i = 0; i < inputs.length; ++i) {
910 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
911 inputs[i].checked = 'checked';
916 function deselect_all_in(elTagName, elClass, elId) {
917 var inputs = document.getElementsByTagName('INPUT');
918 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
919 for(var i = 0; i < inputs.length; ++i) {
920 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
921 inputs[i].checked = '';
926 function confirm_if(expr, message) {
930 return confirm(message);
935 findParentNode (start, elementName, elementClass, elementID)
937 Travels up the DOM hierarchy to find a parent element with the
938 specified tag name, class, and id. All conditions must be met,
939 but any can be ommitted. Returns the BODY element if no match
942 function findParentNode(el, elName, elClass, elId) {
943 while (el.nodeName.toUpperCase() != 'BODY') {
944 if ((!elName || el.nodeName.toUpperCase() == elName) &&
945 (!elClass || el.className.indexOf(elClass) != -1) &&
946 (!elId || el.id == elId)) {
954 findChildNode (start, elementName, elementClass, elementID)
956 Travels down the DOM hierarchy to find all child elements with the
957 specified tag name, class, and id. All conditions must be met,
958 but any can be ommitted.
959 Doesn't examine children of matches.
961 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
962 var children = new Array();
963 for (var i = 0; i < start.childNodes.length; i++) {
964 var classfound = false;
965 var child = start.childNodes[i];
966 if((child.nodeType == 1) &&//element node type
967 (elementClass && (typeof(child.className)=='string'))) {
968 var childClasses = child.className.split(/\s+/);
969 for (var childClassIndex in childClasses) {
970 if (childClasses[childClassIndex]==elementClass) {
976 if(child.nodeType == 1) { //element node type
977 if ( (!tagName || child.nodeName == tagName) &&
978 (!elementClass || classfound)&&
979 (!elementID || child.id == elementID) &&
980 (!elementName || child.name == elementName))
982 children = children.concat(child);
984 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
991 function unmaskPassword(id) {
992 var pw = document.getElementById(id);
993 var chb = document.getElementById(id+'unmask');
996 // first try IE way - it can not set name attribute later
998 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1000 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1002 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1004 var newpw = document.createElement('input');
1005 newpw.setAttribute('autocomplete', 'off');
1006 newpw.setAttribute('name', pw.name);
1008 newpw.setAttribute('type', 'text');
1010 newpw.setAttribute('type', 'password');
1012 newpw.setAttribute('class', pw.getAttribute('class'));
1015 newpw.size = pw.size;
1016 newpw.onblur = pw.onblur;
1017 newpw.onchange = pw.onchange;
1018 newpw.value = pw.value;
1019 pw.parentNode.replaceChild(newpw, pw);
1022 function filterByParent(elCollection, parentFinder) {
1023 var filteredCollection = [];
1024 for (var i = 0; i < elCollection.length; ++i) {
1025 var findParent = parentFinder(elCollection[i]);
1026 if (findParent.nodeName.toUpperCase() != 'BODY') {
1027 filteredCollection.push(elCollection[i]);
1030 return filteredCollection;
1034 All this is here just so that IE gets to handle oversized blocks
1035 in a visually pleasing manner. It does a browser detect. So sue me.
1038 function fix_column_widths() {
1039 var agt = navigator.userAgent.toLowerCase();
1040 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1041 fix_column_width('left-column');
1042 fix_column_width('right-column');
1046 function fix_column_width(colName) {
1047 if(column = document.getElementById(colName)) {
1048 if(!column.offsetWidth) {
1049 setTimeout("fix_column_width('" + colName + "')", 20);
1054 var nodes = column.childNodes;
1056 for(i = 0; i < nodes.length; ++i) {
1057 if(nodes[i].className.indexOf("block") != -1 ) {
1058 if(width < nodes[i].offsetWidth) {
1059 width = nodes[i].offsetWidth;
1064 for(i = 0; i < nodes.length; ++i) {
1065 if(nodes[i].className.indexOf("block") != -1 ) {
1066 nodes[i].style.width = width + 'px';
1074 Insert myValue at current cursor position
1076 function insertAtCursor(myField, myValue) {
1078 if (document.selection) {
1080 sel = document.selection.createRange();
1083 // Mozilla/Netscape support
1084 else if (myField.selectionStart || myField.selectionStart == '0') {
1085 var startPos = myField.selectionStart;
1086 var endPos = myField.selectionEnd;
1087 myField.value = myField.value.substring(0, startPos)
1088 + myValue + myField.value.substring(endPos, myField.value.length);
1090 myField.value += myValue;
1096 Call instead of setting window.onload directly or setting body onload=.
1097 Adds your function to a chain of functions rather than overwriting anything
1100 function addonload(fn) {
1101 var oldhandler=window.onload;
1102 window.onload=function() {
1103 if(oldhandler) oldhandler();
1108 * Replacement for getElementsByClassName in browsers that aren't cool enough
1110 * Relying on the built-in getElementsByClassName is far, far faster than
1113 * Note: the third argument used to be an object with odd behaviour. It now
1114 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1115 * mimicked if you pass an object.
1117 * @param {Node} oElm The top-level node for searching. To search a whole
1118 * document, use `document`.
1119 * @param {String} strTagName filter by tag names
1120 * @param {String} name same as HTML5 spec
1122 function getElementsByClassName(oElm, strTagName, name) {
1123 // for backwards compatibility
1124 if(typeof name == "object") {
1125 var names = new Array();
1126 for(var i=0; i<name.length; i++) names.push(names[i]);
1127 name = names.join('');
1129 // use native implementation if possible
1130 if (oElm.getElementsByClassName && Array.filter) {
1131 if (strTagName == '*') {
1132 return oElm.getElementsByClassName(name);
1134 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1135 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1139 // native implementation unavailable, fall back to slow method
1140 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1141 var arrReturnElements = new Array();
1142 var arrRegExpClassNames = new Array();
1143 var names = name.split(' ');
1144 for(var i=0; i<names.length; i++) {
1145 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1149 for(var j=0; j<arrElements.length; j++) {
1150 oElement = arrElements[j];
1152 for(var k=0; k<arrRegExpClassNames.length; k++) {
1153 if(!arrRegExpClassNames[k].test(oElement.className)) {
1154 bMatchesAll = false;
1159 arrReturnElements.push(oElement);
1162 return (arrReturnElements)
1165 function openpopup(event, args) {
1168 if (event.preventDefault) {
1169 event.preventDefault();
1171 event.returnValue = false;
1175 var fullurl = args.url;
1176 if (!args.url.match(/https?:\/\//)) {
1177 fullurl = M.cfg.wwwroot + args.url;
1179 var windowobj = window.open(fullurl,args.name,args.options);
1183 if (args.fullscreen) {
1184 windowobj.moveTo(0,0);
1185 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1192 /** Close the current browser window. */
1193 function close_window(e) {
1194 if (e.preventDefault) {
1197 e.returnValue = false;
1203 * Used in a couple of modules to hide navigation areas when using AJAX
1206 function show_item(itemid) {
1207 var item = document.getElementById(itemid);
1209 item.style.display = "";
1213 function destroy_item(itemid) {
1214 var item = document.getElementById(itemid);
1216 item.parentNode.removeChild(item);
1220 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1221 * @param controlid the control id.
1223 function focuscontrol(controlid) {
1224 var control = document.getElementById(controlid);
1231 * Transfers keyboard focus to an HTML element based on the old style style of focus
1232 * This function should be removed as soon as it is no longer used
1234 function old_onload_focus(formid, controlname) {
1235 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1236 document.forms[formid].elements[controlname].focus();
1240 function build_querystring(obj) {
1241 return convert_object_to_string(obj, '&');
1244 function build_windowoptionsstring(obj) {
1245 return convert_object_to_string(obj, ',');
1248 function convert_object_to_string(obj, separator) {
1249 if (typeof obj !== 'object') {
1254 k = encodeURIComponent(k);
1256 if(obj[k] instanceof Array) {
1257 for(var i in value) {
1258 list.push(k+'[]='+encodeURIComponent(value[i]));
1261 list.push(k+'='+encodeURIComponent(value));
1264 return list.join(separator);
1267 function stripHTML(str) {
1268 var re = /<\S[^><]*>/g;
1269 var ret = str.replace(re, "");
1273 Number.prototype.fixed=function(n){
1275 return round(Number(this)*pow(10,n))/pow(10,n);
1277 function update_progress_bar (id, width, pt, msg, es){
1279 var status = document.getElementById("status_"+id);
1280 var percent_indicator = document.getElementById("pt_"+id);
1281 var progress_bar = document.getElementById("progress_"+id);
1282 var time_es = document.getElementById("time_"+id);
1283 status.innerHTML = msg;
1284 percent_indicator.innerHTML = percent.fixed(2) + '%';
1285 if(percent == 100) {
1286 progress_bar.style.background = "green";
1287 time_es.style.display = "none";
1289 progress_bar.style.background = "#FFCC66";
1291 time_es.innerHTML = "";
1293 time_es.innerHTML = es.fixed(2)+" sec";
1294 time_es.style.display
1298 progress_bar.style.width = width + "px";
1303 // ===== Deprecated core Javascript functions for Moodle ====
1304 // DO NOT USE!!!!!!!
1305 // Do not put this stuff in separate file because it only adds extra load on servers!
1308 * Used in a couple of modules to hide navigation areas when using AJAX
1310 function hide_item(itemid) {
1311 // use class='hiddenifjs' instead
1312 var item = document.getElementById(itemid);
1314 item.style.display = "none";
1318 M.util.help_icon = {
1321 add : function(Y, properties) {
1323 properties.node = Y.one('#'+properties.id);
1324 if (properties.node) {
1325 properties.node.on('click', this.display, this, properties);
1328 display : function(event, args) {
1329 event.preventDefault();
1330 if (M.util.help_icon.instance === null) {
1331 var Y = M.util.help_icon.Y;
1332 Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1333 var help_content_overlay = {
1338 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1339 // Create an overlay from markup
1340 this.overlay = new Y.Overlay({
1341 headerContent: closebtn,
1348 this.overlay.render(Y.one(document.body));
1350 closebtn.on('click', this.overlay.hide, this.overlay);
1352 var boundingBox = this.overlay.get("boundingBox");
1354 // Hide the menu if the user clicks outside of its content
1355 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1356 var oTarget = event.target;
1357 var menuButton = Y.one("#"+args.id);
1359 if (!oTarget.compareTo(menuButton) &&
1360 !menuButton.contains(oTarget) &&
1361 !oTarget.compareTo(boundingBox) &&
1362 !boundingBox.contains(oTarget)) {
1363 this.overlay.hide();
1367 Y.on("key", this.close, closebtn , "down:13", this);
1368 closebtn.on('click', this.close, this);
1371 close : function(e) {
1373 this.helplink.focus();
1374 this.overlay.hide();
1377 display : function(event, args) {
1378 this.helplink = args.node;
1379 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1380 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1382 var fullurl = args.url;
1383 if (!args.url.match(/https?:\/\//)) {
1384 fullurl = M.cfg.wwwroot + args.url;
1387 var ajaxurl = fullurl + '&ajax=1';
1393 success: function(id, o, node) {
1394 this.display_callback(o.responseText);
1396 failure: function(id, o, node) {
1397 var debuginfo = o.statusText;
1398 if (M.cfg.developerdebug) {
1399 o.statusText += ' (' + ajaxurl + ')';
1401 this.display_callback('bodyContent',debuginfo);
1407 this.overlay.show();
1409 Y.one('#closehelpbox').focus();
1412 display_callback : function(content) {
1413 this.overlay.set('bodyContent', content);
1416 hideContent : function() {
1418 help.overlay.hide();
1421 help_content_overlay.init();
1422 M.util.help_icon.instance = help_content_overlay;
1423 M.util.help_icon.instance.display(event, args);
1426 M.util.help_icon.instance.display(event, args);
1429 init : function(Y) {
1435 * Custom menu namespace
1437 M.core_custom_menu = {
1439 * This method is used to initialise a custom menu given the id that belongs
1440 * to the custom menu's root node.
1443 * @param {string} nodeid
1445 init : function(Y, nodeid) {
1446 var node = Y.one('#'+nodeid);
1448 Y.use('node-menunav', function(Y) {
1450 // Remove the javascript-disabled class.... obviously javascript is enabled.
1451 node.removeClass('javascript-disabled');
1452 // Initialise the menunav plugin
1453 node.plug(Y.Plugin.NodeMenuNav);
1460 * Used to store form manipulation methods and enhancments
1462 M.form = M.form || {};
1465 * Converts a nbsp indented select box into a multi drop down custom control much
1466 * like the custom menu. It also selectable categories on or off.
1468 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1471 * @param {string} id
1472 * @param {Array} options
1474 M.form.init_smartselect = function(Y, id, options) {
1475 if (!id.match(/^id_/)) {
1478 var select = Y.one('select#'+id);
1482 Y.use('event-delegate',function(){
1488 currentvalue : null,
1492 selectablecategories : true,
1500 init : function(Y, id, args, nodes) {
1501 if (typeof(args)=='object') {
1502 for (var i in this.cfg) {
1503 if (args[i] || args[i]===false) {
1504 this.cfg[i] = args[i];
1509 // Display a loading message first up
1510 this.nodes.select = nodes.select;
1512 this.currentvalue = this.nodes.select.get('selectedIndex');
1513 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1515 var options = Array();
1516 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1517 this.nodes.select.all('option').each(function(option, index) {
1518 var rawtext = option.get('innerHTML');
1519 var text = rawtext.replace(/^( )*/, '');
1520 if (rawtext === text) {
1521 text = rawtext.replace(/^(\s)*/, '');
1522 var depth = (rawtext.length - text.length ) + 1;
1524 var depth = ((rawtext.length - text.length )/12)+1;
1526 option.set('innerHTML', text);
1527 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1530 this.structure = [];
1531 var structcount = 0;
1532 for (var i in options) {
1535 this.structure.push(o);
1539 var current = this.structure[structcount-1];
1540 for (var j = 0; j < o.depth-1;j++) {
1541 if (current && current.children) {
1542 current = current.children[current.children.length-1];
1545 if (current && current.children) {
1546 current.children.push(o);
1551 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1552 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1553 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1554 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1556 if (this.cfg.mode == null) {
1557 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1558 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1559 this.cfg.mode = 'compact';
1561 this.cfg.mode = 'spanning';
1565 if (this.cfg.mode == 'compact') {
1566 this.nodes.menu.addClass('compactmenu');
1568 this.nodes.menu.addClass('spanningmenu');
1569 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1572 Y.one(document.body).append(this.nodes.menu);
1573 var pos = this.nodes.select.getXY();
1575 this.nodes.menu.setXY(pos);
1576 this.nodes.menu.on('click', this.handle_click, this);
1578 Y.one(window).on('resize', function(){
1579 var pos = this.nodes.select.getXY();
1581 this.nodes.menu.setXY(pos);
1584 generate_menu_content : function() {
1585 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1586 content += this.generate_submenu_content(this.structure[0], true);
1587 content += '</ul></div>';
1590 generate_submenu_content : function(item, rootelement) {
1591 this.submenucount++;
1593 if (item.children.length > 0) {
1595 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1596 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1597 content += '<div class="smartselect_menu_content">';
1599 content += '<li class="smartselect_submenuitem">';
1600 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1601 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1602 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1603 content += '<div class="smartselect_submenu_content">';
1606 for (var i in item.children) {
1607 content += this.generate_submenu_content(item.children[i],false);
1610 content += '</div>';
1611 content += '</div>';
1617 content += '<li class="smartselect_menuitem">';
1618 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1623 select : function(e) {
1626 this.currenttext = t.get('innerHTML');
1627 this.currentvalue = t.getAttribute('value');
1628 this.nodes.select.set('selectedIndex', this.currentvalue);
1631 handle_click : function(e) {
1632 var target = e.target;
1633 if (target.hasClass('smartselect_mask')) {
1635 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1637 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1638 this.show_sub_menu(e);
1641 show_menu : function(e) {
1643 var menu = e.target.ancestor().one('.smartselect_menu');
1644 menu.addClass('visible');
1645 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1647 show_sub_menu : function(e) {
1649 var target = e.target;
1650 if (!target.hasClass('smartselect_submenuitem')) {
1651 target = target.ancestor('.smartselect_submenuitem');
1653 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1654 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1657 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1658 target.one('.smartselect_submenu').addClass('visible');
1660 hide_menu : function() {
1661 this.nodes.menu.all('.visible').removeClass('visible');
1662 if (this.shownevent) {
1663 this.shownevent.detach();
1667 smartselect.init(Y, id, options, {select:select});
1671 /** List of flv players to be loaded */
1672 M.util.video_players = [];
1673 /** List of mp3 players to be loaded */
1674 M.util.audio_players = [];
1678 * @param id element id
1679 * @param fileurl media url
1682 * @param autosize true means detect size from media
1684 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1685 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1694 M.util.add_audio_player = function (id, fileurl, small) {
1695 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1699 * Initialise all audio and video player, must be called from page footer.
1701 M.util.load_flowplayer = function() {
1702 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1705 if (typeof(flowplayer) == 'undefined') {
1708 var embed_function = function() {
1709 if (loaded || typeof(flowplayer) == 'undefined') {
1717 /* TODO: add CSS color overrides for the flv flow player */
1719 for(var i=0; i<M.util.video_players.length; i++) {
1720 var video = M.util.video_players[i];
1721 if (video.width > 0 && video.height > 0) {
1722 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf', width: video.width, height: video.height};
1724 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf';
1726 flowplayer(video.id, src, {
1727 plugins: {controls: controls},
1729 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1730 onMetaData: function(clip) {
1731 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1732 clip.mvideo.resized = true;
1733 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1734 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1735 // bad luck, we have to guess - we may not get metadata at all
1736 var width = clip.width;
1737 var height = clip.height;
1739 var width = clip.metaData.width;
1740 var height = clip.metaData.height;
1742 var minwidth = 300; // controls are messed up in smaller objects
1743 if (width < minwidth) {
1744 height = (height * minwidth) / width;
1748 var object = this._api();
1749 object.width = width;
1750 object.height = height;
1756 if (M.util.audio_players.length == 0) {
1769 backgroundGradient: [0.5,0,0.3]
1773 for (var j=0; j < document.styleSheets.length; j++) {
1774 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1775 var allrules = document.styleSheets[j].rules;
1776 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1777 var allrules = document.styleSheets[j].cssRules;
1782 for(var i=0; i<allrules.length; i++) {
1784 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1785 if (typeof(allrules[i].cssText) != 'undefined') {
1786 rule = allrules[i].cssText;
1787 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1788 rule = allrules[i].style.cssText;
1790 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1791 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1792 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1793 controls[colprop] = rule;
1800 for(i=0; i<M.util.audio_players.length; i++) {
1801 var audio = M.util.audio_players[i];
1803 controls.controlall = false;
1804 controls.height = 15;
1805 controls.time = false;
1807 controls.controlall = true;
1808 controls.height = 25;
1809 controls.time = true;
1811 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.9.swf', {
1812 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.8.swf'}},
1813 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1818 if (M.cfg.jsrev == -10) {
1819 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.8.min.js';
1821 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.8.min.js&rev=' + M.cfg.jsrev;
1823 var fileref = document.createElement('script');
1824 fileref.setAttribute('type','text/javascript');
1825 fileref.setAttribute('src', jsurl);
1826 fileref.onload = embed_function;
1827 fileref.onreadystatechange = embed_function;
1828 document.getElementsByTagName('head')[0].appendChild(fileref);