[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/course/yui/src/categoryexpander/js/ -> categoryexpander.js (source)

   1  /**
   2   * Adds toggling of subcategory with automatic loading using AJAX.
   3   *
   4   * This also includes application of an animation to improve user experience.
   5   *
   6   * @module moodle-course-categoryexpander
   7   */
   8  
   9  /**
  10   * The course category expander.
  11   *
  12   * @constructor
  13   * @class Y.Moodle.course.categoryexpander
  14   */
  15  
  16  var CSS = {
  17          CONTENTNODE: 'content',
  18          COLLAPSEALL: 'collapse-all',
  19          DISABLED: 'disabled',
  20          LOADED: 'loaded',
  21          NOTLOADED: 'notloaded',
  22          SECTIONCOLLAPSED: 'collapsed',
  23          HASCHILDREN: 'with_children'
  24      },
  25      SELECTORS = {
  26          LOADEDTREES: '.with_children.loaded',
  27          CONTENTNODE: '.content',
  28          CATEGORYLISTENLINK: '.category .info .categoryname',
  29          CATEGORYSPINNERLOCATION: '.categoryname',
  30          CATEGORYWITHCOLLAPSEDLOADEDCHILDREN: '.category.with_children.loaded.collapsed',
  31          CATEGORYWITHMAXIMISEDLOADEDCHILDREN: '.category.with_children.loaded:not(.collapsed)',
  32          COLLAPSEEXPAND: '.collapseexpand',
  33          COURSEBOX: '.coursebox',
  34          COURSEBOXLISTENLINK: '.coursebox .moreinfo',
  35          COURSEBOXSPINNERLOCATION: '.coursename a',
  36          COURSECATEGORYTREE: '.course_category_tree',
  37          PARENTWITHCHILDREN: '.category'
  38      },
  39      NS = Y.namespace('Moodle.course.categoryexpander'),
  40      TYPE_CATEGORY = 0,
  41      TYPE_COURSE = 1,
  42      URL = M.cfg.wwwroot + '/course/category.ajax.php';
  43  
  44  /**
  45   * Set up the category expander.
  46   *
  47   * No arguments are required.
  48   *
  49   * @method init
  50   */
  51  NS.init = function() {
  52      var doc = Y.one(Y.config.doc);
  53      doc.delegate('click', this.toggle_category_expansion, SELECTORS.CATEGORYLISTENLINK, this);
  54      doc.delegate('click', this.toggle_coursebox_expansion, SELECTORS.COURSEBOXLISTENLINK, this);
  55      doc.delegate('click', this.collapse_expand_all, SELECTORS.COLLAPSEEXPAND, this);
  56  
  57      // Only set up they keybaord listeners when tab is first pressed - it
  58      // may never happen and modifying the DOM on a large number of nodes
  59      // can be very expensive.
  60      doc.once('key', this.setup_keyboard_listeners, 'tab', this);
  61  };
  62  
  63  /**
  64   * Set up keyboard expansion for course content.
  65   *
  66   * This includes setting up the delegation but also adding the nodes to the
  67   * tabflow.
  68   *
  69   * @method setup_keyboard_listeners
  70   */
  71  NS.setup_keyboard_listeners = function() {
  72      var doc = Y.one(Y.config.doc);
  73  
  74      Y.log('Setting the tabindex for all expandable course nodes', 'info', 'moodle-course-categoryexpander');
  75      doc.all(SELECTORS.CATEGORYLISTENLINK, SELECTORS.COURSEBOXLISTENLINK, SELECTORS.COLLAPSEEXPAND).setAttribute('tabindex', '0');
  76  
  77  
  78      Y.one(Y.config.doc).delegate('key', this.toggle_category_expansion, 'enter', SELECTORS.CATEGORYLISTENLINK, this);
  79      Y.one(Y.config.doc).delegate('key', this.toggle_coursebox_expansion, 'enter', SELECTORS.COURSEBOXLISTENLINK, this);
  80      Y.one(Y.config.doc).delegate('key', this.collapse_expand_all, 'enter', SELECTORS.COLLAPSEEXPAND, this);
  81  };
  82  
  83  /**
  84   * Toggle the animation of the clicked category node.
  85   *
  86   * @method toggle_category_expansion
  87   * @private
  88   * @param {EventFacade} e
  89   */
  90  NS.toggle_category_expansion = function(e) {
  91      // Load the actual dependencies now that we've been called.
  92      Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
  93          // Overload the toggle_category_expansion with the _toggle_category_expansion function to ensure that
  94          // this function isn't called in the future, and call it for the first time.
  95          NS.toggle_category_expansion = NS._toggle_category_expansion;
  96          NS.toggle_category_expansion(e);
  97      });
  98  };
  99  
 100  /**
 101   * Toggle the animation of the clicked coursebox node.
 102   *
 103   * @method toggle_coursebox_expansion
 104   * @private
 105   * @param {EventFacade} e
 106   */
 107  NS.toggle_coursebox_expansion = function(e) {
 108      // Load the actual dependencies now that we've been called.
 109      Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
 110          // Overload the toggle_coursebox_expansion with the _toggle_coursebox_expansion function to ensure that
 111          // this function isn't called in the future, and call it for the first time.
 112          NS.toggle_coursebox_expansion = NS._toggle_coursebox_expansion;
 113          NS.toggle_coursebox_expansion(e);
 114      });
 115  
 116      e.preventDefault();
 117  };
 118  
 119  NS._toggle_coursebox_expansion = function(e) {
 120      var courseboxnode;
 121  
 122      // Grab the parent category container - this is where the new content will be added.
 123      courseboxnode = e.target.ancestor(SELECTORS.COURSEBOX, true);
 124      e.preventDefault();
 125  
 126      if (courseboxnode.hasClass(CSS.LOADED)) {
 127          // We've already loaded this content so we just need to toggle the view of it.
 128          this.run_expansion(courseboxnode);
 129          return;
 130      }
 131  
 132      this._toggle_generic_expansion({
 133          parentnode: courseboxnode,
 134          childnode: courseboxnode.one(SELECTORS.CONTENTNODE),
 135          spinnerhandle: SELECTORS.COURSEBOXSPINNERLOCATION,
 136          data: {
 137              courseid: courseboxnode.getData('courseid'),
 138              type: TYPE_COURSE
 139          }
 140      });
 141  };
 142  
 143  NS._toggle_category_expansion = function(e) {
 144      var categorynode,
 145          categoryid,
 146          depth;
 147  
 148      if (e.target.test('a') || e.target.test('img')) {
 149          // Return early if either an anchor or an image were clicked.
 150          return;
 151      }
 152  
 153      // Grab the parent category container - this is where the new content will be added.
 154      categorynode = e.target.ancestor(SELECTORS.PARENTWITHCHILDREN, true);
 155  
 156      if (!categorynode.hasClass(CSS.HASCHILDREN)) {
 157          // Nothing to do here - this category has no children.
 158          return;
 159      }
 160  
 161      if (categorynode.hasClass(CSS.LOADED)) {
 162          // We've already loaded this content so we just need to toggle the view of it.
 163          this.run_expansion(categorynode);
 164          return;
 165      }
 166  
 167      // We use Data attributes to store the category.
 168      categoryid = categorynode.getData('categoryid');
 169      depth = categorynode.getData('depth');
 170      if (typeof categoryid === "undefined" || typeof depth === "undefined") {
 171          return;
 172      }
 173  
 174      this._toggle_generic_expansion({
 175          parentnode: categorynode,
 176          childnode: categorynode.one(SELECTORS.CONTENTNODE),
 177          spinnerhandle: SELECTORS.CATEGORYSPINNERLOCATION,
 178          data: {
 179              categoryid: categoryid,
 180              depth: depth,
 181              showcourses: categorynode.getData('showcourses'),
 182              type: TYPE_CATEGORY
 183          }
 184      });
 185  };
 186  
 187  /**
 188   * Wrapper function to handle toggling of generic types.
 189   *
 190   * @method _toggle_generic_expansion
 191   * @private
 192   * @param {Object} config
 193   */
 194  NS._toggle_generic_expansion = function(config) {
 195      if (config.spinnerhandle) {
 196        // Add a spinner to give some feedback to the user.
 197        spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show();
 198      }
 199  
 200      // Fetch the data.
 201      Y.io(URL, {
 202          method: 'POST',
 203          context: this,
 204          on: {
 205              complete: this.process_results
 206          },
 207          data: config.data,
 208          "arguments": {
 209              parentnode: config.parentnode,
 210              childnode: config.childnode,
 211              spinner: spinner
 212          }
 213      });
 214  };
 215  
 216  /**
 217   * Apply the animation on the supplied node.
 218   *
 219   * @method run_expansion
 220   * @private
 221   * @param {Node} categorynode The node to apply the animation to
 222   */
 223  NS.run_expansion = function(categorynode) {
 224      var categorychildren = categorynode.one(SELECTORS.CONTENTNODE),
 225          self = this,
 226          ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE);
 227  
 228      // Add our animation to the categorychildren.
 229      this.add_animation(categorychildren);
 230  
 231  
 232      // If we already have the class, remove it before showing otherwise we perform the
 233      // animation whilst the node is hidden.
 234      if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) {
 235          // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class.
 236          categorychildren.setStyle('height', '0');
 237          categorynode.removeClass(CSS.SECTIONCOLLAPSED);
 238          categorynode.setAttribute('aria-expanded', 'true');
 239          categorychildren.fx.set('reverse', false);
 240      } else {
 241          categorychildren.fx.set('reverse', true);
 242          categorychildren.fx.once('end', function(e, categorynode) {
 243              categorynode.addClass(CSS.SECTIONCOLLAPSED);
 244              categorynode.setAttribute('aria-expanded', 'false');
 245          }, this, categorynode);
 246      }
 247  
 248      categorychildren.fx.once('end', function(e, categorychildren) {
 249          // Remove the styles that the animation has set.
 250          categorychildren.setStyles({
 251              height: '',
 252              opacity: ''
 253          });
 254  
 255          // To avoid memory gobbling, remove the animation. It will be added back if called again.
 256          this.destroy();
 257          self.update_collapsible_actions(ancestor);
 258      }, categorychildren.fx, categorychildren);
 259  
 260      // Now that everything has been set up, run the animation.
 261      categorychildren.fx.run();
 262  };
 263  
 264  /**
 265   * Toggle collapsing of all nodes.
 266   *
 267   * @method collapse_expand_all
 268   * @private
 269   * @param {EventFacade} e
 270   */
 271  NS.collapse_expand_all = function(e) {
 272      // Load the actual dependencies now that we've been called.
 273      Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
 274          // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that
 275          // this function isn't called in the future, and call it for the first time.
 276          NS.collapse_expand_all = NS._collapse_expand_all;
 277          NS.collapse_expand_all(e);
 278      });
 279  
 280      e.preventDefault();
 281  };
 282  
 283  NS._collapse_expand_all = function(e) {
 284      // The collapse/expand button has no actual target but we need to prevent it's default
 285      // action to ensure we don't make the page reload/jump.
 286      e.preventDefault();
 287  
 288      if (e.currentTarget.hasClass(CSS.DISABLED)) {
 289          // The collapse/expand is currently disabled.
 290          return;
 291      }
 292  
 293      var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE);
 294      if (!ancestor) {
 295          return;
 296      }
 297  
 298      var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND);
 299      if (collapseall.hasClass(CSS.COLLAPSEALL)) {
 300          this.collapse_all(ancestor);
 301      } else {
 302          this.expand_all(ancestor);
 303      }
 304      this.update_collapsible_actions(ancestor);
 305  };
 306  
 307  NS.expand_all = function(ancestor) {
 308      var finalexpansions = [];
 309  
 310      ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)
 311          .each(function(c) {
 312          if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
 313              // Expand the hidden children first without animation.
 314              c.removeClass(CSS.SECTIONCOLLAPSED);
 315              c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED);
 316          } else {
 317              finalexpansions.push(c);
 318          }
 319      }, this);
 320  
 321      // Run the final expansion with animation on the visible items.
 322      Y.all(finalexpansions).each(function(c) {
 323          this.run_expansion(c);
 324      }, this);
 325  
 326  };
 327  
 328  NS.collapse_all = function(ancestor) {
 329      var finalcollapses = [];
 330  
 331      ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)
 332          .each(function(c) {
 333          if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) {
 334              finalcollapses.push(c);
 335          } else {
 336              // Collapse the visible items first
 337              this.run_expansion(c);
 338          }
 339      }, this);
 340  
 341      // Run the final collapses now that the these are hidden hidden.
 342      Y.all(finalcollapses).each(function(c) {
 343          c.addClass(CSS.SECTIONCOLLAPSED);
 344          c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED);
 345      }, this);
 346  };
 347  
 348  NS.update_collapsible_actions = function(ancestor) {
 349      var foundmaximisedchildren = false,
 350          // Grab the anchor for the collapseexpand all link.
 351          togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND);
 352  
 353      if (!togglelink) {
 354          // We should always have a togglelink but ensure.
 355          return;
 356      }
 357  
 358      // Search for any visibly expanded children.
 359      ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) {
 360          // If we can find any collapsed ancestors, skip.
 361          if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
 362              return false;
 363          }
 364          foundmaximisedchildren = true;
 365          return true;
 366      });
 367  
 368      if (foundmaximisedchildren) {
 369          // At least one maximised child found. Show the collapseall.
 370          togglelink.setHTML(M.util.get_string('collapseall', 'moodle'))
 371              .addClass(CSS.COLLAPSEALL)
 372              .removeClass(CSS.DISABLED);
 373      } else {
 374          // No maximised children found but there are collapsed children. Show the expandall.
 375          togglelink.setHTML(M.util.get_string('expandall', 'moodle'))
 376              .removeClass(CSS.COLLAPSEALL)
 377              .removeClass(CSS.DISABLED);
 378      }
 379  };
 380  
 381  /**
 382   * Process the data returned by Y.io.
 383   * This includes appending it to the relevant part of the DOM, and applying our animations.
 384   *
 385   * @method process_results
 386   * @private
 387   * @param {String} tid The Transaction ID
 388   * @param {Object} response The Reponse returned by Y.IO
 389   * @param {Object} ioargs The additional arguments provided by Y.IO
 390   */
 391  NS.process_results = function(tid, response, args) {
 392      var newnode,
 393          data;
 394      try {
 395          data = Y.JSON.parse(response.responseText);
 396          if (data.error) {
 397              return new M.core.ajaxException(data);
 398          }
 399      } catch (e) {
 400          return new M.core.exception(e);
 401      }
 402  
 403      // Insert the returned data into a new Node.
 404      newnode = Y.Node.create(data);
 405  
 406      // Append to the existing child location.
 407      args.childnode.appendChild(newnode);
 408  
 409      // Now that we have content, we can swap the classes on the toggled container.
 410      args.parentnode
 411          .addClass(CSS.LOADED)
 412          .removeClass(CSS.NOTLOADED);
 413  
 414      // Toggle the open/close status of the node now that it's content has been loaded.
 415      this.run_expansion(args.parentnode);
 416  
 417      // Remove the spinner now that we've started to show the content.
 418      if (args.spinner) {
 419          args.spinner.hide().destroy();
 420      }
 421  };
 422  
 423  /**
 424   * Add our animation to the Node.
 425   *
 426   * @method add_animation
 427   * @private
 428   * @param {Node} childnode
 429   */
 430  NS.add_animation = function(childnode) {
 431      if (typeof childnode.fx !== "undefined") {
 432          // The animation has already been plugged to this node.
 433          return childnode;
 434      }
 435  
 436      childnode.plug(Y.Plugin.NodeFX, {
 437          from: {
 438              height: 0,
 439              opacity: 0
 440          },
 441          to: {
 442              // This sets a dynamic height in case the node content changes.
 443              height: function(node) {
 444                  // Get expanded height (offsetHeight may be zero).
 445                  return node.get('scrollHeight');
 446              },
 447              opacity: 1
 448          },
 449          duration: 0.2
 450      });
 451  
 452      return childnode;
 453  };


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