[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/yuilib/gallery/gallery-sm-treeview/ -> gallery-sm-treeview.js (source)

   1  YUI.add('gallery-sm-treeview', function (Y, NAME) {
   2  
   3  var Micro = Y.Template.Micro;
   4  
   5  Y.namespace('TreeView').Templates = {
   6      children: Micro.compile(
   7          '<ul class="<%= data.classNames.children %>" ' +
   8  
   9              '<% if (data.node.isRoot()) { %>' +
  10                  'role="tree" tabindex="0"' +
  11              '<% } else { %>' +
  12                  'role="group"' +
  13              '<% } %>' +
  14  
  15          '></ul>'
  16      ),
  17  
  18      node: Micro.compile(
  19          '<li id="<%= data.node.id %>" class="<%= data.nodeClassNames.join(" ") %>" role="treeitem" aria-labelled-by="<%= data.node.id %>-label">' +
  20              '<div class="<%= data.classNames.row %>" data-node-id="<%= data.node.id %>">' +
  21                  '<span class="<%= data.classNames.indicator %>"><s></s></span>' +
  22                  '<span class="<%= data.classNames.icon %>"></span>' +
  23                  '<span id="<%= data.node.id %>-label" class="<%= data.classNames.label %>"><%== data.node.label %></span>' +
  24              '</div>' +
  25          '</li>'
  26      )
  27  };
  28  /*jshint expr:true, onevar:false */
  29  
  30  /**
  31  Provides the `Y.TreeView` widget.
  32  
  33  @module gallery-sm-treeview
  34  @main gallery-sm-treeview
  35  **/
  36  
  37  /**
  38  TreeView widget.
  39  
  40  @class TreeView
  41  @constructor
  42  @extends View
  43  @uses Tree
  44  @uses Tree.Labelable
  45  @uses Tree.Openable
  46  @uses Tree.Selectable
  47  **/
  48  
  49  var getClassName = Y.ClassNameManager.getClassName,
  50  
  51  TreeView = Y.Base.create('treeView', Y.View, [
  52      Y.Tree,
  53      Y.Tree.Labelable,
  54      Y.Tree.Openable,
  55      Y.Tree.Selectable
  56  ], {
  57      // -- Public Properties ----------------------------------------------------
  58  
  59      /**
  60      CSS class names used by this treeview.
  61  
  62      @property {Object} classNames
  63      @param {String} canHaveChildren Class name indicating that a tree node can
  64          contain child nodes (whether or not it actually does).
  65      @param {String} children Class name for a list of child nodes.
  66      @param {String} hasChildren Class name indicating that a tree node has one
  67          or more child nodes.
  68      @param {String} icon Class name for a tree node's icon.
  69      @param {String} indicator Class name for an open/closed indicator.
  70      @param {String} label Class name for a tree node's user-visible label.
  71      @param {String} node Class name for a tree node item.
  72      @param {String} noTouch Class name added to the TreeView container when not
  73          using a touchscreen device.
  74      @param {String} open Class name indicating that a tree node is open.
  75      @param {String} row Class name for a row container encompassing the
  76          indicator and label within a tree node.
  77      @param {String} selected Class name for a tree node that's selected.
  78      @param {String} touch Class name added to the TreeView container when using
  79          a touchscreen device.
  80      @param {String} treeview Class name for the TreeView container.
  81      **/
  82      classNames: {
  83          canHaveChildren: getClassName('treeview-can-have-children'),
  84          children       : getClassName('treeview-children'),
  85          hasChildren    : getClassName('treeview-has-children'),
  86          icon           : getClassName('treeview-icon'),
  87          indicator      : getClassName('treeview-indicator'),
  88          label          : getClassName('treeview-label'),
  89          node           : getClassName('treeview-node'),
  90          noTouch        : getClassName('treeview-notouch'),
  91          open           : getClassName('treeview-open'),
  92          row            : getClassName('treeview-row'),
  93          selected       : getClassName('treeview-selected'),
  94          touch          : getClassName('treeview-touch'),
  95          treeview       : getClassName('treeview')
  96      },
  97  
  98      /**
  99      Whether or not this TreeView has been rendered.
 100  
 101      @property {Boolean} rendered
 102      @default false
 103      **/
 104      rendered: false,
 105  
 106      /**
 107      Default templates used to render this TreeView.
 108  
 109      @property {Object} templates
 110      **/
 111      templates: Y.TreeView.Templates,
 112  
 113      // -- Protected Properties -------------------------------------------------
 114  
 115      /**
 116      Simple way to type-check that this is a TreeView instance.
 117  
 118      @property {Boolean} _isYUITreeView
 119      @default true
 120      @protected
 121      **/
 122      _isYUITreeView: true,
 123  
 124      /**
 125      Cached value of the `lazyRender` attribute.
 126  
 127      @property {Boolean} _lazyRender
 128      @protected
 129      **/
 130  
 131      // -- Lifecycle Methods ----------------------------------------------------
 132  
 133      initializer: function (config) {
 134          if (config && config.templates) {
 135              this.templates = Y.merge(this.templates, config.templates);
 136          }
 137  
 138          this._renderQueue = {};
 139          this._attachTreeViewEvents();
 140      },
 141  
 142      destructor: function () {
 143          clearTimeout(this._renderTimeout);
 144          this._detachTreeViewEvents();
 145  
 146          this._renderQueue = null;
 147      },
 148  
 149      // -- Public Methods -------------------------------------------------------
 150  
 151      destroyNode: function (node, options) {
 152          node._htmlNode = null;
 153          return Y.Tree.prototype.destroyNode.call(this, node, options);
 154      },
 155  
 156      /**
 157      Returns the HTML node (as a `Y.Node` instance) associated with the specified
 158      `Tree.Node` instance, if any.
 159  
 160      @method getHTMLNode
 161      @param {Tree.Node} treeNode Tree node.
 162      @return {Node} `Y.Node` instance associated with the given tree node, or
 163          `undefined` if one was not found.
 164      **/
 165      getHTMLNode: function (treeNode) {
 166          if (!treeNode._htmlNode) {
 167              treeNode._htmlNode = this.get('container').one('#' + treeNode.id);
 168          }
 169  
 170          return treeNode._htmlNode;
 171      },
 172  
 173      /**
 174      Renders this TreeView into its container.
 175  
 176      If the container hasn't already been added to the current document, it will
 177      be appended to the `<body>` element.
 178  
 179      @method render
 180      @chainable
 181      **/
 182      render: function () {
 183          var container     = this.get('container'),
 184              isTouchDevice = 'ontouchstart' in Y.config.win;
 185  
 186          container.addClass(this.classNames.treeview);
 187          container.addClass(this.classNames[isTouchDevice ? 'touch' : 'noTouch']);
 188  
 189          this._childrenNode = this.renderChildren(this.rootNode, {
 190              container: container
 191          });
 192  
 193          if (!container.inDoc()) {
 194              Y.one('body').append(container);
 195          }
 196  
 197          this.rendered = true;
 198  
 199          return this;
 200      },
 201  
 202      /**
 203      Renders the children of the specified tree node.
 204  
 205      If a container is specified, it will be assumed to be an existing rendered
 206      tree node, and the children will be rendered (or re-rendered) inside it.
 207  
 208      @method renderChildren
 209      @param {Tree.Node} treeNode Tree node whose children should be rendered.
 210      @param {Object} [options] Options.
 211          @param {Node} [options.container] `Y.Node` instance of a container into
 212              which the children should be rendered. If the container already
 213              contains rendered children, they will be re-rendered in place.
 214      @return {Node} `Y.Node` instance containing the rendered children.
 215      **/
 216      renderChildren: function (treeNode, options) {
 217          options || (options = {});
 218  
 219          var container    = options.container,
 220              childrenNode = container && container.one('>.' + this.classNames.children),
 221              lazyRender   = this._lazyRender;
 222  
 223          if (!childrenNode) {
 224              childrenNode = Y.Node.create(this.templates.children({
 225                  classNames: this.classNames,
 226                  node      : treeNode,
 227                  treeview  : this // not currently used, but may be useful for custom templates
 228              }));
 229          }
 230  
 231          if (treeNode.hasChildren()) {
 232              childrenNode.set('aria-expanded', treeNode.isOpen());
 233  
 234              for (var i = 0, len = treeNode.children.length; i < len; i++) {
 235                  var child = treeNode.children[i];
 236  
 237                  this.renderNode(child, {
 238                      container     : childrenNode,
 239                      renderChildren: !lazyRender || child.isOpen()
 240                  });
 241              }
 242          }
 243  
 244          // Keep track of whether or not this node's children have been rendered
 245          // so we'll know whether we need to render them later if the node is
 246          // opened.
 247          treeNode.state.renderedChildren = true;
 248  
 249          if (container) {
 250              container.append(childrenNode);
 251          }
 252  
 253          return childrenNode;
 254      },
 255  
 256      /**
 257      Renders the specified tree node and its children (if any).
 258  
 259      If a container is specified, the rendered node will be appended to it.
 260  
 261      @method renderNode
 262      @param {Tree.Node} treeNode Tree node to render.
 263      @param {Object} [options] Options.
 264          @param {Node} [options.container] `Y.Node` instance of a container to
 265              which the rendered tree node should be appended.
 266          @param {Boolean} [options.renderChildren=false] Whether or not to render
 267              this node's children.
 268      @return {Node} `Y.Node` instance of the rendered tree node.
 269      **/
 270      renderNode: function (treeNode, options) {
 271          options || (options = {});
 272  
 273          var classNames     = this.classNames,
 274              hasChildren    = treeNode.hasChildren(),
 275              htmlNode       = treeNode._htmlNode,
 276              nodeClassNames = {},
 277              className;
 278  
 279          // Build the hash of CSS classes for this node.
 280          nodeClassNames[classNames.node]            = true;
 281          nodeClassNames[classNames.canHaveChildren] = !!treeNode.canHaveChildren;
 282          nodeClassNames[classNames.hasChildren]     = hasChildren;
 283  
 284          if (htmlNode) {
 285              // This node has already been rendered, so we just need to update
 286              // the DOM instead of re-rendering it from scratch.
 287              htmlNode.one('.' + classNames.label).setHTML(treeNode.label);
 288  
 289              for (className in nodeClassNames) {
 290                  if (nodeClassNames.hasOwnProperty(className)) {
 291                      htmlNode.toggleClass(className, nodeClassNames[className]);
 292                  }
 293              }
 294          } else {
 295              // This node hasn't been rendered yet, so render it from scratch.
 296              var enabledClassNames = [];
 297  
 298              for (className in nodeClassNames) {
 299                  if (nodeClassNames.hasOwnProperty(className) && nodeClassNames[className]) {
 300                      enabledClassNames.push(className);
 301                  }
 302              }
 303  
 304              htmlNode = treeNode._htmlNode = Y.Node.create(this.templates.node({
 305                  classNames    : classNames,
 306                  nodeClassNames: enabledClassNames,
 307                  node          : treeNode,
 308                  treeview      : this // not currently used, but may be useful for custom templates
 309              }));
 310          }
 311  
 312          this._syncNodeOpenState(treeNode, htmlNode);
 313          this._syncNodeSelectedState(treeNode, htmlNode);
 314  
 315          if (hasChildren) {
 316              if (options.renderChildren) {
 317                  this.renderChildren(treeNode, {
 318                      container: htmlNode
 319                  });
 320              }
 321          } else {
 322              // If children were previously rendered but this node no longer has
 323              // children, remove the empty child list.
 324              var childrenNode = htmlNode.one('>.' + classNames.children);
 325  
 326              if (childrenNode) {
 327                  childrenNode.remove(true);
 328              }
 329          }
 330  
 331          treeNode.state.rendered = true;
 332  
 333          if (options.container) {
 334              options.container.append(htmlNode);
 335          }
 336  
 337          return htmlNode;
 338      },
 339  
 340      // -- Protected Methods ----------------------------------------------------
 341  
 342      _attachTreeViewEvents: function () {
 343          this._treeViewEvents || (this._treeViewEvents = []);
 344  
 345          var classNames = this.classNames,
 346              container  = this.get('container');
 347  
 348          this._treeViewEvents.push(
 349              // Custom events.
 350              this.after({
 351                  add              : this._afterAdd,
 352                  clear            : this._afterClear,
 353                  close            : this._afterClose,
 354                  multiSelectChange: this._afterTreeViewMultiSelectChange, // sheesh
 355                  open             : this._afterOpen,
 356                  remove           : this._afterRemove,
 357                  select           : this._afterSelect,
 358                  unselect         : this._afterUnselect
 359              }),
 360  
 361              // DOM events.
 362              container.on('mousedown', this._onMouseDown, this),
 363  
 364              container.delegate('click', this._onIndicatorClick,
 365                  '.' + classNames.indicator, this),
 366  
 367              container.delegate('click', this._onRowClick,
 368                  '.' + classNames.row, this),
 369  
 370              container.delegate('dblclick', this._onRowDoubleClick,
 371                  '.' + classNames.canHaveChildren + ' > .' + classNames.row, this)
 372          );
 373      },
 374  
 375      _detachTreeViewEvents: function () {
 376          (new Y.EventHandle(this._treeViewEvents)).detach();
 377      },
 378  
 379      _processRenderQueue: function () {
 380          if (!this.rendered) {
 381              return;
 382          }
 383  
 384          var queue = this._renderQueue,
 385              node;
 386  
 387          for (var id in queue) {
 388              if (queue.hasOwnProperty(id)) {
 389                  node = this.getNodeById(id);
 390  
 391                  if (node) {
 392                      this.renderNode(node, queue[id]);
 393                  }
 394              }
 395          }
 396  
 397          this._renderQueue = {};
 398      },
 399  
 400      _queueRender: function (node, options) {
 401          if (!this.rendered) {
 402              return;
 403          }
 404  
 405          var queue = this._renderQueue,
 406              self  = this;
 407  
 408          clearTimeout(this._renderTimeout);
 409  
 410          queue[node.id] = Y.merge(queue[node.id], options);
 411  
 412          this._renderTimeout = setTimeout(function () {
 413              self._processRenderQueue();
 414          }, 15);
 415  
 416          return this;
 417      },
 418  
 419      /**
 420      Setter for the `lazyRender` attribute.
 421  
 422      Just caches the value in a property for faster lookups.
 423  
 424      @method _setLazyRender
 425      @return {Boolean} Value.
 426      @protected
 427      **/
 428      _setLazyRender: function (value) {
 429          /*jshint boss:true */
 430          return this._lazyRender = value;
 431      },
 432  
 433      _syncNodeOpenState: function (node, htmlNode) {
 434          htmlNode || (htmlNode = this.getHTMLNode(node));
 435  
 436          if (!htmlNode) {
 437              return;
 438          }
 439  
 440          if (node.isOpen()) {
 441              htmlNode
 442                  .addClass(this.classNames.open)
 443                  .set('aria-expanded', true);
 444          } else {
 445              htmlNode
 446                  .removeClass(this.classNames.open)
 447                  .set('aria-expanded', false);
 448          }
 449      },
 450  
 451      _syncNodeSelectedState: function (node, htmlNode) {
 452          htmlNode || (htmlNode = this.getHTMLNode(node));
 453  
 454          if (!htmlNode) {
 455              return;
 456          }
 457  
 458          var multiSelect = this.get('multiSelect');
 459  
 460          if (node.isSelected()) {
 461              htmlNode.addClass(this.classNames.selected);
 462  
 463              if (multiSelect) {
 464                  // It's only necessary to set aria-selected when multi-select is
 465                  // enabled and focus can't be used to track the selection state.
 466                  htmlNode.set('aria-selected', true);
 467              } else {
 468                  htmlNode.set('tabIndex', 0);
 469              }
 470          } else {
 471              htmlNode
 472                  .removeClass(this.classNames.selected)
 473                  .removeAttribute('tabIndex');
 474  
 475              if (multiSelect) {
 476                  htmlNode.set('aria-selected', false);
 477              }
 478          }
 479      },
 480  
 481      // -- Protected Event Handlers ---------------------------------------------
 482  
 483      _afterAdd: function (e) {
 484          // Nothing to do if the treeview hasn't been rendered yet.
 485          if (!this.rendered) {
 486              return;
 487          }
 488  
 489          var parent       = e.parent,
 490              parentIsRoot = parent.isRoot(),
 491              treeNode     = e.node,
 492  
 493              htmlChildren,
 494              htmlParent;
 495  
 496          if (parentIsRoot) {
 497              htmlChildren = this._childrenNode;
 498          } else {
 499              htmlParent   = this.getHTMLNode(parent),
 500              htmlChildren = htmlParent && htmlParent.one('>.' + this.classNames.children);
 501          }
 502  
 503          if (htmlChildren) {
 504              // Parent's children have already been rendered. Instead of
 505              // re-rendering all of them, just render the new node and insert it
 506              // at the correct position.
 507              htmlChildren.insert(this.renderNode(treeNode, {
 508                  renderChildren: !this._lazyRender || treeNode.isOpen()
 509              }), e.index);
 510  
 511              // Schedule the parent node to be re-rendered in order to update its
 512              // state. This is done asynchronously and throttled in order to
 513              // avoid re-rendering the parent many times if multiple children are
 514              // added in quick succession.
 515              if (!parentIsRoot) {
 516                  this._queueRender(parent);
 517              }
 518          } else if (!parentIsRoot) {
 519              // Either the parent hasn't been rendered yet, or its children
 520              // haven't been rendered yet. Schedule it to be rendered. This is
 521              // done asynchronously and throttled in order to avoid re-rendering
 522              // the parent many times if multiple children are added in quick
 523              // succession.
 524              this._queueRender(parent, {renderChildren: true});
 525          }
 526      },
 527  
 528      _afterClear: function () {
 529          // Nothing to do if the treeview hasn't been rendered yet.
 530          if (!this.rendered) {
 531              return;
 532          }
 533  
 534          clearTimeout(this._renderTimeout);
 535          this._renderQueue = {};
 536  
 537          delete this._childrenNode;
 538          this.rendered = false;
 539  
 540          this.get('container').empty();
 541          this.render();
 542      },
 543  
 544      _afterClose: function (e) {
 545          if (this.rendered) {
 546              this._syncNodeOpenState(e.node);
 547          }
 548      },
 549  
 550      _afterOpen: function (e) {
 551          if (!this.rendered) {
 552              return;
 553          }
 554  
 555          var treeNode = e.node,
 556              htmlNode = this.getHTMLNode(treeNode);
 557  
 558          // If this node's children haven't been rendered yet, render them.
 559          if (!treeNode.state.renderedChildren) {
 560              this.renderChildren(treeNode, {
 561                  container: htmlNode
 562              });
 563          }
 564  
 565          this._syncNodeOpenState(treeNode, htmlNode);
 566      },
 567  
 568      _afterRemove: function (e) {
 569          if (!this.rendered) {
 570              return;
 571          }
 572  
 573          var treeNode = e.node,
 574              parent   = e.parent;
 575  
 576          // If this node is in the render queue, remove it from the queue.
 577          if (this._renderQueue[treeNode.id]) {
 578              delete this._renderQueue[treeNode.id];
 579          }
 580  
 581          // Remove DOM nodes associated with this node and any of its
 582          // descendants, and mark all nodes as unrendered so that they'll be
 583          // re-rendered if they're reinserted in the tree.
 584          var htmlNode = this.getHTMLNode(treeNode);
 585  
 586          if (htmlNode) {
 587              htmlNode
 588                  .empty()
 589                  .remove(true);
 590  
 591              treeNode._htmlNode = null;
 592          }
 593  
 594          if (!treeNode.state.destroyed) {
 595              treeNode.traverse(function (node) {
 596                  node._htmlNode              = null;
 597                  node.state.rendered         = false;
 598                  node.state.renderedChildren = false;
 599              });
 600          }
 601  
 602          // Re-render the parent to update its state if this was its last child.
 603          if (parent && !parent.hasChildren()) {
 604              this.renderNode(parent);
 605          }
 606      },
 607  
 608      _afterSelect: function (e) {
 609          if (this.rendered) {
 610              this._syncNodeSelectedState(e.node);
 611          }
 612      },
 613  
 614      _afterTreeViewMultiSelectChange: function (e) {
 615          if (!this.rendered) {
 616              return;
 617          }
 618  
 619          var container = this.get('container'),
 620              rootList  = container.one('> .' + this.classNames.children),
 621              htmlNodes = container.all('.' + this.classNames.node);
 622  
 623          if (e.newVal) {
 624              rootList.set('aria-multiselectable', true);
 625              htmlNodes.set('aria-selected', false);
 626          } else {
 627              // When multiselect is disabled, aria-selected must not be set on
 628              // any nodes, since focus is used to indicate selection.
 629              rootList.removeAttribute('aria-multiselectable');
 630              htmlNodes.removeAttribute('aria-selected');
 631          }
 632      },
 633  
 634      _afterUnselect: function (e) {
 635          if (this.rendered) {
 636              this._syncNodeSelectedState(e.node);
 637          }
 638      },
 639  
 640      _onIndicatorClick: function (e) {
 641          var rowNode = e.currentTarget.ancestor('.' + this.classNames.row);
 642  
 643          // Indicator clicks shouldn't toggle selection state, so don't allow
 644          // this event to propagate to the _onRowClick() handler.
 645          e.stopImmediatePropagation();
 646  
 647          this.getNodeById(rowNode.getData('node-id')).toggleOpen();
 648      },
 649  
 650      _onMouseDown: function (e) {
 651          // This prevents the tree from momentarily grabbing focus before focus
 652          // is set on a node.
 653          e.preventDefault();
 654      },
 655  
 656      _onRowClick: function (e) {
 657          // Ignore buttons other than the left button.
 658          if (e.button > 1) {
 659              return;
 660          }
 661  
 662          var node = this.getNodeById(e.currentTarget.getData('node-id'));
 663  
 664          if (this.get('multiSelect')) {
 665              node[node.isSelected() ? 'unselect' : 'select']();
 666          } else {
 667              node.select();
 668          }
 669      },
 670  
 671      _onRowDoubleClick: function (e) {
 672          // Ignore buttons other than the left button.
 673          if (e.button > 1) {
 674              return;
 675          }
 676  
 677          this.getNodeById(e.currentTarget.getData('node-id')).toggleOpen();
 678      }
 679  }, {
 680      ATTRS: {
 681          /**
 682          When `true`, a node's children won't be rendered until the first time
 683          that node is opened.
 684  
 685          This can significantly speed up the time it takes to render a large
 686          tree, but might not make sense if you're using CSS that doesn't hide the
 687          contents of closed nodes.
 688  
 689          @attribute {Boolean} lazyRender
 690          @default true
 691          **/
 692          lazyRender: {
 693              lazyAdd: false, // to ensure that the setter runs on init
 694              setter : '_setLazyRender',
 695              value  : true
 696          }
 697      }
 698  });
 699  
 700  Y.TreeView = Y.mix(TreeView, Y.TreeView);
 701  
 702  
 703  }, 'gallery-2013.06.20-02-07', {
 704      "requires": [
 705          "base-build",
 706          "classnamemanager",
 707          "template-micro",
 708          "tree",
 709          "tree-labelable",
 710          "tree-openable",
 711          "tree-selectable",
 712          "view"
 713      ],
 714      "skinnable": true
 715  });


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