[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/ -> javascript-static.js (source)

   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(/^(&nbsp;)*/, '');
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+'">&nbsp;</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  };


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1