MDL-23344 new option to autofocus username or password on the login form
[moodle.git] / lib / javascript-static.js
CommitLineData
47aa42e2 1// Miscellaneous core Javascript functions for Moodle
e50b4c89 2// Global M object is initilised in inline javascript
34f76e9a 3
e50b4c89
PS
4/**
5 * Add module to list of available modules that can be laoded from YUI.
126590b7 6 * @param {Array} modules
e50b4c89
PS
7 */
8M.yui.add_module = function(modules) {
126590b7 9 for (var modname in modules) {
fc367afb
PS
10 M.yui.loader.modules[modname] = modules[modname];
11 }
e50b4c89 12};
2a102b90
SH
13/**
14 * The gallery version to use when loading YUI modules from the gallery.
15 * Will be changed every time when using local galleries.
16 */
17M.yui.galleryversion = '2010.04.21-21-51';
38224dcb 18
bca09754
PS
19/**
20 * Various utility functions
21 */
d29b8f0f 22M.util = M.util || {};
38224dcb 23
2b728cb5
PS
24/**
25 * Language strings - initialised from page footer.
26 */
d29b8f0f 27M.str = M.str || {};
2b728cb5 28
38224dcb
PS
29/**
30 * Returns url for images.
126590b7
SH
31 * @param {String} imagename
32 * @param {String} component
33 * @return {String}
38224dcb
PS
34 */
35M.util.image_url = function(imagename, component) {
36 var url = M.cfg.wwwroot + '/theme/image.php?theme=' + M.cfg.theme + '&image=' + imagename;
37
38 if (M.cfg.themerev > 0) {
39 url = url + '&rev=' + M.cfg.themerev;
40 }
41
87ad1edc 42 if (component && component != '' && component != 'moodle' && component != 'core') {
38224dcb
PS
43 url = url + '&component=' + component;
44 }
45
46 return url;
47};
48
b4430cd6
PS
49M.util.in_array = function(item, array){
50 for( var i = 0; i<array.length; i++){
51 if(item==array[i]){
52 return true;
53 }
54 }
55 return false;
667c0ae1 56};
b4430cd6 57
38224dcb
PS
58/**
59 * Init a collapsible region, see print_collapsible_region in weblib.php
126590b7
SH
60 * @param {YUI} Y YUI3 instance with all libraries loaded
61 * @param {String} id the HTML id for the div.
62 * @param {String} userpref the user preference that records the state of this box. false if none.
63 * @param {String} strtooltip
38224dcb
PS
64 */
65M.util.init_collapsible_region = function(Y, id, userpref, strtooltip) {
66 Y.use('anim', function(Y) {
3b044ba3 67 new M.util.CollapsibleRegion(Y, id, userpref, strtooltip);
38224dcb
PS
68 });
69};
70
71/**
126590b7 72 * Object to handle a collapsible region : instantiate and forget styled object
3b044ba3 73 *
126590b7 74 * @class
38224dcb 75 * @constructor
126590b7
SH
76 * @param {YUI} Y YUI3 instance with all libraries loaded
77 * @param {String} id The HTML id for the div.
78 * @param {String} userpref The user preference that records the state of this box. false if none.
79 * @param {String} strtooltip
38224dcb
PS
80 */
81M.util.CollapsibleRegion = function(Y, id, userpref, strtooltip) {
38224dcb
PS
82 // Record the pref name
83 this.userpref = userpref;
38224dcb
PS
84
85 // Find the divs in the document.
126590b7
SH
86 this.div = Y.one('#'+id);
87
88 // Get the caption for the collapsible region
89 var caption = this.div.one('#'+id + '_caption');
90 caption.setAttribute('title', strtooltip);
91
92 // Create a link
93 var a = Y.Node.create('<a href="#"></a>');
94 // Create a local scoped lamba function to move nodes to a new link
95 var movenode = function(node){
96 node.remove();
97 a.append(node);
98 };
99 // Apply the lamba function on each of the captions child nodes
100 caption.get('children').each(movenode, this);
101 caption.append(a);
102
103 // Get the height of the div at this point before we shrink it if required
104 var height = this.div.get('offsetHeight');
105 if (this.div.hasClass('collapsed')) {
106 // Add the correct image and record the YUI node created in the process
107 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/collapsed', 'moodle')+'" alt="" />');
108 // Shrink the div as it is collapsed by default
109 this.div.setStyle('height', caption.get('offsetHeight')+'px');
110 } else {
111 // Add the correct image and record the YUI node created in the process
112 this.icon = Y.Node.create('<img src="'+M.util.image_url('t/expanded', 'moodle')+'" alt="" />');
38224dcb 113 }
126590b7 114 a.append(this.icon);
38224dcb
PS
115
116 // Create the animation.
126590b7 117 var animation = new Y.Anim({
38224dcb
PS
118 node: this.div,
119 duration: 0.3,
126590b7
SH
120 easing: Y.Easing.easeBoth,
121 to: {height:caption.get('offsetHeight')},
122 from: {height:height}
38224dcb 123 });
3b044ba3 124
38224dcb 125 // Handler for the animation finishing.
126590b7
SH
126 animation.on('end', function() {
127 this.div.toggleClass('collapsed');
128 if (this.div.hasClass('collapsed')) {
129 this.icon.set('src', M.util.image_url('t/collapsed', 'moodle'));
130 } else {
131 this.icon.set('src', M.util.image_url('t/expanded', 'moodle'));
fc367afb 132 }
38224dcb 133 }, this);
126590b7
SH
134
135 // Hook up the event handler.
136 a.on('click', function(e, animation) {
137 e.preventDefault();
138 // Animate to the appropriate size.
139 if (animation.get('running')) {
140 animation.stop();
141 }
142 animation.set('reverse', this.div.hasClass('collapsed'));
143 // Update the user preference.
144 if (this.userpref) {
41912a26 145 M.util.set_user_preference(this.userpref, !this.div.hasClass('collapsed'));
126590b7
SH
146 }
147 animation.run();
148 }, this, animation);
38224dcb
PS
149};
150
151/**
152 * The user preference that stores the state of this box.
153 * @property userpref
154 * @type String
155 */
156M.util.CollapsibleRegion.prototype.userpref = null;
157
158/**
159 * The key divs that make up this
126590b7
SH
160 * @property div
161 * @type Y.Node
38224dcb
PS
162 */
163M.util.CollapsibleRegion.prototype.div = null;
38224dcb
PS
164
165/**
166 * The key divs that make up this
167 * @property icon
126590b7 168 * @type Y.Node
38224dcb
PS
169 */
170M.util.CollapsibleRegion.prototype.icon = null;
171
41912a26
PS
172/**
173 * Makes a best effort to connect back to Moodle to update a user preference,
174 * however, there is no mechanism for finding out if the update succeeded.
175 *
176 * Before you can use this function in your JavsScript, you must have called
177 * user_preference_allow_ajax_update from moodlelib.php to tell Moodle that
178 * the udpate is allowed, and how to safely clean and submitted values.
179 *
180 * @param String name the name of the setting to udpate.
181 * @param String the value to set it to.
182 */
183M.util.set_user_preference = function(name, value) {
20fb563e 184 YUI(M.yui.loader).use('io', function(Y) {
41912a26
PS
185 var url = M.cfg.wwwroot + '/lib/ajax/setuserpref.php?sesskey=' +
186 M.cfg.sesskey + '&pref=' + encodeURI(name) + '&value=' + encodeURI(value);
187
188 // If we are a developer, ensure that failures are reported.
189 var cfg = {
20fb563e 190 method: 'get',
8dd14645 191 on: {}
41912a26
PS
192 };
193 if (M.cfg.developerdebug) {
194 cfg.on.failure = function(id, o, args) {
20fb563e 195 alert("Error updating user preference '" + name + "' using ajax. Clicking this link will repeat the Ajax call that failed so you can see the error: ");
41912a26
PS
196 }
197 }
198
199 // Make the request.
200 Y.io(url, cfg);
201 });
202};
203
20fb563e
PS
204/**
205 * Prints a confirmation dialog in the style of DOM.confirm().
206 * @param object event A YUI DOM event or null if launched manually
207 * @param string message The message to show in the dialog
208 * @param string url The URL to forward to if YES is clicked. Disabled if fn is given
209 * @param function fn A JS function to run if YES is clicked.
210 */
211M.util.show_confirm_dialog = function(e, args) {
212 var target = e.target;
213 if (e.preventDefault) {
214 e.preventDefault();
215 }
216
217 YUI(M.yui.loader).use('yui2-container', 'yui2-event', function(Y) {
218 var simpledialog = new YAHOO.widget.SimpleDialog('confirmdialog',
f855a5d2 219 {width: '300px',
20fb563e
PS
220 fixedcenter: true,
221 modal: true,
222 visible: false,
223 draggable: false
224 }
225 );
226
2b728cb5 227 simpledialog.setHeader(M.str.admin.confirmation);
20fb563e
PS
228 simpledialog.setBody(args.message);
229 simpledialog.cfg.setProperty('icon', YAHOO.widget.SimpleDialog.ICON_WARN);
230
231 var handle_cancel = function() {
232 simpledialog.hide();
233 };
234
235 var handle_yes = function() {
236 simpledialog.hide();
237
238 if (args.callback) {
239 // args comes from PHP, so callback will be a string, needs to be evaluated by JS
240 var callback = null;
241 if (Y.Lang.isFunction(args.callback)) {
242 callback = args.callback;
243 } else {
244 callback = eval('('+args.callback+')');
245 }
246
247 if (Y.Lang.isObject(args.scope)) {
248 var sc = args.scope;
249 } else {
250 var sc = e.target;
251 }
252
253 if (args.callbackargs) {
254 callback.apply(sc, args.callbackargs);
255 } else {
256 callback.apply(sc);
257 }
258 return;
259 }
260
712b55e1
SH
261 var targetancestor = null,
262 targetform = null;
263
264 if (target.test('a')) {
20fb563e 265 window.location = target.get('href');
712b55e1
SH
266 } else if ((targetancestor = target.ancestor('a')) !== null) {
267 window.location = targetancestor.get('href');
268 } else if (target.test('input')) {
269 targetform = target.ancestor('form');
270 if (targetform && targetform.submit) {
271 targetform.submit();
20fb563e
PS
272 }
273 } else if (M.cfg.developerdebug) {
274 alert("Element of type " + target.get('tagName') + " is not supported by the M.util.show_confirm_dialog function. Use A or INPUT");
275 }
276 };
277
8f78cd5a
TH
278 if (!args.cancellabel) {
279 args.cancellabel = M.str.moodle.cancel;
280 }
281 if (!args.continuelabel) {
282 args.continuelabel = M.str.moodle.yes;
283 }
284
285 var buttons = [
286 {text: args.cancellabel, handler: handle_cancel, isDefault: true},
287 {text: args.continuelabel, handler: handle_yes}
288 ];
20fb563e
PS
289
290 simpledialog.cfg.queueProperty('buttons', buttons);
291
292 simpledialog.render(document.body);
293 simpledialog.show();
294 });
667c0ae1 295};
20fb563e 296
dd9b1882
PS
297/** Useful for full embedding of various stuff */
298M.util.init_maximised_embed = function(Y, id) {
299 var obj = Y.one('#'+id);
300 if (!obj) {
301 return;
302 }
303
dd9b1882
PS
304 var get_htmlelement_size = function(el, prop) {
305 if (Y.Lang.isString(el)) {
306 el = Y.one('#' + el);
307 }
308 var val = el.getStyle(prop);
309 if (val == 'auto') {
310 val = el.getComputedStyle(prop);
311 }
312 return parseInt(val);
313 };
314
315 var resize_object = function() {
316 obj.setStyle('width', '0px');
317 obj.setStyle('height', '0px');
fcd2cbaf 318 var newwidth = get_htmlelement_size('maincontent', 'width') - 35;
dd9b1882 319
fcd2cbaf 320 if (newwidth > 500) {
dd9b1882
PS
321 obj.setStyle('width', newwidth + 'px');
322 } else {
fcd2cbaf 323 obj.setStyle('width', '500px');
dd9b1882 324 }
f6fa64f0
PS
325
326 var headerheight = get_htmlelement_size('page-header', 'height');
327 var footerheight = get_htmlelement_size('page-footer', 'height');
fcd2cbaf 328 var newheight = parseInt(YAHOO.util.Dom.getViewportHeight()) - footerheight - headerheight - 100;
f6fa64f0
PS
329 if (newheight < 400) {
330 newheight = 400;
dd9b1882 331 }
f6fa64f0 332 obj.setStyle('height', newheight+'px');
dd9b1882
PS
333 };
334
335 resize_object();
336 // fix layout if window resized too
337 window.onresize = function() {
338 resize_object();
339 };
340};
341
a9967cf5
PS
342/**
343 * Attach handler to single_select
344 */
edc28287 345M.util.init_select_autosubmit = function(Y, formid, selectid, nothing) {
c7bbb86f
SH
346 Y.use('event-key', function() {
347 var select = Y.one('#'+selectid);
348 if (select) {
349 // Try to get the form by id
350 var form = Y.one('#'+formid) || (function(){
351 // Hmmm the form's id may have been overriden by an internal input
352 // with the name id which will KILL IE.
353 // We need to manually iterate at this point because if the case
354 // above is true YUI's ancestor method will also kill IE!
355 var form = select;
356 while (form && form.get('nodeName').toUpperCase() !== 'FORM') {
357 form = form.ancestor();
358 }
359 return form;
360 })();
361 // Make sure we have the form
362 if (form) {
363 // Create a function to handle our change event
364 var processchange = function(e, lastindex) {
365 if ((nothing===false || select.get('value') != nothing) && lastindex != select.get('selectedIndex')) {
366 this.submit();
367 }
667c0ae1 368 };
c7bbb86f
SH
369 // Attach the change event to the keypress, blur, and click actions.
370 // We don't use the change event because IE fires it on every arrow up/down
371 // event.... usability
372 Y.on('key', processchange, select, 'press:13', form, select.get('selectedIndex'));
373 select.on('blur', processchange, form, select.get('selectedIndex'));
1ede3cb3 374 //little hack for chrome that need onChange event instead of onClick - see MDL-23224
375 if (Y.UA.webkit) {
376 select.on('change', processchange, form, select.get('selectedIndex'));
377 } else {
378 select.on('click', processchange, form, select.get('selectedIndex'));
379 }
c7bbb86f 380 }
4aea3cc7 381 }
4aea3cc7 382 });
a9967cf5 383};
dd9b1882 384
4d10e579
PS
385/**
386 * Attach handler to url_select
387 */
388M.util.init_url_select = function(Y, formid, selectid, nothing) {
4aea3cc7
PS
389 YUI(M.yui.loader).use('node', function(Y) {
390 Y.on('change', function() {
c7bbb86f
SH
391 if ((nothing == false && Y.Lang.isBoolean(nothing)) || Y.one('#'+selectid).get('value') != nothing) {
392 window.location = M.cfg.wwwroot+Y.one('#'+selectid).get('value');
393 }
4aea3cc7
PS
394 },
395 '#'+selectid);
396 });
397};
398
399/**
400 * Breaks out all links to the top frame - used in frametop page layout.
401 */
402M.util.init_frametop = function(Y) {
403 Y.all('a').each(function(node) {
404 node.set('target', '_top');
405 });
406 Y.all('form').each(function(node) {
407 node.set('target', '_top');
408 });
4d10e579
PS
409};
410
24e27ac0
SH
411/**
412 * Finds all nodes that match the given CSS selector and attaches events to them
413 * so that they toggle a given classname when clicked.
414 *
415 * @param {YUI} Y
416 * @param {string} id An id containing elements to target
417 * @param {string} cssselector A selector to use to find targets
418 * @param {string} toggleclassname A classname to toggle
419 */
c8ffba95
SH
420M.util.init_toggle_class_on_click = function(Y, id, cssselector, toggleclassname, togglecssselector) {
421
422 if (togglecssselector == '') {
423 togglecssselector = cssselector;
424 }
425
24e27ac0 426 var node = Y.one('#'+id);
c8ffba95
SH
427 node.all(cssselector).each(function(n){
428 n.on('click', function(e){
24e27ac0 429 e.stopPropagation();
c8ffba95
SH
430 if (e.target.test(cssselector) && !e.target.test('a') && !e.target.test('img')) {
431 if (this.test(togglecssselector)) {
432 this.toggleClass(toggleclassname);
433 } else {
434 this.ancestor(togglecssselector).toggleClass(toggleclassname);
435 }
24e27ac0 436 }
c8ffba95 437 }, n);
24e27ac0
SH
438 });
439 // Attach this click event to the node rather than all selectors... will be much better
440 // for performance
441 node.on('click', function(e){
442 if (e.target.hasClass('addtoall')) {
c8ffba95 443 this.all(togglecssselector).addClass(toggleclassname);
24e27ac0 444 } else if (e.target.hasClass('removefromall')) {
c8ffba95 445 this.all(togglecssselector+'.'+toggleclassname).removeClass(toggleclassname);
24e27ac0
SH
446 }
447 }, node);
667c0ae1 448};
24e27ac0 449
233b6ff9
SH
450/**
451 * Initialises a colour picker
452 *
453 * Designed to be used with admin_setting_configcolourpicker although could be used
454 * anywhere, just give a text input an id and insert a div with the class admin_colourpicker
455 * above or below the input (must have the same parent) and then call this with the
456 * id.
457 *
458 * This code was mostly taken from my [Sam Hemelryk] css theme tool available in
459 * contrib/blocks. For better docs refer to that.
460 *
461 * @param {YUI} Y
462 * @param {int} id
463 * @param {object} previewconf
464 */
465M.util.init_colour_picker = function(Y, id, previewconf) {
466 /**
467 * We need node and event-mouseenter
468 */
469 Y.use('node', 'event-mouseenter', function(){
470 /**
471 * The colour picker object
472 */
473 var colourpicker = {
474 box : null,
475 input : null,
476 image : null,
477 preview : null,
478 current : null,
479 eventClick : null,
480 eventMouseEnter : null,
481 eventMouseLeave : null,
482 eventMouseMove : null,
483 width : 300,
484 height : 100,
485 factor : 5,
486 /**
487 * Initalises the colour picker by putting everything together and wiring the events
488 */
489 init : function() {
490 this.input = Y.one('#'+id);
491 this.box = this.input.ancestor().one('.admin_colourpicker');
492 this.image = Y.Node.create('<img alt="" class="colourdialogue" />');
493 this.image.setAttribute('src', M.util.image_url('i/colourpicker', 'moodle'));
494 this.preview = Y.Node.create('<div class="previewcolour"></div>');
495 this.preview.setStyle('width', this.height/2).setStyle('height', this.height/2).setStyle('backgroundColor', this.input.get('value'));
496 this.current = Y.Node.create('<div class="currentcolour"></div>');
497 this.current.setStyle('width', this.height/2).setStyle('height', this.height/2 -1).setStyle('backgroundColor', this.input.get('value'));
498 this.box.setContent('').append(this.image).append(this.preview).append(this.current);
499
500 if (typeof(previewconf) === 'object' && previewconf !== null) {
501 Y.one('#'+id+'_preview').on('click', function(e){
3c3a3c3c 502 if (Y.Lang.isString(previewconf.selector)) {
bb9e9362
SH
503 Y.all(previewconf.selector).setStyle(previewconf.style, this.input.get('value'));
504 } else {
505 for (var i in previewconf.selector) {
506 Y.all(previewconf.selector[i]).setStyle(previewconf.style, this.input.get('value'));
507 }
508 }
233b6ff9
SH
509 }, this);
510 }
511
512 this.eventClick = this.image.on('click', this.pickColour, this);
513 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
514 },
515 /**
516 * Starts to follow the mouse once it enter the image
517 */
518 startFollow : function(e) {
519 this.eventMouseEnter.detach();
520 this.eventMouseLeave = Y.on('mouseleave', this.endFollow, this.image, this);
521 this.eventMouseMove = this.image.on('mousemove', function(e){
522 this.preview.setStyle('backgroundColor', this.determineColour(e));
523 }, this);
524 },
525 /**
526 * Stops following the mouse
527 */
528 endFollow : function(e) {
529 this.eventMouseMove.detach();
530 this.eventMouseLeave.detach();
531 this.eventMouseEnter = Y.on('mouseenter', this.startFollow, this.image, this);
532 },
533 /**
534 * Picks the colour the was clicked on
535 */
536 pickColour : function(e) {
537 var colour = this.determineColour(e);
538 this.input.set('value', colour);
539 this.current.setStyle('backgroundColor', colour);
540 },
541 /**
542 * Calculates the colour fromthe given co-ordinates
543 */
544 determineColour : function(e) {
545 var eventx = Math.floor(e.pageX-e.target.getX());
546 var eventy = Math.floor(e.pageY-e.target.getY());
547
548 var imagewidth = this.width;
549 var imageheight = this.height;
550 var factor = this.factor;
551 var colour = [255,0,0];
552
553 var matrices = [
554 [ 0, 1, 0],
555 [ -1, 0, 0],
556 [ 0, 0, 1],
557 [ 0, -1, 0],
558 [ 1, 0, 0],
559 [ 0, 0, -1]
560 ];
561
562 var matrixcount = matrices.length;
563 var limit = Math.round(imagewidth/matrixcount);
564 var heightbreak = Math.round(imageheight/2);
565
566 for (var x = 0; x < imagewidth; x++) {
567 var divisor = Math.floor(x / limit);
568 var matrix = matrices[divisor];
569
570 colour[0] += matrix[0]*factor;
571 colour[1] += matrix[1]*factor;
572 colour[2] += matrix[2]*factor;
573
574 if (eventx==x) {
575 break;
576 }
577 }
578
579 var pixel = [colour[0], colour[1], colour[2]];
580 if (eventy < heightbreak) {
581 pixel[0] += Math.floor(((255-pixel[0])/heightbreak) * (heightbreak - eventy));
582 pixel[1] += Math.floor(((255-pixel[1])/heightbreak) * (heightbreak - eventy));
583 pixel[2] += Math.floor(((255-pixel[2])/heightbreak) * (heightbreak - eventy));
584 } else if (eventy > heightbreak) {
585 pixel[0] = Math.floor((imageheight-eventy)*(pixel[0]/heightbreak));
586 pixel[1] = Math.floor((imageheight-eventy)*(pixel[1]/heightbreak));
587 pixel[2] = Math.floor((imageheight-eventy)*(pixel[2]/heightbreak));
588 }
589
590 return this.convert_rgb_to_hex(pixel);
591 },
592 /**
593 * Converts an RGB value to Hex
594 */
595 convert_rgb_to_hex : function(rgb) {
596 var hex = '#';
597 var hexchars = "0123456789ABCDEF";
598 for (var i=0; i<3; i++) {
599 var number = Math.abs(rgb[i]);
600 if (number == 0 || isNaN(number)) {
601 hex += '00';
602 } else {
603 hex += hexchars.charAt((number-number%16)/16)+hexchars.charAt(number%16);
604 }
605 }
606 return hex;
607 }
667c0ae1 608 };
233b6ff9
SH
609 /**
610 * Initialise the colour picker :) Hoorah
611 */
612 colourpicker.init();
613 });
667c0ae1 614};
233b6ff9 615
cbb54cce
SH
616M.util.init_block_hider = function(Y, config) {
617 Y.use('base', 'node', function(Y) {
618 M.util.block_hider = M.util.block_hider || (function(){
619 var blockhider = function() {
620 blockhider.superclass.constructor.apply(this, arguments);
667c0ae1 621 };
cbb54cce
SH
622 blockhider.prototype = {
623 initializer : function(config) {
667c0ae1 624 this.set('block', '#'+this.get('id'));
cbb54cce
SH
625 var b = this.get('block'),
626 t = b.one('.title'),
627 a = null;
628 if (t && (a = t.one('.block_action'))) {
7cf83f47 629 var hide = Y.Node.create('<img class="block-hider-hide" tabindex="0" alt="'+config.tooltipVisible+'" title="'+config.tooltipVisible+'" />');
cbb54cce 630 hide.setAttribute('src', this.get('iconVisible')).on('click', this.updateState, this, true);
7cf83f47
JB
631 hide.on('keypress', this.updateStateKey, this, true);
632 var show = Y.Node.create('<img class="block-hider-show" tabindex="0" alt="'+config.tooltipHidden+'" title="'+config.tooltipHidden+'" />');
cbb54cce 633 show.setAttribute('src', this.get('iconHidden')).on('click', this.updateState, this, false);
7cf83f47 634 show.on('keypress', this.updateStateKey, this, false);
cbb54cce
SH
635 a.insert(show, 0).insert(hide, 0);
636 }
637 },
638 updateState : function(e, hide) {
639 M.util.set_user_preference(this.get('preference'), hide);
640 if (hide) {
641 this.get('block').addClass('hidden');
642 } else {
643 this.get('block').removeClass('hidden');
644 }
7cf83f47
JB
645 },
646 updateStateKey : function(e, hide) {
647 if (e.keyCode == 13) { //allow hide/show via enter key
648 this.updateState(this, hide);
649 }
cbb54cce 650 }
667c0ae1 651 };
cbb54cce
SH
652 Y.extend(blockhider, Y.Base, blockhider.prototype, {
653 NAME : 'blockhider',
654 ATTRS : {
655 id : {},
656 preference : {},
657 iconVisible : {
658 value : M.util.image_url('t/switch_minus', 'moodle')
659 },
660 iconHidden : {
661 value : M.util.image_url('t/switch_plus', 'moodle')
662 },
663 block : {
664 setter : function(node) {
665 return Y.one(node);
666 }
667 }
668 }
669 });
670 return blockhider;
671 })();
672 new M.util.block_hider(config);
673 });
667c0ae1 674};
cbb54cce 675
b532be2d
DM
676/**
677 * Returns a string registered in advance for usage in JavaScript
678 *
679 * If you do not pass the third parameter, the function will just return
680 * the corresponding value from the M.str object. If the third parameter is
681 * provided, the function performs {$a} placeholder substitution in the
682 * same way as PHP get_string() in Moodle does.
683 *
684 * @param {String} identifier string identifier
685 * @param {String} component the component providing the string
686 * @param {Object|String} a optional variable to populate placeholder with
687 */
688M.util.get_string = function(identifier, component, a) {
689 var stringvalue;
690
691 if (M.cfg.developerdebug) {
692 // creating new instance if YUI is not optimal but it seems to be better way then
693 // require the instance via the function API - note that it is used in rare cases
694 // for debugging only anyway
695 var Y = new YUI({ debug : true });
696 }
697
698 if (!M.str.hasOwnProperty(component) || !M.str[component].hasOwnProperty(identifier)) {
699 stringvalue = '[[' + identifier + ',' + component + ']]';
700 if (M.cfg.developerdebug) {
701 Y.log('undefined string ' + stringvalue, 'warn', 'M.util.get_string');
702 }
703 return stringvalue;
704 }
705
706 stringvalue = M.str[component][identifier];
707
708 if (typeof a == 'undefined') {
709 // no placeholder substitution requested
710 return stringvalue;
711 }
712
713 if (typeof a == 'number' || typeof a == 'string') {
714 // replace all occurrences of {$a} with the placeholder value
715 stringvalue = stringvalue.replace(/\{\$a\}/g, a);
716 return stringvalue;
717 }
718
719 if (typeof a == 'object') {
720 // replace {$a->key} placeholders
721 for (var key in a) {
722 if (typeof a[key] != 'number' && typeof a[key] != 'string') {
723 if (M.cfg.developerdebug) {
724 Y.log('invalid value type for $a->' + key, 'warn', 'M.util.get_string');
725 }
726 continue;
727 }
728 var search = '{$a->' + key + '}';
729 search = search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
730 search = new RegExp(search, 'g');
731 stringvalue = stringvalue.replace(search, a[key]);
732 }
733 return stringvalue;
734 }
735
736 if (M.cfg.developerdebug) {
737 Y.log('incorrect placeholder type', 'warn', 'M.util.get_string');
738 }
739 return stringvalue;
740};
741
2d16dad0
PS
742/**
743 * Set focus on username or password field of the login form
744 */
745M.util.focus_login_form = function(Y) {
746 var username = Y.one('#username');
747 var password = Y.one('#password');
748
749 if (username == null || password == null) {
750 // something is wrong here
751 return;
752 }
753
754 var curElement = document.activeElement
755 if (curElement == 'undefined') {
756 // legacy browser - skip refocus protection
757 } else if (curElement.tagName == 'INPUT') {
758 // user was probably faster to focus something, do not mess with focus
759 return;
760 }
761
762 if (username.get('value') == '') {
763 username.focus();
764 } else {
765 password.focus();
766 }
767}
768
769
38224dcb
PS
770//=== old legacy JS code, hopefully to be replaced soon by M.xx.yy and YUI3 code ===
771
63d28811 772function checkall() {
214f5850 773 var inputs = document.getElementsByTagName('input');
774 for (var i = 0; i < inputs.length; i++) {
775 if (inputs[i].type == 'checkbox') {
776 inputs[i].checked = true;
777 }
d2ce367f 778 }
63d28811 779}
780
03f9425f 781function checknone() {
214f5850 782 var inputs = document.getElementsByTagName('input');
783 for (var i = 0; i < inputs.length; i++) {
784 if (inputs[i].type == 'checkbox') {
785 inputs[i].checked = false;
786 }
d2ce367f 787 }
03f9425f 788}
789
45caa363 790/**
791 * Either check, or uncheck, all checkboxes inside the element with id is
792 * @param id the id of the container
793 * @param checked the new state, either '' or 'checked'.
794 */
795function select_all_in_element_with_id(id, checked) {
796 var container = document.getElementById(id);
797 if (!container) {
798 return;
799 }
800 var inputs = container.getElementsByTagName('input');
801 for (var i = 0; i < inputs.length; ++i) {
802 if (inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
803 inputs[i].checked = checked;
804 }
805 }
806}
807
8ceb09e0 808function select_all_in(elTagName, elClass, elId) {
d2ce367f 809 var inputs = document.getElementsByTagName('input');
8ceb09e0 810 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
363cb62c 811 for(var i = 0; i < inputs.length; ++i) {
bee40515 812 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
363cb62c 813 inputs[i].checked = 'checked';
814 }
815 }
816}
817
8ceb09e0 818function deselect_all_in(elTagName, elClass, elId) {
363cb62c 819 var inputs = document.getElementsByTagName('INPUT');
8ceb09e0 820 inputs = filterByParent(inputs, function(el) {return findParentNode(el, elTagName, elClass, elId);});
363cb62c 821 for(var i = 0; i < inputs.length; ++i) {
bee40515 822 if(inputs[i].type == 'checkbox' || inputs[i].type == 'radio') {
363cb62c 823 inputs[i].checked = '';
824 }
825 }
826}
827
828function confirm_if(expr, message) {
829 if(!expr) {
830 return true;
831 }
832 return confirm(message);
833}
47aa42e2 834
835
836/*
837 findParentNode (start, elementName, elementClass, elementID)
50ef8eb9 838
47aa42e2 839 Travels up the DOM hierarchy to find a parent element with the
840 specified tag name, class, and id. All conditions must be met,
841 but any can be ommitted. Returns the BODY element if no match
842 found.
843*/
844function findParentNode(el, elName, elClass, elId) {
45caa363 845 while (el.nodeName.toUpperCase() != 'BODY') {
846 if ((!elName || el.nodeName.toUpperCase() == elName) &&
47aa42e2 847 (!elClass || el.className.indexOf(elClass) != -1) &&
45caa363 848 (!elId || el.id == elId)) {
47aa42e2 849 break;
850 }
851 el = el.parentNode;
852 }
853 return el;
854}
19194f82 855/*
856 findChildNode (start, elementName, elementClass, elementID)
857
858 Travels down the DOM hierarchy to find all child elements with the
859 specified tag name, class, and id. All conditions must be met,
860 but any can be ommitted.
a23f0aaf 861 Doesn't examine children of matches.
19194f82 862*/
863function findChildNodes(start, tagName, elementClass, elementID, elementName) {
864 var children = new Array();
83c9a8a2 865 for (var i = 0; i < start.childNodes.length; i++) {
a23f0aaf 866 var classfound = false;
83c9a8a2 867 var child = start.childNodes[i];
a23f0aaf 868 if((child.nodeType == 1) &&//element node type
b51709c1 869 (elementClass && (typeof(child.className)=='string'))) {
a23f0aaf 870 var childClasses = child.className.split(/\s+/);
b51709c1 871 for (var childClassIndex in childClasses) {
872 if (childClasses[childClassIndex]==elementClass) {
a23f0aaf 873 classfound = true;
874 break;
875 }
876 }
877 }
f07b9627 878 if(child.nodeType == 1) { //element node type
879 if ( (!tagName || child.nodeName == tagName) &&
880 (!elementClass || classfound)&&
881 (!elementID || child.id == elementID) &&
882 (!elementName || child.name == elementName))
883 {
884 children = children.concat(child);
885 } else {
886 children = children.concat(findChildNodes(child, tagName, elementClass, elementID, elementName));
887 }
19194f82 888 }
19194f82 889 }
890 return children;
891}
47aa42e2 892
54bb33eb 893function unmaskPassword(id) {
239ade45 894 var pw = document.getElementById(id);
54bb33eb 895 var chb = document.getElementById(id+'unmask');
239ade45 896
897 try {
898 // first try IE way - it can not set name attribute later
899 if (chb.checked) {
900 var newpw = document.createElement('<input type="text" name="'+pw.name+'">');
901 } else {
902 var newpw = document.createElement('<input type="password" name="'+pw.name+'">');
903 }
eba8cd63 904 newpw.attributes['class'].nodeValue = pw.attributes['class'].nodeValue;
239ade45 905 } catch (e) {
906 var newpw = document.createElement('input');
907 newpw.setAttribute('name', pw.name);
908 if (chb.checked) {
909 newpw.setAttribute('type', 'text');
910 } else {
911 newpw.setAttribute('type', 'password');
912 }
eba8cd63 913 newpw.setAttribute('class', pw.getAttribute('class'));
239ade45 914 }
915 newpw.id = pw.id;
916 newpw.size = pw.size;
917 newpw.onblur = pw.onblur;
918 newpw.onchange = pw.onchange;
919 newpw.value = pw.value;
920 pw.parentNode.replaceChild(newpw, pw);
921}
922
47aa42e2 923function filterByParent(elCollection, parentFinder) {
924 var filteredCollection = [];
45caa363 925 for (var i = 0; i < elCollection.length; ++i) {
47aa42e2 926 var findParent = parentFinder(elCollection[i]);
45caa363 927 if (findParent.nodeName.toUpperCase != 'BODY') {
47aa42e2 928 filteredCollection.push(elCollection[i]);
929 }
930 }
931 return filteredCollection;
932}
933
7979105c 934/*
935 All this is here just so that IE gets to handle oversized blocks
936 in a visually pleasing manner. It does a browser detect. So sue me.
937*/
938
939function fix_column_widths() {
940 var agt = navigator.userAgent.toLowerCase();
941 if ((agt.indexOf("msie") != -1) && (agt.indexOf("opera") == -1)) {
942 fix_column_width('left-column');
943 fix_column_width('right-column');
944 }
945}
946
947function fix_column_width(colName) {
948 if(column = document.getElementById(colName)) {
949 if(!column.offsetWidth) {
950 setTimeout("fix_column_width('" + colName + "')", 20);
951 return;
952 }
953
954 var width = 0;
955 var nodes = column.childNodes;
956
957 for(i = 0; i < nodes.length; ++i) {
6605ff8c 958 if(nodes[i].className.indexOf("block") != -1 ) {
7979105c 959 if(width < nodes[i].offsetWidth) {
960 width = nodes[i].offsetWidth;
961 }
962 }
963 }
964
965 for(i = 0; i < nodes.length; ++i) {
6605ff8c 966 if(nodes[i].className.indexOf("block") != -1 ) {
7979105c 967 nodes[i].style.width = width + 'px';
968 }
969 }
970 }
971}
d13c5938 972
973
974/*
9f439b17 975 Insert myValue at current cursor position
976 */
d13c5938 977function insertAtCursor(myField, myValue) {
9f439b17 978 // IE support
979 if (document.selection) {
980 myField.focus();
981 sel = document.selection.createRange();
982 sel.text = myValue;
983 }
984 // Mozilla/Netscape support
985 else if (myField.selectionStart || myField.selectionStart == '0') {
986 var startPos = myField.selectionStart;
987 var endPos = myField.selectionEnd;
988 myField.value = myField.value.substring(0, startPos)
989 + myValue + myField.value.substring(endPos, myField.value.length);
990 } else {
991 myField.value += myValue;
992 }
d13c5938 993}
7470d6de 994
995
996/*
c0056e22 997 Call instead of setting window.onload directly or setting body onload=.
998 Adds your function to a chain of functions rather than overwriting anything
999 that exists.
1000*/
7470d6de 1001function addonload(fn) {
1002 var oldhandler=window.onload;
1003 window.onload=function() {
1004 if(oldhandler) oldhandler();
c0056e22 1005 fn();
7470d6de 1006 }
1007}
7d2a0492 1008/**
1009 * Replacement for getElementsByClassName in browsers that aren't cool enough
6f5e0852 1010 *
7d2a0492 1011 * Relying on the built-in getElementsByClassName is far, far faster than
1012 * using YUI.
6f5e0852 1013 *
7d2a0492 1014 * Note: the third argument used to be an object with odd behaviour. It now
1015 * acts like the 'name' in the HTML5 spec, though the old behaviour is still
1016 * mimicked if you pass an object.
1017 *
1018 * @param {Node} oElm The top-level node for searching. To search a whole
1019 * document, use `document`.
1020 * @param {String} strTagName filter by tag names
1021 * @param {String} name same as HTML5 spec
1022 */
1023function getElementsByClassName(oElm, strTagName, name) {
1024 // for backwards compatibility
1025 if(typeof name == "object") {
1026 var names = new Array();
1027 for(var i=0; i<name.length; i++) names.push(names[i]);
1028 name = names.join('');
1029 }
1030 // use native implementation if possible
1031 if (oElm.getElementsByClassName && Array.filter) {
1032 if (strTagName == '*') {
1033 return oElm.getElementsByClassName(name);
1034 } else {
1035 return Array.filter(oElm.getElementsByClassName(name), function(el) {
1036 return el.nodeName.toLowerCase() == strTagName.toLowerCase();
1037 });
1038 }
1039 }
1040 // native implementation unavailable, fall back to slow method
c849ed1e 1041 var arrElements = (strTagName == "*" && oElm.all)? oElm.all : oElm.getElementsByTagName(strTagName);
1042 var arrReturnElements = new Array();
1043 var arrRegExpClassNames = new Array();
7d2a0492 1044 var names = name.split(' ');
1045 for(var i=0; i<names.length; i++) {
1046 arrRegExpClassNames.push(new RegExp("(^|\\s)" + names[i].replace(/\-/g, "\\-") + "(\\s|$)"));
c849ed1e 1047 }
1048 var oElement;
1049 var bMatchesAll;
b51709c1 1050 for(var j=0; j<arrElements.length; j++) {
c849ed1e 1051 oElement = arrElements[j];
1052 bMatchesAll = true;
b51709c1 1053 for(var k=0; k<arrRegExpClassNames.length; k++) {
1054 if(!arrRegExpClassNames[k].test(oElement.className)) {
c849ed1e 1055 bMatchesAll = false;
1056 break;
1057 }
1058 }
b51709c1 1059 if(bMatchesAll) {
c849ed1e 1060 arrReturnElements.push(oElement);
1061 }
1062 }
1063 return (arrReturnElements)
1064}
77241d9b 1065
f8065dd2 1066function openpopup(event, args) {
1067
2cf81209 1068 if (event) {
c7e3e61c
SH
1069 if (event.preventDefault) {
1070 event.preventDefault();
1071 } else {
1072 event.returnValue = false;
1073 }
2cf81209 1074 }
3a42ad12 1075
2cf81209
SH
1076 var fullurl = args.url;
1077 if (!args.url.match(/https?:\/\//)) {
1078 fullurl = M.cfg.wwwroot + args.url;
77241d9b 1079 }
2cf81209
SH
1080 var windowobj = window.open(fullurl,args.name,args.options);
1081 if (!windowobj) {
1082 return true;
1083 }
1084 if (args.fullscreen) {
1085 windowobj.moveTo(0,0);
1086 windowobj.resizeTo(screen.availWidth,screen.availHeight);
1087 }
1088 windowobj.focus();
3a42ad12 1089
2cf81209 1090 return false;
77241d9b 1091}
740939ec 1092
b166403f 1093/** Close the current browser window. */
7a5c78e0 1094function close_window(e) {
c7e3e61c
SH
1095 if (e.preventDefault) {
1096 e.preventDefault();
1097 } else {
1098 e.returnValue = false;
1099 }
1db6b9b8 1100 window.close();
b166403f 1101}
1102
9f319372 1103/**
1104 * Used in a couple of modules to hide navigation areas when using AJAX
1105 */
34a2777c 1106
2538037f 1107function show_item(itemid) {
1108 var item = document.getElementById(itemid);
1109 if (item) {
1110 item.style.display = "";
1111 }
1112}
1113
1114function destroy_item(itemid) {
1115 var item = document.getElementById(itemid);
1116 if (item) {
1117 item.parentNode.removeChild(item);
1118 }
1119}
34a2777c 1120/**
1121 * Tranfer keyboard focus to the HTML element with the given id, if it exists.
1122 * @param controlid the control id.
1123 */
1124function focuscontrol(controlid) {
1125 var control = document.getElementById(controlid);
1126 if (control) {
1127 control.focus();
1128 }
e29380f3 1129}
1130
e11a8328 1131/**
1132 * Transfers keyboard focus to an HTML element based on the old style style of focus
1133 * This function should be removed as soon as it is no longer used
1134 */
428acddb 1135function old_onload_focus(formid, controlname) {
5f8bce50 1136 if (document.forms[formid] && document.forms[formid].elements && document.forms[formid].elements[controlname]) {
428acddb 1137 document.forms[formid].elements[controlname].focus();
e11a8328 1138 }
1139}
1140
1bcb7eb5 1141function build_querystring(obj) {
aa281068
AD
1142 return convert_object_to_string(obj, '&');
1143}
1144
1145function build_windowoptionsstring(obj) {
1146 return convert_object_to_string(obj, ',');
1147}
1148
1149function convert_object_to_string(obj, separator) {
1bcb7eb5 1150 if (typeof obj !== 'object') {
1151 return null;
1152 }
1153 var list = [];
1154 for(var k in obj) {
1155 k = encodeURIComponent(k);
1156 var value = obj[k];
1157 if(obj[k] instanceof Array) {
1158 for(var i in value) {
1159 list.push(k+'[]='+encodeURIComponent(value[i]));
1160 }
1161 } else {
1162 list.push(k+'='+encodeURIComponent(value));
1163 }
1164 }
aa281068 1165 return list.join(separator);
1bcb7eb5 1166}
d25e2ca3 1167
1168function stripHTML(str) {
1169 var re = /<\S[^><]*>/g;
1170 var ret = str.replace(re, "");
1171 return ret;
1172}
1173
fd4faf98 1174Number.prototype.fixed=function(n){
1175 with(Math)
1176 return round(Number(this)*pow(10,n))/pow(10,n);
667c0ae1 1177};
fd4faf98 1178function update_progress_bar (id, width, pt, msg, es){
dfd9f745 1179 var percent = pt;
fd4faf98 1180 var status = document.getElementById("status_"+id);
1181 var percent_indicator = document.getElementById("pt_"+id);
1182 var progress_bar = document.getElementById("progress_"+id);
1183 var time_es = document.getElementById("time_"+id);
1184 status.innerHTML = msg;
1185 percent_indicator.innerHTML = percent.fixed(2) + '%';
1186 if(percent == 100) {
1187 progress_bar.style.background = "green";
1188 time_es.style.display = "none";
1189 } else {
1190 progress_bar.style.background = "#FFCC66";
dfd9f745
PS
1191 if (es == '?'){
1192 time_es.innerHTML = "";
fd4faf98 1193 }else {
1194 time_es.innerHTML = es.fixed(2)+" sec";
1195 time_es.style.display
1196 = "block";
1197 }
1198 }
1199 progress_bar.style.width = width + "px";
1200
1201}
bf6c37c7 1202
1203function frame_breakout(e, properties) {
1204 this.setAttribute('target', properties.framename);
41f23791 1205}
78946b9b 1206
73b62703
PS
1207
1208// ===== Deprecated core Javascript functions for Moodle ====
1209// DO NOT USE!!!!!!!
e50b4c89 1210// Do not put this stuff in separate file because it only adds extra load on servers!
73b62703 1211
2d65597b
PS
1212/**
1213 * Used in a couple of modules to hide navigation areas when using AJAX
1214 */
1215function hide_item(itemid) {
3b044ba3 1216 // use class='hiddenifjs' instead
2d65597b
PS
1217 var item = document.getElementById(itemid);
1218 if (item) {
1219 item.style.display = "none";
1220 }
1221}
36282d85 1222
2cf81209
SH
1223M.util.help_icon = {
1224 Y : null,
36282d85 1225 instance : null,
2cf81209
SH
1226 add : function(Y, properties) {
1227 this.Y = Y;
61ddb953
RW
1228 properties.node = Y.one('#'+properties.id);
1229 if (properties.node) {
1230 properties.node.on('click', this.display, this, properties);
f855a5d2 1231 }
2cf81209
SH
1232 },
1233 display : function(event, args) {
1234 event.preventDefault();
1235 if (M.util.help_icon.instance === null) {
1236 var Y = M.util.help_icon.Y;
61ddb953 1237 Y.use('overlay', 'io', 'event-mouseenter', 'node', 'event-key', function(Y) {
2cf81209 1238 var help_content_overlay = {
61ddb953 1239 helplink : null,
2cf81209
SH
1240 overlay : null,
1241 init : function() {
1242
61ddb953 1243 var closebtn = Y.Node.create('<a id="closehelpbox" href="#"><img src="'+M.util.image_url('t/delete', 'moodle')+'" /></a>');
2cf81209
SH
1244 // Create an overlay from markup
1245 this.overlay = new Y.Overlay({
1246 headerContent: closebtn,
1247 bodyContent: '',
61ddb953 1248 id: 'helppopupbox',
3a42ad12 1249 width:'400px',
2cf81209 1250 visible : false,
3a42ad12 1251 constrain : true
2cf81209
SH
1252 });
1253 this.overlay.render(Y.one(document.body));
1254
1255 closebtn.on('click', this.overlay.hide, this.overlay);
3a42ad12 1256
2cf81209 1257 var boundingBox = this.overlay.get("boundingBox");
2cf81209
SH
1258
1259 // Hide the menu if the user clicks outside of its content
1260 boundingBox.get("ownerDocument").on("mousedown", function (event) {
1261 var oTarget = event.target;
1262 var menuButton = Y.one("#"+args.id);
3a42ad12 1263
2cf81209
SH
1264 if (!oTarget.compareTo(menuButton) &&
1265 !menuButton.contains(oTarget) &&
1266 !oTarget.compareTo(boundingBox) &&
1267 !boundingBox.contains(oTarget)) {
1268 this.overlay.hide();
1269 }
1270 }, this);
61ddb953
RW
1271
1272 Y.on("key", this.close, closebtn , "down:13", this);
1273 closebtn.on('click', this.close, this);
1274 },
1275
1276 close : function(e) {
1277 e.preventDefault();
1278 this.helplink.focus();
1279 this.overlay.hide();
2cf81209
SH
1280 },
1281
1282 display : function(event, args) {
61ddb953 1283 this.helplink = args.node;
2cf81209 1284 this.overlay.set('bodyContent', Y.Node.create('<img src="'+M.cfg.loadingicon+'" class="spinner" />'));
61ddb953 1285 this.overlay.set("align", {node:args.node, points:[Y.WidgetPositionAlign.TL, Y.WidgetPositionAlign.RC]});
2cf81209
SH
1286
1287 var fullurl = args.url;
1288 if (!args.url.match(/https?:\/\//)) {
1289 fullurl = M.cfg.wwwroot + args.url;
36282d85 1290 }
36282d85 1291
2cf81209
SH
1292 var ajaxurl = fullurl + '&ajax=1';
1293
1294 var cfg = {
1295 method: 'get',
1296 context : this,
1297 on: {
1298 success: function(id, o, node) {
1299 this.display_callback(o.responseText);
1300 },
1301 failure: function(id, o, node) {
1302 var debuginfo = o.statusText;
1303 if (M.cfg.developerdebug) {
1304 o.statusText += ' (' + ajaxurl + ')';
1305 }
1306 this.display_callback('bodyContent',debuginfo);
1307 }
1308 }
1309 };
36282d85 1310
2cf81209
SH
1311 Y.io(ajaxurl, cfg);
1312 this.overlay.show();
61ddb953
RW
1313
1314 Y.one('#closehelpbox').focus();
2cf81209 1315 },
36282d85 1316
2cf81209 1317 display_callback : function(content) {
61ddb953 1318 this.overlay.set('bodyContent', content);
2cf81209
SH
1319 },
1320
1321 hideContent : function() {
1322 help = this;
1323 help.overlay.hide();
36282d85 1324 }
667c0ae1 1325 };
2cf81209
SH
1326 help_content_overlay.init();
1327 M.util.help_icon.instance = help_content_overlay;
1328 M.util.help_icon.instance.display(event, args);
1329 });
1330 } else {
1331 M.util.help_icon.instance.display(event, args);
1332 }
1333 },
3a42ad12
RW
1334 init : function(Y) {
1335 this.Y = Y;
36282d85 1336 }
667c0ae1 1337};
d2dbd0c0
SH
1338
1339/**
1340 * Custom menu namespace
1341 */
1342M.core_custom_menu = {
1343 /**
1344 * This method is used to initialise a custom menu given the id that belongs
1345 * to the custom menu's root node.
3a42ad12 1346 *
d2dbd0c0
SH
1347 * @param {YUI} Y
1348 * @param {string} nodeid
1349 */
1350 init : function(Y, nodeid) {
242b78f7
SH
1351 var node = Y.one('#'+nodeid);
1352 if (node) {
1353 Y.use('node-menunav', function(Y) {
1354 // Get the node
1355 // Remove the javascript-disabled class.... obviously javascript is enabled.
1356 node.removeClass('javascript-disabled');
1357 // Initialise the menunav plugin
1358 node.plug(Y.Plugin.NodeMenuNav);
1359 });
1360 }
d2dbd0c0 1361 }
667c0ae1 1362};
572dd8ec
SH
1363
1364/**
1365 * Used to store form manipulation methods and enhancments
1366 */
1367M.form = M.form || {};
1368
1369/**
1370 * Converts a nbsp indented select box into a multi drop down custom control much
1371 * like the custom menu. It also selectable categories on or off.
1372 *
1373 * $form->init_javascript_enhancement('elementname','smartselect', array('selectablecategories'=>true|false, 'mode'=>'compact'|'spanning'));
1374 *
1375 * @param {YUI} Y
1376 * @param {string} id
1377 * @param {Array} options
1378 */
1379M.form.init_smartselect = function(Y, id, options) {
882e66fe
SH
1380 if (!id.match(/^id_/)) {
1381 id = 'id_'+id;
1382 }
1383 var select = Y.one('select#'+id);
572dd8ec
SH
1384 if (!select) {
1385 return false;
1386 }
1387 Y.use('event-delegate',function(){
1388 var smartselect = {
1389 id : id,
1390 structure : [],
1391 options : [],
1392 submenucount : 0,
1393 currentvalue : null,
1394 currenttext : null,
1395 shownevent : null,
1396 cfg : {
1397 selectablecategories : true,
1398 mode : null
1399 },
1400 nodes : {
1401 select : null,
1402 loading : null,
1403 menu : null
1404 },
1405 init : function(Y, id, args, nodes) {
1406 if (typeof(args)=='object') {
1407 for (var i in this.cfg) {
1408 if (args[i] || args[i]===false) {
1409 this.cfg[i] = args[i];
1410 }
1411 }
1412 }
3a42ad12 1413
572dd8ec
SH
1414 // Display a loading message first up
1415 this.nodes.select = nodes.select;
1416
1417 this.currentvalue = this.nodes.select.get('selectedIndex');
1418 this.currenttext = this.nodes.select.all('option').item(this.currentvalue).get('innerHTML');
1419
1420 var options = Array();
667c0ae1 1421 options[''] = {text:this.currenttext,value:'',depth:0,children:[]};
572dd8ec
SH
1422 this.nodes.select.all('option').each(function(option, index) {
1423 var rawtext = option.get('innerHTML');
1424 var text = rawtext.replace(/^(&nbsp;)*/, '');
1425 if (rawtext === text) {
1426 text = rawtext.replace(/^(\s)*/, '');
1427 var depth = (rawtext.length - text.length ) + 1;
1428 } else {
1429 var depth = ((rawtext.length - text.length )/12)+1;
1430 }
1431 option.set('innerHTML', text);
1432 options['i'+index] = {text:text,depth:depth,index:index,children:[]};
1433 }, this);
1434
1435 this.structure = [];
1436 var structcount = 0;
1437 for (var i in options) {
1438 var o = options[i];
1439 if (o.depth == 0) {
1440 this.structure.push(o);
1441 structcount++;
1442 } else {
1443 var d = o.depth;
1444 var current = this.structure[structcount-1];
1445 for (var j = 0; j < o.depth-1;j++) {
1446 if (current && current.children) {
1447 current = current.children[current.children.length-1];
1448 }
1449 }
1450 if (current && current.children) {
1451 current.children.push(o);
1452 }
1453 }
1454 }
1455
1456 this.nodes.menu = Y.Node.create(this.generate_menu_content());
1457 this.nodes.menu.one('.smartselect_mask').setStyle('opacity', 0.01);
1458 this.nodes.menu.one('.smartselect_mask').setStyle('width', (this.nodes.select.get('offsetWidth')+5)+'px');
1459 this.nodes.menu.one('.smartselect_mask').setStyle('height', (this.nodes.select.get('offsetHeight'))+'px');
1460
1461 if (this.cfg.mode == null) {
1462 var formwidth = this.nodes.select.ancestor('form').get('offsetWidth');
1463 if (formwidth < 400 || this.nodes.menu.get('offsetWidth') < formwidth*2) {
1464 this.cfg.mode = 'compact';
1465 } else {
1466 this.cfg.mode = 'spanning';
1467 }
1468 }
1469
1470 if (this.cfg.mode == 'compact') {
1471 this.nodes.menu.addClass('compactmenu');
1472 } else {
1473 this.nodes.menu.addClass('spanningmenu');
1474 this.nodes.menu.delegate('mouseover', this.show_sub_menu, '.smartselect_submenuitem', this);
1475 }
1476
1477 Y.one(document.body).append(this.nodes.menu);
1478 var pos = this.nodes.select.getXY();
1479 pos[0] += 1;
1480 this.nodes.menu.setXY(pos);
1481 this.nodes.menu.on('click', this.handle_click, this);
32690ac2
SH
1482
1483 Y.one(window).on('resize', function(){
1484 var pos = this.nodes.select.getXY();
1485 pos[0] += 1;
1486 this.nodes.menu.setXY(pos);
1487 }, this);
572dd8ec
SH
1488 },
1489 generate_menu_content : function() {
1490 var content = '<div id="'+this.id+'_smart_select" class="smartselect">';
1491 content += this.generate_submenu_content(this.structure[0], true);
1492 content += '</ul></div>';
1493 return content;
1494 },
1495 generate_submenu_content : function(item, rootelement) {
1496 this.submenucount++;
1497 var content = '';
1498 if (item.children.length > 0) {
1499 if (rootelement) {
1500 content += '<div class="smartselect_mask" href="#ss_submenu'+this.submenucount+'">&nbsp;</div>';
1501 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_menu">';
1502 content += '<div class="smartselect_menu_content">';
1503 } else {
1504 content += '<li class="smartselect_submenuitem">';
1505 var categoryclass = (this.cfg.selectablecategories)?'selectable':'notselectable';
1506 content += '<a class="smartselect_menuitem_label '+categoryclass+'" href="#ss_submenu'+this.submenucount+'" value="'+item.index+'">'+item.text+'</a>';
1507 content += '<div id="ss_submenu'+this.submenucount+'" class="smartselect_submenu">';
1508 content += '<div class="smartselect_submenu_content">';
1509 }
1510 content += '<ul>';
1511 for (var i in item.children) {
1512 content += this.generate_submenu_content(item.children[i],false);
1513 }
1514 content += '</ul>';
1515 content += '</div>';
1516 content += '</div>';
1517 if (rootelement) {
1518 } else {
1519 content += '</li>';
1520 }
1521 } else {
1522 content += '<li class="smartselect_menuitem">';
1523 content += '<a class="smartselect_menuitem_content selectable" href="#" value="'+item.index+'">'+item.text+'</a>';
1524 content += '</li>';
1525 }
1526 return content;
1527 },
1528 select : function(e) {
1529 var t = e.target;
1530 e.halt();
1531 this.currenttext = t.get('innerHTML');
1532 this.currentvalue = t.getAttribute('value');
1533 this.nodes.select.set('selectedIndex', this.currentvalue);
1534 this.hide_menu();
1535 },
1536 handle_click : function(e) {
1537 var target = e.target;
1538 if (target.hasClass('smartselect_mask')) {
1539 this.show_menu(e);
1540 } else if (target.hasClass('selectable') || target.hasClass('smartselect_menuitem')) {
1541 this.select(e);
1542 } else if (target.hasClass('smartselect_menuitem_label') || target.hasClass('smartselect_submenuitem')) {
1543 this.show_sub_menu(e);
1544 }
1545 },
1546 show_menu : function(e) {
1547 e.halt();
1548 var menu = e.target.ancestor().one('.smartselect_menu');
1549 menu.addClass('visible');
1550 this.shownevent = Y.one(document.body).on('click', this.hide_menu, this);
1551 },
1552 show_sub_menu : function(e) {
1553 e.halt();
1554 var target = e.target;
1555 if (!target.hasClass('smartselect_submenuitem')) {
1556 target = target.ancestor('.smartselect_submenuitem');
1557 }
1558 if (this.cfg.mode == 'compact' && target.one('.smartselect_submenu').hasClass('visible')) {
1559 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1560 return;
1561 }
1562 target.ancestor('ul').all('.smartselect_submenu.visible').removeClass('visible');
1563 target.one('.smartselect_submenu').addClass('visible');
1564 },
1565 hide_menu : function() {
1566 this.nodes.menu.all('.visible').removeClass('visible');
1567 if (this.shownevent) {
1568 this.shownevent.detach();
1569 }
1570 }
667c0ae1 1571 };
572dd8ec
SH
1572 smartselect.init(Y, id, options, {select:select});
1573 });
667c0ae1 1574};
3a42ad12 1575
fcd2cbaf
PS
1576/** List of flv players to be loaded */
1577M.util.video_players = [];
1578/** List of mp3 players to be loaded */
1579M.util.audio_players = [];
1580
1581/**
1582 * Add video player
1583 * @param id element id
1584 * @param fileurl media url
1585 * @param width
1586 * @param height
1587 * @param autosize true means detect size from media
1588 */
1589M.util.add_video_player = function (id, fileurl, width, height, autosize) {
1590 M.util.video_players.push({id: id, fileurl: fileurl, width: width, height: height, autosize: autosize, resized: false});
667c0ae1 1591};
3a42ad12 1592
fcd2cbaf
PS
1593/**
1594 * Add audio player.
1595 * @param id
1596 * @param fileurl
1597 * @param small
1598 */
1599M.util.add_audio_player = function (id, fileurl, small) {
1600 M.util.audio_players.push({id: id, fileurl: fileurl, small: small});
667c0ae1 1601};
3a42ad12 1602
fcd2cbaf
PS
1603/**
1604 * Initialise all audio and video player, must be called from page footer.
1605 */
1606M.util.load_flowplayer = function() {
1607 if (M.util.video_players.length == 0 && M.util.audio_players.length == 0) {
1608 return;
1609 }
1610 if (typeof(flowplayer) == 'undefined') {
1611 var loaded = false;
1612
1613 var embed_function = function() {
1614 if (loaded || typeof(flowplayer) == 'undefined') {
1615 return;
3a42ad12 1616 }
fcd2cbaf
PS
1617 loaded = true;
1618
1619 var controls = {
1620 autoHide: true
1621 }
1622 /* TODO: add CSS color overrides for the flv flow player */
1623
1624 for(var i=0; i<M.util.video_players.length; i++) {
1625 var video = M.util.video_players[i];
1626 if (video.width > 0 && video.height > 0) {
1627 var src = {src: M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', width: video.width, height: video.height};
1628 } else {
1629 var src = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf';
1630 }
1631 flowplayer(video.id, src, {
1632 plugins: {controls: controls},
1633 clip: {
1634 url: video.fileurl, autoPlay: false, autoBuffering: true, scaling: 'fit', mvideo: video,
1635 onMetaData: function(clip) {
1636 if (clip.mvideo.autosize && !clip.mvideo.resized) {
1637 clip.mvideo.resized = true;
1638 //alert("metadata!!! "+clip.width+' '+clip.height+' '+JSON.stringify(clip.metaData));
1639 if (typeof(clip.metaData.width) == 'undefined' || typeof(clip.metaData.height) == 'undefined') {
1640 // bad luck, we have to guess - we may not get metadata at all
1641 var width = clip.width;
1642 var height = clip.height;
1643 } else {
1644 var width = clip.metaData.width;
1645 var height = clip.metaData.height;
1646 }
1647 var minwidth = 300; // controls are messed up in smaller objects
1648 if (width < minwidth) {
1649 height = (height * minwidth) / width;
1650 width = minwidth;
1651 }
1652
1653 var object = this._api();
1654 object.width = width;
1655 object.height = height;
1656 }
1657 }
1658 }
1659 });
1660 }
1661 if (M.util.audio_players.length == 0) {
1662 return;
1663 }
1664 var controls = {
1665 autoHide: false,
1666 fullscreen: false,
1667 next: false,
1668 previous: false,
1669 scrubber: true,
1670 play: true,
1671 pause: true,
1672 volume: true,
1673 mute: false,
1674 backgroundGradient: [0.5,0,0.3]
1675 };
1676
1677 var rule;
1678 for (var j=0; j < document.styleSheets.length; j++) {
1679 if (typeof (document.styleSheets[j].rules) != 'undefined') {
1680 var allrules = document.styleSheets[j].rules;
1681 } else if (typeof (document.styleSheets[j].cssRules) != 'undefined') {
1682 var allrules = document.styleSheets[j].cssRules;
1683 } else {
1684 // why??
1685 continue;
1686 }
1687 for(var i=0; i<allrules.length; i++) {
1688 rule = '';
1689 if (/^\.mp3flowplayer_.*Color$/.test(allrules[i].selectorText)) {
1690 if (typeof(allrules[i].cssText) != 'undefined') {
1691 rule = allrules[i].style.cssText;
1692 } else if (typeof(allrules[i].style.cssText) != 'undefined') {
1693 rule = allrules[i].style.cssText;
1694 }
1695 if (rule != '' && /.*color\s*:\s*([^;]+).*/gi.test(rule)) {
1696 rule = rule.replace(/.*color\s*:\s*([^;]+).*/gi, '$1');
1697 var colprop = allrules[i].selectorText.replace(/^\.mp3flowplayer_/, '');
1698 controls[colprop] = rule;
1699 }
1700 }
1701 }
1702 allrules = false;
1703 }
1704
1705 for(i=0; i<M.util.audio_players.length; i++) {
1706 var audio = M.util.audio_players[i];
1707 if (audio.small) {
1708 controls.controlall = false;
1709 controls.height = 15;
1710 controls.time = false;
1711 } else {
1712 controls.controlall = true;
1713 controls.height = 25;
1714 controls.time = true;
1715 }
1716 flowplayer(audio.id, M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.7.swf', {
1717 plugins: {controls: controls, audio: {url: M.cfg.wwwroot + '/lib/flowplayer/flowplayer.audio-3.2.2.swf'}},
1718 clip: {url: audio.fileurl, provider: "audio", autoPlay: false}
1719 });
1720 }
1721 }
1722
1723 if (M.cfg.jsrev == -10) {
1724 var jsurl = M.cfg.wwwroot + '/lib/flowplayer/flowplayer-3.2.6.js';
1725 } else {
1726 var jsurl = M.cfg.wwwroot + '/lib/javascript.php?file=/lib/flowplayer/flowplayer-3.2.6.js&rev=' + M.cfg.jsrev;
1727 }
1728 var fileref = document.createElement('script');
1729 fileref.setAttribute('type','text/javascript');
1730 fileref.setAttribute('src', jsurl);
1731 fileref.onload = embed_function;
1732 fileref.onreadystatechange = embed_function;
1733 document.getElementsByTagName('head')[0].appendChild(fileref);
1734 }
1735}