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];
14 * The gallery version to use when loading YUI modules from the gallery.
15 * Will be changed every time when using local galleries.
17 M.yui.galleryversion = '2010.04.21-21-51';
20 * Various utility functions
22 M.util = M.util || {};
25 * Language strings - initialised from page footer.
30 * Returns url for images.
31 * @param {String} imagename
32 * @param {String} component
35 M.util.image_url = function(imagename, component) {
37 if (!component || component == '' || component == 'moodle' || component == 'core') {
41 var url = M.cfg.wwwroot + '/theme/image.php';
42 if (M.cfg.themerev > 0 && M.cfg.slasharguments == 1) {
43 if (!M.cfg.svgicons) {
46 url += '/' + M.cfg.theme + '/' + component + '/' + M.cfg.themerev + '/' + imagename;
48 url += '?theme=' + M.cfg.theme + '&component=' + component + '&rev=' + M.cfg.themerev + '&image=' + imagename;
49 if (!M.cfg.svgicons) {
57 M.util.in_array = function(item, array){
58 for( var i = 0; i<array.length; i++){
67 * Init a collapsible region, see print_collapsible_region in weblib.php
68 * @param {YUI} Y YUI3 instance with all libraries loaded
69 * @param {String} id the HTML id for the div.
70 * @param {String} userpref the user preference that records the state of this box. false if none.
71 * @param {String} strtooltip
73 M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
74 Y.use('anim', function(Y) {
75 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
80 * Object to handle a collapsible region : instantiate and forget styled object
84 * @param {YUI} Y YUI3 instance with all libraries loaded
85 * @param {String} id The HTML id for the div.
86 * @param {String} userpref The user preference that records the state of this box. false if none.
87 * @param {String} strtooltip
89 M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
90 // Record the pref name
91 this.userpref = userpref;
93 // Find the divs in the document.
94 this.div = Y.one('#'+id);
96 // Get the caption for the collapsible region
97 var caption = this.div.one('#'+id + '_caption');
100 var a = Y.Node.create('<a href="#"></a>');
101 a.setAttribute('title', strtooltip);
103 // Get all the nodes from caption, remove them and append them to <a>
104 while (caption.hasChildNodes()) {
105 child = caption.get('firstChild');
111 // Get the height of the div at this point before we shrink it if required
112 var height = this.div.get('offsetHeight');
113 var collapsedimage = 't/collapsed'; // ltr mode
114 if (right_to_left()) {
115 collapsedimage = 't/collapsed_rtl';
117 collapsedimage = 't/collapsed';
119 if (this.div.hasClass('collapsed')) {
120 // Add the correct image and record the YUI node created in the process
121 this.icon = Y.Node.create('<img src="'+M.util.image_url(collapsedimage, 'moodle')+'" alt="" />');
122 // Shrink the div as it is collapsed by default
123 this.div.setStyle('height', caption.get('offsetHeight')+'px');
125 // Add the correct image and record the YUI node created in the process
126 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
130 // Create the animation.
131 var animation = new Y.Anim({
134 easing: Y.Easing.easeBoth,
135 to: {height:caption.get('offsetHeight')},
136 from: {height:height}
139 // Handler for the animation finishing.
140 animation.on('end', function() {
141 this.div.toggleClass('collapsed');
142 var collapsedimage = 't/collapsed'; // ltr mode
143 if (right_to_left()) {
144 collapsedimage = 't/collapsed_rtl';
146 collapsedimage = 't/collapsed';
148 if (this.div.hasClass('collapsed')) {
149 this.icon.set('src', M.util.image_url(collapsedimage, 'moodle'));
151 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
155 // Hook up the event handler.
156 a.on('click', function(e, animation) {
158 // Animate to the appropriate size.
159 if (animation.get('running')) {
162 animation.set('reverse', this.div.hasClass('collapsed'));
163 // Update the user preference.
165 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
172 * The user preference that stores the state of this box.
176 M.util.CollapsibleRegion.prototype.userpref = null;
179 * The key divs that make up this
183 M.util.CollapsibleRegion.prototype.div = null;
186 * The key divs that make up this
190 M.util.CollapsibleRegion.prototype.icon = null;
193 * Makes a best effort to connect back to Moodle to update a user preference,
194 * however, there is no mechanism for finding out if the update succeeded.
196 * Before you can use this function in your JavsScript, you must have called
197 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
198 * the udpate is allowed, and how to safely clean and submitted values.
200 * @param String name the name of the setting to udpate.
201 * @param String the value to set it to.
203 M.util.set_user_preference = function(name, value) {
204 YUI().use('io', function(Y) {
205 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
206 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
208 // If we are a developer, ensure that failures are reported.
213 if (M.cfg.developerdebug) {
214 cfg.on.failure = function(id, o, args) {
215 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
225 * Prints a confirmation dialog in the style of DOM.confirm().
227 * @method show_confirm_dialog
228 * @param {EventFacade} e
229 * @param {Object} args
230 * @param {String} args.message The question to ask the user
231 * @param {Function} [args.callback] A callback to apply on confirmation.
232 * @param {Object} [args.scope] The scope to use when calling the callback.
233 * @param {Object} [args.callbackargs] Any arguments to pass to the callback.
234 * @param {String} [args.cancellabel] The label to use on the cancel button.
235 * @param {String} [args.continuelabel] The label to use on the continue button.
237 M.util.show_confirm_dialog = function(e, args) {
238 var target = e.target;
239 if (e.preventDefault) {
243 YUI().use('moodle-core-notification-confirm', function(Y) {
244 var confirmationDialogue = new M.core.confirm({
250 title: M.util.get_string('confirmation', 'admin'),
251 noLabel: M.util.get_string('cancel', 'moodle'),
252 question: args.message
255 // The dialogue was submitted with a positive value indication.
256 confirmationDialogue.on('complete-yes', function(e) {
257 // Handle any callbacks.
259 if (!Y.Lang.isFunction(args.callback)) {
260 Y.log('Callbacks to show_confirm_dialog must now be functions. Please update your code to pass in a function instead.',
261 'warn', 'M.util.show_confirm_dialog');
265 var scope = e.target;
266 if (Y.Lang.isObject(args.scope)) {
270 var callbackargs = args.callbackargs || [];
271 args.callback.apply(scope, callbackargs);
275 var targetancestor = null,
278 if (target.test('a')) {
279 window.location = target.get('href');
281 } else if ((targetancestor = target.ancestor('a')) !== null) {
282 window.location = targetancestor.get('href');
284 } else if (target.test('input')) {
285 targetform = target.ancestor('form', true);
289 if (target.get('name') && target.get('value')) {
290 targetform.append('<input type="hidden" name="' + target.get('name') +
291 '" value="' + target.get('value') + '">');
295 } else if (target.test('form')) {
299 Y.log("Element of type " + target.get('tagName') +
300 " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM",
301 'warn', 'javascript-static');
305 if (args.cancellabel) {
306 confirmationDialogue.set('noLabel', args.cancellabel);
309 if (args.continuelabel) {
310 confirmationDialogue.set('yesLabel', args.continuelabel);
313 confirmationDialogue.render()
318 /** Useful for full embedding of various stuff */
319 M.util.init_maximised_embed = function(Y, id) {
320 var obj = Y.one('#'+id);
325 var get_htmlelement_size = function(el, prop) {
326 if (Y.Lang.isString(el)) {
327 el = Y.one('#' + el);
329 // Ensure element exists.
331 var val = el.getStyle(prop);
333 val = el.getComputedStyle(prop);
345 var resize_object = function() {
346 obj.setStyle('display', 'none');
347 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
349 if (newwidth > 500) {
350 obj.setStyle('width', newwidth + 'px');
352 obj.setStyle('width', '500px');
355 var headerheight = get_htmlelement_size('page-header', 'height');
356 var footerheight = get_htmlelement_size('page-footer', 'height');
357 var newheight = parseInt(Y.one('body').get('docHeight')) - footerheight - headerheight - 100;
358 if (newheight < 400) {
361 obj.setStyle('height', newheight+'px');
362 obj.setStyle('display', '');
366 // fix layout if window resized too
367 Y.use('event-resize', function (Y) {
368 Y.on("windowresize", function() {
375 * Breaks out all links to the top frame - used in frametop page layout.
377 M.util.init_frametop = function(Y) {
378 Y.all('a').each(function(node) {
379 node.set('target', '_top');
381 Y.all('form').each(function(node) {
382 node.set('target', '_top');
387 * Finds all nodes that match the given CSS selector and attaches events to them
388 * so that they toggle a given classname when clicked.
391 * @param {string} id An id containing elements to target
392 * @param {string} cssselector A selector to use to find targets
393 * @param {string} toggleclassname A classname to toggle
395 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
397 if (togglecssselector == '') {
398 togglecssselector = cssselector;
401 var node = Y.one('#'+id);
402 node.all(cssselector).each(function(n){
403 n.on('click', function(e){
405 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
406 if (this.test(togglecssselector)) {
407 this.toggleClass(toggleclassname);
409 this.ancestor(togglecssselector).toggleClass(toggleclassname);
414 // Attach this click event to the node rather than all selectors... will be much better
416 node.on('click', function(e){
417 if (e.target.hasClass('addtoall')) {
418 this.all(togglecssselector).addClass(toggleclassname);
419 } else if (e.target.hasClass('removefromall')) {
420 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
426 * Initialises a colour picker
428 * Designed to be used with admin_setting_configcolourpicker although could be used
429 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
430 * above or below the input (must have the same parent) and then call this with the
433 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
434 * contrib/blocks. For better docs refer to that.
438 * @param {object} previewconf
440 M.util.init_colour_picker = function(Y, id, previewconf) {
442 * We need node and event-mouseenter
444 Y.use('node', 'event-mouseenter', function(){
446 * The colour picker object
455 eventMouseEnter : null,
456 eventMouseLeave : null,
457 eventMouseMove : null,
462 * Initalises the colour picker by putting everything together and wiring the events
465 this.input = Y.one('#'+id);
466 this.box = this.input.ancestor().one('.admin_colourpicker');
467 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
468 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
469 this.preview = Y.Node.create('<div class="previewcolour"></div>');
470 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
471 this.current = Y.Node.create('<div class="currentcolour"></div>');
472 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
473 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
475 if (typeof(previewconf) === 'object' && previewconf !== null) {
476 Y.one('#'+id+'_preview').on('click', function(e){
477 if (Y.Lang.isString(previewconf.selector)) {
478 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
480 for (var i in previewconf.selector) {
481 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
487 this.eventClick = this.image.on('click', this.pickColour, this);
488 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
491 * Starts to follow the mouse once it enter the image
493 startFollow : function(e) {
494 this.eventMouseEnter.detach();
495 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
496 this.eventMouseMove = this.image.on('mousemove', function(e){
497 this.preview.setStyle('backgroundColor', this.determineColour(e));
501 * Stops following the mouse
503 endFollow : function(e) {
504 this.eventMouseMove.detach();
505 this.eventMouseLeave.detach();
506 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
509 * Picks the colour the was clicked on
511 pickColour : function(e) {
512 var colour = this.determineColour(e);
513 this.input.set('value', colour);
514 this.current.setStyle('backgroundColor', colour);
517 * Calculates the colour fromthe given co-ordinates
519 determineColour : function(e) {
520 var eventx = Math.floor(e.pageX-e.target.getX());
521 var eventy = Math.floor(e.pageY-e.target.getY());
523 var imagewidth = this.width;
524 var imageheight = this.height;
525 var factor = this.factor;
526 var colour = [255,0,0];
537 var matrixcount = matrices.length;
538 var limit = Math.round(imagewidth/matrixcount);
539 var heightbreak = Math.round(imageheight/2);
541 for (var x = 0; x < imagewidth; x++) {
542 var divisor = Math.floor(x / limit);
543 var matrix = matrices[divisor];
545 colour[0] += matrix[0]*factor;
546 colour[1] += matrix[1]*factor;
547 colour[2] += matrix[2]*factor;
554 var pixel = [colour[0], colour[1], colour[2]];
555 if (eventy < heightbreak) {
556 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
557 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
558 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
559 } else if (eventy > heightbreak) {
560 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
561 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
562 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
565 return this.convert_rgb_to_hex(pixel);
568 * Converts an RGB value to Hex
570 convert_rgb_to_hex : function(rgb) {
572 var hexchars = "0123456789ABCDEF";
573 for (var i=0; i<3; i++) {
574 var number = Math.abs(rgb[i]);
575 if (number == 0 || isNaN(number)) {
578 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
585 * Initialise the colour picker :) Hoorah
591 M.util.init_block_hider = function(Y, config) {
592 Y.use('base', 'node', function(Y) {
593 M.util.block_hider = M.util.block_hider || (function(){
594 var blockhider = function() {
595 blockhider.superclass.constructor.apply(this, arguments);
597 blockhider.prototype = {
598 initializer : function(config) {
599 this.set('block', '#'+this.get('id'));
600 var b = this.get('block'),
605 if (t && (a = t.one('.block_action'))) {
606 hide = Y.Node.create('<img />')
607 .addClass('block-hider-hide')
609 alt: config.tooltipVisible,
610 src: this.get('iconVisible'),
612 'title': config.tooltipVisible
614 hide.on('keypress', this.updateStateKey, this, true);
615 hide.on('click', this.updateState, this, true);
617 show = Y.Node.create('<img />')
618 .addClass('block-hider-show')
620 alt: config.tooltipHidden,
621 src: this.get('iconHidden'),
623 'title': config.tooltipHidden
625 show.on('keypress', this.updateStateKey, this, false);
626 show.on('click', this.updateState, this, false);
628 a.insert(show, 0).insert(hide, 0);
631 updateState : function(e, hide) {
632 M.util.set_user_preference(this.get('preference'), hide);
634 this.get('block').addClass('hidden');
636 this.get('block').removeClass('hidden');
639 updateStateKey : function(e, hide) {
640 if (e.keyCode == 13) { //allow hide/show via enter key
641 this.updateState(this, hide);
645 Y.extend(blockhider, Y.Base, blockhider.prototype, {
651 value : M.util.image_url('t/switch_minus', 'moodle')
654 value : M.util.image_url('t/switch_plus', 'moodle')
657 setter : function(node) {
665 new M.util.block_hider(config);
670 * @var pending_js - The keys are the list of all pending js actions.
673 M.util.pending_js = [];
674 M.util.complete_js = [];
677 * Register any long running javascript code with a unique identifier.
678 * Should be followed with a call to js_complete with a matching
679 * idenfitier when the code is complete. May also be called with no arguments
680 * to test if there is any js calls pending. This is relied on by behat so that
681 * it can wait for all pending updates before interacting with a page.
682 * @param String uniqid - optional, if provided,
683 * registers this identifier until js_complete is called.
684 * @return boolean - True if there is any pending js.
686 M.util.js_pending = function(uniqid) {
687 if (uniqid !== false) {
688 M.util.pending_js.push(uniqid);
691 return M.util.pending_js.length;
695 M.util.js_pending('init');
698 * Register listeners for Y.io start/end so we can wait for them in behat.
700 YUI.add('moodle-core-io', function(Y) {
701 Y.on('io:start', function(id) {
702 M.util.js_pending('io:' + id);
704 Y.on('io:end', function(id) {
705 M.util.js_complete('io:' + id);
715 * Unregister any long running javascript code by unique identifier.
716 * This function should form a matching pair with js_pending
718 * @param String uniqid - required, unregisters this identifier
719 * @return boolean - True if there is any pending js.
721 M.util.js_complete = function(uniqid) {
722 // Use the Y.Array.indexOf instead of the native because some older browsers do not support
723 // the native function. Y.Array polyfills the native function if it does not exist.
724 var index = Y.Array.indexOf(M.util.pending_js, uniqid);
726 M.util.complete_js.push(M.util.pending_js.splice(index, 1));
729 return M.util.pending_js.length;
733 * Returns a string registered in advance for usage in JavaScript
735 * If you do not pass the third parameter, the function will just return
736 * the corresponding value from the M.str object. If the third parameter is
737 * provided, the function performs {$a} placeholder substitution in the
738 * same way as PHP get_string() in Moodle does.
740 * @param {String} identifier string identifier
741 * @param {String} component the component providing the string
742 * @param {Object|String} a optional variable to populate placeholder with
744 M.util.get_string = function(identifier, component, a) {
747 if (M.cfg.developerdebug) {
748 // creating new instance if YUI is not optimal but it seems to be better way then
749 // require the instance via the function API - note that it is used in rare cases
750 // for debugging only anyway
751 // To ensure we don't kill browser performance if hundreds of get_string requests
752 // are made we cache the instance we generate within the M.util namespace.
753 // We don't publicly define the variable so that it doesn't get abused.
754 if (typeof M.util.get_string_yui_instance === 'undefined') {
755 M.util.get_string_yui_instance = new YUI({ debug : true });
757 var Y = M.util.get_string_yui_instance;
760 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
761 stringvalue = '[[' + identifier + ',' + component + ']]';
762 if (M.cfg.developerdebug) {
763 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
768 stringvalue = M.str[component][identifier];
770 if (typeof a == 'undefined') {
771 // no placeholder substitution requested
775 if (typeof a == 'number' || typeof a == 'string') {
776 // replace all occurrences of {$a} with the placeholder value
777 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
781 if (typeof a == 'object') {
782 // replace {$a->key} placeholders
784 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
785 if (M.cfg.developerdebug) {
786 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
790 var search = '{$a->' + key + '}';
791 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
792 search = new RegExp(search, 'g');
793 stringvalue = stringvalue.replace(search, a[key]);
798 if (M.cfg.developerdebug) {
799 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
805 * Set focus on username or password field of the login form
807 M.util.focus_login_form = function(Y) {
808 var username = Y.one('#username');
809 var password = Y.one('#password');
811 if (username == null || password == null) {
812 // something is wrong here
816 var curElement = document.activeElement
817 if (curElement == 'undefined') {
818 // legacy browser - skip refocus protection
819 } else if (curElement.tagName == 'INPUT') {
820 // user was probably faster to focus something, do not mess with focus
824 if (username.get('value') == '') {
832 * Set focus on login error message
834 M.util.focus_login_error = function(Y) {
835 var errorlog = Y.one('#loginerrormessage');
842 * Adds lightbox hidden element that covers the whole node.
845 * @param {Node} the node lightbox should be added to
846 * @retun {Node} created lightbox node
848 M.util.add_lightbox = function(Y, node) {
849 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
851 // Check if lightbox is already there
852 if (node.one('.lightbox')) {
853 return node.one('.lightbox');
856 node.setStyle('position', 'relative');
857 var waiticon = Y.Node.create('<img />')
859 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
862 'position' : 'relative',
866 var lightbox = Y.Node.create('<div></div>')
869 'position' : 'absolute',
874 'backgroundColor' : 'white',
875 'textAlign' : 'center'
877 .setAttribute('class', 'lightbox')
880 lightbox.appendChild(waiticon);
881 node.append(lightbox);
886 * Appends a hidden spinner element to the specified node.
889 * @param {Node} the node the spinner should be added to
890 * @return {Node} created spinner node
892 M.util.add_spinner = function(Y, node) {
893 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
895 // Check if spinner is already there
896 if (node.one('.spinner')) {
897 return node.one('.spinner');
900 var spinner = Y.Node.create('<img />')
901 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
903 .addClass('iconsmall')
906 node.append(spinner);
910 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
912 function checkall() {
913 var inputs = document.getElementsByTagName('input');
914 for (var i = 0; i < inputs.length; i++) {
915 if (inputs[i].type == 'checkbox') {
916 if (inputs[i].disabled || inputs[i].readOnly) {
919 inputs[i].checked = true;
924 function checknone() {
925 var inputs = document.getElementsByTagName('input');
926 for (var i = 0; i < inputs.length; i++) {
927 if (inputs[i].type == 'checkbox') {
928 if (inputs[i].disabled || inputs[i].readOnly) {
931 inputs[i].checked = false;
937 * Either check, or uncheck, all checkboxes inside the element with id is
938 * @param id the id of the container
939 * @param checked the new state, either '' or 'checked'.
941 function select_all_in_element_with_id(id, checked) {
942 var container = document.getElementById(id);
946 var inputs = container.getElementsByTagName('input');
947 for (var i = 0; i < inputs.length; ++i) {
948 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
949 inputs[i].checked = checked;
954 function select_all_in(elTagName, elClass, elId) {
955 var inputs = document.getElementsByTagName('input');
956 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
957 for(var i = 0; i < inputs.length; ++i) {
958 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
959 inputs[i].checked = 'checked';
964 function deselect_all_in(elTagName, elClass, elId) {
965 var inputs = document.getElementsByTagName('INPUT');
966 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
967 for(var i = 0; i < inputs.length; ++i) {
968 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
969 inputs[i].checked = '';
974 function confirm_if(expr, message) {
978 return confirm(message);
983 findParentNode (start, elementName, elementClass, elementID)
985 Travels up the DOM hierarchy to find a parent element with the
986 specified tag name, class, and id. All conditions must be met,
987 but any can be ommitted. Returns the BODY element if no match
990 function findParentNode(el, elName, elClass, elId) {
991 while (el.nodeName.toUpperCase() != 'BODY') {
992 if ((!elName || el.nodeName.toUpperCase() == elName) &&
993 (!elClass || el.className.indexOf(elClass) != -1) &&
994 (!elId || el.id == elId)) {
1002 function unmaskPassword(id) {
1003 var pw = document.getElementById(id);
1004 var chb = document.getElementById(id+'unmask');
1006 // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1007 // Replacing existing child with a new one, removed all yui properties for the node. Therefore, this
1008 // functionality won't work in IE8 or lower.
1009 // This is a temporary fixed to allow other browsers to function properly.
1010 if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1014 pw.type = "password";
1016 } else { //IE Browser version 8 or lower
1018 // first try IE way - it can not set name attribute later
1020 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1022 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1024 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1026 var newpw = document.createElement('input');
1027 newpw.setAttribute('autocomplete', 'off');
1028 newpw.setAttribute('name', pw.name);
1030 newpw.setAttribute('type', 'text');
1032 newpw.setAttribute('type', 'password');
1034 newpw.setAttribute('class', pw.getAttribute('class'));
1037 newpw.size = pw.size;
1038 newpw.onblur = pw.onblur;
1039 newpw.onchange = pw.onchange;
1040 newpw.value = pw.value;
1041 pw.parentNode.replaceChild(newpw, pw);
1045 function filterByParent(elCollection, parentFinder) {
1046 var filteredCollection = [];
1047 for (var i = 0; i < elCollection.length; ++i) {
1048 var findParent = parentFinder(elCollection[i]);
1049 if (findParent.nodeName.toUpperCase() != 'BODY') {
1050 filteredCollection.push(elCollection[i]);
1053 return filteredCollection;
1057 All this is here just so that IE gets to handle oversized blocks
1058 in a visually pleasing manner. It does a browser detect. So sue me.
1061 function fix_column_widths() {
1062 var agt = navigator.userAgent.toLowerCase();
1063 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1064 fix_column_width('left-column');
1065 fix_column_width('right-column');
1069 function fix_column_width(colName) {
1070 if(column = document.getElementById(colName)) {
1071 if(!column.offsetWidth) {
1072 setTimeout("fix_column_width('" + colName + "')", 20);
1077 var nodes = column.childNodes;
1079 for(i = 0; i < nodes.length; ++i) {
1080 if(nodes[i].className.indexOf("block") != -1 ) {
1081 if(width < nodes[i].offsetWidth) {
1082 width = nodes[i].offsetWidth;
1087 for(i = 0; i < nodes.length; ++i) {
1088 if(nodes[i].className.indexOf("block") != -1 ) {
1089 nodes[i].style.width = width + 'px';
1097 Insert myValue at current cursor position
1099 function insertAtCursor(myField, myValue) {
1101 if (document.selection) {
1103 sel = document.selection.createRange();
1106 // Mozilla/Netscape support
1107 else if (myField.selectionStart || myField.selectionStart == '0') {
1108 var startPos = myField.selectionStart;
1109 var endPos = myField.selectionEnd;
1110 myField.value = myField.value.substring(0, startPos)
1111 + myValue + myField.value.substring(endPos, myField.value.length);
1113 myField.value += myValue;
1118 * Increment a file name.
1120 * @param string file name.
1121 * @param boolean ignoreextension do not extract the extension prior to appending the
1122 * suffix. Useful when incrementing folder names.
1123 * @return string the incremented file name.
1125 function increment_filename(filename, ignoreextension) {
1127 var basename = filename;
1129 // Split the file name into the basename + extension.
1130 if (!ignoreextension) {
1131 var dotpos = filename.lastIndexOf('.');
1132 if (dotpos !== -1) {
1133 basename = filename.substr(0, dotpos);
1134 extension = filename.substr(dotpos, filename.length);
1138 // Look to see if the name already has (NN) at the end of it.
1140 var hasnumber = basename.match(/^(.*) \((\d+)\)$/);
1141 if (hasnumber !== null) {
1142 // Note the current number & remove it from the basename.
1143 number = parseInt(hasnumber[2], 10);
1144 basename = hasnumber[1];
1148 var newname = basename + ' (' + number + ')' + extension;
1153 * Return whether we are in right to left mode or not.
1157 function right_to_left() {
1158 var body = Y.one('body');
1160 if (body && body.hasClass('dir-rtl')) {
1166 function openpopup(event, args) {
1169 if (event.preventDefault) {
1170 event.preventDefault();
1172 event.returnValue = false;
1176 // Make sure the name argument is set and valid.
1177 var nameregex = /[^a-z0-9_]/i;
1178 if (typeof args.name !== 'string') {
1179 args.name = '_blank';
1180 } else if (args.name.match(nameregex)) {
1181 // Cleans window name because IE does not support funky ones.
1182 if (M.cfg.developerdebug) {
1183 alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup(): ' + args.name);
1185 args.name = args.name.replace(nameregex, '_');
1188 var fullurl = args.url;
1189 if (!args.url.match(/https?:\/\//)) {
1190 fullurl = M.cfg.wwwroot + args.url;
1192 if (args.fullscreen) {
1193 args.options = args.options.
1194 replace(/top=\d+/, 'top=0').
1195 replace(/left=\d+/, 'left=0').
1196 replace(/width=\d+/, 'width=' + screen.availWidth).
1197 replace(/height=\d+/, 'height=' + screen.availHeight);
1199 var windowobj = window.open(fullurl,args.name,args.options);
1204 if (args.fullscreen) {
1205 // In some browser / OS combinations (E.g. Chrome on Windows), the
1206 // window initially opens slighly too big. The width and heigh options
1207 // seem to control the area inside the browser window, so what with
1208 // scroll-bars, etc. the actual window is bigger than the screen.
1209 // Therefore, we need to fix things up after the window is open.
1210 var hackcount = 100;
1211 var get_size_exactly_right = function() {
1212 windowobj.moveTo(0, 0);
1213 windowobj.resizeTo(screen.availWidth, screen.availHeight);
1215 // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1216 // something like windowobj.resizeTo(1280, 1024) too soon (up to
1217 // about 50ms) after the window is open, then it actually behaves
1218 // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1219 // check that the resize actually worked, and if not, repeatedly try
1220 // again after a short delay until it works (but with a limit of
1221 // hackcount repeats.
1222 if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1224 setTimeout(get_size_exactly_right, 10);
1227 setTimeout(get_size_exactly_right, 0);
1234 /** Close the current browser window. */
1235 function close_window(e) {
1236 if (e.preventDefault) {
1239 e.returnValue = false;
1245 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1246 * @param controlid the control id.
1248 function focuscontrol(controlid) {
1249 var control = document.getElementById(controlid);
1256 * Transfers keyboard focus to an HTML element based on the old style style of focus
1257 * This function should be removed as soon as it is no longer used
1259 function old_onload_focus(formid, controlname) {
1260 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1261 document.forms[formid].elements[controlname].focus();
1265 function build_querystring(obj) {
1266 return convert_object_to_string(obj, '&');
1269 function build_windowoptionsstring(obj) {
1270 return convert_object_to_string(obj, ',');
1273 function convert_object_to_string(obj, separator) {
1274 if (typeof obj !== 'object') {
1279 k = encodeURIComponent(k);
1281 if(obj[k] instanceof Array) {
1282 for(var i in value) {
1283 list.push(k+'[]='+encodeURIComponent(value[i]));
1286 list.push(k+'='+encodeURIComponent(value));
1289 return list.join(separator);
1292 function stripHTML(str) {
1293 var re = /<\S[^><]*>/g;
1294 var ret = str.replace(re, "");
1298 function updateProgressBar(id, percent, msg, estimate) {
1299 var progressIndicator = Y.one('#' + id);
1300 if (!progressIndicator) {
1304 var progressBar = progressIndicator.one('.bar'),
1305 statusIndicator = progressIndicator.one('h2'),
1306 estimateIndicator = progressIndicator.one('p');
1308 statusIndicator.set('innerHTML', Y.Escape.html(msg));
1309 progressBar.set('innerHTML', Y.Escape.html('' + percent + '%'));
1310 if (percent === 100) {
1311 progressIndicator.addClass('progress-success');
1312 estimateIndicator.set('innerHTML', null);
1315 estimateIndicator.set('innerHTML', Y.Escape.html(estimate));
1317 estimateIndicator.set('innerHTML', null);
1319 progressIndicator.removeClass('progress-success');
1321 progressBar.setAttribute('aria-valuenow', percent);
1322 progressBar.setStyle('width', percent + '%');
1325 // ===== Deprecated core Javascript functions for Moodle ====
1326 // DO NOT USE!!!!!!!
1327 // Do not put this stuff in separate file because it only adds extra load on servers!
1331 * @deprecated since Moodle 2.7.
1334 function show_item() {
1335 throw new Error('show_item can not be used any more. Please use Y.Node.show.');
1339 * @method destroy_item
1340 * @deprecated since Moodle 2.7.
1341 * @see Y.Node.destroy
1343 function destroy_item() {
1344 throw new Error('destroy_item can not be used any more. Please use Y.Node.destroy.');
1349 * @deprecated since Moodle 2.7.
1352 function hide_item() {
1353 throw new Error('hide_item can not be used any more. Please use Y.Node.hide.');
1358 * @deprecated since Moodle 2.7 - please do not use this function any more.
1360 function addonload() {
1361 throw new Error('addonload can not be used any more.');
1365 * @method getElementsByClassName
1366 * @deprecated Since Moodle 2.7 - please do not use this function any more.
1370 function getElementsByClassName() {
1371 throw new Error('getElementsByClassName can not be used any more. Please use Y.one or Y.all.');
1375 * @method findChildNodes
1376 * @deprecated since Moodle 2.7 - please do not use this function any more.
1379 function findChildNodes() {
1380 throw new Error('findChildNodes can not be used any more. Please use Y.all.');
1383 M.util.help_popups = {
1384 setup : function(Y) {
1385 Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1387 open_popup : function(e) {
1388 // Prevent the default page action
1391 // Grab the anchor that was clicked
1392 var anchor = e.target.ancestor('a', true);
1395 'url' : anchor.getAttribute('href'),
1413 args.options = options.join(',');
1420 * Custom menu namespace
1422 M.core_custom_menu = {
1424 * This method is used to initialise a custom menu given the id that belongs
1425 * to the custom menu's root node.
1428 * @param {string} nodeid
1430 init : function(Y, nodeid) {
1431 var node = Y.one('#'+nodeid);
1433 Y.use('node-menunav', function(Y) {
1435 // Remove the javascript-disabled class.... obviously javascript is enabled.
1436 node.removeClass('javascript-disabled');
1437 // Initialise the menunav plugin
1438 node.plug(Y.Plugin.NodeMenuNav);
1445 * Used to store form manipulation methods and enhancments
1447 M.form = M.form || {};
1450 * Converts a nbsp indented select box into a multi drop down custom control much
1451 * like the custom menu. It also selectable categories on or off.
1453 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1456 * @param {string} id
1457 * @param {Array} options
1459 M.form.init_smartselect = function(Y, id, options) {
1460 if (!id.match(/^id_/)) {
1463 var select = Y.one('select#'+id);
1467 Y.use('event-delegate',function(){
1473 currentvalue : null,
1477 selectablecategories : true,
1485 init : function(Y, id, args, nodes) {
1486 if (typeof(args)=='object') {
1487 for (var i in this.cfg) {
1488 if (args[i] || args[i]===false) {
1489 this.cfg[i] = args[i];
1494 // Display a loading message first up
1495 this.nodes.select = nodes.select;
1497 this.currentvalue = this.nodes.select.get('selectedIndex');
1498 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1500 var options = Array();
1501 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1502 this.nodes.select.all('option').each(function(option, index) {
1503 var rawtext = option.get('innerHTML');
1504 var text = rawtext.replace(/^( )*/, '');
1505 if (rawtext === text) {
1506 text = rawtext.replace(/^(\s)*/, '');
1507 var depth = (rawtext.length - text.length ) + 1;
1509 var depth = ((rawtext.length - text.length )/12)+1;
1511 option.set('innerHTML', text);
1512 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1515 this.structure = [];
1516 var structcount = 0;
1517 for (var i in options) {
1520 this.structure.push(o);
1524 var current = this.structure[structcount-1];
1525 for (var j = 0; j < o.depth-1;j++) {
1526 if (current && current.children) {
1527 current = current.children[current.children.length-1];
1530 if (current && current.children) {
1531 current.children.push(o);
1536 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1537 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1538 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1539 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1541 if (this.cfg.mode == null) {
1542 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1543 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1544 this.cfg.mode = 'compact';
1546 this.cfg.mode = 'spanning';
1550 if (this.cfg.mode == 'compact') {
1551 this.nodes.menu.addClass('compactmenu');
1553 this.nodes.menu.addClass('spanningmenu');
1554 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1557 Y.one(document.body).append(this.nodes.menu);
1558 var pos = this.nodes.select.getXY();
1560 this.nodes.menu.setXY(pos);
1561 this.nodes.menu.on('click', this.handle_click, this);
1563 Y.one(window).on('resize', function(){
1564 var pos = this.nodes.select.getXY();
1566 this.nodes.menu.setXY(pos);
1569 generate_menu_content : function() {
1570 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1571 content += this.generate_submenu_content(this.structure[0], true);
1572 content += '</ul></div>';
1575 generate_submenu_content : function(item, rootelement) {
1576 this.submenucount++;
1578 if (item.children.length > 0) {
1580 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1581 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1582 content += '<div class="smartselect_menu_content">';
1584 content += '<li class="smartselect_submenuitem">';
1585 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1586 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1587 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1588 content += '<div class="smartselect_submenu_content">';
1591 for (var i in item.children) {
1592 content += this.generate_submenu_content(item.children[i],false);
1595 content += '</div>';
1596 content += '</div>';
1602 content += '<li class="smartselect_menuitem">';
1603 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1608 select : function(e) {
1611 this.currenttext = t.get('innerHTML');
1612 this.currentvalue = t.getAttribute('value');
1613 this.nodes.select.set('selectedIndex', this.currentvalue);
1616 handle_click : function(e) {
1617 var target = e.target;
1618 if (target.hasClass('smartselect_mask')) {
1620 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1622 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1623 this.show_sub_menu(e);
1626 show_menu : function(e) {
1628 var menu = e.target.ancestor().one('.smartselect_menu');
1629 menu.addClass('visible');
1630 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1632 show_sub_menu : function(e) {
1634 var target = e.target;
1635 if (!target.hasClass('smartselect_submenuitem')) {
1636 target = target.ancestor('.smartselect_submenuitem');
1638 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1639 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1642 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1643 target.one('.smartselect_submenu').addClass('visible');
1645 hide_menu : function() {
1646 this.nodes.menu.all('.visible').removeClass('visible');
1647 if (this.shownevent) {
1648 this.shownevent.detach();
1652 smartselect.init(Y, id, options, {select:select});
1656 /** List of flv players to be loaded */
1657 M.util.video_players = [];
1658 /** List of mp3 players to be loaded */
1659 M.util.audio_players = [];
1663 * @param id element id
1664 * @param fileurl media url
1667 * @param autosize true means detect size from media
1669 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1670 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1679 M.util.add_audio_player = function (id, fileurl, small) {
1680 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1684 * Initialise all audio and video player, must be called from page footer.
1686 M.util.load_flowplayer = function() {
1687 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1690 if (typeof(flowplayer) == 'undefined') {
1693 var embed_function = function() {
1694 if (loaded || typeof(flowplayer) == 'undefined') {
1700 url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
1703 /* TODO: add CSS color overrides for the flv flow player */
1705 for(var i=0; i<M.util.video_players.length; i++) {
1706 var video = M.util.video_players[i];
1707 if (video.width > 0 && video.height > 0) {
1708 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php', width: video.width, height: video.height};
1710 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.18.swf.php';
1712 flowplayer(video.id, src, {
1713 plugins: {controls: controls},
1715 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1716 onMetaData: function(clip) {
1717 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1718 clip.mvideo.resized = true;
1719 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1720 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1721 // bad luck, we have to guess - we may not get metadata at all
1722 var width = clip.width;
1723 var height = clip.height;
1725 var width = clip.metaData.width;
1726 var height = clip.metaData.height;
1728 var minwidth = 300; // controls are messed up in smaller objects
1729 if (width < minwidth) {
1730 height = (height * minwidth) / width;
1734 var object = this._api();
1735 object.width = width;
1736 object.height = height;
1742 if (M.util.audio_players.length == 0) {
1746 url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.controls-3.2.16.swf.php',
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.php', {
1812 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.11.swf.php'}},
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);