1 // Miscellaneous core Javascript functions for Moodle
2 // Global M object is initilised in inline javascript
5 * Add module to list of available modules that can be laoded from YUI.
6 * @param {Array} modules
8 M.yui.add_module = function(modules) {
9 for (var modname in modules) {
10 M.yui.loader.modules[modname] = modules[modname];
14 * The gallery version to use when loading YUI modules from the gallery.
15 * Will be changed every time when using local galleries.
17 M.yui.galleryversion = '2010.04.21-21-51';
20 * Various utility functions
22 M.util = M.util || {};
25 * Language strings - initialised from page footer.
30 * Returns url for images.
31 * @param {String} imagename
32 * @param {String} component
35 M.util.image_url = function(imagename, component) {
36 var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename;
38 if (M.cfg.themerev > 0) {
39 url = url + '&rev=' + M.cfg.themerev;
42 if (component && component != '' && component != 'moodle' && component != 'core') {
43 url = url + '&component=' + component;
49 M.util.in_array = function(item, array){
50 for( var i = 0; i<array.length; i++){
59 * Init a collapsible region, see print_collapsible_region in weblib.php
60 * @param {YUI} Y YUI3 instance with all libraries loaded
61 * @param {String} id the HTML id for the div.
62 * @param {String} userpref the user preference that records the state of this box. false if none.
63 * @param {String} strtooltip
65 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
66 Y.use('anim', function(Y) {
67 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
72 * Object to handle a collapsible region : instantiate and forget styled object
76 * @param {YUI} Y YUI3 instance with all libraries loaded
77 * @param {String} id The HTML id for the div.
78 * @param {String} userpref The user preference that records the state of this box. false if none.
79 * @param {String} strtooltip
81 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
82 // Record the pref name
83 this.userpref = userpref;
85 // Find the divs in the document.
86 this.div = Y.one('#'+id);
88 // Get the caption for the collapsible region
89 var caption = this.div.one('#'+id + '_caption');
90 caption.setAttribute('title', strtooltip);
93 var a = Y.Node.create('<a href="#"></a>');
94 // Create a local scoped lamba function to move nodes to a new link
95 var movenode = function(node){
99 // Apply the lamba function on each of the captions child nodes
100 caption.get('children').each(movenode, this);
103 // Get the height of the div at this point before we shrink it if required
104 var height = this.div.get('offsetHeight');
105 if (this.div.hasClass('collapsed')) {
106 // Add the correct image and record the YUI node created in the process
107 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
108 // Shrink the div as it is collapsed by default
109 this.div.setStyle('height', caption.get('offsetHeight')+'px');
111 // Add the correct image and record the YUI node created in the process
112 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
116 // Create the animation.
117 var animation = new Y.Anim({
120 easing: Y.Easing.easeBoth,
121 to: {height:caption.get('offsetHeight')},
122 from: {height:height}
125 // Handler for the animation finishing.
126 animation.on('end', function() {
127 this.div.toggleClass('collapsed');
128 if (this.div.hasClass('collapsed')) {
129 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
131 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
135 // Hook up the event handler.
136 a.on('click', function(e, animation) {
138 // Animate to the appropriate size.
139 if (animation.get('running')) {
142 animation.set('reverse', this.div.hasClass('collapsed'));
143 // Update the user preference.
145 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
152 * The user preference that stores the state of this box.
156 M.util.CollapsibleRegion.prototype.userpref = null;
159 * The key divs that make up this
163 M.util.CollapsibleRegion.prototype.div = null;
166 * The key divs that make up this
170 M.util.CollapsibleRegion.prototype.icon = null;
173 * Makes a best effort to connect back to Moodle to update a user preference,
174 * however, there is no mechanism for finding out if the update succeeded.
176 * Before you can use this function in your JavsScript, you must have called
177 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
178 * the udpate is allowed, and how to safely clean and submitted values.
180 * @param String name the name of the setting to udpate.
181 * @param String the value to set it to.
183 M.util.set_user_preference = function(name, value) {
184 YUI(M.yui.loader).use('io', function(Y) {
185 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
186 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
188 // If we are a developer, ensure that failures are reported.
193 if (M.cfg.developerdebug) {
194 cfg.on.failure = function(id, o, args) {
195 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
205 * Prints a confirmation dialog in the style of DOM.confirm().
206 * @param object event A YUI DOM event or null if launched manually
207 * @param string message The message to show in the dialog
208 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
209 * @param function fn A JS function to run if YES is clicked.
211 M.util.show_confirm_dialog = function(e, args) {
212 var target = e.target;
213 if (e.preventDefault) {
217 YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
218 var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
227 simpledialog.setHeader(M.str.admin.confirmation);
228 simpledialog.setBody(args.message);
229 simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
231 var handle_cancel = function() {
235 var handle_yes = function() {
239 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
241 if (Y.Lang.isFunction(args.callback)) {
242 callback = args.callback;
244 callback = eval('('+args.callback+')');
247 if (Y.Lang.isObject(args.scope)) {
253 if (args.callbackargs) {
254 callback.apply(sc, args.callbackargs);
261 var targetancestor = null,
264 if (target.test('a')) {
265 window.location = target.get('href');
267 } else if ((targetancestor = target.ancestor('a')) !== null) {
268 window.location = targetancestor.get('href');
270 } else if (target.test('input')) {
271 targetform = target.ancestor('form');
275 if (target.get('name') && target.get('value')) {
276 targetform.append('<input type="hidden" name="' + target.get('name') +
277 '" value="' + target.get('value') + '">');
281 } else if (target.get('tagName').toLowerCase() == 'form') {
282 // We cannot use target.test('form') on the previous line because of
283 // http://yuilibrary.com/projects/yui3/ticket/2531561
286 } else if (M.cfg.developerdebug) {
287 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT");
291 if (!args.cancellabel) {
292 args.cancellabel = M.str.moodle.cancel;
294 if (!args.continuelabel) {
295 args.continuelabel = M.str.moodle.yes;
299 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
300 {text: args.continuelabel, handler: handle_yes}
303 simpledialog.cfg.queueProperty('buttons', buttons);
305 simpledialog.render(document.body);
310 /** Useful for full embedding of various stuff */
311 M.util.init_maximised_embed = function(Y, id) {
312 var obj = Y.one('#'+id);
317 var get_htmlelement_size = function(el, prop) {
318 if (Y.Lang.isString(el)) {
319 el = Y.one('#' + el);
321 var val = el.getStyle(prop);
323 val = el.getComputedStyle(prop);
325 return parseInt(val);
328 var resize_object = function() {
329 obj.setStyle('width', '0px');
330 obj.setStyle('height', '0px');
331 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
333 if (newwidth > 500) {
334 obj.setStyle('width', newwidth + 'px');
336 obj.setStyle('width', '500px');
339 var headerheight = get_htmlelement_size('page-header', 'height');
340 var footerheight = get_htmlelement_size('page-footer', 'height');
341 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
342 if (newheight < 400) {
345 obj.setStyle('height', newheight+'px');
349 // fix layout if window resized too
350 window.onresize = function() {
356 * Attach handler to single_select
358 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
359 Y.use('event-key', function() {
360 var select = Y.one('#'+selectid);
362 // Try to get the form by id
363 var form = Y.one('#'+formid) || (function(){
364 // Hmmm the form's id may have been overriden by an internal input
365 // with the name id which will KILL IE.
366 // We need to manually iterate at this point because if the case
367 // above is true YUI's ancestor method will also kill IE!
369 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
370 form = form.ancestor();
374 // Make sure we have the form
376 // Create a function to handle our change event
377 var processchange = function(e, paramobject) {
378 if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
379 //prevent event bubbling and detach handlers to prevent multiple submissions caused by double clicking
381 paramobject.eventkeypress.detach();
382 paramobject.eventblur.detach();
383 paramobject.eventchangeorblur.detach();
388 // Attach the change event to the keypress, blur, and click actions.
389 // We don't use the change event because IE fires it on every arrow up/down
390 // event.... usability
391 var paramobject = new Object();
392 paramobject.lastindex = select.get('selectedIndex');
393 paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
394 paramobject.eventblur = select.on('blur', processchange, form, paramobject);
395 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
397 paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
399 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
407 * Attach handler to url_select
409 M.util.init_url_select = function(Y, formid, selectid, nothing) {
410 YUI(M.yui.loader).use('node', function(Y) {
411 Y.on('change', function() {
412 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
413 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
421 * Breaks out all links to the top frame - used in frametop page layout.
423 M.util.init_frametop = function(Y) {
424 Y.all('a').each(function(node) {
425 node.set('target', '_top');
427 Y.all('form').each(function(node) {
428 node.set('target', '_top');
433 * Finds all nodes that match the given CSS selector and attaches events to them
434 * so that they toggle a given classname when clicked.
437 * @param {string} id An id containing elements to target
438 * @param {string} cssselector A selector to use to find targets
439 * @param {string} toggleclassname A classname to toggle
441 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
443 if (togglecssselector == '') {
444 togglecssselector = cssselector;
447 var node = Y.one('#'+id);
448 node.all(cssselector).each(function(n){
449 n.on('click', function(e){
451 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
452 if (this.test(togglecssselector)) {
453 this.toggleClass(toggleclassname);
455 this.ancestor(togglecssselector).toggleClass(toggleclassname);
460 // Attach this click event to the node rather than all selectors... will be much better
462 node.on('click', function(e){
463 if (e.target.hasClass('addtoall')) {
464 this.all(togglecssselector).addClass(toggleclassname);
465 } else if (e.target.hasClass('removefromall')) {
466 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
472 * Initialises a colour picker
474 * Designed to be used with admin_setting_configcolourpicker although could be used
475 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
476 * above or below the input (must have the same parent) and then call this with the
479 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
480 * contrib/blocks. For better docs refer to that.
484 * @param {object} previewconf
486 M.util.init_colour_picker = function(Y, id, previewconf) {
488 * We need node and event-mouseenter
490 Y.use('node', 'event-mouseenter', function(){
492 * The colour picker object
501 eventMouseEnter : null,
502 eventMouseLeave : null,
503 eventMouseMove : null,
508 * Initalises the colour picker by putting everything together and wiring the events
511 this.input = Y.one('#'+id);
512 this.box = this.input.ancestor().one('.admin_colourpicker');
513 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
514 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
515 this.preview = Y.Node.create('<div class="previewcolour"></div>');
516 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
517 this.current = Y.Node.create('<div class="currentcolour"></div>');
518 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
519 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
521 if (typeof(previewconf) === 'object' && previewconf !== null) {
522 Y.one('#'+id+'_preview').on('click', function(e){
523 if (Y.Lang.isString(previewconf.selector)) {
524 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
526 for (var i in previewconf.selector) {
527 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
533 this.eventClick = this.image.on('click', this.pickColour, this);
534 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
537 * Starts to follow the mouse once it enter the image
539 startFollow : function(e) {
540 this.eventMouseEnter.detach();
541 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
542 this.eventMouseMove = this.image.on('mousemove', function(e){
543 this.preview.setStyle('backgroundColor', this.determineColour(e));
547 * Stops following the mouse
549 endFollow : function(e) {
550 this.eventMouseMove.detach();
551 this.eventMouseLeave.detach();
552 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
555 * Picks the colour the was clicked on
557 pickColour : function(e) {
558 var colour = this.determineColour(e);
559 this.input.set('value', colour);
560 this.current.setStyle('backgroundColor', colour);
563 * Calculates the colour fromthe given co-ordinates
565 determineColour : function(e) {
566 var eventx = Math.floor(e.pageX-e.target.getX());
567 var eventy = Math.floor(e.pageY-e.target.getY());
569 var imagewidth = this.width;
570 var imageheight = this.height;
571 var factor = this.factor;
572 var colour = [255,0,0];
583 var matrixcount = matrices.length;
584 var limit = Math.round(imagewidth/matrixcount);
585 var heightbreak = Math.round(imageheight/2);
587 for (var x = 0; x < imagewidth; x++) {
588 var divisor = Math.floor(x / limit);
589 var matrix = matrices[divisor];
591 colour[0] += matrix[0]*factor;
592 colour[1] += matrix[1]*factor;
593 colour[2] += matrix[2]*factor;
600 var pixel = [colour[0], colour[1], colour[2]];
601 if (eventy < heightbreak) {
602 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
603 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
604 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
605 } else if (eventy > heightbreak) {
606 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
607 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
608 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
611 return this.convert_rgb_to_hex(pixel);
614 * Converts an RGB value to Hex
616 convert_rgb_to_hex : function(rgb) {
618 var hexchars = "0123456789ABCDEF";
619 for (var i=0; i<3; i++) {
620 var number = Math.abs(rgb[i]);
621 if (number == 0 || isNaN(number)) {
624 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
631 * Initialise the colour picker :) Hoorah
637 M.util.init_block_hider = function(Y, config) {
638 Y.use('base', 'node', function(Y) {
639 M.util.block_hider = M.util.block_hider || (function(){
640 var blockhider = function() {
641 blockhider.superclass.constructor.apply(this, arguments);
643 blockhider.prototype = {
644 initializer : function(config) {
645 this.set('block', '#'+this.get('id'));
646 var b = this.get('block'),
649 if (t && (a = t.one('.block_action'))) {
650 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
651 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
652 hide.on('keypress', this.updateStateKey, this, true);
653 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
654 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
655 show.on('keypress', this.updateStateKey, this, false);
656 a.insert(show, 0).insert(hide, 0);
659 updateState : function(e, hide) {
660 M.util.set_user_preference(this.get('preference'), hide);
662 this.get('block').addClass('hidden');
664 this.get('block').removeClass('hidden');
667 updateStateKey : function(e, hide) {
668 if (e.keyCode == 13) { //allow hide/show via enter key
669 this.updateState(this, hide);
673 Y.extend(blockhider, Y.Base, blockhider.prototype, {
679 value : M.util.image_url('t/switch_minus', 'moodle')
682 value : M.util.image_url('t/switch_plus', 'moodle')
685 setter : function(node) {
693 new M.util.block_hider(config);
698 * Returns a string registered in advance for usage in JavaScript
700 * If you do not pass the third parameter, the function will just return
701 * the corresponding value from the M.str object. If the third parameter is
702 * provided, the function performs {$a} placeholder substitution in the
703 * same way as PHP get_string() in Moodle does.
705 * @param {String} identifier string identifier
706 * @param {String} component the component providing the string
707 * @param {Object|String} a optional variable to populate placeholder with
709 M.util.get_string = function(identifier, component, a) {
712 if (M.cfg.developerdebug) {
713 // creating new instance if YUI is not optimal but it seems to be better way then
714 // require the instance via the function API - note that it is used in rare cases
715 // for debugging only anyway
716 var Y = new YUI({ debug : true });
719 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
720 stringvalue = '[[' + identifier + ',' + component + ']]';
721 if (M.cfg.developerdebug) {
722 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
727 stringvalue = M.str[component][identifier];
729 if (typeof a == 'undefined') {
730 // no placeholder substitution requested
734 if (typeof a == 'number' || typeof a == 'string') {
735 // replace all occurrences of {$a} with the placeholder value
736 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
740 if (typeof a == 'object') {
741 // replace {$a->key} placeholders
743 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
744 if (M.cfg.developerdebug) {
745 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
749 var search = '{$a->' + key + '}';
750 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
751 search = new RegExp(search, 'g');
752 stringvalue = stringvalue.replace(search, a[key]);
757 if (M.cfg.developerdebug) {
758 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
764 * Set focus on username or password field of the login form
766 M.util.focus_login_form = function(Y) {
767 var username = Y.one('#username');
768 var password = Y.one('#password');
770 if (username == null || password == null) {
771 // something is wrong here
775 var curElement = document.activeElement
776 if (curElement == 'undefined') {
777 // legacy browser - skip refocus protection
778 } else if (curElement.tagName == 'INPUT') {
779 // user was probably faster to focus something, do not mess with focus
783 if (username.get('value') == '') {
791 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
793 function checkall() {
794 var inputs = document.getElementsByTagName('input');
795 for (var i = 0; i < inputs.length; i++) {
796 if (inputs[i].type == 'checkbox') {
797 inputs[i].checked = true;
802 function checknone() {
803 var inputs = document.getElementsByTagName('input');
804 for (var i = 0; i < inputs.length; i++) {
805 if (inputs[i].type == 'checkbox') {
806 inputs[i].checked = false;
812 * Either check, or uncheck, all checkboxes inside the element with id is
813 * @param id the id of the container
814 * @param checked the new state, either '' or 'checked'.
816 function select_all_in_element_with_id(id, checked) {
817 var container = document.getElementById(id);
821 var inputs = container.getElementsByTagName('input');
822 for (var i = 0; i < inputs.length; ++i) {
823 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
824 inputs[i].checked = checked;
829 function select_all_in(elTagName, elClass, elId) {
830 var inputs = document.getElementsByTagName('input');
831 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
832 for(var i = 0; i < inputs.length; ++i) {
833 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
834 inputs[i].checked = 'checked';
839 function deselect_all_in(elTagName, elClass, elId) {
840 var inputs = document.getElementsByTagName('INPUT');
841 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
842 for(var i = 0; i < inputs.length; ++i) {
843 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
844 inputs[i].checked = '';
849 function confirm_if(expr, message) {
853 return confirm(message);
858 findParentNode (start, elementName, elementClass, elementID)
860 Travels up the DOM hierarchy to find a parent element with the
861 specified tag name, class, and id. All conditions must be met,
862 but any can be ommitted. Returns the BODY element if no match
865 function findParentNode(el, elName, elClass, elId) {
866 while (el.nodeName.toUpperCase() != 'BODY') {
867 if ((!elName || el.nodeName.toUpperCase() == elName) &&
868 (!elClass || el.className.indexOf(elClass) != -1) &&
869 (!elId || el.id == elId)) {
877 findChildNode (start, elementName, elementClass, elementID)
879 Travels down the DOM hierarchy to find all child elements with the
880 specified tag name, class, and id. All conditions must be met,
881 but any can be ommitted.
882 Doesn't examine children of matches.
884 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
885 var children = new Array();
886 for (var i = 0; i < start.childNodes.length; i++) {
887 var classfound = false;
888 var child = start.childNodes[i];
889 if((child.nodeType == 1) &&//element node type
890 (elementClass && (typeof(child.className)=='string'))) {
891 var childClasses = child.className.split(/\s+/);
892 for (var childClassIndex in childClasses) {
893 if (childClasses[childClassIndex]==elementClass) {
899 if(child.nodeType == 1) { //element node type
900 if ( (!tagName || child.nodeName == tagName) &&
901 (!elementClass || classfound)&&
902 (!elementID || child.id == elementID) &&
903 (!elementName || child.name == elementName))
905 children = children.concat(child);
907 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
914 function unmaskPassword(id) {
915 var pw = document.getElementById(id);
916 var chb = document.getElementById(id+'unmask');
919 // first try IE way - it can not set name attribute later
921 var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
923 var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
925 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
927 var newpw = document.createElement('input');
928 newpw.setAttribute('name', pw.name);
930 newpw.setAttribute('type', 'text');
932 newpw.setAttribute('type', 'password');
934 newpw.setAttribute('class', pw.getAttribute('class'));
937 newpw.size = pw.size;
938 newpw.onblur = pw.onblur;
939 newpw.onchange = pw.onchange;
940 newpw.value = pw.value;
941 pw.parentNode.replaceChild(newpw, pw);
944 function filterByParent(elCollection, parentFinder) {
945 var filteredCollection = [];
946 for (var i = 0; i < elCollection.length; ++i) {
947 var findParent = parentFinder(elCollection[i]);
948 if (findParent.nodeName.toUpperCase() != 'BODY') {
949 filteredCollection.push(elCollection[i]);
952 return filteredCollection;
956 All this is here just so that IE gets to handle oversized blocks
957 in a visually pleasing manner. It does a browser detect. So sue me.
960 function fix_column_widths() {
961 var agt = navigator.userAgent.toLowerCase();
962 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
963 fix_column_width('left-column');
964 fix_column_width('right-column');
968 function fix_column_width(colName) {
969 if(column = document.getElementById(colName)) {
970 if(!column.offsetWidth) {
971 setTimeout("fix_column_width('" + colName + "')", 20);
976 var nodes = column.childNodes;
978 for(i = 0; i < nodes.length; ++i) {
979 if(nodes[i].className.indexOf("block") != -1 ) {
980 if(width < nodes[i].offsetWidth) {
981 width = nodes[i].offsetWidth;
986 for(i = 0; i < nodes.length; ++i) {
987 if(nodes[i].className.indexOf("block") != -1 ) {
988 nodes[i].style.width = width + 'px';
996 Insert myValue at current cursor position
998 function insertAtCursor(myField, myValue) {
1000 if (document.selection) {
1002 sel = document.selection.createRange();
1005 // Mozilla/Netscape support
1006 else if (myField.selectionStart || myField.selectionStart == '0') {
1007 var startPos = myField.selectionStart;
1008 var endPos = myField.selectionEnd;
1009 myField.value = myField.value.substring(0, startPos)
1010 + myValue + myField.value.substring(endPos, myField.value.length);
1012 myField.value += myValue;
1018 Call instead of setting window.onload directly or setting body onload=.
1019 Adds your function to a chain of functions rather than overwriting anything
1022 function addonload(fn) {
1023 var oldhandler=window.onload;
1024 window.onload=function() {
1025 if(oldhandler) oldhandler();
1030 * Replacement for getElementsByClassName in browsers that aren't cool enough
1032 * Relying on the built-in getElementsByClassName is far, far faster than
1035 * Note: the third argument used to be an object with odd behaviour. It now
1036 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1037 * mimicked if you pass an object.
1039 * @param {Node} oElm The top-level node for searching. To search a whole
1040 * document, use `document`.
1041 * @param {String} strTagName filter by tag names
1042 * @param {String} name same as HTML5 spec
1044 function getElementsByClassName(oElm, strTagName, name) {
1045 // for backwards compatibility
1046 if(typeof name == "object") {
1047 var names = new Array();
1048 for(var i=0; i<name.length; i++) names.push(names[i]);
1049 name = names.join('');
1051 // use native implementation if possible
1052 if (oElm.getElementsByClassName && Array.filter) {
1053 if (strTagName == '*') {
1054 return oElm.getElementsByClassName(name);
1056 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1057 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1061 // native implementation unavailable, fall back to slow method
1062 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1063 var arrReturnElements = new Array();
1064 var arrRegExpClassNames = new Array();
1065 var names = name.split(' ');
1066 for(var i=0; i<names.length; i++) {
1067 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1071 for(var j=0; j<arrElements.length; j++) {
1072 oElement = arrElements[j];
1074 for(var k=0; k<arrRegExpClassNames.length; k++) {
1075 if(!arrRegExpClassNames[k].test(oElement.className)) {
1076 bMatchesAll = false;
1081 arrReturnElements.push(oElement);
1084 return (arrReturnElements)
1087 function openpopup(event, args) {
1090 if (event.preventDefault) {
1091 event.preventDefault();
1093 event.returnValue = false;
1097 var fullurl = args.url;
1098 if (!args.url.match(/https?:\/\//)) {
1099 fullurl = M.cfg.wwwroot + args.url;
1101 var windowobj = window.open(fullurl,args.name,args.options);
1105 if (args.fullscreen) {
1106 windowobj.moveTo(0,0);
1107 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1114 /** Close the current browser window. */
1115 function close_window(e) {
1116 if (e.preventDefault) {
1119 e.returnValue = false;
1125 * Used in a couple of modules to hide navigation areas when using AJAX
1128 function show_item(itemid) {
1129 var item = document.getElementById(itemid);
1131 item.style.display = "";
1135 function destroy_item(itemid) {
1136 var item = document.getElementById(itemid);
1138 item.parentNode.removeChild(item);
1142 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1143 * @param controlid the control id.
1145 function focuscontrol(controlid) {
1146 var control = document.getElementById(controlid);
1153 * Transfers keyboard focus to an HTML element based on the old style style of focus
1154 * This function should be removed as soon as it is no longer used
1156 function old_onload_focus(formid, controlname) {
1157 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1158 document.forms[formid].elements[controlname].focus();
1162 function build_querystring(obj) {
1163 return convert_object_to_string(obj, '&');
1166 function build_windowoptionsstring(obj) {
1167 return convert_object_to_string(obj, ',');
1170 function convert_object_to_string(obj, separator) {
1171 if (typeof obj !== 'object') {
1176 k = encodeURIComponent(k);
1178 if(obj[k] instanceof Array) {
1179 for(var i in value) {
1180 list.push(k+'[]='+encodeURIComponent(value[i]));
1183 list.push(k+'='+encodeURIComponent(value));
1186 return list.join(separator);
1189 function stripHTML(str) {
1190 var re = /<\S[^><]*>/g;
1191 var ret = str.replace(re, "");
1195 Number.prototype.fixed=function(n){
1197 return round(Number(this)*pow(10,n))/pow(10,n);
1199 function update_progress_bar (id, width, pt, msg, es){
1201 var status = document.getElementById("status_"+id);
1202 var percent_indicator = document.getElementById("pt_"+id);
1203 var progress_bar = document.getElementById("progress_"+id);
1204 var time_es = document.getElementById("time_"+id);
1205 status.innerHTML = msg;
1206 percent_indicator.innerHTML = percent.fixed(2) + '%';
1207 if(percent == 100) {
1208 progress_bar.style.background = "green";
1209 time_es.style.display = "none";
1211 progress_bar.style.background = "#FFCC66";
1213 time_es.innerHTML = "";
1215 time_es.innerHTML = es.fixed(2)+" sec";
1216 time_es.style.display
1220 progress_bar.style.width = width + "px";
1225 // ===== Deprecated core Javascript functions for Moodle ====
1226 // DO NOT USE!!!!!!!
1227 // Do not put this stuff in separate file because it only adds extra load on servers!
1230 * Used in a couple of modules to hide navigation areas when using AJAX
1232 function hide_item(itemid) {
1233 // use class='hiddenifjs' instead
1234 var item = document.getElementById(itemid);
1236 item.style.display = "none";
1240 M.util.help_icon = {
1243 add : function(Y, properties) {
1245 properties.node = Y.one('#'+properties.id);
1246 if (properties.node) {
1247 properties.node.on('click', this.display, this, properties);
1250 display : function(event, args) {
1251 event.preventDefault();
1252 if (M.util.help_icon.instance === null) {
1253 var Y = M.util.help_icon.Y;
1254 Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', function(Y) {
1255 var help_content_overlay = {
1260 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
1261 // Create an overlay from markup
1262 this.overlay = new Y.Overlay({
1263 headerContent: closebtn,
1270 this.overlay.render(Y.one(document.body));
1272 closebtn.on('click', this.overlay.hide, this.overlay);
1274 var boundingBox = this.overlay.get("boundingBox");
1276 // Hide the menu if the user clicks outside of its content
1277 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1278 var oTarget = event.target;
1279 var menuButton = Y.one("#"+args.id);
1281 if (!oTarget.compareTo(menuButton) &&
1282 !menuButton.contains(oTarget) &&
1283 !oTarget.compareTo(boundingBox) &&
1284 !boundingBox.contains(oTarget)) {
1285 this.overlay.hide();
1289 Y.on("key", this.close, closebtn , "down:13", this);
1290 closebtn.on('click', this.close, this);
1293 close : function(e) {
1295 this.helplink.focus();
1296 this.overlay.hide();
1299 display : function(event, args) {
1300 this.helplink = args.node;
1301 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1302 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
1304 var fullurl = args.url;
1305 if (!args.url.match(/https?:\/\//)) {
1306 fullurl = M.cfg.wwwroot + args.url;
1309 var ajaxurl = fullurl + '&ajax=1';
1315 success: function(id, o, node) {
1316 this.display_callback(o.responseText);
1318 failure: function(id, o, node) {
1319 var debuginfo = o.statusText;
1320 if (M.cfg.developerdebug) {
1321 o.statusText += ' (' + ajaxurl + ')';
1323 this.display_callback('bodyContent',debuginfo);
1329 this.overlay.show();
1331 Y.one('#closehelpbox').focus();
1334 display_callback : function(content) {
1335 this.overlay.set('bodyContent', content);
1338 hideContent : function() {
1340 help.overlay.hide();
1343 help_content_overlay.init();
1344 M.util.help_icon.instance = help_content_overlay;
1345 M.util.help_icon.instance.display(event, args);
1348 M.util.help_icon.instance.display(event, args);
1351 init : function(Y) {
1357 * Custom menu namespace
1359 M.core_custom_menu = {
1361 * This method is used to initialise a custom menu given the id that belongs
1362 * to the custom menu's root node.
1365 * @param {string} nodeid
1367 init : function(Y, nodeid) {
1368 var node = Y.one('#'+nodeid);
1370 Y.use('node-menunav', function(Y) {
1372 // Remove the javascript-disabled class.... obviously javascript is enabled.
1373 node.removeClass('javascript-disabled');
1374 // Initialise the menunav plugin
1375 node.plug(Y.Plugin.NodeMenuNav);
1382 * Used to store form manipulation methods and enhancments
1384 M.form = M.form || {};
1387 * Converts a nbsp indented select box into a multi drop down custom control much
1388 * like the custom menu. It also selectable categories on or off.
1390 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1393 * @param {string} id
1394 * @param {Array} options
1396 M.form.init_smartselect = function(Y, id, options) {
1397 if (!id.match(/^id_/)) {
1400 var select = Y.one('select#'+id);
1404 Y.use('event-delegate',function(){
1410 currentvalue : null,
1414 selectablecategories : true,
1422 init : function(Y, id, args, nodes) {
1423 if (typeof(args)=='object') {
1424 for (var i in this.cfg) {
1425 if (args[i] || args[i]===false) {
1426 this.cfg[i] = args[i];
1431 // Display a loading message first up
1432 this.nodes.select = nodes.select;
1434 this.currentvalue = this.nodes.select.get('selectedIndex');
1435 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1437 var options = Array();
1438 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1439 this.nodes.select.all('option').each(function(option, index) {
1440 var rawtext = option.get('innerHTML');
1441 var text = rawtext.replace(/^( )*/, '');
1442 if (rawtext === text) {
1443 text = rawtext.replace(/^(\s)*/, '');
1444 var depth = (rawtext.length - text.length ) + 1;
1446 var depth = ((rawtext.length - text.length )/12)+1;
1448 option.set('innerHTML', text);
1449 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1452 this.structure = [];
1453 var structcount = 0;
1454 for (var i in options) {
1457 this.structure.push(o);
1461 var current = this.structure[structcount-1];
1462 for (var j = 0; j < o.depth-1;j++) {
1463 if (current && current.children) {
1464 current = current.children[current.children.length-1];
1467 if (current && current.children) {
1468 current.children.push(o);
1473 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1474 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1475 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1476 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1478 if (this.cfg.mode == null) {
1479 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1480 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1481 this.cfg.mode = 'compact';
1483 this.cfg.mode = 'spanning';
1487 if (this.cfg.mode == 'compact') {
1488 this.nodes.menu.addClass('compactmenu');
1490 this.nodes.menu.addClass('spanningmenu');
1491 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1494 Y.one(document.body).append(this.nodes.menu);
1495 var pos = this.nodes.select.getXY();
1497 this.nodes.menu.setXY(pos);
1498 this.nodes.menu.on('click', this.handle_click, this);
1500 Y.one(window).on('resize', function(){
1501 var pos = this.nodes.select.getXY();
1503 this.nodes.menu.setXY(pos);
1506 generate_menu_content : function() {
1507 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1508 content += this.generate_submenu_content(this.structure[0], true);
1509 content += '</ul></div>';
1512 generate_submenu_content : function(item, rootelement) {
1513 this.submenucount++;
1515 if (item.children.length > 0) {
1517 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1518 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1519 content += '<div class="smartselect_menu_content">';
1521 content += '<li class="smartselect_submenuitem">';
1522 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1523 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1524 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1525 content += '<div class="smartselect_submenu_content">';
1528 for (var i in item.children) {
1529 content += this.generate_submenu_content(item.children[i],false);
1532 content += '</div>';
1533 content += '</div>';
1539 content += '<li class="smartselect_menuitem">';
1540 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1545 select : function(e) {
1548 this.currenttext = t.get('innerHTML');
1549 this.currentvalue = t.getAttribute('value');
1550 this.nodes.select.set('selectedIndex', this.currentvalue);
1553 handle_click : function(e) {
1554 var target = e.target;
1555 if (target.hasClass('smartselect_mask')) {
1557 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1559 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1560 this.show_sub_menu(e);
1563 show_menu : function(e) {
1565 var menu = e.target.ancestor().one('.smartselect_menu');
1566 menu.addClass('visible');
1567 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1569 show_sub_menu : function(e) {
1571 var target = e.target;
1572 if (!target.hasClass('smartselect_submenuitem')) {
1573 target = target.ancestor('.smartselect_submenuitem');
1575 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1576 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1579 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1580 target.one('.smartselect_submenu').addClass('visible');
1582 hide_menu : function() {
1583 this.nodes.menu.all('.visible').removeClass('visible');
1584 if (this.shownevent) {
1585 this.shownevent.detach();
1589 smartselect.init(Y, id, options, {select:select});
1593 /** List of flv players to be loaded */
1594 M.util.video_players = [];
1595 /** List of mp3 players to be loaded */
1596 M.util.audio_players = [];
1600 * @param id element id
1601 * @param fileurl media url
1604 * @param autosize true means detect size from media
1606 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1607 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1616 M.util.add_audio_player = function (id, fileurl, small) {
1617 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1621 * Initialise all audio and video player, must be called from page footer.
1623 M.util.load_flowplayer = function() {
1624 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1627 if (typeof(flowplayer) == 'undefined') {
1630 var embed_function = function() {
1631 if (loaded || typeof(flowplayer) == 'undefined') {
1639 /* TODO: add CSS color overrides for the flv flow player */
1641 for(var i=0; i<M.util.video_players.length; i++) {
1642 var video = M.util.video_players[i];
1643 if (video.width > 0 && video.height > 0) {
1644 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1646 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1648 flowplayer(video.id, src, {
1649 plugins: {controls: controls},
1651 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1652 onMetaData: function(clip) {
1653 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1654 clip.mvideo.resized = true;
1655 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1656 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1657 // bad luck, we have to guess - we may not get metadata at all
1658 var width = clip.width;
1659 var height = clip.height;
1661 var width = clip.metaData.width;
1662 var height = clip.metaData.height;
1664 var minwidth = 300; // controls are messed up in smaller objects
1665 if (width < minwidth) {
1666 height = (height * minwidth) / width;
1670 var object = this._api();
1671 object.width = width;
1672 object.height = height;
1678 if (M.util.audio_players.length == 0) {
1691 backgroundGradient: [0.5,0,0.3]
1695 for (var j=0; j < document.styleSheets.length; j++) {
1696 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1697 var allrules = document.styleSheets[j].rules;
1698 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1699 var allrules = document.styleSheets[j].cssRules;
1704 for(var i=0; i<allrules.length; i++) {
1706 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1707 if (typeof(allrules[i].cssText) != 'undefined') {
1708 rule = allrules[i].cssText;
1709 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1710 rule = allrules[i].style.cssText;
1712 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1713 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1714 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1715 controls[colprop] = rule;
1722 for(i=0; i<M.util.audio_players.length; i++) {
1723 var audio = M.util.audio_players[i];
1725 controls.controlall = false;
1726 controls.height = 15;
1727 controls.time = false;
1729 controls.controlall = true;
1730 controls.height = 25;
1731 controls.time = true;
1733 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1734 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1735 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1740 if (M.cfg.jsrev == -10) {
1741 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1743 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1745 var fileref = document.createElement('script');
1746 fileref.setAttribute('type','text/javascript');
1747 fileref.setAttribute('src', jsurl);
1748 fileref.onload = embed_function;
1749 fileref.onreadystatechange = embed_function;
1750 document.getElementsByTagName('head')[0].appendChild(fileref);