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().
226 * @param object event A YUI DOM event or null if launched manually
227 * @param string message The message to show in the dialog
228 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
229 * @param function fn A JS function to run if YES is clicked.
231 M.util.show_confirm_dialog = function(e, args) {
232 var target = e.target;
233 if (e.preventDefault) {
237 YUI().use('yui2-container', 'yui2-event', function(Y) {
238 var simpledialog = new Y.YUI2.widget.SimpleDialog('confirmdialog',
247 simpledialog.setHeader(M.str.admin.confirmation);
248 simpledialog.setBody(args.message);
249 simpledialog.cfg.setProperty('icon', Y.YUI2.widget.SimpleDialog.ICON_WARN);
251 var handle_cancel = function() {
255 var handle_yes = function() {
259 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
261 if (Y.Lang.isFunction(args.callback)) {
262 callback = args.callback;
264 callback = eval('('+args.callback+')');
267 if (Y.Lang.isObject(args.scope)) {
273 if (args.callbackargs) {
274 callback.apply(sc, args.callbackargs);
281 var targetancestor = null,
284 if (target.test('a')) {
285 window.location = target.get('href');
287 } else if ((targetancestor = target.ancestor('a')) !== null) {
288 window.location = targetancestor.get('href');
290 } else if (target.test('input')) {
291 targetform = target.ancestor(function(node) { return node.get('tagName').toLowerCase() == 'form'; });
292 // We cannot use target.ancestor('form') on the previous line
293 // because of http://yuilibrary.com/projects/yui3/ticket/2531561
297 if (target.get('name') && target.get('value')) {
298 targetform.append('<input type="hidden" name="' + target.get('name') +
299 '" value="' + target.get('value') + '">');
303 } else if (target.get('tagName').toLowerCase() == 'form') {
304 // We cannot use target.test('form') on the previous line because of
305 // http://yuilibrary.com/projects/yui3/ticket/2531561
308 } else if (M.cfg.developerdebug) {
309 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A, INPUT, or FORM");
313 if (!args.cancellabel) {
314 args.cancellabel = M.str.moodle.cancel;
316 if (!args.continuelabel) {
317 args.continuelabel = M.str.moodle.yes;
321 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
322 {text: args.continuelabel, handler: handle_yes}
325 simpledialog.cfg.queueProperty('buttons', buttons);
327 simpledialog.render(document.body);
332 /** Useful for full embedding of various stuff */
333 M.util.init_maximised_embed = function(Y, id) {
334 var obj = Y.one('#'+id);
339 var get_htmlelement_size = function(el, prop) {
340 if (Y.Lang.isString(el)) {
341 el = Y.one('#' + el);
343 // Ensure element exists.
345 var val = el.getStyle(prop);
347 val = el.getComputedStyle(prop);
349 return parseInt(val);
355 var resize_object = function() {
356 obj.setStyle('width', '0px');
357 obj.setStyle('height', '0px');
358 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
360 if (newwidth > 500) {
361 obj.setStyle('width', newwidth + 'px');
363 obj.setStyle('width', '500px');
366 var headerheight = get_htmlelement_size('page-header', 'height');
367 var footerheight = get_htmlelement_size('page-footer', 'height');
368 var newheight = parseInt(Y.one('body').get('winHeight')) - footerheight - headerheight - 100;
369 if (newheight < 400) {
372 obj.setStyle('height', newheight+'px');
376 // fix layout if window resized too
377 window.onresize = function() {
383 * Attach handler to single_select
385 * This code was deprecated in Moodle 2.4 and will be removed in Moodle 2.6
387 * Please see lib/yui/formautosubmit/formautosubmit.js for its replacement
389 M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
390 if (M.cfg.developerdebug) {
391 Y.log("You are using a deprecated function call (M.util.init_select_autosubmit). Please look at rewriting your call to use moodle-core-formautosubmit");
393 Y.use('event-key', function() {
394 var select = Y.one('#'+selectid);
396 // Try to get the form by id
397 var form = Y.one('#'+formid) || (function(){
398 // Hmmm the form's id may have been overriden by an internal input
399 // with the name id which will KILL IE.
400 // We need to manually iterate at this point because if the case
401 // above is true YUI's ancestor method will also kill IE!
403 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
404 form = form.ancestor();
408 // Make sure we have the form
411 // Create a function to handle our change event
412 var processchange = function(e, paramobject) {
413 if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
414 // chrome doesn't pick up on a click when selecting an element in a select menu, so we use
415 // the on change event to fire this function. This just checks to see if a button was
416 // first pressed before redirecting to the appropriate page.
417 if (Y.UA.os == 'windows' && Y.UA.chrome){
418 if (buttonflag == 1) {
429 paramobject.lastindex = select.get('selectedIndex');
432 var changedown = function(e, paramobject) {
433 if ((nothing===false || select.get('value') != nothing) && paramobject.lastindex != select.get('selectedIndex')) {
434 if(e.keyCode == 13) {
437 paramobject.lastindex = select.get('selectedIndex');
441 var paramobject = new Object();
442 paramobject.lastindex = select.get('selectedIndex');
443 paramobject.eventchangeorblur = select.on('click', processchange, form, paramobject);
444 // Bad hack to circumvent problems with different browsers on different systems.
445 if (Y.UA.os == 'macintosh') {
447 paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
449 paramobject.eventkeypress = Y.on('key', processchange, select, 'press:13', form, paramobject);
451 if(Y.UA.os == 'windows' && Y.UA.chrome) {
452 paramobject.eventchangeorblur = select.on('change', processchange, form, paramobject);
454 paramobject.eventkeypress = Y.on('keydown', changedown, select, '', form, paramobject);
462 * Attach handler to url_select
463 * Deprecated from 2.4 onwards.
464 * Please use @see init_select_autosubmit() for redirecting to a url (above).
465 * This function has accessability issues and also does not use the formid passed through as a parameter.
467 M.util.init_url_select = function(Y, formid, selectid, nothing) {
468 if (M.cfg.developerdebug) {
469 Y.log("You are using a deprecated function call (M.util.init_url_select). Please look at rewriting your call to use moodle-core-formautosubmit");
471 YUI().use('node', function(Y) {
472 Y.on('change', function() {
473 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
474 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
482 * Breaks out all links to the top frame - used in frametop page layout.
484 M.util.init_frametop = function(Y) {
485 Y.all('a').each(function(node) {
486 node.set('target', '_top');
488 Y.all('form').each(function(node) {
489 node.set('target', '_top');
494 * Finds all nodes that match the given CSS selector and attaches events to them
495 * so that they toggle a given classname when clicked.
498 * @param {string} id An id containing elements to target
499 * @param {string} cssselector A selector to use to find targets
500 * @param {string} toggleclassname A classname to toggle
502 M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
504 if (togglecssselector == '') {
505 togglecssselector = cssselector;
508 var node = Y.one('#'+id);
509 node.all(cssselector).each(function(n){
510 n.on('click', function(e){
512 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
513 if (this.test(togglecssselector)) {
514 this.toggleClass(toggleclassname);
516 this.ancestor(togglecssselector).toggleClass(toggleclassname);
521 // Attach this click event to the node rather than all selectors... will be much better
523 node.on('click', function(e){
524 if (e.target.hasClass('addtoall')) {
525 this.all(togglecssselector).addClass(toggleclassname);
526 } else if (e.target.hasClass('removefromall')) {
527 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
533 * Initialises a colour picker
535 * Designed to be used with admin_setting_configcolourpicker although could be used
536 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
537 * above or below the input (must have the same parent) and then call this with the
540 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
541 * contrib/blocks. For better docs refer to that.
545 * @param {object} previewconf
547 M.util.init_colour_picker = function(Y, id, previewconf) {
549 * We need node and event-mouseenter
551 Y.use('node', 'event-mouseenter', function(){
553 * The colour picker object
562 eventMouseEnter : null,
563 eventMouseLeave : null,
564 eventMouseMove : null,
569 * Initalises the colour picker by putting everything together and wiring the events
572 this.input = Y.one('#'+id);
573 this.box = this.input.ancestor().one('.admin_colourpicker');
574 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
575 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
576 this.preview = Y.Node.create('<div class="previewcolour"></div>');
577 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
578 this.current = Y.Node.create('<div class="currentcolour"></div>');
579 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
580 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
582 if (typeof(previewconf) === 'object' && previewconf !== null) {
583 Y.one('#'+id+'_preview').on('click', function(e){
584 if (Y.Lang.isString(previewconf.selector)) {
585 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
587 for (var i in previewconf.selector) {
588 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
594 this.eventClick = this.image.on('click', this.pickColour, this);
595 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
598 * Starts to follow the mouse once it enter the image
600 startFollow : function(e) {
601 this.eventMouseEnter.detach();
602 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
603 this.eventMouseMove = this.image.on('mousemove', function(e){
604 this.preview.setStyle('backgroundColor', this.determineColour(e));
608 * Stops following the mouse
610 endFollow : function(e) {
611 this.eventMouseMove.detach();
612 this.eventMouseLeave.detach();
613 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
616 * Picks the colour the was clicked on
618 pickColour : function(e) {
619 var colour = this.determineColour(e);
620 this.input.set('value', colour);
621 this.current.setStyle('backgroundColor', colour);
624 * Calculates the colour fromthe given co-ordinates
626 determineColour : function(e) {
627 var eventx = Math.floor(e.pageX-e.target.getX());
628 var eventy = Math.floor(e.pageY-e.target.getY());
630 var imagewidth = this.width;
631 var imageheight = this.height;
632 var factor = this.factor;
633 var colour = [255,0,0];
644 var matrixcount = matrices.length;
645 var limit = Math.round(imagewidth/matrixcount);
646 var heightbreak = Math.round(imageheight/2);
648 for (var x = 0; x < imagewidth; x++) {
649 var divisor = Math.floor(x / limit);
650 var matrix = matrices[divisor];
652 colour[0] += matrix[0]*factor;
653 colour[1] += matrix[1]*factor;
654 colour[2] += matrix[2]*factor;
661 var pixel = [colour[0], colour[1], colour[2]];
662 if (eventy < heightbreak) {
663 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
664 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
665 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
666 } else if (eventy > heightbreak) {
667 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
668 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
669 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
672 return this.convert_rgb_to_hex(pixel);
675 * Converts an RGB value to Hex
677 convert_rgb_to_hex : function(rgb) {
679 var hexchars = "0123456789ABCDEF";
680 for (var i=0; i<3; i++) {
681 var number = Math.abs(rgb[i]);
682 if (number == 0 || isNaN(number)) {
685 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
692 * Initialise the colour picker :) Hoorah
698 M.util.init_block_hider = function(Y, config) {
699 Y.use('base', 'node', function(Y) {
700 M.util.block_hider = M.util.block_hider || (function(){
701 var blockhider = function() {
702 blockhider.superclass.constructor.apply(this, arguments);
704 blockhider.prototype = {
705 initializer : function(config) {
706 this.set('block', '#'+this.get('id'));
707 var b = this.get('block'),
710 if (t && (a = t.one('.block_action'))) {
711 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
712 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
713 hide.on('keypress', this.updateStateKey, this, true);
714 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
715 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
716 show.on('keypress', this.updateStateKey, this, false);
717 a.insert(show, 0).insert(hide, 0);
720 updateState : function(e, hide) {
721 M.util.set_user_preference(this.get('preference'), hide);
723 this.get('block').addClass('hidden');
725 this.get('block').removeClass('hidden');
728 updateStateKey : function(e, hide) {
729 if (e.keyCode == 13) { //allow hide/show via enter key
730 this.updateState(this, hide);
734 Y.extend(blockhider, Y.Base, blockhider.prototype, {
740 value : M.util.image_url('t/switch_minus', 'moodle')
743 value : M.util.image_url('t/switch_plus', 'moodle')
746 setter : function(node) {
754 new M.util.block_hider(config);
759 * Returns a string registered in advance for usage in JavaScript
761 * If you do not pass the third parameter, the function will just return
762 * the corresponding value from the M.str object. If the third parameter is
763 * provided, the function performs {$a} placeholder substitution in the
764 * same way as PHP get_string() in Moodle does.
766 * @param {String} identifier string identifier
767 * @param {String} component the component providing the string
768 * @param {Object|String} a optional variable to populate placeholder with
770 M.util.get_string = function(identifier, component, a) {
773 if (M.cfg.developerdebug) {
774 // creating new instance if YUI is not optimal but it seems to be better way then
775 // require the instance via the function API - note that it is used in rare cases
776 // for debugging only anyway
777 // To ensure we don't kill browser performance if hundreds of get_string requests
778 // are made we cache the instance we generate within the M.util namespace.
779 // We don't publicly define the variable so that it doesn't get abused.
780 if (typeof M.util.get_string_yui_instance === 'undefined') {
781 M.util.get_string_yui_instance = new YUI({ debug : true });
783 var Y = M.util.get_string_yui_instance;
786 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
787 stringvalue = '[[' + identifier + ',' + component + ']]';
788 if (M.cfg.developerdebug) {
789 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
794 stringvalue = M.str[component][identifier];
796 if (typeof a == 'undefined') {
797 // no placeholder substitution requested
801 if (typeof a == 'number' || typeof a == 'string') {
802 // replace all occurrences of {$a} with the placeholder value
803 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
807 if (typeof a == 'object') {
808 // replace {$a->key} placeholders
810 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
811 if (M.cfg.developerdebug) {
812 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
816 var search = '{$a->' + key + '}';
817 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
818 search = new RegExp(search, 'g');
819 stringvalue = stringvalue.replace(search, a[key]);
824 if (M.cfg.developerdebug) {
825 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
831 * Set focus on username or password field of the login form
833 M.util.focus_login_form = function(Y) {
834 var username = Y.one('#username');
835 var password = Y.one('#password');
837 if (username == null || password == null) {
838 // something is wrong here
842 var curElement = document.activeElement
843 if (curElement == 'undefined') {
844 // legacy browser - skip refocus protection
845 } else if (curElement.tagName == 'INPUT') {
846 // user was probably faster to focus something, do not mess with focus
850 if (username.get('value') == '') {
858 * Set focus on login error message
860 M.util.focus_login_error = function(Y) {
861 var errorlog = Y.one('#loginerrormessage');
868 * Adds lightbox hidden element that covers the whole node.
871 * @param {Node} the node lightbox should be added to
872 * @retun {Node} created lightbox node
874 M.util.add_lightbox = function(Y, node) {
875 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
877 // Check if lightbox is already there
878 if (node.one('.lightbox')) {
879 return node.one('.lightbox');
882 node.setStyle('position', 'relative');
883 var waiticon = Y.Node.create('<img />')
885 'src' : M.util.image_url(WAITICON.pix, WAITICON.component)
888 'position' : 'relative',
892 var lightbox = Y.Node.create('<div></div>')
895 'position' : 'absolute',
900 'backgroundColor' : 'white',
901 'textAlign' : 'center'
903 .setAttribute('class', 'lightbox')
906 lightbox.appendChild(waiticon);
907 node.append(lightbox);
912 * Appends a hidden spinner element to the specified node.
915 * @param {Node} the node the spinner should be added to
916 * @return {Node} created spinner node
918 M.util.add_spinner = function(Y, node) {
919 var WAITICON = {'pix':"i/loading_small",'component':'moodle'};
921 // Check if spinner is already there
922 if (node.one('.spinner')) {
923 return node.one('.spinner');
926 var spinner = Y.Node.create('<img />')
927 .setAttribute('src', M.util.image_url(WAITICON.pix, WAITICON.component))
929 .addClass('iconsmall')
932 node.append(spinner);
936 //=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
938 function checkall() {
939 var inputs = document.getElementsByTagName('input');
940 for (var i = 0; i < inputs.length; i++) {
941 if (inputs[i].type == 'checkbox') {
942 if (inputs[i].disabled || inputs[i].readOnly) {
945 inputs[i].checked = true;
950 function checknone() {
951 var inputs = document.getElementsByTagName('input');
952 for (var i = 0; i < inputs.length; i++) {
953 if (inputs[i].type == 'checkbox') {
954 if (inputs[i].disabled || inputs[i].readOnly) {
957 inputs[i].checked = false;
963 * Either check, or uncheck, all checkboxes inside the element with id is
964 * @param id the id of the container
965 * @param checked the new state, either '' or 'checked'.
967 function select_all_in_element_with_id(id, checked) {
968 var container = document.getElementById(id);
972 var inputs = container.getElementsByTagName('input');
973 for (var i = 0; i < inputs.length; ++i) {
974 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
975 inputs[i].checked = checked;
980 function select_all_in(elTagName, elClass, elId) {
981 var inputs = document.getElementsByTagName('input');
982 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
983 for(var i = 0; i < inputs.length; ++i) {
984 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
985 inputs[i].checked = 'checked';
990 function deselect_all_in(elTagName, elClass, elId) {
991 var inputs = document.getElementsByTagName('INPUT');
992 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
993 for(var i = 0; i < inputs.length; ++i) {
994 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
995 inputs[i].checked = '';
1000 function confirm_if(expr, message) {
1004 return confirm(message);
1009 findParentNode (start, elementName, elementClass, elementID)
1011 Travels up the DOM hierarchy to find a parent element with the
1012 specified tag name, class, and id. All conditions must be met,
1013 but any can be ommitted. Returns the BODY element if no match
1016 function findParentNode(el, elName, elClass, elId) {
1017 while (el.nodeName.toUpperCase() != 'BODY') {
1018 if ((!elName || el.nodeName.toUpperCase() == elName) &&
1019 (!elClass || el.className.indexOf(elClass) != -1) &&
1020 (!elId || el.id == elId)) {
1028 findChildNode (start, elementName, elementClass, elementID)
1030 Travels down the DOM hierarchy to find all child elements with the
1031 specified tag name, class, and id. All conditions must be met,
1032 but any can be ommitted.
1033 Doesn't examine children of matches.
1035 function findChildNodes(start, tagName, elementClass, elementID, elementName) {
1036 var children = new Array();
1037 for (var i = 0; i < start.childNodes.length; i++) {
1038 var classfound = false;
1039 var child = start.childNodes[i];
1040 if((child.nodeType == 1) &&//element node type
1041 (elementClass && (typeof(child.className)=='string'))) {
1042 var childClasses = child.className.split(/\s+/);
1043 for (var childClassIndex in childClasses) {
1044 if (childClasses[childClassIndex]==elementClass) {
1050 if(child.nodeType == 1) { //element node type
1051 if ( (!tagName || child.nodeName == tagName) &&
1052 (!elementClass || classfound)&&
1053 (!elementID || child.id == elementID) &&
1054 (!elementName || child.name == elementName))
1056 children = children.concat(child);
1058 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
1065 function unmaskPassword(id) {
1066 var pw = document.getElementById(id);
1067 var chb = document.getElementById(id+'unmask');
1069 // MDL-30438 - The capability to changing the value of input type is not supported by IE8 or lower.
1070 // Replacing existing child with a new one, removed all yui properties for the node. Therefore, this
1071 // functionality won't work in IE8 or lower.
1072 // This is a temporary fixed for 2.4 or lower branches to allow other browsers to function properly.
1073 if (Y.UA.ie == 0 || Y.UA.ie >= 9) {
1077 pw.type = "password";
1079 } else { //IE Browser version 8 or lower
1081 // first try IE way - it can not set name attribute later
1083 var newpw = document.createElement('<input type="text" autocomplete="off" name="'+pw.name+'">');
1085 var newpw = document.createElement('<input type="password" autocomplete="off" name="'+pw.name+'">');
1087 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
1089 var newpw = document.createElement('input');
1090 newpw.setAttribute('autocomplete', 'off');
1091 newpw.setAttribute('name', pw.name);
1093 newpw.setAttribute('type', 'text');
1095 newpw.setAttribute('type', 'password');
1097 newpw.setAttribute('class', pw.getAttribute('class'));
1100 newpw.size = pw.size;
1101 newpw.onblur = pw.onblur;
1102 newpw.onchange = pw.onchange;
1103 newpw.value = pw.value;
1104 pw.parentNode.replaceChild(newpw, pw);
1108 function filterByParent(elCollection, parentFinder) {
1109 var filteredCollection = [];
1110 for (var i = 0; i < elCollection.length; ++i) {
1111 var findParent = parentFinder(elCollection[i]);
1112 if (findParent.nodeName.toUpperCase() != 'BODY') {
1113 filteredCollection.push(elCollection[i]);
1116 return filteredCollection;
1120 All this is here just so that IE gets to handle oversized blocks
1121 in a visually pleasing manner. It does a browser detect. So sue me.
1124 function fix_column_widths() {
1125 var agt = navigator.userAgent.toLowerCase();
1126 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
1127 fix_column_width('left-column');
1128 fix_column_width('right-column');
1132 function fix_column_width(colName) {
1133 if(column = document.getElementById(colName)) {
1134 if(!column.offsetWidth) {
1135 setTimeout("fix_column_width('" + colName + "')", 20);
1140 var nodes = column.childNodes;
1142 for(i = 0; i < nodes.length; ++i) {
1143 if(nodes[i].className.indexOf("block") != -1 ) {
1144 if(width < nodes[i].offsetWidth) {
1145 width = nodes[i].offsetWidth;
1150 for(i = 0; i < nodes.length; ++i) {
1151 if(nodes[i].className.indexOf("block") != -1 ) {
1152 nodes[i].style.width = width + 'px';
1160 Insert myValue at current cursor position
1162 function insertAtCursor(myField, myValue) {
1164 if (document.selection) {
1166 sel = document.selection.createRange();
1169 // Mozilla/Netscape support
1170 else if (myField.selectionStart || myField.selectionStart == '0') {
1171 var startPos = myField.selectionStart;
1172 var endPos = myField.selectionEnd;
1173 myField.value = myField.value.substring(0, startPos)
1174 + myValue + myField.value.substring(endPos, myField.value.length);
1176 myField.value += myValue;
1182 Call instead of setting window.onload directly or setting body onload=.
1183 Adds your function to a chain of functions rather than overwriting anything
1186 function addonload(fn) {
1187 var oldhandler=window.onload;
1188 window.onload=function() {
1189 if(oldhandler) oldhandler();
1194 * Replacement for getElementsByClassName in browsers that aren't cool enough
1196 * Relying on the built-in getElementsByClassName is far, far faster than
1199 * Note: the third argument used to be an object with odd behaviour. It now
1200 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1201 * mimicked if you pass an object.
1203 * @param {Node} oElm The top-level node for searching. To search a whole
1204 * document, use `document`.
1205 * @param {String} strTagName filter by tag names
1206 * @param {String} name same as HTML5 spec
1208 function getElementsByClassName(oElm, strTagName, name) {
1209 // for backwards compatibility
1210 if(typeof name == "object") {
1211 var names = new Array();
1212 for(var i=0; i<name.length; i++) names.push(names[i]);
1213 name = names.join('');
1215 // use native implementation if possible
1216 if (oElm.getElementsByClassName && Array.filter) {
1217 if (strTagName == '*') {
1218 return oElm.getElementsByClassName(name);
1220 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1221 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1225 // native implementation unavailable, fall back to slow method
1226 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1227 var arrReturnElements = new Array();
1228 var arrRegExpClassNames = new Array();
1229 var names = name.split(' ');
1230 for(var i=0; i<names.length; i++) {
1231 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
1235 for(var j=0; j<arrElements.length; j++) {
1236 oElement = arrElements[j];
1238 for(var k=0; k<arrRegExpClassNames.length; k++) {
1239 if(!arrRegExpClassNames[k].test(oElement.className)) {
1240 bMatchesAll = false;
1245 arrReturnElements.push(oElement);
1248 return (arrReturnElements)
1252 * Return whether we are in right to left mode or not.
1256 function right_to_left() {
1257 var body = Y.one('body');
1259 if (body && body.hasClass('dir-rtl')) {
1265 function openpopup(event, args) {
1268 if (event.preventDefault) {
1269 event.preventDefault();
1271 event.returnValue = false;
1275 // Make sure the name argument is set and valid.
1276 var nameregex = /[^a-z0-9_]/i;
1277 if (typeof args.name !== 'string') {
1278 args.name = '_blank';
1279 } else if (args.name.match(nameregex)) {
1280 // Cleans window name because IE does not support funky ones.
1281 args.name = args.name.replace(nameregex, '_');
1282 if (M.cfg.developerdebug) {
1283 alert('DEVELOPER NOTICE: Invalid \'name\' passed to openpopup()');
1287 var fullurl = args.url;
1288 if (!args.url.match(/https?:\/\//)) {
1289 fullurl = M.cfg.wwwroot + args.url;
1291 if (args.fullscreen) {
1292 args.options = args.options.
1293 replace(/top=\d+/, 'top=0').
1294 replace(/left=\d+/, 'left=0').
1295 replace(/width=\d+/, 'width=' + screen.availWidth).
1296 replace(/height=\d+/, 'height=' + screen.availHeight);
1298 var windowobj = window.open(fullurl,args.name,args.options);
1303 if (args.fullscreen) {
1304 // In some browser / OS combinations (E.g. Chrome on Windows), the
1305 // window initially opens slighly too big. The width and heigh options
1306 // seem to control the area inside the browser window, so what with
1307 // scroll-bars, etc. the actual window is bigger than the screen.
1308 // Therefore, we need to fix things up after the window is open.
1309 var hackcount = 100;
1310 var get_size_exactly_right = function() {
1311 windowobj.moveTo(0, 0);
1312 windowobj.resizeTo(screen.availWidth, screen.availHeight);
1314 // Unfortunately, it seems that in Chrome on Ubuntu, if you call
1315 // something like windowobj.resizeTo(1280, 1024) too soon (up to
1316 // about 50ms) after the window is open, then it actually behaves
1317 // as if you called windowobj.resizeTo(0, 0). Therefore, we need to
1318 // check that the resize actually worked, and if not, repeatedly try
1319 // again after a short delay until it works (but with a limit of
1320 // hackcount repeats.
1321 if (hackcount > 0 && (windowobj.innerHeight < 10 || windowobj.innerWidth < 10)) {
1323 setTimeout(get_size_exactly_right, 10);
1326 setTimeout(get_size_exactly_right, 0);
1333 /** Close the current browser window. */
1334 function close_window(e) {
1335 if (e.preventDefault) {
1338 e.returnValue = false;
1344 * Used in a couple of modules to hide navigation areas when using AJAX
1347 function show_item(itemid) {
1348 var item = document.getElementById(itemid);
1350 item.style.display = "";
1354 function destroy_item(itemid) {
1355 var item = document.getElementById(itemid);
1357 item.parentNode.removeChild(item);
1361 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1362 * @param controlid the control id.
1364 function focuscontrol(controlid) {
1365 var control = document.getElementById(controlid);
1372 * Transfers keyboard focus to an HTML element based on the old style style of focus
1373 * This function should be removed as soon as it is no longer used
1375 function old_onload_focus(formid, controlname) {
1376 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
1377 document.forms[formid].elements[controlname].focus();
1381 function build_querystring(obj) {
1382 return convert_object_to_string(obj, '&');
1385 function build_windowoptionsstring(obj) {
1386 return convert_object_to_string(obj, ',');
1389 function convert_object_to_string(obj, separator) {
1390 if (typeof obj !== 'object') {
1395 k = encodeURIComponent(k);
1397 if(obj[k] instanceof Array) {
1398 for(var i in value) {
1399 list.push(k+'[]='+encodeURIComponent(value[i]));
1402 list.push(k+'='+encodeURIComponent(value));
1405 return list.join(separator);
1408 function stripHTML(str) {
1409 var re = /<\S[^><]*>/g;
1410 var ret = str.replace(re, "");
1414 Number.prototype.fixed=function(n){
1416 return round(Number(this)*pow(10,n))/pow(10,n);
1418 function update_progress_bar (id, width, pt, msg, es){
1420 var status = document.getElementById("status_"+id);
1421 var percent_indicator = document.getElementById("pt_"+id);
1422 var progress_bar = document.getElementById("progress_"+id);
1423 var time_es = document.getElementById("time_"+id);
1424 status.innerHTML = msg;
1425 percent_indicator.innerHTML = percent.fixed(2) + '%';
1426 if(percent == 100) {
1427 progress_bar.style.background = "green";
1428 time_es.style.display = "none";
1430 progress_bar.style.background = "#FFCC66";
1432 time_es.innerHTML = "";
1434 time_es.innerHTML = es.fixed(2)+" sec";
1435 time_es.style.display
1439 progress_bar.style.width = width + "px";
1444 // ===== Deprecated core Javascript functions for Moodle ====
1445 // DO NOT USE!!!!!!!
1446 // Do not put this stuff in separate file because it only adds extra load on servers!
1449 * Used in a couple of modules to hide navigation areas when using AJAX
1451 function hide_item(itemid) {
1452 // use class='hiddenifjs' instead
1453 var item = document.getElementById(itemid);
1455 item.style.display = "none";
1459 M.util.help_popups = {
1460 setup : function(Y) {
1461 Y.one('body').delegate('click', this.open_popup, 'a.helplinkpopup', this);
1463 open_popup : function(e) {
1464 // Prevent the default page action
1467 // Grab the anchor that was clicked
1468 var anchor = e.target.ancestor('a', true);
1471 'url' : anchor.getAttribute('href'),
1489 args.options = options.join(',');
1495 M.util.help_icon = {
1498 initialised : false,
1499 setup : function(Y) {
1500 if (this.initialised) {
1501 // Exit early if we have already completed setup
1505 Y.one('body').delegate('click', this.display, 'span.helplink a.tooltip', this);
1506 this.initialised = true;
1508 add : function(Y, properties) {
1511 display : function(event) {
1512 event.preventDefault();
1513 if (M.util.help_icon.instance === null) {
1514 var Y = M.util.help_icon.Y;
1515 Y.use('overlay', 'io-base', 'event-mouseenter', 'node', 'event-key', 'escape', function(Y) {
1516 var help_content_overlay = {
1521 var strclose = Y.Escape.html(M.str.form.close);
1522 var footerbtn = Y.Node.create('<button class="closebtn">'+strclose+'</button>');
1523 // Create an overlay from markup
1524 this.overlay = new Y.Overlay({
1525 footerContent: footerbtn,
1532 this.overlay.render(Y.one(document.body));
1534 footerbtn.on('click', this.close, this);
1536 var boundingBox = this.overlay.get("boundingBox");
1538 // Hide the menu if the user clicks outside of its content
1539 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1540 var oTarget = event.target;
1541 var menuButton = this.helplink;
1543 if (!oTarget.compareTo(menuButton) &&
1544 !menuButton.contains(oTarget) &&
1545 !oTarget.compareTo(boundingBox) &&
1546 !boundingBox.contains(oTarget)) {
1547 this.overlay.hide();
1552 close : function(e) {
1554 this.helplink.focus();
1555 this.overlay.hide();
1558 display : function(event) {
1559 var overlayPosition;
1560 this.helplink = event.target.ancestor('span.helplink a', true);
1561 if (Y.one('html').get('dir') === 'rtl') {
1562 overlayPosition = [Y.WidgetPositionAlign.TR, Y.WidgetPositionAlign.LC];
1564 overlayPosition = [Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC];
1567 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
1568 this.overlay.set("align", {node:this.helplink, points: overlayPosition});
1577 success: function(id, o, node) {
1578 this.display_callback(o.responseText);
1580 failure: function(id, o, node) {
1581 var debuginfo = o.statusText;
1582 if (M.cfg.developerdebug) {
1583 o.statusText += ' (' + ajaxurl + ')';
1585 this.display_callback('bodyContent',debuginfo);
1590 Y.io(this.helplink.get('href'), cfg);
1591 this.overlay.show();
1594 display_callback : function(content) {
1595 var contentnode, heading;
1596 contentnode = Y.Node.create('<div role="alert">' + content + '</div>');
1597 this.overlay.set('bodyContent', contentnode);
1598 heading = contentnode.one('h1');
1600 heading.set('tabIndex', 0);
1605 hideContent : function() {
1607 help.overlay.hide();
1610 help_content_overlay.init();
1611 M.util.help_icon.instance = help_content_overlay;
1612 M.util.help_icon.instance.display(event);
1615 M.util.help_icon.instance.display(event);
1618 init : function(Y) {
1624 * Custom menu namespace
1626 M.core_custom_menu = {
1628 * This method is used to initialise a custom menu given the id that belongs
1629 * to the custom menu's root node.
1632 * @param {string} nodeid
1634 init : function(Y, nodeid) {
1635 var node = Y.one('#'+nodeid);
1637 Y.use('node-menunav', function(Y) {
1639 // Remove the javascript-disabled class.... obviously javascript is enabled.
1640 node.removeClass('javascript-disabled');
1641 // Initialise the menunav plugin
1642 node.plug(Y.Plugin.NodeMenuNav);
1649 * Used to store form manipulation methods and enhancments
1651 M.form = M.form || {};
1654 * Converts a nbsp indented select box into a multi drop down custom control much
1655 * like the custom menu. It also selectable categories on or off.
1657 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1660 * @param {string} id
1661 * @param {Array} options
1663 M.form.init_smartselect = function(Y, id, options) {
1664 if (!id.match(/^id_/)) {
1667 var select = Y.one('select#'+id);
1671 Y.use('event-delegate',function(){
1677 currentvalue : null,
1681 selectablecategories : true,
1689 init : function(Y, id, args, nodes) {
1690 if (typeof(args)=='object') {
1691 for (var i in this.cfg) {
1692 if (args[i] || args[i]===false) {
1693 this.cfg[i] = args[i];
1698 // Display a loading message first up
1699 this.nodes.select = nodes.select;
1701 this.currentvalue = this.nodes.select.get('selectedIndex');
1702 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1704 var options = Array();
1705 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
1706 this.nodes.select.all('option').each(function(option, index) {
1707 var rawtext = option.get('innerHTML');
1708 var text = rawtext.replace(/^( )*/, '');
1709 if (rawtext === text) {
1710 text = rawtext.replace(/^(\s)*/, '');
1711 var depth = (rawtext.length - text.length ) + 1;
1713 var depth = ((rawtext.length - text.length )/12)+1;
1715 option.set('innerHTML', text);
1716 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1719 this.structure = [];
1720 var structcount = 0;
1721 for (var i in options) {
1724 this.structure.push(o);
1728 var current = this.structure[structcount-1];
1729 for (var j = 0; j < o.depth-1;j++) {
1730 if (current && current.children) {
1731 current = current.children[current.children.length-1];
1734 if (current && current.children) {
1735 current.children.push(o);
1740 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1741 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1742 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1743 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1745 if (this.cfg.mode == null) {
1746 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1747 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1748 this.cfg.mode = 'compact';
1750 this.cfg.mode = 'spanning';
1754 if (this.cfg.mode == 'compact') {
1755 this.nodes.menu.addClass('compactmenu');
1757 this.nodes.menu.addClass('spanningmenu');
1758 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1761 Y.one(document.body).append(this.nodes.menu);
1762 var pos = this.nodes.select.getXY();
1764 this.nodes.menu.setXY(pos);
1765 this.nodes.menu.on('click', this.handle_click, this);
1767 Y.one(window).on('resize', function(){
1768 var pos = this.nodes.select.getXY();
1770 this.nodes.menu.setXY(pos);
1773 generate_menu_content : function() {
1774 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1775 content += this.generate_submenu_content(this.structure[0], true);
1776 content += '</ul></div>';
1779 generate_submenu_content : function(item, rootelement) {
1780 this.submenucount++;
1782 if (item.children.length > 0) {
1784 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'"> </div>';
1785 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1786 content += '<div class="smartselect_menu_content">';
1788 content += '<li class="smartselect_submenuitem">';
1789 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1790 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1791 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1792 content += '<div class="smartselect_submenu_content">';
1795 for (var i in item.children) {
1796 content += this.generate_submenu_content(item.children[i],false);
1799 content += '</div>';
1800 content += '</div>';
1806 content += '<li class="smartselect_menuitem">';
1807 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1812 select : function(e) {
1815 this.currenttext = t.get('innerHTML');
1816 this.currentvalue = t.getAttribute('value');
1817 this.nodes.select.set('selectedIndex', this.currentvalue);
1820 handle_click : function(e) {
1821 var target = e.target;
1822 if (target.hasClass('smartselect_mask')) {
1824 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1826 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1827 this.show_sub_menu(e);
1830 show_menu : function(e) {
1832 var menu = e.target.ancestor().one('.smartselect_menu');
1833 menu.addClass('visible');
1834 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1836 show_sub_menu : function(e) {
1838 var target = e.target;
1839 if (!target.hasClass('smartselect_submenuitem')) {
1840 target = target.ancestor('.smartselect_submenuitem');
1842 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1843 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1846 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1847 target.one('.smartselect_submenu').addClass('visible');
1849 hide_menu : function() {
1850 this.nodes.menu.all('.visible').removeClass('visible');
1851 if (this.shownevent) {
1852 this.shownevent.detach();
1856 smartselect.init(Y, id, options, {select:select});
1860 /** List of flv players to be loaded */
1861 M.util.video_players = [];
1862 /** List of mp3 players to be loaded */
1863 M.util.audio_players = [];
1867 * @param id element id
1868 * @param fileurl media url
1871 * @param autosize true means detect size from media
1873 M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1874 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
1883 M.util.add_audio_player = function (id, fileurl, small) {
1884 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
1888 * Initialise all audio and video player, must be called from page footer.
1890 M.util.load_flowplayer = function() {
1891 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1894 if (typeof(flowplayer) == 'undefined') {
1897 var embed_function = function() {
1898 if (loaded || typeof(flowplayer) == 'undefined') {
1906 /* TODO: add CSS color overrides for the flv flow player */
1908 for(var i=0; i<M.util.video_players.length; i++) {
1909 var video = M.util.video_players[i];
1910 if (video.width > 0 && video.height > 0) {
1911 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', width: video.width, height: video.height};
1913 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf';
1915 flowplayer(video.id, src, {
1916 plugins: {controls: controls},
1918 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1919 onMetaData: function(clip) {
1920 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1921 clip.mvideo.resized = true;
1922 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1923 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1924 // bad luck, we have to guess - we may not get metadata at all
1925 var width = clip.width;
1926 var height = clip.height;
1928 var width = clip.metaData.width;
1929 var height = clip.metaData.height;
1931 var minwidth = 300; // controls are messed up in smaller objects
1932 if (width < minwidth) {
1933 height = (height * minwidth) / width;
1937 var object = this._api();
1938 object.width = width;
1939 object.height = height;
1945 if (M.util.audio_players.length == 0) {
1958 backgroundGradient: [0.5,0,0.3]
1962 for (var j=0; j < document.styleSheets.length; j++) {
1964 // To avoid javascript security violation accessing cross domain stylesheets
1965 var allrules = false;
1967 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1968 allrules = document.styleSheets[j].rules;
1969 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1970 allrules = document.styleSheets[j].cssRules;
1979 // On cross domain style sheets Chrome V8 allows access to rules but returns null
1984 for(var i=0; i<allrules.length; i++) {
1986 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1987 if (typeof(allrules[i].cssText) != 'undefined') {
1988 rule = allrules[i].cssText;
1989 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1990 rule = allrules[i].style.cssText;
1992 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1993 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1994 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1995 controls[colprop] = rule;
2002 for(i=0; i<M.util.audio_players.length; i++) {
2003 var audio = M.util.audio_players[i];
2005 controls.controlall = false;
2006 controls.height = 15;
2007 controls.time = false;
2009 controls.controlall = true;
2010 controls.height = 25;
2011 controls.time = true;
2013 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.14.swf', {
2014 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.10.swf'}},
2015 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
2020 if (M.cfg.jsrev == -1) {
2021 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.11.js';
2023 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?jsfile=/lib/flowplayer/flowplayer-3.2.11.min.js&rev=' + M.cfg.jsrev;
2025 var fileref = document.createElement('script');
2026 fileref.setAttribute('type','text/javascript');
2027 fileref.setAttribute('src', jsurl);
2028 fileref.onload = embed_function;
2029 fileref.onreadystatechange = embed_function;
2030 document.getElementsByTagName('head')[0].appendChild(fileref);