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')) {
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, 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');
638 this.get('block').removeClass('hidden');
641 updateStateKey : function(e, hide) {
642 if (e.keyCode == 13) { //allow hide/show via enter key
643 this.updateState(this, hide);
647 Y.extend(blockhider, Y.Base, blockhider.prototype, {
653 value : M.util.image_url('t/switch_minus', 'moodle')
656 value : M.util.image_url('t/switch_plus', 'moodle')
659 setter : function(node) {
667 new M.util.block_hider(config);
672 * @var pending_js - The keys are the list of all pending js actions.
675 M.util.pending_js = [];
676 M.util.complete_js = [];
679 * Register any long running javascript code with a unique identifier.
680 * Should be followed with a call to js_complete with a matching
681 * idenfitier when the code is complete. May also be called with no arguments
682 * to test if there is any js calls pending. This is relied on by behat so that
683 * it can wait for all pending updates before interacting with a page.
684 * @param String uniqid - optional, if provided,
685 * registers this identifier until js_complete is called.
686 * @return boolean - True if there is any pending js.
688 M.util.js_pending = function(uniqid) {
689 if (uniqid !== false) {
690 M.util.pending_js.push(uniqid);
693 return M.util.pending_js.length;
697 M.util.js_pending('init');
700 * Register listeners for Y.io start/end so we can wait for them in behat.
702 YUI.add('moodle-core-io', function(Y) {
703 Y.on('io:start', function(id) {
704 M.util.js_pending('io:' + id);
706 Y.on('io:end', function(id) {
707 M.util.js_complete('io:' + id);
717 * Unregister any long running javascript code by unique identifier.
718 * This function should form a matching pair with js_pending
720 * @param String uniqid - required, unregisters this identifier
721 * @return boolean - True if there is any pending js.
723 M.util.js_complete = function(uniqid) {
724 // Use the Y.Array.indexOf instead of the native because some older browsers do not support
725 // the native function. Y.Array polyfills the native function if it does not exist.
726 var index = Y.Array.indexOf(M.util.pending_js, uniqid);
728 M.util.complete_js.push(M.util.pending_js.splice(index, 1));
731 return M.util.pending_js.length;
735 * Returns a string registered in advance for usage in JavaScript
737 * If you do not pass the third parameter, the function will just return
738 * the corresponding value from the M.str object. If the third parameter is
739 * provided, the function performs {$a} placeholder substitution in the
740 * same way as PHP get_string() in Moodle does.
742 * @param {String} identifier string identifier
743 * @param {String} component the component providing the string
744 * @param {Object|String} a optional variable to populate placeholder with
746 M.util.get_string = function(identifier, component, a) {
749 if (M.cfg.developerdebug) {
750 // creating new instance if YUI is not optimal but it seems to be better way then
751 // require the instance via the function API - note that it is used in rare cases
752 // for debugging only anyway
753 // To ensure we don't kill browser performance if hundreds of get_string requests
754 // are made we cache the instance we generate within the M.util namespace.
755 // We don't publicly define the variable so that it doesn't get abused.
756 if (typeof M.util.get_string_yui_instance === 'undefined') {
757 M.util.get_string_yui_instance = new YUI({ debug : true });
759 var Y = M.util.get_string_yui_instance;
762 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
763 stringvalue = '[[' + identifier + ',' + component + ']]';
764 if (M.cfg.developerdebug) {
765 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
770 stringvalue = M.str[component][identifier];
772 if (typeof a == 'undefined') {
773 // no placeholder substitution requested
777 if (typeof a == 'number' || typeof a == 'string') {
778 // replace all occurrences of {$a} with the placeholder value
779 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
783 if (typeof a == 'object') {
784 // replace {$a->key} placeholders
786 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
787 if (M.cfg.developerdebug) {
788 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
792 var search = '{$a->' + key + '}';
793 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
794 search = new RegExp(search, 'g');
795 stringvalue = stringvalue.replace(search, a[key]);
800 if (M.cfg.developerdebug) {
801 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
807 * Set focus on username or password field of the login form
809 M.util.focus_login_form = function(Y) {
810 var username = Y.one('#username');
811 var password = Y.one('#password');
813 if (username == null || password == null) {
814 // something is wrong here
818 var curElement = document.activeElement
819 if (curElement == 'undefined') {
820 // legacy browser - skip refocus protection
821 } else if (curElement.tagName == 'INPUT') {
822 // user was probably faster to focus something, do not mess with focus
826 if (username.get('value') == '') {
834 * Set focus on login error message
836 M.util.focus_login_error = function(Y) {
837 var errorlog = Y.one('#loginerrormessage');
844 * Adds lightbox hidden element that covers the whole node.
847 * @param {Node} the node lightbox should be added to
848 * @retun {Node} created lightbox node
850 M.util.add_lightbox = function(Y, node) {
851 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
853 // Check if lightbox is already there
854 if (node.one('.lightbox')) {
855 return node.one('.lightbox');
858 node.setStyle('position', 'relative');
859 var waiticon = Y.Node.create('<img />')
861 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
864 'position' : 'relative',
868 var lightbox = Y.Node.create('<div></div>')
871 'position' : 'absolute',
876 'backgroundColor' : 'white',
877 'textAlign' : 'center'
879 .setAttribute('class', 'lightbox')
882 lightbox.appendChild(waiticon);
883 node.append(lightbox);
888 * Appends a hidden spinner element to the specified node.
891 * @param {Node} the node the spinner should be added to
892 * @return {Node} created spinner node
894 M.util.add_spinner = function(Y, node) {
895 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
897 // Check if spinner is already there
898 if (node.one('.spinner')) {
899 return node.one('.spinner');
902 var spinner = Y.Node.create('<img />')
903 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
905 .addClass('iconsmall')
908 node.append(spinner);
912 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
914 function checkall() {
915 var inputs = document.getElementsByTagName('input');
916 for (var i = 0; i < inputs.length; i++) {
917 if (inputs[i].type == 'checkbox') {
918 if (inputs[i].disabled || inputs[i].readOnly) {
921 inputs[i].checked = true;
926 function checknone() {
927 var inputs = document.getElementsByTagName('input');
928 for (var i = 0; i < inputs.length; i++) {
929 if (inputs[i].type == 'checkbox') {
930 if (inputs[i].disabled || inputs[i].readOnly) {
933 inputs[i].checked = false;
939 * Either check, or uncheck, all checkboxes inside the element with id is
940 * @param id the id of the container
941 * @param checked the new state, either '' or 'checked'.
943 function select_all_in_element_with_id(id, checked) {
944 var container = document.getElementById(id);
948 var inputs = container.getElementsByTagName('input');
949 for (var i = 0; i < inputs.length; ++i) {
950 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
951 inputs[i].checked = checked;
956 function select_all_in(elTagName, elClass, elId) {
957 var inputs = document.getElementsByTagName('input');
958 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
959 for(var i = 0; i < inputs.length; ++i) {
960 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
961 inputs[i].checked = 'checked';
966 function deselect_all_in(elTagName, elClass, elId) {
967 var inputs = document.getElementsByTagName('INPUT');
968 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
969 for(var i = 0; i < inputs.length; ++i) {
970 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
971 inputs[i].checked = '';
976 function confirm_if(expr, message) {
980 return confirm(message);
985 findParentNode (start, elementName, elementClass, elementID)
987 Travels up the DOM hierarchy to find a parent element with the
988 specified tag name, class, and id. All conditions must be met,
989 but any can be ommitted. Returns the BODY element if no match
992 function findParentNode(el, elName, elClass, elId) {
993 while (el.nodeName.toUpperCase() != 'BODY') {
994 if ((!elName || el.nodeName.toUpperCase() == elName) &&
995 (!elClass || el.className.indexOf(elClass) != -1) &&
996 (!elId || el.id == elId)) {
1004 function unmaskPassword(id) {
1005 var pw = document.getElementById(id);
1006 var chb = document.getElementById(id+'unmask');
1008 // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1009 // Replacing existing child with a new one, removed all yui properties for the node. Therefore, this
1010 // functionality won't work in IE8 or lower.
1011 // This is a temporary fixed to allow other browsers to function properly.
1012 if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1016 pw.type = "password";
1018 } else { //IE Browser version 8 or lower
1020 // first try IE way - it can not set name attribute later
1022 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1024 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1026 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1028 var newpw = document.createElement('input');
1029 newpw.setAttribute('autocomplete', 'off');
1030 newpw.setAttribute('name', pw.name);
1032 newpw.setAttribute('type', 'text');
1034 newpw.setAttribute('type', 'password');
1036 newpw.setAttribute('class', pw.getAttribute('class'));
1039 newpw.size = pw.size;
1040 newpw.onblur = pw.onblur;
1041 newpw.onchange = pw.onchange;
1042 newpw.value = pw.value;
1043 pw.parentNode.replaceChild(newpw, pw);
1047 function filterByParent(elCollection, parentFinder) {
1048 var filteredCollection = [];
1049 for (var i = 0; i < elCollection.length; ++i) {
1050 var findParent = parentFinder(elCollection[i]);
1051 if (findParent.nodeName.toUpperCase() != 'BODY') {
1052 filteredCollection.push(elCollection[i]);
1055 return filteredCollection;
1059 All this is here just so that IE gets to handle oversized blocks
1060 in a visually pleasing manner. It does a browser detect. So sue me.
1063 function fix_column_widths() {
1064 var agt = navigator.userAgent.toLowerCase();
1065 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1066 fix_column_width('left-column');
1067 fix_column_width('right-column');
1071 function fix_column_width(colName) {
1072 if(column = document.getElementById(colName)) {
1073 if(!column.offsetWidth) {
1074 setTimeout("fix_column_width('" + colName + "')", 20);
1079 var nodes = column.childNodes;
1081 for(i = 0; i < nodes.length; ++i) {
1082 if(nodes[i].className.indexOf("block") != -1 ) {
1083 if(width < nodes[i].offsetWidth) {
1084 width = nodes[i].offsetWidth;
1089 for(i = 0; i < nodes.length; ++i) {
1090 if(nodes[i].className.indexOf("block") != -1 ) {
1091 nodes[i].style.width = width + 'px';
1099 Insert myValue at current cursor position
1101 function insertAtCursor(myField, myValue) {
1103 if (document.selection) {
1105 sel = document.selection.createRange();
1108 // Mozilla/Netscape support
1109 else if (myField.selectionStart || myField.selectionStart == '0') {
1110 var startPos = myField.selectionStart;
1111 var endPos = myField.selectionEnd;
1112 myField.value = myField.value.substring(0, startPos)
1113 + myValue + myField.value.substring(endPos, myField.value.length);
1115 myField.value += myValue;
1120 * Increment a file name.
1122 * @param string file name.
1123 * @param boolean ignoreextension do not extract the extension prior to appending the
1124 * suffix. Useful when incrementing folder names.
1125 * @return string the incremented file name.
1127 function increment_filename(filename, ignoreextension) {
1129 var basename = filename;
1131 // Split the file name into the basename + extension.
1132 if (!ignoreextension) {
1133 var dotpos = filename.lastIndexOf('.');
1134 if (dotpos !== -1) {
1135 basename = filename.substr(0, dotpos);
1136 extension = filename.substr(dotpos, filename.length);
1140 // Look to see if the name already has (NN) at the end of it.
1142 var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1143 if (hasnumber !== null) {
1144 // Note the current number & remove it from the basename.
1145 number = parseInt(hasnumber[2], 10);
1146 basename = hasnumber[1];
1150 var newname = basename + ' (' + number + ')' + extension;
1155 * Return whether we are in right to left mode or not.
1159 function right_to_left() {
1160 var body = Y.one('body');
1162 if (body && body.hasClass('dir-rtl')) {
1168 function openpopup(event, args) {
1171 if (event.preventDefault) {
1172 event.preventDefault();
1174 event.returnValue = false;
1178 // Make sure the name argument is set and valid.
1179 var nameregex = /[^a-z0-9_]/i;
1180 if (typeof args.name !== 'string') {
1181 args.name = '_blank';
1182 } else if (args.name.match(nameregex)) {
1183 // Cleans window name because IE does not support funky ones.
1184 if (M.cfg.developerdebug) {
1185 alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1187 args.name = args.name.replace(nameregex, '_');
1190 var fullurl = args.url;
1191 if (!args.url.match(/https?:\/\//)) {
1192 fullurl = M.cfg.wwwroot + args.url;
1194 if (args.fullscreen) {
1195 args.options = args.options.
1196 replace(/top=\d+/, 'top=0').
1197 replace(/left=\d+/, 'left=0').
1198 replace(/width=\d+/, 'width=' + screen.availWidth).
1199 replace(/height=\d+/, 'height=' + screen.availHeight);
1201 var windowobj = window.open(fullurl,args.name,args.options);
1206 if (args.fullscreen) {
1207 // In some browser / OS combinations (E.g. Chrome on Windows), the
1208 // window initially opens slighly too big. The width and heigh options
1209 // seem to control the area inside the browser window, so what with
1210 // scroll-bars, etc. the actual window is bigger than the screen.
1211 // Therefore, we need to fix things up after the window is open.
1212 var hackcount = 100;
1213 var get_size_exactly_right = function() {
1214 windowobj.moveTo(0, 0);
1215 windowobj.resizeTo(screen.availWidth, screen.availHeight);
1217 // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1218 // something like windowobj.resizeTo(1280, 1024) too soon (up to
1219 // about 50ms) after the window is open, then it actually behaves
1220 // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1221 // check that the resize actually worked, and if not, repeatedly try
1222 // again after a short delay until it works (but with a limit of
1223 // hackcount repeats.
1224 if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1226 setTimeout(get_size_exactly_right, 10);
1229 setTimeout(get_size_exactly_right, 0);
1236 /** Close the current browser window. */
1237 function close_window(e) {
1238 if (e.preventDefault) {
1241 e.returnValue = false;
1247 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1248 * @param controlid the control id.
1250 function focuscontrol(controlid) {
1251 var control = document.getElementById(controlid);
1258 * Transfers keyboard focus to an HTML element based on the old style style of focus
1259 * This function should be removed as soon as it is no longer used
1261 function old_onload_focus(formid, controlname) {
1262 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1263 document.forms[formid].elements[controlname].focus();
1267 function build_querystring(obj) {
1268 return convert_object_to_string(obj, '&');
1271 function build_windowoptionsstring(obj) {
1272 return convert_object_to_string(obj, ',');
1275 function convert_object_to_string(obj, separator) {
1276 if (typeof obj !== 'object') {
1281 k = encodeURIComponent(k);
1283 if(obj[k] instanceof Array) {
1284 for(var i in value) {
1285 list.push(k+'[]='+encodeURIComponent(value[i]));
1288 list.push(k+'='+encodeURIComponent(value));
1291 return list.join(separator);
1294 function stripHTML(str) {
1295 var re = /<\S[^><]*>/g;
1296 var ret = str.replace(re, "");
1300 function updateProgressBar(id, percent, msg, estimate) {
1301 var progressIndicator = Y.one('#' + id);
1302 if (!progressIndicator) {
1306 var progressBar = progressIndicator.one('.bar'),
1307 statusIndicator = progressIndicator.one('h2'),
1308 estimateIndicator = progressIndicator.one('p');
1310 statusIndicator.set('innerHTML', Y.Escape.html(msg));
1311 progressBar.set('innerHTML', Y.Escape.html('' + percent + '%'));
1312 if (percent === 100) {
1313 progressIndicator.addClass('progress-success');
1314 estimateIndicator.set('innerHTML', null);
1317 estimateIndicator.set('innerHTML', Y.Escape.html(estimate));
1319 estimateIndicator.set('innerHTML', null);
1321 progressIndicator.removeClass('progress-success');
1323 progressBar.setAttribute('aria-valuenow', percent);
1324 progressBar.setStyle('width', percent + '%');
1327 // ===== Deprecated core Javascript functions for Moodle ====
1328 // DO NOT USE!!!!!!!
1329 // Do not put this stuff in separate file because it only adds extra load on servers!
1333 * @deprecated since Moodle 2.7.
1336 function show_item() {
1337 throw new Error('show_item can not be used any more. Please use Y.Node.show.');
1341 * @method destroy_item
1342 * @deprecated since Moodle 2.7.
1343 * @see Y.Node.destroy
1345 function destroy_item() {
1346 throw new Error('destroy_item can not be used any more. Please use Y.Node.destroy.');
1351 * @deprecated since Moodle 2.7.
1354 function hide_item() {
1355 throw new Error('hide_item can not be used any more. Please use Y.Node.hide.');
1360 * @deprecated since Moodle 2.7 - please do not use this function any more.
1362 function addonload() {
1363 throw new Error('addonload can not be used any more.');
1367 * @method getElementsByClassName
1368 * @deprecated Since Moodle 2.7 - please do not use this function any more.
1372 function getElementsByClassName() {
1373 throw new Error('getElementsByClassName can not be used any more. Please use Y.one or Y.all.');
1377 * @method findChildNodes
1378 * @deprecated since Moodle 2.7 - please do not use this function any more.
1381 function findChildNodes() {
1382 throw new Error('findChildNodes can not be used any more. Please use Y.all.');
1385 M.util.help_popups = {
1386 setup : function(Y) {
1387 Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1389 open_popup : function(e) {
1390 // Prevent the default page action
1393 // Grab the anchor that was clicked
1394 var anchor = e.target.ancestor('a', true);
1397 'url' : anchor.getAttribute('href'),
1415 args.options = options.join(',');
1422 * Custom menu namespace
1424 M.core_custom_menu = {
1426 * This method is used to initialise a custom menu given the id that belongs
1427 * to the custom menu's root node.
1430 * @param {string} nodeid
1432 init : function(Y, nodeid) {
1433 var node = Y.one('#'+nodeid);
1435 Y.use('node-menunav', function(Y) {
1437 // Remove the javascript-disabled class.... obviously javascript is enabled.
1438 node.removeClass('javascript-disabled');
1439 // Initialise the menunav plugin
1440 node.plug(Y.Plugin.NodeMenuNav);
1447 * Used to store form manipulation methods and enhancments
1449 M.form = M.form || {};
1452 * Converts a nbsp indented select box into a multi drop down custom control much
1453 * like the custom menu. It also selectable categories on or off.
1455 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1458 * @param {string} id
1459 * @param {Array} options
1461 M.form.init_smartselect = function(Y, id, options) {
1462 if (!id.match(/^id_/)) {
1465 var select = Y.one('select#'+id);
1469 Y.use('event-delegate',function(){
1475 currentvalue : null,
1479 selectablecategories : true,
1487 init : function(Y, id, args, nodes) {
1488 if (typeof(args)=='object') {
1489 for (var i in this.cfg) {
1490 if (args[i] || args[i]===false) {
1491 this.cfg[i] = args[i];
1496 // Display a loading message first up
1497 this.nodes.select = nodes.select;
1499 this.currentvalue = this.nodes.select.get('selectedIndex');
1500 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1502 var options = Array();
1503 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1504 this.nodes.select.all('option').each(function(option, index) {
1505 var rawtext = option.get('innerHTML');
1506 var text = rawtext.replace(/^( )*/, '');
1507 if (rawtext === text) {
1508 text = rawtext.replace(/^(\s)*/, '');
1509 var depth = (rawtext.length - text.length ) + 1;
1511 var depth = ((rawtext.length - text.length )/12)+1;
1513 option.set('innerHTML', text);
1514 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1517 this.structure = [];
1518 var structcount = 0;
1519 for (var i in options) {
1522 this.structure.push(o);
1526 var current = this.structure[structcount-1];
1527 for (var j = 0; j < o.depth-1;j++) {
1528 if (current && current.children) {
1529 current = current.children[current.children.length-1];
1532 if (current && current.children) {
1533 current.children.push(o);
1538 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1539 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1540 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1541 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1543 if (this.cfg.mode == null) {
1544 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1545 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1546 this.cfg.mode = 'compact';
1548 this.cfg.mode = 'spanning';
1552 if (this.cfg.mode == 'compact') {
1553 this.nodes.menu.addClass('compactmenu');
1555 this.nodes.menu.addClass('spanningmenu');
1556 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1559 Y.one(document.body).append(this.nodes.menu);
1560 var pos = this.nodes.select.getXY();
1562 this.nodes.menu.setXY(pos);
1563 this.nodes.menu.on('click', this.handle_click, this);
1565 Y.one(window).on('resize', function(){
1566 var pos = this.nodes.select.getXY();
1568 this.nodes.menu.setXY(pos);
1571 generate_menu_content : function() {
1572 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1573 content += this.generate_submenu_content(this.structure[0], true);
1574 content += '</ul></div>';
1577 generate_submenu_content : function(item, rootelement) {
1578 this.submenucount++;
1580 if (item.children.length > 0) {
1582 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1583 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1584 content += '<div class="smartselect_menu_content">';
1586 content += '<li class="smartselect_submenuitem">';
1587 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1588 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1589 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1590 content += '<div class="smartselect_submenu_content">';
1593 for (var i in item.children) {
1594 content += this.generate_submenu_content(item.children[i],false);
1597 content += '</div>';
1598 content += '</div>';
1604 content += '<li class="smartselect_menuitem">';
1605 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1610 select : function(e) {
1613 this.currenttext = t.get('innerHTML');
1614 this.currentvalue = t.getAttribute('value');
1615 this.nodes.select.set('selectedIndex', this.currentvalue);
1618 handle_click : function(e) {
1619 var target = e.target;
1620 if (target.hasClass('smartselect_mask')) {
1622 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1624 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1625 this.show_sub_menu(e);
1628 show_menu : function(e) {
1630 var menu = e.target.ancestor().one('.smartselect_menu');
1631 menu.addClass('visible');
1632 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1634 show_sub_menu : function(e) {
1636 var target = e.target;
1637 if (!target.hasClass('smartselect_submenuitem')) {
1638 target = target.ancestor('.smartselect_submenuitem');
1640 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1641 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1644 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1645 target.one('.smartselect_submenu').addClass('visible');
1647 hide_menu : function() {
1648 this.nodes.menu.all('.visible').removeClass('visible');
1649 if (this.shownevent) {
1650 this.shownevent.detach();
1654 smartselect.init(Y, id, options, {select:select});
1658 /** List of flv players to be loaded */
1659 M.util.video_players = [];
1660 /** List of mp3 players to be loaded */
1661 M.util.audio_players = [];
1665 * @param id element id
1666 * @param fileurl media url
1669 * @param autosize true means detect size from media
1671 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1672 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1681 M.util.add_audio_player = function (id, fileurl, small) {
1682 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1686 * Initialise all audio and video player, must be called from page footer.
1688 M.util.load_flowplayer = function() {
1689 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1692 if (typeof(flowplayer) == 'undefined') {
1695 var embed_function = function() {
1696 if (loaded || typeof(flowplayer) == 'undefined') {
1704 /* TODO: add CSS color overrides for the flv flow player */
1706 for(var i=0; i<M.util.video_players.length; i++) {
1707 var video = M.util.video_players[i];
1708 if (video.width > 0 && video.height > 0) {
1709 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', width: video.width, height: video.height};
1711 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf';
1713 flowplayer(video.id, src, {
1714 plugins: {controls: controls},
1716 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1717 onMetaData: function(clip) {
1718 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1719 clip.mvideo.resized = true;
1720 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1721 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1722 // bad luck, we have to guess - we may not get metadata at all
1723 var width = clip.width;
1724 var height = clip.height;
1726 var width = clip.metaData.width;
1727 var height = clip.metaData.height;
1729 var minwidth = 300; // controls are messed up in smaller objects
1730 if (width < minwidth) {
1731 height = (height * minwidth) / width;
1735 var object = this._api();
1736 object.width = width;
1737 object.height = height;
1743 if (M.util.audio_players.length == 0) {
1756 backgroundGradient: [0.5,0,0.3]
1760 for (var j=0; j < document.styleSheets.length; j++) {
1762 // To avoid javascript security violation accessing cross domain stylesheets
1763 var allrules = false;
1765 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1766 allrules = document.styleSheets[j].rules;
1767 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1768 allrules = document.styleSheets[j].cssRules;
1777 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1782 for(var i=0; i<allrules.length; i++) {
1784 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1785 if (typeof(allrules[i].cssText) != 'undefined') {
1786 rule = allrules[i].cssText;
1787 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1788 rule = allrules[i].style.cssText;
1790 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1791 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1792 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1793 controls[colprop] = rule;
1800 for(i=0; i<M.util.audio_players.length; i++) {
1801 var audio = M.util.audio_players[i];
1803 controls.controlall = false;
1804 controls.height = 15;
1805 controls.time = false;
1807 controls.controlall = true;
1808 controls.height = 25;
1809 controls.time = true;
1811 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf', {
1812 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf'}},
1813 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1818 if (M.cfg.jsrev == -1) {
1819 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.13.js';
1821 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.13.min.js&rev=' + M.cfg.jsrev;
1823 var fileref = document.createElement('script');
1824 fileref.setAttribute('type','text/javascript');
1825 fileref.setAttribute('src', jsurl);
1826 fileref.onload = embed_function;
1827 fileref.onreadystatechange = embed_function;
1828 document.getElementsByTagName('head')[0].appendChild(fileref);
1833 * Initiates the listeners for skiplink interaction
1837 M.util.init_skiplink = function(Y) {
1838 Y.one(Y.config.doc.body).delegate('click', function(e) {
1840 e.stopPropagation();
1841 Y.one(this.getAttribute('data-target')).focus();