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