[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
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 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |