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 loaded from YUI.
6 * @param {Array} modules
8 M.yui.add_module = function(modules) {
9 for (var modname in modules) {
10 YUI_config.modules[modname] = modules[modname];
12 // Ensure thaat the YUI_config is applied to the main YUI instance.
13 Y.applyConfig(YUI_config);
16 * The gallery version to use when loading YUI modules from the gallery.
17 * Will be changed every time when using local galleries.
19 M.yui.galleryversion = '2010.04.21-21-51';
22 * Various utility functions
24 M.util = M.util || {};
27 * Language strings - initialised from page footer.
32 * Returns url for images.
33 * @param {String} imagename
34 * @param {String} component
37 M.util.image_url = function(imagename, component) {
39 if (!component || component == '' || component == 'moodle' || component == 'core') {
43 var url = M.cfg.wwwroot + '/theme/image.php';
44 if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
45 if (!M.cfg.svgicons) {
48 url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
50 url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
51 if (!M.cfg.svgicons) {
59 M.util.in_array = function(item, array){
60 for( var i = 0; i<array.length; i++){
69 * Init a collapsible region, see print_collapsible_region in weblib.php
70 * @param {YUI} Y YUI3 instance with all libraries loaded
71 * @param {String} id the HTML id for the div.
72 * @param {String} userpref the user preference that records the state of this box. false if none.
73 * @param {String} strtooltip
75 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
76 Y.use('anim', function(Y) {
77 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
82 * Object to handle a collapsible region : instantiate and forget styled object
86 * @param {YUI} Y YUI3 instance with all libraries loaded
87 * @param {String} id The HTML id for the div.
88 * @param {String} userpref The user preference that records the state of this box. false if none.
89 * @param {String} strtooltip
91 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
92 // Record the pref name
93 this.userpref = userpref;
95 // Find the divs in the document.
96 this.div = Y.one('#'+id);
98 // Get the caption for the collapsible region
99 var caption = this.div.one('#'+id + '_caption');
102 var a = Y.Node.create('<a href="#"></a>');
103 a.setAttribute('title', strtooltip);
105 // Get all the nodes from caption, remove them and append them to <a>
106 while (caption.hasChildNodes()) {
107 child = caption.get('firstChild');
113 // Get the height of the div at this point before we shrink it if required
114 var height = this.div.get('offsetHeight');
115 var collapsedimage = 't/collapsed'; // ltr mode
116 if (right_to_left()) {
117 collapsedimage = 't/collapsed_rtl';
119 collapsedimage = 't/collapsed';
121 if (this.div.hasClass('collapsed')) {
122 // Add the correct image and record the YUI node created in the process
123 this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
124 // Shrink the div as it is collapsed by default
125 this.div.setStyle('height', caption.get('offsetHeight')+'px');
127 // Add the correct image and record the YUI node created in the process
128 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
132 // Create the animation.
133 var animation = new Y.Anim({
136 easing: Y.Easing.easeBoth,
137 to: {height:caption.get('offsetHeight')},
138 from: {height:height}
141 // Handler for the animation finishing.
142 animation.on('end', function() {
143 this.div.toggleClass('collapsed');
144 var collapsedimage = 't/collapsed'; // ltr mode
145 if (right_to_left()) {
146 collapsedimage = 't/collapsed_rtl';
148 collapsedimage = 't/collapsed';
150 if (this.div.hasClass('collapsed')) {
151 this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
153 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
157 // Hook up the event handler.
158 a.on('click', function(e, animation) {
160 // Animate to the appropriate size.
161 if (animation.get('running')) {
164 animation.set('reverse', this.div.hasClass('collapsed'));
165 // Update the user preference.
167 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
174 * The user preference that stores the state of this box.
178 M.util.CollapsibleRegion.prototype.userpref = null;
181 * The key divs that make up this
185 M.util.CollapsibleRegion.prototype.div = null;
188 * The key divs that make up this
192 M.util.CollapsibleRegion.prototype.icon = null;
195 * Makes a best effort to connect back to Moodle to update a user preference,
196 * however, there is no mechanism for finding out if the update succeeded.
198 * Before you can use this function in your JavsScript, you must have called
199 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
200 * the udpate is allowed, and how to safely clean and submitted values.
202 * @param String name the name of the setting to udpate.
203 * @param String the value to set it to.
205 M.util.set_user_preference = function(name, value) {
206 YUI().use('io', function(Y) {
207 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
208 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
210 // If we are a developer, ensure that failures are reported.
215 if (M.cfg.developerdebug) {
216 cfg.on.failure = function(id, o, args) {
217 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
227 * Prints a confirmation dialog in the style of DOM.confirm().
229 * @method show_confirm_dialog
230 * @param {EventFacade} e
231 * @param {Object} args
232 * @param {String} args.message The question to ask the user
233 * @param {Function} [args.callback] A callback to apply on confirmation.
234 * @param {Object} [args.scope] The scope to use when calling the callback.
235 * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
236 * @param {String} [args.cancellabel] The label to use on the cancel button.
237 * @param {String} [args.continuelabel] The label to use on the continue button.
239 M.util.show_confirm_dialog = function(e, args) {
240 var target = e.target;
241 if (e.preventDefault) {
245 YUI().use('moodle-core-notification-confirm', function(Y) {
246 var confirmationDialogue = new M.core.confirm({
252 title: M.util.get_string('confirmation', 'admin'),
253 noLabel: M.util.get_string('cancel', 'moodle'),
254 question: args.message
257 // The dialogue was submitted with a positive value indication.
258 confirmationDialogue.on('complete-yes', function(e) {
259 // Handle any callbacks.
261 if (!Y.Lang.isFunction(args.callback)) {
262 Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
263 'warn', 'M.util.show_confirm_dialog');
267 var scope = e.target;
268 if (Y.Lang.isObject(args.scope)) {
272 var callbackargs = args.callbackargs || [];
273 args.callback.apply(scope, callbackargs);
277 var targetancestor = null,
280 if (target.test('a')) {
281 window.location = target.get('href');
283 } else if ((targetancestor = target.ancestor('a')) !== null) {
284 window.location = targetancestor.get('href');
286 } else if (target.test('input') || target.test('button')) {
287 targetform = target.ancestor('form', true);
291 if (target.get('name') && target.get('value')) {
292 targetform.append('<input type="hidden" name="' + target.get('name') +
293 '" value="' + target.get('value') + '">');
297 } else if (target.test('form')) {
301 Y.log("Element of type " + target.get('tagName') +
302 " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, BUTTON or FORM",
303 'warn', 'javascript-static');
307 if (args.cancellabel) {
308 confirmationDialogue.set('noLabel', args.cancellabel);
311 if (args.continuelabel) {
312 confirmationDialogue.set('yesLabel', args.continuelabel);
315 confirmationDialogue.render()
320 /** Useful for full embedding of various stuff */
321 M.util.init_maximised_embed = function(Y, id) {
322 var obj = Y.one('#'+id);
327 var get_htmlelement_size = function(el, prop) {
328 if (Y.Lang.isString(el)) {
329 el = Y.one('#' + el);
331 // Ensure element exists.
333 var val = el.getStyle(prop);
335 val = el.getComputedStyle(prop);
347 var resize_object = function() {
348 obj.setStyle('display', 'none');
349 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
351 if (newwidth > 500) {
352 obj.setStyle('width', newwidth + 'px');
354 obj.setStyle('width', '500px');
357 var headerheight = get_htmlelement_size('page-header', 'height');
358 var footerheight = get_htmlelement_size('page-footer', 'height');
359 var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
360 if (newheight < 400) {
363 obj.setStyle('height', newheight+'px');
364 obj.setStyle('display', '');
368 // fix layout if window resized too
369 Y.use('event-resize', function (Y) {
370 Y.on("windowresize", function() {
377 * Breaks out all links to the top frame - used in frametop page layout.
379 M.util.init_frametop = function(Y) {
380 Y.all('a').each(function(node) {
381 node.set('target', '_top');
383 Y.all('form').each(function(node) {
384 node.set('target', '_top');
389 * Finds all nodes that match the given CSS selector and attaches events to them
390 * so that they toggle a given classname when clicked.
393 * @param {string} id An id containing elements to target
394 * @param {string} cssselector A selector to use to find targets
395 * @param {string} toggleclassname A classname to toggle
397 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
399 if (togglecssselector == '') {
400 togglecssselector = cssselector;
403 var node = Y.one('#'+id);
404 node.all(cssselector).each(function(n){
405 n.on('click', function(e){
407 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
408 if (this.test(togglecssselector)) {
409 this.toggleClass(toggleclassname);
411 this.ancestor(togglecssselector).toggleClass(toggleclassname);
416 // Attach this click event to the node rather than all selectors... will be much better
418 node.on('click', function(e){
419 if (e.target.hasClass('addtoall')) {
420 this.all(togglecssselector).addClass(toggleclassname);
421 } else if (e.target.hasClass('removefromall')) {
422 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
428 * Initialises a colour picker
430 * Designed to be used with admin_setting_configcolourpicker although could be used
431 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
432 * above or below the input (must have the same parent) and then call this with the
435 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
436 * contrib/blocks. For better docs refer to that.
440 * @param {object} previewconf
442 M.util.init_colour_picker = function(Y, id, previewconf) {
444 * We need node and event-mouseenter
446 Y.use('node', 'event-mouseenter', function(){
448 * The colour picker object
457 eventMouseEnter : null,
458 eventMouseLeave : null,
459 eventMouseMove : null,
464 * Initalises the colour picker by putting everything together and wiring the events
467 this.input = Y.one('#'+id);
468 this.box = this.input.ancestor().one('.admin_colourpicker');
469 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
470 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
471 this.preview = Y.Node.create('<div class="previewcolour"></div>');
472 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
473 this.current = Y.Node.create('<div class="currentcolour"></div>');
474 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
475 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
477 if (typeof(previewconf) === 'object' && previewconf !== null) {
478 Y.one('#'+id+'_preview').on('click', function(e){
479 if (Y.Lang.isString(previewconf.selector)) {
480 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
482 for (var i in previewconf.selector) {
483 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
489 this.eventClick = this.image.on('click', this.pickColour, this);
490 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
493 * Starts to follow the mouse once it enter the image
495 startFollow : function(e) {
496 this.eventMouseEnter.detach();
497 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
498 this.eventMouseMove = this.image.on('mousemove', function(e){
499 this.preview.setStyle('backgroundColor', this.determineColour(e));
503 * Stops following the mouse
505 endFollow : function(e) {
506 this.eventMouseMove.detach();
507 this.eventMouseLeave.detach();
508 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
511 * Picks the colour the was clicked on
513 pickColour : function(e) {
514 var colour = this.determineColour(e);
515 this.input.set('value', colour);
516 this.current.setStyle('backgroundColor', colour);
519 * Calculates the colour fromthe given co-ordinates
521 determineColour : function(e) {
522 var eventx = Math.floor(e.pageX-e.target.getX());
523 var eventy = Math.floor(e.pageY-e.target.getY());
525 var imagewidth = this.width;
526 var imageheight = this.height;
527 var factor = this.factor;
528 var colour = [255,0,0];
539 var matrixcount = matrices.length;
540 var limit = Math.round(imagewidth/matrixcount);
541 var heightbreak = Math.round(imageheight/2);
543 for (var x = 0; x < imagewidth; x++) {
544 var divisor = Math.floor(x / limit);
545 var matrix = matrices[divisor];
547 colour[0] += matrix[0]*factor;
548 colour[1] += matrix[1]*factor;
549 colour[2] += matrix[2]*factor;
556 var pixel = [colour[0], colour[1], colour[2]];
557 if (eventy < heightbreak) {
558 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
559 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
560 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
561 } else if (eventy > heightbreak) {
562 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
563 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
564 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
567 return this.convert_rgb_to_hex(pixel);
570 * Converts an RGB value to Hex
572 convert_rgb_to_hex : function(rgb) {
574 var hexchars = "0123456789ABCDEF";
575 for (var i=0; i<3; i++) {
576 var number = Math.abs(rgb[i]);
577 if (number == 0 || isNaN(number)) {
580 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
587 * Initialise the colour picker :) Hoorah
593 M.util.init_block_hider = function(Y, config) {
594 Y.use('base', 'node', function(Y) {
595 M.util.block_hider = M.util.block_hider || (function(){
596 var blockhider = function() {
597 blockhider.superclass.constructor.apply(this, arguments);
599 blockhider.prototype = {
600 initializer : function(config) {
601 this.set('block', '#'+this.get('id'));
602 var b = this.get('block'),
607 if (t && (a = t.one('.block_action'))) {
608 hide = Y.Node.create('<img />')
609 .addClass('block-hider-hide')
611 alt: config.tooltipVisible,
612 src: this.get('iconVisible'),
614 'title': config.tooltipVisible
616 hide.on('keypress', this.updateStateKey, this, true);
617 hide.on('click', this.updateState, this, true);
619 show = Y.Node.create('<img />')
620 .addClass('block-hider-show')
622 alt: config.tooltipHidden,
623 src: this.get('iconHidden'),
625 'title': config.tooltipHidden
627 show.on('keypress', this.updateStateKey, this, false);
628 show.on('click', this.updateState, this, false);
630 a.insert(show, 0).insert(hide, 0);
633 updateState : function(e, hide) {
634 M.util.set_user_preference(this.get('preference'), hide);
636 this.get('block').addClass('hidden');
637 this.get('block').one('.block-hider-show').focus();
639 this.get('block').removeClass('hidden');
640 this.get('block').one('.block-hider-hide').focus();
643 updateStateKey : function(e, hide) {
644 if (e.keyCode == 13) { //allow hide/show via enter key
645 this.updateState(this, hide);
649 Y.extend(blockhider, Y.Base, blockhider.prototype, {
655 value : M.util.image_url('t/switch_minus', 'moodle')
658 value : M.util.image_url('t/switch_plus', 'moodle')
661 setter : function(node) {
669 new M.util.block_hider(config);
674 * @var pending_js - The keys are the list of all pending js actions.
677 M.util.pending_js = [];
678 M.util.complete_js = [];
681 * Register any long running javascript code with a unique identifier.
682 * Should be followed with a call to js_complete with a matching
683 * idenfitier when the code is complete. May also be called with no arguments
684 * to test if there is any js calls pending. This is relied on by behat so that
685 * it can wait for all pending updates before interacting with a page.
686 * @param String uniqid - optional, if provided,
687 * registers this identifier until js_complete is called.
688 * @return boolean - True if there is any pending js.
690 M.util.js_pending = function(uniqid) {
691 if (uniqid !== false) {
692 M.util.pending_js.push(uniqid);
695 return M.util.pending_js.length;
699 M.util.js_pending('init');
702 * Register listeners for Y.io start/end so we can wait for them in behat.
704 YUI.add('moodle-core-io', function(Y) {
705 Y.on('io:start', function(id) {
706 M.util.js_pending('io:' + id);
708 Y.on('io:end', function(id) {
709 M.util.js_complete('io:' + id);
719 * Unregister any long running javascript code by unique identifier.
720 * This function should form a matching pair with js_pending
722 * @param String uniqid - required, unregisters this identifier
723 * @return boolean - True if there is any pending js.
725 M.util.js_complete = function(uniqid) {
726 // Use the Y.Array.indexOf instead of the native because some older browsers do not support
727 // the native function. Y.Array polyfills the native function if it does not exist.
728 var index = Y.Array.indexOf(M.util.pending_js, uniqid);
730 M.util.complete_js.push(M.util.pending_js.splice(index, 1));
733 return M.util.pending_js.length;
737 * Returns a string registered in advance for usage in JavaScript
739 * If you do not pass the third parameter, the function will just return
740 * the corresponding value from the M.str object. If the third parameter is
741 * provided, the function performs {$a} placeholder substitution in the
742 * same way as PHP get_string() in Moodle does.
744 * @param {String} identifier string identifier
745 * @param {String} component the component providing the string
746 * @param {Object|String} a optional variable to populate placeholder with
748 M.util.get_string = function(identifier, component, a) {
751 if (M.cfg.developerdebug) {
752 // creating new instance if YUI is not optimal but it seems to be better way then
753 // require the instance via the function API - note that it is used in rare cases
754 // for debugging only anyway
755 // To ensure we don't kill browser performance if hundreds of get_string requests
756 // are made we cache the instance we generate within the M.util namespace.
757 // We don't publicly define the variable so that it doesn't get abused.
758 if (typeof M.util.get_string_yui_instance === 'undefined') {
759 M.util.get_string_yui_instance = new YUI({ debug : true });
761 var Y = M.util.get_string_yui_instance;
764 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
765 stringvalue = '[[' + identifier + ',' + component + ']]';
766 if (M.cfg.developerdebug) {
767 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
772 stringvalue = M.str[component][identifier];
774 if (typeof a == 'undefined') {
775 // no placeholder substitution requested
779 if (typeof a == 'number' || typeof a == 'string') {
780 // replace all occurrences of {$a} with the placeholder value
781 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
785 if (typeof a == 'object') {
786 // replace {$a->key} placeholders
788 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
789 if (M.cfg.developerdebug) {
790 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
794 var search = '{$a->' + key + '}';
795 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
796 search = new RegExp(search, 'g');
797 stringvalue = stringvalue.replace(search, a[key]);
802 if (M.cfg.developerdebug) {
803 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
809 * Set focus on username or password field of the login form
811 M.util.focus_login_form = function(Y) {
812 var username = Y.one('#username');
813 var password = Y.one('#password');
815 if (username == null || password == null) {
816 // something is wrong here
820 var curElement = document.activeElement
821 if (curElement == 'undefined') {
822 // legacy browser - skip refocus protection
823 } else if (curElement.tagName == 'INPUT') {
824 // user was probably faster to focus something, do not mess with focus
828 if (username.get('value') == '') {
836 * Set focus on login error message
838 M.util.focus_login_error = function(Y) {
839 var errorlog = Y.one('#loginerrormessage');
846 * Adds lightbox hidden element that covers the whole node.
849 * @param {Node} the node lightbox should be added to
850 * @retun {Node} created lightbox node
852 M.util.add_lightbox = function(Y, node) {
853 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
855 // Check if lightbox is already there
856 if (node.one('.lightbox')) {
857 return node.one('.lightbox');
860 node.setStyle('position', 'relative');
861 var waiticon = Y.Node.create('<img />')
863 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
866 'position' : 'relative',
870 var lightbox = Y.Node.create('<div></div>')
873 'position' : 'absolute',
878 'backgroundColor' : 'white',
879 'textAlign' : 'center'
881 .setAttribute('class', 'lightbox')
884 lightbox.appendChild(waiticon);
885 node.append(lightbox);
890 * Appends a hidden spinner element to the specified node.
893 * @param {Node} the node the spinner should be added to
894 * @return {Node} created spinner node
896 M.util.add_spinner = function(Y, node) {
897 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
899 // Check if spinner is already there
900 if (node.one('.spinner')) {
901 return node.one('.spinner');
904 var spinner = Y.Node.create('<img />')
905 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
907 .addClass('iconsmall')
910 node.append(spinner);
914 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
916 function checkall() {
917 var inputs = document.getElementsByTagName('input');
918 for (var i = 0; i < inputs.length; i++) {
919 if (inputs[i].type == 'checkbox') {
920 if (inputs[i].disabled || inputs[i].readOnly) {
923 inputs[i].checked = true;
928 function checknone() {
929 var inputs = document.getElementsByTagName('input');
930 for (var i = 0; i < inputs.length; i++) {
931 if (inputs[i].type == 'checkbox') {
932 if (inputs[i].disabled || inputs[i].readOnly) {
935 inputs[i].checked = false;
941 * Either check, or uncheck, all checkboxes inside the element with id is
942 * @param id the id of the container
943 * @param checked the new state, either '' or 'checked'.
945 function select_all_in_element_with_id(id, checked) {
946 var container = document.getElementById(id);
950 var inputs = container.getElementsByTagName('input');
951 for (var i = 0; i < inputs.length; ++i) {
952 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
953 inputs[i].checked = checked;
958 function select_all_in(elTagName, elClass, elId) {
959 var inputs = document.getElementsByTagName('input');
960 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
961 for(var i = 0; i < inputs.length; ++i) {
962 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
963 inputs[i].checked = 'checked';
968 function deselect_all_in(elTagName, elClass, elId) {
969 var inputs = document.getElementsByTagName('INPUT');
970 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
971 for(var i = 0; i < inputs.length; ++i) {
972 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
973 inputs[i].checked = '';
978 function confirm_if(expr, message) {
982 return confirm(message);
987 findParentNode (start, elementName, elementClass, elementID)
989 Travels up the DOM hierarchy to find a parent element with the
990 specified tag name, class, and id. All conditions must be met,
991 but any can be ommitted. Returns the BODY element if no match
994 function findParentNode(el, elName, elClass, elId) {
995 while (el.nodeName.toUpperCase() != 'BODY') {
996 if ((!elName || el.nodeName.toUpperCase() == elName) &&
997 (!elClass || el.className.indexOf(elClass) != -1) &&
998 (!elId || el.id == elId)) {
1006 function unmaskPassword(id) {
1007 var pw = document.getElementById(id);
1008 var chb = document.getElementById(id+'unmask');
1010 // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1011 // Replacing existing child with a new one, removed all yui properties for the node. Therefore, this
1012 // functionality won't work in IE8 or lower.
1013 // This is a temporary fixed to allow other browsers to function properly.
1014 if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1018 pw.type = "password";
1020 } else { //IE Browser version 8 or lower
1022 // first try IE way - it can not set name attribute later
1024 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1026 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1028 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1030 var newpw = document.createElement('input');
1031 newpw.setAttribute('autocomplete', 'off');
1032 newpw.setAttribute('name', pw.name);
1034 newpw.setAttribute('type', 'text');
1036 newpw.setAttribute('type', 'password');
1038 newpw.setAttribute('class', pw.getAttribute('class'));
1041 newpw.size = pw.size;
1042 newpw.onblur = pw.onblur;
1043 newpw.onchange = pw.onchange;
1044 newpw.value = pw.value;
1045 pw.parentNode.replaceChild(newpw, pw);
1049 function filterByParent(elCollection, parentFinder) {
1050 var filteredCollection = [];
1051 for (var i = 0; i < elCollection.length; ++i) {
1052 var findParent = parentFinder(elCollection[i]);
1053 if (findParent.nodeName.toUpperCase() != 'BODY') {
1054 filteredCollection.push(elCollection[i]);
1057 return filteredCollection;
1061 All this is here just so that IE gets to handle oversized blocks
1062 in a visually pleasing manner. It does a browser detect. So sue me.
1065 function fix_column_widths() {
1066 var agt = navigator.userAgent.toLowerCase();
1067 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1068 fix_column_width('left-column');
1069 fix_column_width('right-column');
1073 function fix_column_width(colName) {
1074 if(column = document.getElementById(colName)) {
1075 if(!column.offsetWidth) {
1076 setTimeout("fix_column_width('" + colName + "')", 20);
1081 var nodes = column.childNodes;
1083 for(i = 0; i < nodes.length; ++i) {
1084 if(nodes[i].className.indexOf("block") != -1 ) {
1085 if(width < nodes[i].offsetWidth) {
1086 width = nodes[i].offsetWidth;
1091 for(i = 0; i < nodes.length; ++i) {
1092 if(nodes[i].className.indexOf("block") != -1 ) {
1093 nodes[i].style.width = width + 'px';
1101 Insert myValue at current cursor position
1103 function insertAtCursor(myField, myValue) {
1105 if (document.selection) {
1107 sel = document.selection.createRange();
1110 // Mozilla/Netscape support
1111 else if (myField.selectionStart || myField.selectionStart == '0') {
1112 var startPos = myField.selectionStart;
1113 var endPos = myField.selectionEnd;
1114 myField.value = myField.value.substring(0, startPos)
1115 + myValue + myField.value.substring(endPos, myField.value.length);
1117 myField.value += myValue;
1122 * Increment a file name.
1124 * @param string file name.
1125 * @param boolean ignoreextension do not extract the extension prior to appending the
1126 * suffix. Useful when incrementing folder names.
1127 * @return string the incremented file name.
1129 function increment_filename(filename, ignoreextension) {
1131 var basename = filename;
1133 // Split the file name into the basename + extension.
1134 if (!ignoreextension) {
1135 var dotpos = filename.lastIndexOf('.');
1136 if (dotpos !== -1) {
1137 basename = filename.substr(0, dotpos);
1138 extension = filename.substr(dotpos, filename.length);
1142 // Look to see if the name already has (NN) at the end of it.
1144 var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1145 if (hasnumber !== null) {
1146 // Note the current number & remove it from the basename.
1147 number = parseInt(hasnumber[2], 10);
1148 basename = hasnumber[1];
1152 var newname = basename + ' (' + number + ')' + extension;
1157 * Return whether we are in right to left mode or not.
1161 function right_to_left() {
1162 var body = Y.one('body');
1164 if (body && body.hasClass('dir-rtl')) {
1170 function openpopup(event, args) {
1173 if (event.preventDefault) {
1174 event.preventDefault();
1176 event.returnValue = false;
1180 // Make sure the name argument is set and valid.
1181 var nameregex = /[^a-z0-9_]/i;
1182 if (typeof args.name !== 'string') {
1183 args.name = '_blank';
1184 } else if (args.name.match(nameregex)) {
1185 // Cleans window name because IE does not support funky ones.
1186 if (M.cfg.developerdebug) {
1187 alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1189 args.name = args.name.replace(nameregex, '_');
1192 var fullurl = args.url;
1193 if (!args.url.match(/https?:\/\//)) {
1194 fullurl = M.cfg.wwwroot + args.url;
1196 if (args.fullscreen) {
1197 args.options = args.options.
1198 replace(/top=\d+/, 'top=0').
1199 replace(/left=\d+/, 'left=0').
1200 replace(/width=\d+/, 'width=' + screen.availWidth).
1201 replace(/height=\d+/, 'height=' + screen.availHeight);
1203 var windowobj = window.open(fullurl,args.name,args.options);
1208 if (args.fullscreen) {
1209 // In some browser / OS combinations (E.g. Chrome on Windows), the
1210 // window initially opens slighly too big. The width and heigh options
1211 // seem to control the area inside the browser window, so what with
1212 // scroll-bars, etc. the actual window is bigger than the screen.
1213 // Therefore, we need to fix things up after the window is open.
1214 var hackcount = 100;
1215 var get_size_exactly_right = function() {
1216 windowobj.moveTo(0, 0);
1217 windowobj.resizeTo(screen.availWidth, screen.availHeight);
1219 // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1220 // something like windowobj.resizeTo(1280, 1024) too soon (up to
1221 // about 50ms) after the window is open, then it actually behaves
1222 // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1223 // check that the resize actually worked, and if not, repeatedly try
1224 // again after a short delay until it works (but with a limit of
1225 // hackcount repeats.
1226 if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1228 setTimeout(get_size_exactly_right, 10);
1231 setTimeout(get_size_exactly_right, 0);
1238 /** Close the current browser window. */
1239 function close_window(e) {
1240 if (e.preventDefault) {
1243 e.returnValue = false;
1249 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1250 * @param controlid the control id.
1252 function focuscontrol(controlid) {
1253 var control = document.getElementById(controlid);
1260 * Transfers keyboard focus to an HTML element based on the old style style of focus
1261 * This function should be removed as soon as it is no longer used
1263 function old_onload_focus(formid, controlname) {
1264 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1265 document.forms[formid].elements[controlname].focus();
1269 function build_querystring(obj) {
1270 return convert_object_to_string(obj, '&');
1273 function build_windowoptionsstring(obj) {
1274 return convert_object_to_string(obj, ',');
1277 function convert_object_to_string(obj, separator) {
1278 if (typeof obj !== 'object') {
1283 k = encodeURIComponent(k);
1285 if(obj[k] instanceof Array) {
1286 for(var i in value) {
1287 list.push(k+'[]='+encodeURIComponent(value[i]));
1290 list.push(k+'='+encodeURIComponent(value));
1293 return list.join(separator);
1296 function stripHTML(str) {
1297 var re = /<\S[^><]*>/g;
1298 var ret = str.replace(re, "");
1302 function updateProgressBar(id, percent, msg, estimate) {
1304 el = document.getElementById(id),
1311 eventData.message = msg;
1312 eventData.percent = percent;
1313 eventData.estimate = estimate;
1316 event = new CustomEvent('update', {
1321 } catch (exception) {
1322 if (!(exception instanceof TypeError)) {
1325 event = document.createEvent('CustomEvent');
1326 event.initCustomEvent('update', false, true, eventData);
1327 event.prototype = window.Event.prototype;
1330 el.dispatchEvent(event);
1333 // ===== Deprecated core Javascript functions for Moodle ====
1334 // DO NOT USE!!!!!!!
1335 // Do not put this stuff in separate file because it only adds extra load on servers!
1339 * @deprecated since Moodle 2.7.
1342 function show_item() {
1343 throw new Error('show_item can not be used any more. Please use Y.Node.show.');
1347 * @method destroy_item
1348 * @deprecated since Moodle 2.7.
1349 * @see Y.Node.destroy
1351 function destroy_item() {
1352 throw new Error('destroy_item can not be used any more. Please use Y.Node.destroy.');
1357 * @deprecated since Moodle 2.7.
1360 function hide_item() {
1361 throw new Error('hide_item can not be used any more. Please use Y.Node.hide.');
1366 * @deprecated since Moodle 2.7 - please do not use this function any more.
1368 function addonload() {
1369 throw new Error('addonload can not be used any more.');
1373 * @method getElementsByClassName
1374 * @deprecated Since Moodle 2.7 - please do not use this function any more.
1378 function getElementsByClassName() {
1379 throw new Error('getElementsByClassName can not be used any more. Please use Y.one or Y.all.');
1383 * @method findChildNodes
1384 * @deprecated since Moodle 2.7 - please do not use this function any more.
1387 function findChildNodes() {
1388 throw new Error('findChildNodes can not be used any more. Please use Y.all.');
1391 M.util.help_popups = {
1392 setup : function(Y) {
1393 Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1395 open_popup : function(e) {
1396 // Prevent the default page action
1399 // Grab the anchor that was clicked
1400 var anchor = e.target.ancestor('a', true);
1403 'url' : anchor.getAttribute('href'),
1421 args.options = options.join(',');
1428 * Custom menu namespace
1430 M.core_custom_menu = {
1432 * This method is used to initialise a custom menu given the id that belongs
1433 * to the custom menu's root node.
1436 * @param {string} nodeid
1438 init : function(Y, nodeid) {
1439 var node = Y.one('#'+nodeid);
1441 Y.use('node-menunav', function(Y) {
1443 // Remove the javascript-disabled class.... obviously javascript is enabled.
1444 node.removeClass('javascript-disabled');
1445 // Initialise the menunav plugin
1446 node.plug(Y.Plugin.NodeMenuNav);
1453 * Used to store form manipulation methods and enhancments
1455 M.form = M.form || {};
1458 * Converts a nbsp indented select box into a multi drop down custom control much
1459 * like the custom menu. It also selectable categories on or off.
1461 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1464 * @param {string} id
1465 * @param {Array} options
1467 M.form.init_smartselect = function(Y, id, options) {
1468 if (!id.match(/^id_/)) {
1471 var select = Y.one('select#'+id);
1475 Y.use('event-delegate',function(){
1481 currentvalue : null,
1485 selectablecategories : true,
1493 init : function(Y, id, args, nodes) {
1494 if (typeof(args)=='object') {
1495 for (var i in this.cfg) {
1496 if (args[i] || args[i]===false) {
1497 this.cfg[i] = args[i];
1502 // Display a loading message first up
1503 this.nodes.select = nodes.select;
1505 this.currentvalue = this.nodes.select.get('selectedIndex');
1506 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1508 var options = Array();
1509 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1510 this.nodes.select.all('option').each(function(option, index) {
1511 var rawtext = option.get('innerHTML');
1512 var text = rawtext.replace(/^( )*/, '');
1513 if (rawtext === text) {
1514 text = rawtext.replace(/^(\s)*/, '');
1515 var depth = (rawtext.length - text.length ) + 1;
1517 var depth = ((rawtext.length - text.length )/12)+1;
1519 option.set('innerHTML', text);
1520 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1523 this.structure = [];
1524 var structcount = 0;
1525 for (var i in options) {
1528 this.structure.push(o);
1532 var current = this.structure[structcount-1];
1533 for (var j = 0; j < o.depth-1;j++) {
1534 if (current && current.children) {
1535 current = current.children[current.children.length-1];
1538 if (current && current.children) {
1539 current.children.push(o);
1544 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1545 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1546 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1547 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1549 if (this.cfg.mode == null) {
1550 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1551 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1552 this.cfg.mode = 'compact';
1554 this.cfg.mode = 'spanning';
1558 if (this.cfg.mode == 'compact') {
1559 this.nodes.menu.addClass('compactmenu');
1561 this.nodes.menu.addClass('spanningmenu');
1562 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1565 Y.one(document.body).append(this.nodes.menu);
1566 var pos = this.nodes.select.getXY();
1568 this.nodes.menu.setXY(pos);
1569 this.nodes.menu.on('click', this.handle_click, this);
1571 Y.one(window).on('resize', function(){
1572 var pos = this.nodes.select.getXY();
1574 this.nodes.menu.setXY(pos);
1577 generate_menu_content : function() {
1578 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1579 content += this.generate_submenu_content(this.structure[0], true);
1580 content += '</ul></div>';
1583 generate_submenu_content : function(item, rootelement) {
1584 this.submenucount++;
1586 if (item.children.length > 0) {
1588 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1589 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1590 content += '<div class="smartselect_menu_content">';
1592 content += '<li class="smartselect_submenuitem">';
1593 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1594 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1595 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1596 content += '<div class="smartselect_submenu_content">';
1599 for (var i in item.children) {
1600 content += this.generate_submenu_content(item.children[i],false);
1603 content += '</div>';
1604 content += '</div>';
1610 content += '<li class="smartselect_menuitem">';
1611 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1616 select : function(e) {
1619 this.currenttext = t.get('innerHTML');
1620 this.currentvalue = t.getAttribute('value');
1621 this.nodes.select.set('selectedIndex', this.currentvalue);
1624 handle_click : function(e) {
1625 var target = e.target;
1626 if (target.hasClass('smartselect_mask')) {
1628 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1630 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1631 this.show_sub_menu(e);
1634 show_menu : function(e) {
1636 var menu = e.target.ancestor().one('.smartselect_menu');
1637 menu.addClass('visible');
1638 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1640 show_sub_menu : function(e) {
1642 var target = e.target;
1643 if (!target.hasClass('smartselect_submenuitem')) {
1644 target = target.ancestor('.smartselect_submenuitem');
1646 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1647 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1650 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1651 target.one('.smartselect_submenu').addClass('visible');
1653 hide_menu : function() {
1654 this.nodes.menu.all('.visible').removeClass('visible');
1655 if (this.shownevent) {
1656 this.shownevent.detach();
1660 smartselect.init(Y, id, options, {select:select});
1664 /** List of flv players to be loaded */
1665 M.util.video_players = [];
1666 /** List of mp3 players to be loaded */
1667 M.util.audio_players = [];
1671 * @param id element id
1672 * @param fileurl media url
1675 * @param autosize true means detect size from media
1677 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1678 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1687 M.util.add_audio_player = function (id, fileurl, small) {
1688 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1692 * Initialise all audio and video player, must be called from page footer.
1694 M.util.load_flowplayer = function() {
1695 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1698 if (typeof(flowplayer) == 'undefined') {
1701 var embed_function = function() {
1702 if (loaded || typeof(flowplayer) == 'undefined') {
1708 url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1711 /* TODO: add CSS color overrides for the flv flow player */
1713 for(var i=0; i<M.util.video_players.length; i++) {
1714 var video = M.util.video_players[i];
1715 if (video.width > 0 && video.height > 0) {
1716 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', width: video.width, height: video.height};
1718 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php';
1720 flowplayer(video.id, src, {
1721 plugins: {controls: controls},
1723 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1724 onMetaData: function(clip) {
1725 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1726 clip.mvideo.resized = true;
1727 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1728 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1729 // bad luck, we have to guess - we may not get metadata at all
1730 var width = clip.width;
1731 var height = clip.height;
1733 var width = clip.metaData.width;
1734 var height = clip.metaData.height;
1736 var minwidth = 300; // controls are messed up in smaller objects
1737 if (width < minwidth) {
1738 height = (height * minwidth) / width;
1742 var object = this._api();
1743 object.width = width;
1744 object.height = height;
1750 if (M.util.audio_players.length == 0) {
1754 url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1764 backgroundGradient: [0.5,0,0.3]
1768 for (var j=0; j < document.styleSheets.length; j++) {
1770 // To avoid javascript security violation accessing cross domain stylesheets
1771 var allrules = false;
1773 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1774 allrules = document.styleSheets[j].rules;
1775 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1776 allrules = document.styleSheets[j].cssRules;
1785 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1790 for(var i=0; i<allrules.length; i++) {
1792 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1793 if (typeof(allrules[i].cssText) != 'undefined') {
1794 rule = allrules[i].cssText;
1795 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1796 rule = allrules[i].style.cssText;
1798 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1799 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1800 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1801 controls[colprop] = rule;
1808 for(i=0; i<M.util.audio_players.length; i++) {
1809 var audio = M.util.audio_players[i];
1811 controls.controlall = false;
1812 controls.height = 15;
1813 controls.time = false;
1815 controls.controlall = true;
1816 controls.height = 25;
1817 controls.time = true;
1819 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', {
1820 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf.php'}},
1821 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1826 if (M.cfg.jsrev == -1) {
1827 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1829 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1831 var fileref = document.createElement('script');
1832 fileref.setAttribute('type','text/javascript');
1833 fileref.setAttribute('src', jsurl);
1834 fileref.onload = embed_function;
1835 fileref.onreadystatechange = embed_function;
1836 document.getElementsByTagName('head')[0].appendChild(fileref);
1841 * Initiates the listeners for skiplink interaction
1845 M.util.init_skiplink = function(Y) {
1846 Y.one(Y.config.doc.body).delegate('click', function(e) {
1848 e.stopPropagation();
1849 var node = Y.one(this.getAttribute('href'));
1850 node.setAttribute('tabindex', '-1');