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