[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/availability/yui/src/form/js/ -> form.js (source)

   1  /**
   2   * Provides interface for users to edit availability settings on the
   3   * module/section editing form.
   4   *
   5   * The system works using this JavaScript plus form.js files inside each
   6   * condition plugin.
   7   *
   8   * The overall concept is that data is held in a textarea in the form in JSON
   9   * format. This JavaScript converts the textarea into a set of controls
  10   * generated here and by the relevant plugins.
  11   *
  12   * (Almost) all data is held directly by the state of the HTML controls, and
  13   * can be updated to the form field by calling the 'update' method, which
  14   * this code and the plugins call if any HTML control changes.
  15   *
  16   * @module moodle-core_availability-form
  17   */
  18  M.core_availability = M.core_availability || {};
  19  
  20  /**
  21   * Core static functions for availability settings in editing form.
  22   *
  23   * @class M.core_availability.form
  24   * @static
  25   */
  26  M.core_availability.form = {
  27      /**
  28       * Object containing installed plugins. They are indexed by plugin name.
  29       *
  30       * @property plugins
  31       * @type Object
  32       */
  33      plugins : {},
  34  
  35      /**
  36       * Availability field (textarea).
  37       *
  38       * @property field
  39       * @type Y.Node
  40       */
  41      field : null,
  42  
  43      /**
  44       * Main div that replaces the availability field.
  45       *
  46       * @property mainDiv
  47       * @type Y.Node
  48       */
  49      mainDiv : null,
  50  
  51      /**
  52       * Object that represents the root of the tree.
  53       *
  54       * @property rootList
  55       * @type M.core_availability.List
  56       */
  57      rootList : null,
  58  
  59      /**
  60       * Counter used when creating anything that needs an id.
  61       *
  62       * @property idCounter
  63       * @type Number
  64       */
  65      idCounter : 0,
  66  
  67      /**
  68       * Called to initialise the system when the page loads. This method will
  69       * also call the init method for each plugin.
  70       *
  71       * @method init
  72       */
  73      init : function(pluginParams) {
  74          // Init all plugins.
  75          for(var plugin in pluginParams) {
  76              var params = pluginParams[plugin];
  77              var pluginClass = M[params[0]].form;
  78              pluginClass.init.apply(pluginClass, params);
  79          }
  80  
  81          // Get the availability field, hide it, and replace with the main div.
  82          this.field = Y.one('#id_availabilityconditionsjson');
  83          this.field.setAttribute('aria-hidden', 'true');
  84          // The fcontainer class here is inappropriate, but is necessary
  85          // because otherwise it is impossible to make Behat work correctly on
  86          // these controls as Behat incorrectly decides they're a moodleform
  87          // textarea. IMO Behat should not know about moodleforms at all and
  88          // should look purely at HTML elements on the page, but until it is
  89          // fixed to do this or fixed in some other way to only detect moodleform
  90          // elements that specifically match what those elements should look like,
  91          // then there is no good solution.
  92          this.mainDiv = Y.Node.create('<div class="availability-field fcontainer"></div>');
  93          this.field.insert(this.mainDiv, 'after');
  94  
  95          // Get top-level tree as JSON.
  96          var value = this.field.get('value');
  97          var data = null;
  98          if (value !== '') {
  99              try {
 100                  data = Y.JSON.parse(value);
 101              } catch(x) {
 102                  // If the JSON data is not valid, treat it as empty.
 103                  this.field.set('value', '');
 104              }
 105          }
 106          this.rootList = new M.core_availability.List(data, true);
 107          this.mainDiv.appendChild(this.rootList.node);
 108  
 109          // Update JSON value after loading (to reflect any changes that need
 110          // to be made to make it valid).
 111          this.update();
 112          this.rootList.renumber();
 113  
 114          // Mark main area as dynamically updated.
 115          this.mainDiv.setAttribute('aria-live', 'polite');
 116  
 117          // Listen for form submission - to avoid having our made-up fields
 118          // submitted, we need to disable them all before submit.
 119          this.field.ancestor('form').on('submit', function() {
 120              this.mainDiv.all('input,textarea,select').set('disabled', true);
 121          }, this);
 122      },
 123  
 124      /**
 125       * Called at any time to update the hidden field value.
 126       *
 127       * This should be called whenever any value changes in the form settings.
 128       *
 129       * @method update
 130       */
 131      update : function() {
 132          // Convert tree to value.
 133          var jsValue = this.rootList.getValue();
 134  
 135          // Store any errors (for form reporting) in 'errors' value if present.
 136          var errors = [];
 137          this.rootList.fillErrors(errors);
 138          if (errors.length !== 0) {
 139              jsValue.errors = errors;
 140          }
 141  
 142          // Set into hidden form field, JS-encoded.
 143          this.field.set('value', Y.JSON.stringify(jsValue));
 144      }
 145  };
 146  
 147  
 148  /**
 149   * Base object for plugins. Plugins should use Y.Object to extend this class.
 150   *
 151   * @class M.core_availability.plugin
 152   * @static
 153   */
 154  M.core_availability.plugin = {
 155      /**
 156       * True if users are allowed to add items of this plugin at the moment.
 157       *
 158       * @property allowAdd
 159       * @type Boolean
 160       */
 161      allowAdd : false,
 162  
 163      /**
 164       * Called (from PHP) to initialise the plugin. Should usually not be
 165       * overridden by child plugin.
 166       *
 167       * @method init
 168       * @param {String} component Component name e.g. 'availability_date'
 169       */
 170      init : function(component, allowAdd, params) {
 171          var name = component.replace(/^availability_/, '');
 172          this.allowAdd = allowAdd;
 173          M.core_availability.form.plugins[name] = this;
 174          this.initInner.apply(this, params);
 175      },
 176  
 177      /**
 178       * Init method for plugin to override. (Default does nothing.)
 179       *
 180       * This method will receive any parameters defined in frontend.php
 181       * get_javascript_init_params.
 182       *
 183       * @method initInner
 184       * @protected
 185       */
 186      initInner : function() {
 187      },
 188  
 189      /**
 190       * Gets a YUI node representing the controls for this plugin on the form.
 191       *
 192       * Must be implemented by sub-object; default throws an exception.
 193       *
 194       * @method getNode
 195       * @return {Y.Node} YUI node
 196       */
 197      getNode : function() {
 198          throw 'getNode not implemented';
 199      },
 200  
 201      /**
 202       * Fills in the value from this plugin's controls into a value object,
 203       * which will later be converted to JSON and stored in the form field.
 204       *
 205       * Must be implemented by sub-object; default throws an exception.
 206       *
 207       * @method fillValue
 208       * @param {Object} value Value object (to be written to)
 209       * @param {Y.Node} node YUI node (same one returned from getNode)
 210       */
 211      fillValue : function() {
 212          throw 'fillValue not implemented';
 213      },
 214  
 215      /**
 216       * Fills in any errors from this plugin's controls. If there are any
 217       * errors, push them into the supplied array.
 218       *
 219       * Errors are Moodle language strings in format component:string, e.g.
 220       * 'availability_date:error_date_past_end_of_world'.
 221       *
 222       * The default implementation does nothing.
 223       *
 224       * @method fillErrors
 225       * @param {Array} errors Array of errors (push new errors here)
 226       * @param {Y.Node} node YUI node (same one returned from getNode)
 227       */
 228      fillErrors : function() {
 229      },
 230  
 231      /**
 232       * Focuses the first thing in the plugin after it has been added.
 233       *
 234       * The default implementation uses a simple algorithm to identify the
 235       * first focusable input/select and then focuses it.
 236       */
 237      focusAfterAdd : function(node) {
 238          var target = node.one('input:not([disabled]),select:not([disabled])');
 239          target.focus();
 240      }
 241  };
 242  
 243  
 244  /**
 245   * Maintains a list of children and settings for how they are combined.
 246   *
 247   * @class M.core_availability.List
 248   * @constructor
 249   * @param {Object} json Decoded JSON value
 250   * @param {Boolean} [false] root True if this is root level list
 251   * @param {Boolean} [false] root True if parent is root level list
 252   */
 253  M.core_availability.List = function(json, root, parentRoot) {
 254      // Set default value for children. (You can't do this in the prototype
 255      // definition, or it ends up sharing the same array between all of them.)
 256      this.children = [];
 257  
 258      if (root !== undefined) {
 259          this.root = root;
 260      }
 261      var strings = M.str.availability;
 262      // Create DIV structure (without kids).
 263      this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' +
 264              '<div class="availability-inner">' +
 265              '<div class="availability-header">' + strings.listheader_sign_before +
 266              ' <label><span class="accesshide">' + strings.label_sign +
 267              ' </span><select class="availability-neg" title="' + strings.label_sign + '">' +
 268              '<option value="">' + strings.listheader_sign_pos + '</option>' +
 269              '<option value="!">' + strings.listheader_sign_neg + '</option></select></label> ' +
 270              '<span class="availability-single">' + strings.listheader_single + '</span>' +
 271              '<span class="availability-multi">' + strings.listheader_multi_before +
 272              ' <label><span class="accesshide">' + strings.label_multi + ' </span>' +
 273              '<select class="availability-op" title="' + strings.label_multi + '"><option value="&">' +
 274              strings.listheader_multi_and + '</option>' +
 275              '<option value="|">' + strings.listheader_multi_or + '</option></select></label> ' +
 276              strings.listheader_multi_after + '</span></div>' +
 277              '<div class="availability-children"></div>' +
 278              '<div class="availability-none">' + M.str.moodle.none + '</div>' +
 279              '<div class="availability-button"></div></div></div>');
 280      if (!root) {
 281          this.node.addClass('availability-childlist');
 282      }
 283      this.inner = this.node.one('> .availability-inner');
 284  
 285      var shown = true;
 286      if (root) {
 287          // If it's the root, add an eye icon as first thing in header.
 288          if (json && json.show !== undefined) {
 289              shown = json.show;
 290          }
 291          this.eyeIcon = new M.core_availability.EyeIcon(false, shown);
 292          this.node.one('.availability-header').get('firstChild').insert(
 293                  this.eyeIcon.span, 'before');
 294      } else if (parentRoot) {
 295          // When the parent is root, add an eye icon before the main list div.
 296          if (json && json.showc !== undefined) {
 297              shown = json.showc;
 298          }
 299          this.eyeIcon = new M.core_availability.EyeIcon(false, shown);
 300          this.inner.insert(this.eyeIcon.span, 'before');
 301      }
 302  
 303      if (!root) {
 304          // If it's not the root, add a delete button to the 'none' option.
 305          // You can only delete lists when they have no children so this will
 306          // automatically appear at the correct time.
 307          var deleteIcon = new M.core_availability.DeleteIcon(this);
 308          var noneNode = this.node.one('.availability-none');
 309          noneNode.appendChild(document.createTextNode(' '));
 310          noneNode.appendChild(deleteIcon.span);
 311  
 312          // Also if it's not the root, none is actually invalid, so add a label.
 313          noneNode.appendChild(Y.Node.create('<span class="label label-warning">' +
 314                  M.str.availability.invalid + '</span>'));
 315      }
 316  
 317      // Create the button and add it.
 318      var button = Y.Node.create('<button type="button" class="btn btn-default">' +
 319              M.str.availability.addrestriction + '</button>');
 320      button.on("click", function() { this.clickAdd(); }, this);
 321      this.node.one('div.availability-button').appendChild(button);
 322  
 323      if (json) {
 324          // Set operator from JSON data.
 325          switch (json.op) {
 326              case '&' :
 327              case '|' :
 328                  this.node.one('.availability-neg').set('value', '');
 329                  break;
 330              case '!&' :
 331              case '!|' :
 332                  this.node.one('.availability-neg').set('value', '!');
 333                  break;
 334          }
 335          switch (json.op) {
 336              case '&' :
 337              case '!&' :
 338                  this.node.one('.availability-op').set('value', '&');
 339                  break;
 340              case '|' :
 341              case '!|' :
 342                  this.node.one('.availability-op').set('value', '|');
 343                  break;
 344          }
 345  
 346          // Construct children.
 347          for (var i = 0; i < json.c.length; i++) {
 348              var child = json.c[i];
 349              if (this.root && json && json.showc !== undefined) {
 350                  child.showc = json.showc[i];
 351              }
 352              var newItem;
 353              if (child.type !== undefined) {
 354                  // Plugin type.
 355                  newItem = new M.core_availability.Item(child, this.root);
 356              } else {
 357                  // List type.
 358                  newItem = new M.core_availability.List(child, false, this.root);
 359              }
 360              this.addChild(newItem);
 361          }
 362      }
 363  
 364      // Add update listeners to the dropdowns.
 365      this.node.one('.availability-neg').on('change', function() {
 366          // Update hidden field and HTML.
 367          M.core_availability.form.update();
 368          this.updateHtml();
 369      }, this);
 370      this.node.one('.availability-op').on('change', function() {
 371          // Update hidden field.
 372          M.core_availability.form.update();
 373          this.updateHtml();
 374      }, this);
 375  
 376      // Update HTML to hide unnecessary parts.
 377      this.updateHtml();
 378  };
 379  
 380  /**
 381   * Adds a child to the end of the list (in HTML and stored data).
 382   *
 383   * @method addChild
 384   * @private
 385   * @param {M.core_availability.Item|M.core_availability.List} newItem Child to add
 386   */
 387  M.core_availability.List.prototype.addChild = function(newItem) {
 388      if (this.children.length > 0) {
 389          // Create connecting label (text will be filled in later by updateHtml).
 390          this.inner.one('.availability-children').appendChild(Y.Node.create(
 391                  '<div class="availability-connector">' +
 392                  '<span class="label"></span>' +
 393                  '</div>'));
 394      }
 395      // Add item to array and to HTML.
 396      this.children.push(newItem);
 397      this.inner.one('.availability-children').appendChild(newItem.node);
 398  };
 399  
 400  /**
 401   * Focuses something after a new list is added.
 402   *
 403   * @method focusAfterAdd
 404   */
 405  M.core_availability.List.prototype.focusAfterAdd = function() {
 406      this.inner.one('button').focus();
 407  };
 408  
 409  /**
 410   * Checks whether this list uses the individual show icons or the single one.
 411   *
 412   * (Basically, AND and the equivalent NOT OR list can have individual show icons
 413   * so that you hide the activity entirely if a user fails one condition, but
 414   * may display it with information about the condition if they fail a different
 415   * one. That isn't possible with OR and NOT AND because for those types, there
 416   * is not really a concept of which single condition caused the user to fail
 417   * it.)
 418   *
 419   * Method can only be called on the root list.
 420   *
 421   * @method isIndividualShowIcons
 422   * @return {Boolean} True if using the individual icons
 423   */
 424  M.core_availability.List.prototype.isIndividualShowIcons = function() {
 425      if (!this.root) {
 426          throw 'Can only call this on root list';
 427      }
 428      var neg = this.node.one('.availability-neg').get('value') === '!';
 429      var isor = this.node.one('.availability-op').get('value') === '|';
 430      return (!neg && !isor) || (neg && isor);
 431  };
 432  
 433  /**
 434   * Renumbers the list and all children.
 435   *
 436   * @method renumber
 437   * @param {String} parentNumber Number to use in heading for this list
 438   */
 439  M.core_availability.List.prototype.renumber = function(parentNumber) {
 440      // Update heading for list.
 441      var headingParams = { count: this.children.length };
 442      var prefix;
 443      if (parentNumber === undefined) {
 444          headingParams.number = '';
 445          prefix = '';
 446      } else {
 447          headingParams.number = parentNumber + ':';
 448          prefix = parentNumber + '.';
 449      }
 450      var heading = M.util.get_string('setheading', 'availability', headingParams);
 451      this.node.one('> h3').set('innerHTML', heading);
 452  
 453      // Do children.
 454      for (var i = 0; i < this.children.length; i++) {
 455          var child = this.children[i];
 456          child.renumber(prefix + (i + 1));
 457      }
 458  };
 459  
 460  /**
 461   * Updates HTML for the list based on the current values, for example showing
 462   * the 'None' text if there are no children.
 463   *
 464   * @method updateHtml
 465   */
 466  M.core_availability.List.prototype.updateHtml = function() {
 467      // Control children appearing or not appearing.
 468      if (this.children.length > 0) {
 469          this.inner.one('> .availability-children').removeAttribute('aria-hidden');
 470          this.inner.one('> .availability-none').setAttribute('aria-hidden', 'true');
 471          this.inner.one('> .availability-header').removeAttribute('aria-hidden');
 472          if (this.children.length > 1) {
 473              this.inner.one('.availability-single').setAttribute('aria-hidden', 'true');
 474              this.inner.one('.availability-multi').removeAttribute('aria-hidden');
 475          } else {
 476              this.inner.one('.availability-single').removeAttribute('aria-hidden');
 477              this.inner.one('.availability-multi').setAttribute('aria-hidden', 'true');
 478          }
 479      } else {
 480          this.inner.one('> .availability-children').setAttribute('aria-hidden', 'true');
 481          this.inner.one('> .availability-none').removeAttribute('aria-hidden');
 482          this.inner.one('> .availability-header').setAttribute('aria-hidden', 'true');
 483      }
 484  
 485      // For root list, control eye icons.
 486      if (this.root) {
 487          var showEyes = this.isIndividualShowIcons();
 488  
 489          // Individual icons.
 490          for (var i = 0; i < this.children.length; i++) {
 491              var child = this.children[i];
 492              if (showEyes) {
 493                  child.eyeIcon.span.removeAttribute('aria-hidden');
 494              } else {
 495                  child.eyeIcon.span.setAttribute('aria-hidden', 'true');
 496              }
 497          }
 498  
 499          // Single icon is the inverse.
 500          if (showEyes) {
 501              this.eyeIcon.span.setAttribute('aria-hidden', 'true');
 502          } else {
 503              this.eyeIcon.span.removeAttribute('aria-hidden');
 504          }
 505      }
 506  
 507      // Update connector text.
 508      var connectorText;
 509      if (this.inner.one('.availability-op').get('value') === '&') {
 510          connectorText = M.str.availability.and;
 511      } else {
 512          connectorText = M.str.availability.or;
 513      }
 514      this.inner.all('> .availability-children > .availability-connector span.label').each(function(span) {
 515          span.set('innerHTML', connectorText);
 516      });
 517  };
 518  
 519  /**
 520   * Deletes a descendant item (Item or List). Called when the user clicks a
 521   * delete icon.
 522   *
 523   * This is a recursive function.
 524   *
 525   * @method deleteDescendant
 526   * @param {M.core_availability.Item|M.core_availability.List} descendant Item to delete
 527   * @return {Boolean} True if it was deleted
 528   */
 529  M.core_availability.List.prototype.deleteDescendant = function(descendant) {
 530      // Loop through children.
 531      for (var i = 0; i < this.children.length; i++) {
 532          var child = this.children[i];
 533          if (child === descendant) {
 534              // Remove from internal array.
 535              this.children.splice(i, 1);
 536              var target = child.node;
 537              // Remove one of the connector nodes around target (if any left).
 538              if (this.children.length > 0) {
 539                  if (target.previous('.availability-connector')) {
 540                      target.previous('.availability-connector').remove();
 541                  } else {
 542                      target.next('.availability-connector').remove();
 543                  }
 544              }
 545              // Remove target itself.
 546              this.inner.one('> .availability-children').removeChild(target);
 547              // Update the form and the list HTML.
 548              M.core_availability.form.update();
 549              this.updateHtml();
 550              // Focus add button for this list.
 551              this.inner.one('> .availability-button').one('button').focus();
 552              return true;
 553          } else if (child instanceof M.core_availability.List) {
 554              // Recursive call.
 555              var found = child.deleteDescendant(descendant);
 556              if (found) {
 557                  return true;
 558              }
 559          }
 560      }
 561  
 562      return false;
 563  };
 564  
 565  /**
 566   * Shows the 'add restriction' dialogue box.
 567   *
 568   * @method clickAdd
 569   */
 570  M.core_availability.List.prototype.clickAdd = function() {
 571      var content = Y.Node.create('<div>' +
 572              '<ul class="list-unstyled"></ul>' +
 573              '<div class="availability-buttons mdl-align">' +
 574              '<button type="button" class="btn btn-default">' + M.str.moodle.cancel +
 575              '</button></div></div>');
 576      var cancel = content.one('button');
 577  
 578      // Make a list of all the dialog options.
 579      var dialogRef = { dialog: null };
 580      var ul = content.one('ul');
 581      var li, id, button, label;
 582      for (var type in M.core_availability.form.plugins) {
 583          // Plugins might decide not to display their add button.
 584          if (!M.core_availability.form.plugins[type].allowAdd) {
 585              continue;
 586          }
 587          // Add entry for plugin.
 588          li = Y.Node.create('<li class="clearfix"></li>');
 589          id = 'availability_addrestriction_' + type;
 590          var pluginStrings = M.str['availability_' + type];
 591          button = Y.Node.create('<button type="button" class="btn btn-default"' +
 592                  'id="' + id + '">' + pluginStrings.title + '</button>');
 593          button.on('click', this.getAddHandler(type, dialogRef), this);
 594          li.appendChild(button);
 595          label = Y.Node.create('<label for="' + id + '">' +
 596                  pluginStrings.description + '</label>');
 597          li.appendChild(label);
 598          ul.appendChild(li);
 599      }
 600      // Extra entry for lists.
 601      li = Y.Node.create('<li class="clearfix"></li>');
 602      id = 'availability_addrestriction_list_';
 603      button = Y.Node.create('<button type="button" class="btn btn-default"' +
 604              'id="' + id + '">' + M.str.availability.condition_group + '</button>');
 605      button.on('click', this.getAddHandler(null, dialogRef), this);
 606      li.appendChild(button);
 607      label = Y.Node.create('<label for="' + id + '">' +
 608              M.str.availability.condition_group_info + '</label>');
 609      li.appendChild(label);
 610      ul.appendChild(li);
 611  
 612      var config = {
 613          headerContent : M.str.availability.addrestriction,
 614          bodyContent : content,
 615          additionalBaseClass : 'availability-dialogue',
 616          draggable : true,
 617          modal : true,
 618          closeButton : false,
 619          width : '450px'
 620      };
 621      dialogRef.dialog = new M.core.dialogue(config);
 622      dialogRef.dialog.show();
 623      cancel.on('click', function() {
 624          dialogRef.dialog.destroy();
 625          // Focus the button they clicked originally.
 626          this.inner.one('> .availability-button').one('button').focus();
 627      }, this);
 628  };
 629  
 630  /**
 631   * Gets an add handler function used by the dialogue to add a particular item.
 632   *
 633   * @method getAddHandler
 634   * @param {String|Null} type Type name of plugin or null to add lists
 635   * @param {Object} dialogRef Reference to object that contains dialog
 636   * @param {M.core.dialogue} dialogRef.dialog Dialog object
 637   * @return {Function} Add handler function to call when adding that thing
 638   */
 639  M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) {
 640      return function() {
 641          if (type) {
 642              // Create an Item object to represent the child.
 643              newItem = new M.core_availability.Item({ type: type, creating: true }, this.root);
 644          } else {
 645              // Create a new List object to represent the child.
 646              newItem = new M.core_availability.List({ c: [], showc: true }, false, this.root);
 647          }
 648          // Add to list.
 649          this.addChild(newItem);
 650          // Update the form and list HTML.
 651          M.core_availability.form.update();
 652          M.core_availability.form.rootList.renumber();
 653          this.updateHtml();
 654          // Hide dialog.
 655          dialogRef.dialog.destroy();
 656          newItem.focusAfterAdd();
 657      };
 658  };
 659  
 660  /**
 661   * Gets the value of the list ready to convert to JSON and fill form field.
 662   *
 663   * @method getValue
 664   * @return {Object} Value of list suitable for use in JSON
 665   */
 666  M.core_availability.List.prototype.getValue = function() {
 667      // Work out operator from selects.
 668      var value = {};
 669      value.op = this.node.one('.availability-neg').get('value') +
 670              this.node.one('.availability-op').get('value');
 671  
 672      // Work out children from list.
 673      value.c = [];
 674      var i;
 675      for (i = 0; i < this.children.length; i++) {
 676          value.c.push(this.children[i].getValue());
 677      }
 678  
 679      // Work out show/showc for root level.
 680      if (this.root) {
 681          if (this.isIndividualShowIcons()) {
 682              value.showc = [];
 683              for (i = 0; i < this.children.length; i++) {
 684                  value.showc.push(!this.children[i].eyeIcon.isHidden());
 685              }
 686          } else {
 687              value.show = !this.eyeIcon.isHidden();
 688          }
 689      }
 690      return value;
 691  };
 692  
 693  /**
 694   * Checks whether this list has any errors (incorrect user input). If so,
 695   * an error string identifier in the form langfile:langstring should be pushed
 696   * into the errors array.
 697   *
 698   * @method fillErrors
 699   * @param {Array} errors Array of errors so far
 700   */
 701  M.core_availability.List.prototype.fillErrors = function(errors) {
 702      // List with no items is an error (except root).
 703      if (this.children.length === 0 && !this.root) {
 704          errors.push('availability:error_list_nochildren');
 705      }
 706      // Pass to children.
 707      for (var i = 0; i < this.children.length; i++) {
 708          this.children[i].fillErrors(errors);
 709      }
 710  };
 711  
 712  /**
 713   * Eye icon for this list (null if none).
 714   *
 715   * @property eyeIcon
 716   * @type M.core_availability.EyeIcon
 717   */
 718  M.core_availability.List.prototype.eyeIcon = null;
 719  
 720  /**
 721   * True if list is special root level list.
 722   *
 723   * @property root
 724   * @type Boolean
 725   */
 726  M.core_availability.List.prototype.root = false;
 727  
 728  /**
 729   * Array containing children (Lists or Items).
 730   *
 731   * @property children
 732   * @type M.core_availability.List[]|M.core_availability.Item[]
 733   */
 734  M.core_availability.List.prototype.children = null;
 735  
 736  /**
 737   * HTML outer node for list.
 738   *
 739   * @property node
 740   * @type Y.Node
 741   */
 742  M.core_availability.List.prototype.node = null;
 743  
 744  /**
 745   * HTML node for inner div that actually is the displayed list.
 746   *
 747   * @property node
 748   * @type Y.Node
 749   */
 750  M.core_availability.List.prototype.inner = null;
 751  
 752  
 753  /**
 754   * Represents a single condition.
 755   *
 756   * @class M.core_availability.Item
 757   * @constructor
 758   * @param {Object} json Decoded JSON value
 759   * @param {Boolean} root True if this item is a child of the root list.
 760   */
 761  M.core_availability.Item = function(json, root) {
 762      this.pluginType = json.type;
 763      if (M.core_availability.form.plugins[json.type] === undefined) {
 764          // Handle undefined plugins.
 765          this.plugin = null;
 766          this.pluginNode = Y.Node.create('<div class="availability-warning">' +
 767                  M.str.availability.missingplugin + '</div>');
 768      } else {
 769          // Plugin is known.
 770          this.plugin = M.core_availability.form.plugins[json.type];
 771          this.pluginNode = this.plugin.getNode(json);
 772  
 773          // Add a class with the plugin Frankenstyle name to make CSS easier in plugin.
 774          this.pluginNode.addClass('availability_' + json.type);
 775      }
 776  
 777      this.node = Y.Node.create('<div class="availability-item"><h3 class="accesshide"></h3></div>');
 778  
 779      // Add eye icon if required. This icon is added for root items, but may be
 780      // hidden depending on the selected list operator.
 781      if (root) {
 782          var shown = true;
 783          if(json.showc !== undefined) {
 784              shown = json.showc;
 785          }
 786          this.eyeIcon = new M.core_availability.EyeIcon(true, shown);
 787          this.node.appendChild(this.eyeIcon.span);
 788      }
 789  
 790      // Add plugin controls.
 791      this.pluginNode.addClass('availability-plugincontrols');
 792      this.node.appendChild(this.pluginNode);
 793  
 794      // Add delete button for node.
 795      var deleteIcon = new M.core_availability.DeleteIcon(this);
 796      this.node.appendChild(deleteIcon.span);
 797  
 798      // Add the invalid marker (empty).
 799      this.node.appendChild(document.createTextNode(' '));
 800      this.node.appendChild(Y.Node.create('<span class="label label-warning"/>'));
 801  };
 802  
 803  /**
 804   * Obtains the value of this condition, which will be serialized into JSON
 805   * format and stored in the form.
 806   *
 807   * @method getValue
 808   * @return {Object} JavaScript object containing value of this item
 809   */
 810  M.core_availability.Item.prototype.getValue = function() {
 811      value = { 'type' : this.pluginType };
 812      if (this.plugin) {
 813          this.plugin.fillValue(value, this.pluginNode);
 814      }
 815      return value;
 816  };
 817  
 818  /**
 819   * Checks whether this condition has any errors (incorrect user input). If so,
 820   * an error string identifier in the form langfile:langstring should be pushed
 821   * into the errors array.
 822   *
 823   * @method fillErrors
 824   * @param {Array} errors Array of errors so far
 825   */
 826  M.core_availability.Item.prototype.fillErrors = function(errors) {
 827      var before = errors.length;
 828      if (this.plugin) {
 829          // Pass to plugin.
 830          this.plugin.fillErrors(errors, this.pluginNode);
 831      } else {
 832          // Unknown plugin is an error
 833          errors.push('core_availability:item_unknowntype');
 834      }
 835      // If any errors were added, add the marker to this item.
 836      var errorLabel = this.node.one('> .label-warning');
 837      if (errors.length !== before && !errorLabel.get('firstChild')) {
 838          errorLabel.appendChild(document.createTextNode(M.str.availability.invalid));
 839      } else if (errors.length === before && errorLabel.get('firstChild')) {
 840          errorLabel.get('firstChild').remove();
 841      }
 842  };
 843  
 844  /**
 845   * Renumbers the item.
 846   *
 847   * @method renumber
 848   * @param {String} number Number to use in heading for this item
 849   */
 850  M.core_availability.Item.prototype.renumber = function(number) {
 851      // Update heading for item.
 852      var headingParams = { number: number };
 853      if (this.plugin) {
 854          headingParams.type = M.str['availability_' + this.pluginType].title;
 855      } else {
 856          headingParams.type = '[' + this.pluginType + ']';
 857      }
 858      headingParams.number = number + ':';
 859      var heading = M.util.get_string('itemheading', 'availability', headingParams);
 860      this.node.one('> h3').set('innerHTML', heading);
 861  };
 862  
 863  /**
 864   * Focuses something after a new item is added.
 865   *
 866   * @method focusAfterAdd
 867   */
 868  M.core_availability.Item.prototype.focusAfterAdd = function() {
 869      this.plugin.focusAfterAdd(this.pluginNode);
 870  };
 871  
 872  /**
 873   * Name of plugin.
 874   *
 875   * @property pluginType
 876   * @type String
 877   */
 878  M.core_availability.Item.prototype.pluginType = null;
 879  
 880  /**
 881   * Object representing plugin form controls.
 882   *
 883   * @property plugin
 884   * @type Object
 885   */
 886  M.core_availability.Item.prototype.plugin = null;
 887  
 888  /**
 889   * Eye icon for item.
 890   *
 891   * @property eyeIcon
 892   * @type M.core_availability.EyeIcon
 893   */
 894  M.core_availability.Item.prototype.eyeIcon = null;
 895  
 896  /**
 897   * HTML node for item.
 898   *
 899   * @property node
 900   * @type Y.Node
 901   */
 902  M.core_availability.Item.prototype.node = null;
 903  
 904  /**
 905   * Inner part of node that is owned by plugin.
 906   *
 907   * @property pluginNode
 908   * @type Y.Node
 909   */
 910  M.core_availability.Item.prototype.pluginNode = null;
 911  
 912  
 913  /**
 914   * Eye icon (to control show/hide of the activity if the user fails a condition).
 915   *
 916   * There are individual eye icons (show/hide control for a single condition) and
 917   * 'all' eye icons (show/hide control that applies to the entire item, whatever
 918   * reason it fails for). This is necessary because the individual conditions
 919   * don't make sense for OR and AND NOT lists.
 920   *
 921   * @class M.core_availability.EyeIcon
 922   * @constructor
 923   * @param {Boolean} individual True if the icon is controlling a single condition
 924   * @param {Boolean} shown True if icon is initially in shown state
 925   */
 926  M.core_availability.EyeIcon = function(individual, shown) {
 927      this.individual = individual;
 928      this.span = Y.Node.create('<a class="availability-eye" href="#" role="button">');
 929      var icon = Y.Node.create('<img />');
 930      this.span.appendChild(icon);
 931  
 932      // Set up button text and icon.
 933      var suffix = individual ? '_individual' : '_all';
 934      var setHidden = function() {
 935          icon.set('src', M.util.image_url('i/show', 'core'));
 936          icon.set('alt', M.str.availability['hidden' + suffix]);
 937          this.span.set('title', M.str.availability['hidden' + suffix] + ' \u2022 ' +
 938                  M.str.availability.show_verb);
 939      };
 940      var setShown = function() {
 941          icon.set('src', M.util.image_url('i/hide', 'core'));
 942          icon.set('alt', M.str.availability['shown' + suffix]);
 943          this.span.set('title', M.str.availability['shown' + suffix] + ' \u2022 ' +
 944                  M.str.availability.hide_verb);
 945      };
 946      if(shown) {
 947          setShown.call(this);
 948      } else {
 949          setHidden.call(this);
 950      }
 951  
 952      // Update when button is clicked.
 953      var click = function(e) {
 954          e.preventDefault();
 955          if (this.isHidden()) {
 956              setShown.call(this);
 957          } else {
 958              setHidden.call(this);
 959          }
 960          M.core_availability.form.update();
 961      };
 962      this.span.on('click', click, this);
 963      this.span.on('key', click, 'up:32', this);
 964      this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this);
 965  };
 966  
 967  /**
 968   * True if this eye icon is an individual one (see above).
 969   *
 970   * @property individual
 971   * @type Boolean
 972   */
 973  M.core_availability.EyeIcon.prototype.individual = false;
 974  
 975  /**
 976   * YUI node for the span that contains this icon.
 977   *
 978   * @property span
 979   * @type Y.Node
 980   */
 981  M.core_availability.EyeIcon.prototype.span = null;
 982  
 983  /**
 984   * Checks the current state of the icon.
 985   *
 986   * @method isHidden
 987   * @return {Boolean} True if this icon is set to 'hidden'
 988   */
 989  M.core_availability.EyeIcon.prototype.isHidden = function() {
 990      var suffix = this.individual ? '_individual' : '_all';
 991      var compare = M.str.availability['hidden' + suffix];
 992      return this.span.one('img').get('alt') === compare;
 993  };
 994  
 995  
 996  /**
 997   * Delete icon (to delete an Item or List).
 998   *
 999   * @class M.core_availability.DeleteIcon
1000   * @constructor
1001   * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete
1002   */
1003  M.core_availability.DeleteIcon = function(toDelete) {
1004      this.span = Y.Node.create('<a class="availability-delete" href="#" title="' +
1005              M.str.moodle['delete'] + '" role="button">');
1006      var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') +
1007              '" alt="' + M.str.moodle['delete'] + '" />');
1008      this.span.appendChild(img);
1009      var click = function(e) {
1010          e.preventDefault();
1011          M.core_availability.form.rootList.deleteDescendant(toDelete);
1012          M.core_availability.form.rootList.renumber();
1013      };
1014      this.span.on('click', click, this);
1015      this.span.on('key', click, 'up:32', this);
1016      this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this);
1017  };
1018  
1019  /**
1020   * YUI node for the span that contains this icon.
1021   *
1022   * @property span
1023   * @type Y.Node
1024   */
1025  M.core_availability.DeleteIcon.prototype.span = null;


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1