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