[ Index ]

PHP Cross Reference of Unnamed Project

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      var spinner;
 196      if (config.spinnerhandle) {
 197        // Add a spinner to give some feedback to the user.
 198        spinner = M.util.add_spinner(Y, config.parentnode.one(config.spinnerhandle)).show();
 199      }
 200  
 201      // Fetch the data.
 202      Y.io(URL, {
 203          method: 'POST',
 204          context: this,
 205          on: {
 206              complete: this.process_results
 207          },
 208          data: config.data,
 209          "arguments": {
 210              parentnode: config.parentnode,
 211              childnode: config.childnode,
 212              spinner: spinner
 213          }
 214      });
 215  };
 216  
 217  /**
 218   * Apply the animation on the supplied node.
 219   *
 220   * @method run_expansion
 221   * @private
 222   * @param {Node} categorynode The node to apply the animation to
 223   */
 224  NS.run_expansion = function(categorynode) {
 225      var categorychildren = categorynode.one(SELECTORS.CONTENTNODE),
 226          self = this,
 227          ancestor = categorynode.ancestor(SELECTORS.COURSECATEGORYTREE);
 228  
 229      // Add our animation to the categorychildren.
 230      this.add_animation(categorychildren);
 231  
 232  
 233      // If we already have the class, remove it before showing otherwise we perform the
 234      // animation whilst the node is hidden.
 235      if (categorynode.hasClass(CSS.SECTIONCOLLAPSED)) {
 236          // To avoid a jump effect, we need to set the height of the children to 0 here before removing the SECTIONCOLLAPSED class.
 237          categorychildren.setStyle('height', '0');
 238          categorynode.removeClass(CSS.SECTIONCOLLAPSED);
 239          categorynode.setAttribute('aria-expanded', 'true');
 240          categorychildren.fx.set('reverse', false);
 241      } else {
 242          categorychildren.fx.set('reverse', true);
 243          categorychildren.fx.once('end', function(e, categorynode) {
 244              categorynode.addClass(CSS.SECTIONCOLLAPSED);
 245              categorynode.setAttribute('aria-expanded', 'false');
 246          }, this, categorynode);
 247      }
 248  
 249      categorychildren.fx.once('end', function(e, categorychildren) {
 250          // Remove the styles that the animation has set.
 251          categorychildren.setStyles({
 252              height: '',
 253              opacity: ''
 254          });
 255  
 256          // To avoid memory gobbling, remove the animation. It will be added back if called again.
 257          this.destroy();
 258          self.update_collapsible_actions(ancestor);
 259      }, categorychildren.fx, categorychildren);
 260  
 261      // Now that everything has been set up, run the animation.
 262      categorychildren.fx.run();
 263  };
 264  
 265  /**
 266   * Toggle collapsing of all nodes.
 267   *
 268   * @method collapse_expand_all
 269   * @private
 270   * @param {EventFacade} e
 271   */
 272  NS.collapse_expand_all = function(e) {
 273      // Load the actual dependencies now that we've been called.
 274      Y.use('io-base', 'json-parse', 'moodle-core-notification', 'anim-node-plugin', function() {
 275          // Overload the collapse_expand_all with the _collapse_expand_all function to ensure that
 276          // this function isn't called in the future, and call it for the first time.
 277          NS.collapse_expand_all = NS._collapse_expand_all;
 278          NS.collapse_expand_all(e);
 279      });
 280  
 281      e.preventDefault();
 282  };
 283  
 284  NS._collapse_expand_all = function(e) {
 285      // The collapse/expand button has no actual target but we need to prevent it's default
 286      // action to ensure we don't make the page reload/jump.
 287      e.preventDefault();
 288  
 289      if (e.currentTarget.hasClass(CSS.DISABLED)) {
 290          // The collapse/expand is currently disabled.
 291          return;
 292      }
 293  
 294      var ancestor = e.currentTarget.ancestor(SELECTORS.COURSECATEGORYTREE);
 295      if (!ancestor) {
 296          return;
 297      }
 298  
 299      var collapseall = ancestor.one(SELECTORS.COLLAPSEEXPAND);
 300      if (collapseall.hasClass(CSS.COLLAPSEALL)) {
 301          this.collapse_all(ancestor);
 302      } else {
 303          this.expand_all(ancestor);
 304      }
 305      this.update_collapsible_actions(ancestor);
 306  };
 307  
 308  NS.expand_all = function(ancestor) {
 309      var finalexpansions = [];
 310  
 311      ancestor.all(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)
 312          .each(function(c) {
 313          if (c.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
 314              // Expand the hidden children first without animation.
 315              c.removeClass(CSS.SECTIONCOLLAPSED);
 316              c.all(SELECTORS.LOADEDTREES).removeClass(CSS.SECTIONCOLLAPSED);
 317          } else {
 318              finalexpansions.push(c);
 319          }
 320      }, this);
 321  
 322      // Run the final expansion with animation on the visible items.
 323      Y.all(finalexpansions).each(function(c) {
 324          this.run_expansion(c);
 325      }, this);
 326  
 327  };
 328  
 329  NS.collapse_all = function(ancestor) {
 330      var finalcollapses = [];
 331  
 332      ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)
 333          .each(function(c) {
 334          if (c.ancestor(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN)) {
 335              finalcollapses.push(c);
 336          } else {
 337              // Collapse the visible items first
 338              this.run_expansion(c);
 339          }
 340      }, this);
 341  
 342      // Run the final collapses now that the these are hidden hidden.
 343      Y.all(finalcollapses).each(function(c) {
 344          c.addClass(CSS.SECTIONCOLLAPSED);
 345          c.all(SELECTORS.LOADEDTREES).addClass(CSS.SECTIONCOLLAPSED);
 346      }, this);
 347  };
 348  
 349  NS.update_collapsible_actions = function(ancestor) {
 350      var foundmaximisedchildren = false,
 351          // Grab the anchor for the collapseexpand all link.
 352          togglelink = ancestor.one(SELECTORS.COLLAPSEEXPAND);
 353  
 354      if (!togglelink) {
 355          // We should always have a togglelink but ensure.
 356          return;
 357      }
 358  
 359      // Search for any visibly expanded children.
 360      ancestor.all(SELECTORS.CATEGORYWITHMAXIMISEDLOADEDCHILDREN).each(function(n) {
 361          // If we can find any collapsed ancestors, skip.
 362          if (n.ancestor(SELECTORS.CATEGORYWITHCOLLAPSEDLOADEDCHILDREN)) {
 363              return false;
 364          }
 365          foundmaximisedchildren = true;
 366          return true;
 367      });
 368  
 369      if (foundmaximisedchildren) {
 370          // At least one maximised child found. Show the collapseall.
 371          togglelink.setHTML(M.util.get_string('collapseall', 'moodle'))
 372              .addClass(CSS.COLLAPSEALL)
 373              .removeClass(CSS.DISABLED);
 374      } else {
 375          // No maximised children found but there are collapsed children. Show the expandall.
 376          togglelink.setHTML(M.util.get_string('expandall', 'moodle'))
 377              .removeClass(CSS.COLLAPSEALL)
 378              .removeClass(CSS.DISABLED);
 379      }
 380  };
 381  
 382  /**
 383   * Process the data returned by Y.io.
 384   * This includes appending it to the relevant part of the DOM, and applying our animations.
 385   *
 386   * @method process_results
 387   * @private
 388   * @param {String} tid The Transaction ID
 389   * @param {Object} response The Reponse returned by Y.IO
 390   * @param {Object} ioargs The additional arguments provided by Y.IO
 391   */
 392  NS.process_results = function(tid, response, args) {
 393      var newnode,
 394          data;
 395      try {
 396          data = Y.JSON.parse(response.responseText);
 397          if (data.error) {
 398              return new M.core.ajaxException(data);
 399          }
 400      } catch (e) {
 401          return new M.core.exception(e);
 402      }
 403  
 404      // Insert the returned data into a new Node.
 405      newnode = Y.Node.create(data);
 406  
 407      // Append to the existing child location.
 408      args.childnode.appendChild(newnode);
 409  
 410      // Now that we have content, we can swap the classes on the toggled container.
 411      args.parentnode
 412          .addClass(CSS.LOADED)
 413          .removeClass(CSS.NOTLOADED);
 414  
 415      // Toggle the open/close status of the node now that it's content has been loaded.
 416      this.run_expansion(args.parentnode);
 417  
 418      // Remove the spinner now that we've started to show the content.
 419      if (args.spinner) {
 420          args.spinner.hide().destroy();
 421      }
 422  };
 423  
 424  /**
 425   * Add our animation to the Node.
 426   *
 427   * @method add_animation
 428   * @private
 429   * @param {Node} childnode
 430   */
 431  NS.add_animation = function(childnode) {
 432      if (typeof childnode.fx !== "undefined") {
 433          // The animation has already been plugged to this node.
 434          return childnode;
 435      }
 436  
 437      childnode.plug(Y.Plugin.NodeFX, {
 438          from: {
 439              height: 0,
 440              opacity: 0
 441          },
 442          to: {
 443              // This sets a dynamic height in case the node content changes.
 444              height: function(node) {
 445                  // Get expanded height (offsetHeight may be zero).
 446                  return node.get('scrollHeight');
 447              },
 448              opacity: 1
 449          },
 450          duration: 0.2
 451      });
 452  
 453      return childnode;
 454  };


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