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