1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 sc_require('mixins/tree_item_content'); 9 sc_require('mixins/collection_content'); 10 11 /** 12 @ignore 13 @class 14 15 A TreeNode is an internal class that will manage a single item in a tree 16 when trying to display the item in a hierarchy. 17 18 When displaying a tree of objects, a tree item object will be nested to 19 cover every object that might have child views, ignoring those that will 20 definitely not. (Any node which has children or may have children should 21 advertise this by exposing an array at its treeItemChildrenKey property; 22 any node which does not do so is assumed to be permanently childless, so 23 we optimize by not observing it. In CS terms, nodes can implicitly 24 advertise whether they are *leaves-or-branches* and should be observed, 25 or are *permanently leaves*, and may remain unobserved.) 26 27 TreeNode stores an array which contains either a number pointing to the 28 next place in the array there is a child item or it contains a child item. 29 30 @extends SC.Object 31 @extends SC.Array 32 @extends SC.CollectionContent 33 @since SproutCore 1.0 34 */ 35 SC.TreeItemObserver = SC.Object.extend(SC.Array, SC.CollectionContent, { 36 37 //@if(debug) 38 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 39 40 /* @private */ 41 toString: function () { 42 var item = this.get('item'), 43 ret = sc_super(); 44 45 return item ? "%@:\n ↳ %@".fmt(ret, item) : ret; 46 }, 47 48 /* END DEBUG ONLY PROPERTIES AND METHODS */ 49 //@endif 50 51 /** @private */ 52 _cachedItem: null, 53 54 /** @private */ 55 _cachedDelegate: null, 56 57 /** 58 The node in the tree this observer will manage. Set when creating the 59 object. If you are creating an observer manually, you must set this to 60 a non-null value. 61 */ 62 item: null, 63 64 /** 65 The controller delegate. If the item does not implement the 66 TreeItemContent method, delegate properties will be used to determine how 67 to access the content. Set automatically when a tree item is created. 68 69 If you are creating an observer manually, you must set this to a non-null 70 value. 71 */ 72 delegate: null, 73 74 /** 75 The key used to retrieve children from the observed item. If a 76 delegate exists, the key will be the value of the `treeItemChildrenKey` 77 property of the delegate. Otherwise, the key will be `treeItemChildren`. 78 79 @type String 80 @default 'treeItemChildren' 81 */ 82 treeItemChildrenKey: 'treeItemChildren', 83 84 /** 85 The key used to identify the expanded state of the observed item. 86 If a delegate exists, the key will be the value of the `treeItemIsExpandedKey` 87 property of the delegate. Otherwise, the key will be `treeItemIsExpanded`. 88 89 @type String 90 @default 'treeItemIsExpanded' 91 */ 92 treeItemIsExpandedKey: 'treeItemIsExpanded', 93 94 // .......................................................... 95 // FOR NESTED OBSERVERS 96 // 97 98 /** 99 The parent TreeItemObserver for this observer. Must be set on create. 100 */ 101 parentObserver: null, 102 103 /** 104 The parent item for the observer item. Computed automatically from the 105 parent. If the value of this is null, then this is the root of the tree. 106 */ 107 parentItem: function () { 108 var p = this.get('parentObserver'); 109 return p ? p.get('item') : null; 110 }.property('parentObserver').cacheable(), 111 112 /** 113 Index location in parent's children array. If this is the root item 114 in the tree, should be null. 115 */ 116 index: null, 117 118 outlineLevel: 0, 119 120 // .......................................................... 121 // EXTRACTED FROM ITEM 122 // 123 124 /** 125 Array of child tree items. Extracted from the item automatically on init. 126 */ 127 children: null, 128 129 /** 130 Disclosure state of this item. Must be SC.BRANCH_OPEN or SC.BRANCH_CLOSED 131 If this is the root of a item tree, the observer will have children but 132 no parent or parent item. IN this case the disclosure state is always 133 SC.BRANCH_OPEN. 134 135 @property 136 @type Number 137 */ 138 disclosureState: SC.BRANCH_OPEN, 139 140 /** 141 IndexSet of children with branches. This will ask the delegate to name 142 these indexes. The default implementation will iterate over the children 143 of the item but a more optimized version could avoid touching each item. 144 145 @property 146 @type SC.IndexSet 147 */ 148 branchIndexes: function () { 149 var item = this.get('item'), 150 len, pitem, idx, children, ret; 151 152 // no item - no branches 153 if (!item) return SC.IndexSet.EMPTY; 154 155 // if item is treeItemContent then ask it directly 156 else if (item.isTreeItemContent) { 157 pitem = this.get('parentItem'); 158 idx = this.get('index'); 159 return item.treeItemBranchIndexes(pitem, idx); 160 161 // otherwise, loop over children and determine disclosure state for each 162 } else { 163 children = this.get('children'); 164 if (!children) return null; // no children - no branches 165 ret = SC.IndexSet.create(); 166 len = children.get('length'); 167 pitem = item; // save parent 168 169 for (idx = 0; idx < len; idx++) { 170 item = children.objectAt(idx); 171 if (!item) continue; 172 if (!this._computeChildren(item, pitem, idx)) continue; // no children 173 if (this._computeDisclosureState(item, pitem, idx) !== SC.LEAF_NODE) { 174 ret.add(idx); 175 } 176 } 177 178 return ret.get('length') > 0 ? ret : null; 179 } 180 }.property('children').cacheable(), 181 182 /** 183 Returns YES if the item itself should be shown, NO if only its children 184 should be shown. Normally returns YES unless the parentObject is null. 185 */ 186 isHeaderVisible: function () { 187 return !!this.get('parentObserver'); 188 }.property('parentObserver').cacheable(), 189 190 /** 191 Get the current length of the tree item including any of its children. 192 */ 193 length: 0, 194 195 // .......................................................... 196 // SC.ARRAY SUPPORT 197 // 198 199 /** 200 Get the object at the specified index. This will talk the tree info 201 to determine the proper place. The offset should be relative to the 202 start of this tree item. Calls recursively down the tree. 203 204 This should only be called with an index you know is in the range of item 205 or its children based on looking at the length. 206 207 @param {Number} index 208 @param {Boolean} omitMaterializing 209 @returns {Object} 210 */ 211 objectAt: function (index, omitMaterializing) { 212 var len = this.get('length'), 213 item = this.get('item'), 214 cache = this._objectAtCache, 215 cur = index, 216 indexes, children; 217 218 if (index >= len) return undefined; 219 if (this.get('isHeaderVisible')) { 220 if (index === 0) return item; 221 else cur--; 222 } 223 item = null; 224 225 if (!cache) cache = this._objectAtCache = []; 226 if ((item = cache[index]) !== undefined) return item; 227 228 children = this.get('children'); 229 if (!children) return undefined; // no children - nothing to get 230 231 // loop through branch indexes, reducing the offset until it matches 232 // something we might actually return. 233 indexes = this.get('branchIndexes'); 234 if (indexes) { 235 indexes.forEach(function (i) { 236 if (item || (i > cur)) return; // past end - nothing to do 237 238 var observer = this.branchObserverAt(i), len; 239 if (!observer) return; // nothing to do 240 241 // if cur lands inside of this observer's length, use objectAt to get 242 // otherwise, just remove len from cur. 243 len = observer.get('length'); 244 if (i + len > cur) { 245 item = observer.objectAt(cur - i, omitMaterializing); 246 cur = -1; 247 } else { 248 cur = cur - (len - 1); 249 } 250 251 }, this); 252 } 253 254 if (cur >= 0) item = children.objectAt(cur, omitMaterializing); // get internal if needed 255 cache[index] = item; // save in cache 256 257 return item; 258 }, 259 260 /** 261 Implements SC.Array.replace() primitive. For this method to succeed, the 262 range you replace must lie entirely within the same parent item, otherwise 263 this will raise an exception. 264 265 ### The Operation Parameter 266 267 Note that this replace method accepts an additional parameter "operation" 268 which is used when you try to insert an item on a boundary between 269 branches whether it should be inserted at the end of the previous group 270 after the group. If you don't pass operation, the default is 271 SC.DROP_BEFORE, which is the expected behavior. 272 273 Even if the operation is SC.DROP_AFTER, you should still pass the actual 274 index where you expect the item to be inserted. For example, if you want 275 to insert AFTER the last index of an 3-item array, you would still call: 276 277 observer.replace(3, 0, [object1 .. objectN], SC.DROP_AFTER) 278 279 The operation is simply used to disambiguate whether the insertion is 280 intended to be AFTER the previous item or BEFORE the items you are 281 replacing. 282 283 @param {Number} start the starting index 284 @param {Number} amt the number of items to replace 285 @param {SC.Array} objects array of objects to insert 286 @param {Number} operation either SC.DROP_BEFORE or SC.DROP_AFTER 287 @returns {SC.TreeItemObserver} receiver 288 */ 289 replace: function (start, amt, objects, operation) { 290 291 var cur = start, 292 observer = null, 293 indexes, len, max; 294 295 if (operation === undefined) operation = SC.DROP_BEFORE; 296 297 // adjust the start location based on branches, possibly passing on to an 298 // observer. 299 if (this.get('isHeaderVisible')) cur--; // exclude my own header item 300 if (cur < 0) throw new Error("Tree Item cannot replace itself"); 301 302 // remove branch lengths. If the adjusted start location lands inside of 303 // another branch, then just let that observer handle it. 304 indexes = this.get('branchIndexes'); 305 if (indexes) { 306 indexes.forEach(function (i) { 307 if (observer || (i >= cur)) return; // nothing to do 308 observer = this.branchObserverAt(i); 309 if (!observer) return; // nothing to do 310 311 len = observer.get('length'); 312 313 // if this branch range is before the start loc, just remove it and 314 // go on. If cur is somewhere inside of the range, then save to pass 315 // on. Note use of operation to determine the ambiguous end op. 316 if ((i + len === cur) && operation === SC.DROP_AFTER) { 317 cur = cur - i; 318 } else if (i + len > cur) { 319 cur = cur - i; // put inside of nested range 320 } else { 321 cur = cur - (len - 1); 322 observer = null; 323 } 324 }, this); 325 } 326 327 // if an observer was saved, pass on call. 328 if (observer) { 329 observer.replace(cur, amt, objects, operation); 330 return this; 331 } 332 333 // no observer was saved, which means cur points to an index inside of 334 // our own range. Now amt just needs to be adjusted to remove any 335 // visible branches as well. 336 max = cur + amt; 337 if (amt > 1 && indexes) { // if amt is 1 no need... 338 indexes.forEachIn(cur, indexes.get('max') - cur, function (i) { 339 if (i > max) return; // nothing to do 340 if (!(observer = this.branchObserverAt(i))) return; // nothing to do 341 len = observer.get('length'); 342 max = max - (len - 1); 343 }, this); 344 } 345 346 // get amt back out. if amt is negative, it means that the range passed 347 // was not cleanly inside of this range. raise an exception. 348 amt = max - cur; 349 350 // ok, now that we are adjusted, get the children and forward the replace 351 // call on. if there are no children, bad news... 352 var children = this.get('children'); 353 if (!children) throw new Error("cannot replace() tree item with no children"); 354 355 if ((amt < 0) || (max > children.get('length'))) { 356 throw new Error("replace() range must lie within a single tree item"); 357 } 358 359 children.replace(cur, amt, objects, operation); 360 361 // don't call enumerableContentDidChange() here because, as an observer, 362 // we should be notified by the children array itself. 363 364 return this; 365 }, 366 367 /** 368 Called whenever the content for the passed observer has changed. Default 369 version notifies the parent if it exists and updates the length. 370 371 The start, amt and delta params should reflect changes to the children 372 array, not to the expanded range for the wrapper. 373 */ 374 observerContentDidChange: function (start, amt, delta) { 375 376 // clear caches 377 this.invalidateBranchObserversAt(start); 378 this._objectAtCache = this._outlineLevelCache = null; 379 this._disclosureStateCache = null; 380 this.notifyPropertyChange('branchIndexes'); 381 382 var oldlen = this.get('length'), 383 newlen = this._computeLength(), 384 parent = this.get('parentObserver'), set; 385 386 // update length if needed 387 if (oldlen !== newlen) { 388 this.set('length', newlen); 389 } 390 391 // if we have a parent, notify that parent that we have changed. 392 if (!this._notifyParent) return this; // nothing more to do 393 394 if (parent) { 395 set = SC.IndexSet.create(this.get('index')); 396 parent._childrenRangeDidChange(parent.get('children'), null, '[]', set); 397 398 // otherwise, note the enumerable content has changed. note that we need 399 // to convert the passed change to reflect the computed range 400 } else { 401 if (oldlen === newlen) { 402 amt = this.expandChildIndex(start + amt); 403 start = this.expandChildIndex(start); 404 amt = amt - start; 405 delta = 0; 406 407 } else { 408 start = this.expandChildIndex(start); 409 amt = newlen - start; 410 delta = newlen - oldlen; 411 } 412 413 var removedCount = amt; 414 var addedCount = delta + removedCount; 415 this.arrayContentDidChange(start, removedCount, addedCount); 416 } 417 }, 418 419 /** 420 Accepts a child index and expands it to reflect any nested groups. 421 */ 422 expandChildIndex: function (index) { 423 424 var ret = index; 425 if (this.get('isHeaderVisible')) index++; 426 427 // fast path 428 var branches = this.get('branchIndexes'); 429 if (!branches || branches.get('length') === 0) return ret; 430 431 // we have branches, adjust for their length 432 branches.forEachIn(0, index, function (idx) { 433 ret += this.branchObserverAt(idx).get('length') - 1; 434 }, this); 435 436 return ret; // add 1 for item header 437 }, 438 439 // .......................................................... 440 // SC.COLLECTION CONTENT SUPPORT 441 // 442 443 /** SC.CollectionContent 444 Called by the collection view to return any group indexes. The default 445 implementation will compute the indexes one time based on the delegate 446 treeItemIsGrouped 447 */ 448 contentGroupIndexes: function (view, content) { 449 var ret; 450 451 if (content !== this) return null; // only care about receiver 452 453 // If this is not the root item, never do grouping 454 if (this.get('parentObserver')) return null; 455 456 var item = this.get('item'), group, indexes, cur, padding; 457 458 if (item && item.isTreeItemContent) group = item.get('treeItemIsGrouped'); 459 else group = !!this.delegate.get('treeItemIsGrouped'); 460 461 // If grouping is enabled, build an index set with all of our local groups. 462 if (group) { 463 ret = SC.IndexSet.create(); 464 indexes = this.get('branchIndexes'); 465 466 if (indexes) { 467 // Start at the minimum index, which is equal for the tree and flat array 468 cur = indexes.min(); 469 470 // Padding is the difference between the tree index and array index for the current tree index 471 padding = 0; 472 indexes.forEach(function (i) { 473 ret.add(i + padding, 1); 474 475 var observer = this.branchObserverAt(i); 476 if (observer) { 477 padding += observer.get('length') - 1; 478 cur += padding; 479 } 480 }, this); 481 } 482 } else { 483 ret = null; 484 } 485 486 return ret; 487 }, 488 489 /** SC.CollectionContent */ 490 contentIndexIsGroup: function (view, content, idx) { 491 var indexes = this.contentGroupIndexes(view, content); 492 return indexes ? indexes.contains(idx) : NO; 493 }, 494 495 /** 496 Returns the outline level for the specified index. 497 */ 498 contentIndexOutlineLevel: function (view, content, index) { 499 if (content !== this) return -1; // only care about us 500 501 var cache = this._outlineLevelCache; 502 if (cache && (cache[index] !== undefined)) return cache[index]; 503 if (!cache) cache = this._outlineLevelCache = []; 504 505 var len = this.get('length'), 506 cur = index, 507 ret = null, 508 indexes; 509 510 if (index >= len) return -1; 511 512 if (this.get('isHeaderVisible')) { 513 if (index === 0) { 514 cache[0] = this.get('outlineLevel') - 1; 515 return cache[0]; 516 } else { 517 cur--; 518 } 519 } 520 521 // loop through branch indexes, reducing the offset until it matches 522 // something we might actually return. 523 indexes = this.get('branchIndexes'); 524 if (indexes) { 525 indexes.forEach(function (i) { 526 if ((ret !== null) || (i > cur)) return; // past end - nothing to do 527 528 var observer = this.branchObserverAt(i), len; 529 if (!observer) return; // nothing to do 530 531 // if cur lands inside of this observer's length, use objectAt to get 532 // otherwise, just remove len from cur. 533 len = observer.get('length'); 534 if (i + len > cur) { 535 ret = observer.contentIndexOutlineLevel(view, observer, cur - i); 536 cur = -1; 537 } else { 538 cur = cur - (len - 1); 539 } 540 541 }, this); 542 } 543 544 if (cur >= 0) ret = this.get('outlineLevel'); // get internal if needed 545 cache[index] = ret; // save in cache 546 return ret; 547 }, 548 549 /** 550 Returns the disclosure state for the specified index. 551 */ 552 contentIndexDisclosureState: function (view, content, index) { 553 if (content !== this) return -1; // only care about us 554 555 var cache = this._disclosureStateCache; 556 if (cache && (cache[index] !== undefined)) return cache[index]; 557 if (!cache) cache = this._disclosureStateCache = []; 558 559 var len = this.get('length'), 560 cur = index, 561 ret = null, 562 indexes; 563 564 if (index >= len) return SC.LEAF_NODE; 565 566 if (this.get('isHeaderVisible')) { 567 if (index === 0) { 568 cache[0] = this.get('disclosureState'); 569 return cache[0]; 570 } else { 571 cur--; 572 } 573 } 574 575 // loop through branch indexes, reducing the offset until it matches 576 // something we might actually return. 577 indexes = this.get('branchIndexes'); 578 if (indexes) { 579 indexes.forEach(function (i) { 580 if ((ret !== null) || (i > cur)) return; // past end - nothing to do 581 582 var observer = this.branchObserverAt(i), len; 583 if (!observer) return; // nothing to do 584 585 // if cur lands inside of this observer's length, use objectAt to get 586 // otherwise, just remove len from cur. 587 len = observer.get('length'); 588 if (i + len > cur) { 589 ret = observer.contentIndexDisclosureState(view, observer, cur - i); 590 cur = -1; 591 } else { 592 cur = cur - (len - 1); 593 } 594 595 }, this); 596 } 597 598 if (cur >= 0) ret = SC.LEAF_NODE; // otherwise its a leaf node 599 cache[index] = ret; // save in cache 600 return ret; 601 }, 602 603 /** 604 Expands the specified content index. This will search down until it finds 605 the branchObserver responsible for this item and then calls _collapse on 606 it. 607 */ 608 contentIndexExpand: function (view, content, idx) { 609 var indexes, cur = idx, children, item; 610 611 if (content !== this) return; // only care about us 612 if (this.get('isHeaderVisible')) { 613 if (idx === 0) { 614 this._expand(this.get('item')); 615 return; 616 } else { 617 cur--; 618 } 619 } 620 621 indexes = this.get('branchIndexes'); 622 if (indexes) { 623 indexes.forEach(function (i) { 624 if (i >= cur) return; // past end - nothing to do 625 var observer = this.branchObserverAt(i), len; 626 if (!observer) return; 627 628 len = observer.get('length'); 629 if (i + len > cur) { 630 observer.contentIndexExpand(view, observer, cur - i); 631 cur = -1; //done 632 } else { 633 cur = cur - (len - 1); 634 } 635 636 }, this); 637 } 638 639 // if we are still inside of the range then maybe pass on to a child item 640 if (cur >= 0) { 641 children = this.get('children'); 642 item = children ? children.objectAt(cur) : null; 643 if (item) this._expand(item, this.get('item'), cur); 644 } 645 }, 646 647 /** 648 Called to collapse a content index item if it is currently in an open 649 disclosure state. The default implementation does nothing. 650 651 @param {SC.CollectionView} view the collection view 652 @param {SC.Array} content the content object 653 @param {Number} idx the content index 654 @returns {void} 655 */ 656 contentIndexCollapse: function (view, content, idx) { 657 var indexes, children, item, cur = idx; 658 659 if (content !== this) return; // only care about us 660 if (this.get('isHeaderVisible')) { 661 if (idx === 0) { 662 this._collapse(this.get('item')); 663 return; 664 } else { 665 cur--; 666 } 667 } 668 669 indexes = this.get('branchIndexes'); 670 if (indexes) { 671 indexes.forEach(function (i) { 672 if (i >= cur) return; // past end - nothing to do 673 var observer = this.branchObserverAt(i), len; 674 if (!observer) return; 675 676 len = observer.get('length'); 677 if (i + len > cur) { 678 observer.contentIndexCollapse(view, observer, cur - i); 679 cur = -1; //done 680 } else { 681 cur = cur - (len - 1); 682 } 683 684 }, this); 685 } 686 687 // if we are still inside of the range then maybe pass on to a child item 688 if (cur >= 0) { 689 children = this.get('children'); 690 item = children ? children.objectAt(cur) : null; 691 if (item) this._collapse(item, this.get('item'), cur); 692 } 693 }, 694 695 // .......................................................... 696 // BRANCH NODES 697 // 698 699 /** 700 Returns the branch item for the specified index. If none exists yet, it 701 will be created. 702 */ 703 branchObserverAt: function (index) { 704 var byIndex = this._branchObserversByIndex, 705 indexes = this._branchObserverIndexes, 706 ret, item, children; 707 708 if (!byIndex) byIndex = this._branchObserversByIndex = []; 709 if (!indexes) { 710 indexes = this._branchObserverIndexes = SC.IndexSet.create(); 711 } 712 713 ret = byIndex[index]; 714 if (ret) return ret; // use cache 715 716 // no observer for this content exists, create one 717 children = this.get('children'); 718 item = children ? children.objectAt(index) : null; 719 if (!item) return null; // can't create an observer for a null item 720 721 byIndex[index] = ret = SC.TreeItemObserver.create({ 722 item: item, 723 delegate: this.get('delegate'), 724 parentObserver: this, 725 index: index, 726 outlineLevel: this.get('outlineLevel') + 1 727 }); 728 729 indexes.add(index); // save for later invalidation 730 return ret; 731 }, 732 733 /** 734 Invalidates any branch observers on or after the specified index range. 735 */ 736 invalidateBranchObserversAt: function (index) { 737 var byIndex = this._branchObserversByIndex, 738 indexes = this._branchObserverIndexes; 739 740 if (!byIndex || byIndex.length <= index) return this; // nothing to do 741 if (index < 0) index = 0; 742 743 // destroy any observer on or after the range 744 indexes.forEachIn(index, indexes.get('max') - index, function (i) { 745 var observer = byIndex[i]; 746 if (observer) observer.destroy(); 747 }, this); 748 749 byIndex.length = index; // truncate to dump extra indexes 750 751 return this; 752 }, 753 754 // .......................................................... 755 // INTERNAL METHODS 756 // 757 758 /** @private */ 759 _cleanUpCachedDelegate: function () { 760 var cachedDelegate = this._cachedDelegate; 761 762 if (cachedDelegate) { 763 cachedDelegate.removeObserver('treeItemIsExpandedKey', this, this.treeItemIsExpandedKeyDidChange); 764 cachedDelegate.removeObserver('treeItemChildrenKey', this, this.treeItemChildrenKeyDidChange); 765 cachedDelegate.removeObserver('treeItemIsGrouped', this, this.treeItemIsGroupedDidChange); 766 767 // Remove the delegate specific key observers from the cached item. 768 this._cleanUpCachedItem(); 769 770 // Reset the delegate specific keys. 771 this.set('treeItemChildrenKey', 'treeItemChildren'); 772 this.set('treeItemIsExpandedKey', 'treeItemIsExpanded'); 773 774 // Remove the cache. 775 this._cachedDelegate = null; 776 } 777 }, 778 779 /** @private */ 780 _cleanUpCachedItem: function () { 781 var cachedItem = this._cachedItem, 782 treeItemIsExpandedKey = this.get('treeItemIsExpandedKey'), 783 treeItemChildrenKey = this.get('treeItemChildrenKey'); 784 785 if (cachedItem) { 786 cachedItem.removeObserver(treeItemIsExpandedKey, this, this._itemIsExpandedDidChange); 787 cachedItem.removeObserver(treeItemChildrenKey, this, this._itemChildrenDidChange); 788 789 // Remove the cache. 790 this._cachedItem = null; 791 } 792 }, 793 794 /** SC.Object.prototype.init */ 795 init: function () { 796 sc_super(); 797 798 // Initialize the item and the delegate. Be sure to set up the delegate first, 799 // because it determines the keys to observe on the item. 800 this._delegateDidChange(); 801 this._itemDidChange(); 802 803 this._notifyParent = YES; // avoid infinite loops 804 }, 805 806 /** SC.Object.prototype.destroy 807 Called just before a branch observer is removed. Should stop any 808 observing and invalidate any child observers. 809 */ 810 destroy: function () { 811 this.invalidateBranchObserversAt(0); 812 this._objectAtCache = null; 813 this._notifyParent = NO; // parent doesn't care anymore 814 815 // Cleanup the observed item and delegate. 816 this._cleanUpCachedItem(); 817 this._cleanUpCachedDelegate(); 818 819 var children = this._children, 820 ro = this._childrenRangeObserver; 821 if (children && ro) children.removeRangeObserver(ro); 822 823 this.set('length', 0); 824 825 sc_super(); 826 }, 827 828 /** @private */ 829 _itemDidChange: function () { 830 var item = this.get('item'), 831 treeItemChildrenKey, 832 treeItemIsExpandedKey; 833 834 treeItemIsExpandedKey = this.get('treeItemIsExpandedKey'); 835 treeItemChildrenKey = this.get('treeItemChildrenKey'); 836 837 // Cleanup the previous observed item. 838 this._cleanUpCachedItem(); 839 840 //@if(debug) 841 // Add some developer support to prevent broken behavior. 842 if (!item) { throw new Error("Developer Error: SC.TreeItemObserver: Item cannot be null and must be set on create."); } 843 844 if (item.hasObserverFor(treeItemIsExpandedKey)) { 845 SC.warn("Developer Warning: SC.TreeItemObserver: Item '%@' appears to already be assigned to a tree item observer. This will cause strange behavior working with the item.".fmt(item)); 846 } 847 //@endif 848 849 item.addObserver(treeItemIsExpandedKey, this, this._itemIsExpandedDidChange); 850 item.addObserver(treeItemChildrenKey, this, this._itemChildrenDidChange); 851 852 // Fire the observer functions once to initialize. 853 this.beginPropertyChanges(); 854 this._itemIsExpandedDidChange(); 855 this._itemChildrenDidChange(); 856 this.endPropertyChanges(); 857 858 // Track the item so that when it changes we can clean-up. 859 this._cachedItem = item; 860 }.observes('item'), 861 862 /** @private */ 863 _itemIsExpandedDidChange: function () { 864 var state = this.get('disclosureState'), 865 item = this.get('item'), 866 next; 867 868 next = this._computeDisclosureState(item); 869 if (state !== next) { this.set('disclosureState', next); } 870 }, 871 872 /** @private */ 873 _itemChildrenDidChange: function () { 874 var children = this.get('children'), 875 item = this.get('item'), 876 next; 877 878 next = this._computeChildren(item); 879 if (children !== next) { this.set('children', next); } 880 }, 881 882 /** @private 883 Called whenever the children or disclosure state changes. Begins or ends 884 observing on the children array so that changes can propogate outward. 885 */ 886 _childrenDidChange: function () { 887 var state = this.get('disclosureState'), 888 cur = state === SC.BRANCH_OPEN ? this.get('children') : null, 889 last = this._children, 890 ro = this._childrenRangeObserver; 891 892 if (last === cur) return this; //nothing to do 893 894 if (ro) last.removeRangeObserver(ro); 895 896 if (cur) { 897 this._childrenRangeObserver = cur.addRangeObserver(null, this, this._childrenRangeDidChange); 898 } else { 899 this._childrenRangeObserver = null; 900 } 901 902 this._children = cur; 903 this._childrenRangeDidChange(cur, null, '[]', null); 904 }.observes("children", "disclosureState"), 905 906 /** @private 907 Called anytime the actual content of the children has changed. If this 908 changes the length property, then notifies the parent that the content 909 might have changed. 910 */ 911 _childrenRangeDidChange: function (array, objects, key, indexes) { 912 var children = this.get('children'), 913 len = children ? children.get('length') : 0, 914 min = indexes ? indexes.get('min') : 0, 915 max = indexes ? indexes.get('max') : len, 916 old = this._childrenLen || 0; 917 918 this._childrenLen = len; // save for future calls 919 this.observerContentDidChange(min, max - min, len - old); 920 }, 921 922 /** @private 923 Computes the current disclosure state of the item by asking the item or 924 the delegate. If no pitem or index is passed, the parentItem and index 925 will be used. 926 */ 927 _computeDisclosureState: function (item, pitem, index) { 928 var key; 929 930 // no item - assume leaf node 931 if (!item || !this._computeChildren(item)) return SC.LEAF_NODE; 932 933 // item implement TreeItemContent - call directly 934 else if (item.isTreeItemContent) { 935 if (pitem === undefined) pitem = this.get('parentItem'); 936 if (index === undefined) index = this.get('index'); 937 return item.treeItemDisclosureState(pitem, index); 938 939 // otherwise get treeItemDisclosureStateKey from delegate 940 } else { 941 key = this.get('treeItemIsExpandedKey'); 942 return item.get(key) ? SC.BRANCH_OPEN : SC.BRANCH_CLOSED; 943 } 944 }, 945 946 /** @private 947 Collapse the item at the specified index. This will either directly 948 modify the property on the item or call the treeItemCollapse() method. 949 */ 950 _collapse: function (item, pitem, index) { 951 var key; 952 953 // no item - assume leaf node 954 if (!item || !this._computeChildren(item)) return this; 955 956 // item implement TreeItemContent - call directly 957 else if (item.isTreeItemContent) { 958 if (pitem === undefined) pitem = this.get('parentItem'); 959 if (index === undefined) index = this.get('index'); 960 item.treeItemCollapse(pitem, index); 961 962 // otherwise get treeItemDisclosureStateKey from delegate 963 } else { 964 key = this.get('treeItemIsExpandedKey'); 965 item.setIfChanged(key, NO); 966 } 967 968 return this; 969 }, 970 971 /** @private Each time the delegate changes, observe it for changes to its keys. */ 972 _delegateDidChange: function () { 973 var delegate = this.get('delegate'); 974 975 // Clean up the previous observed delegate. 976 this._cleanUpCachedDelegate(); 977 978 if (delegate) { 979 delegate.addObserver('treeItemChildrenKey', this, this.treeItemChildrenKeyDidChange); 980 delegate.addObserver('treeItemIsExpandedKey', this, this.treeItemIsExpandedKeyDidChange); 981 delegate.addObserver('treeItemIsGrouped', this, this.treeItemIsGroupedDidChange); 982 983 // Fire the observer functions once to initialize. 984 this.treeItemChildrenKeyDidChange(); 985 this.treeItemIsExpandedKeyDidChange(); 986 this.treeItemIsGroupedDidChange(); 987 } 988 989 // Re-initialize the item to match the new delegate. 990 this._itemDidChange(); 991 992 // Cache the previous delegate so we can clean up. 993 this._cachedDelegate = delegate; 994 }.observes('delegate'), 995 996 /** @private 997 Expand the item at the specified index. This will either directly 998 modify the property on the item or call the treeItemExpand() method. 999 */ 1000 _expand: function (item, pitem, index) { 1001 var key; 1002 1003 // no item - assume leaf node 1004 if (!item || !this._computeChildren(item)) return this; 1005 1006 // item implement TreeItemContent - call directly 1007 else if (item.isTreeItemContent) { 1008 if (pitem === undefined) pitem = this.get('parentItem'); 1009 if (index === undefined) index = this.get('index'); 1010 item.treeItemExpand(pitem, index); 1011 1012 // otherwise get treeItemDisclosureStateKey from delegate 1013 } else { 1014 key = this.get('treeItemIsExpandedKey'); 1015 item.setIfChanged(key, YES); 1016 } 1017 1018 return this; 1019 }, 1020 1021 /** @private 1022 Computes the children for the passed item. 1023 */ 1024 _computeChildren: function (item) { 1025 var key; 1026 1027 if (!item) { // no item - no children 1028 return null; 1029 } else if (item.isTreeItemContent) { // item implements TreeItemContent - call directly 1030 return item.get('treeItemChildren'); 1031 } else { // otherwise get treeItemChildrenKey from delegate 1032 key = this.get('treeItemChildrenKey'); 1033 return item.get(key); 1034 } 1035 }, 1036 1037 /** @private 1038 Computes the length of the array by looking at children. 1039 */ 1040 _computeLength: function () { 1041 var ret = this.get('isHeaderVisible') ? 1 : 0, 1042 state = this.get('disclosureState'), 1043 children = this.get('children'), 1044 indexes; 1045 1046 // if disclosure is open, add children count + length of branch observers. 1047 if ((state === SC.BRANCH_OPEN) && children) { 1048 ret += children.get('length'); 1049 1050 indexes = this.get('branchIndexes'); 1051 if (indexes) { 1052 indexes.forEach(function (idx) { 1053 var observer = this.branchObserverAt(idx); 1054 ret += observer.get('length') - 1; 1055 }, this); 1056 } 1057 } 1058 return ret; 1059 }, 1060 1061 /** @private */ 1062 treeItemChildrenKeyDidChange: function () { 1063 var del = this.get('delegate'), 1064 key; 1065 1066 key = del ? del.get('treeItemChildrenKey') : 'treeItemChildren'; 1067 this.set('treeItemChildrenKey', key ? key : 'treeItemChildren'); 1068 }, 1069 1070 /** @private */ 1071 treeItemIsExpandedKeyDidChange: function () { 1072 var del = this.get('delegate'), 1073 key; 1074 1075 key = del ? del.get('treeItemIsExpandedKey') : 'treeItemIsExpanded'; 1076 this.set('treeItemIsExpandedKey', key ? key : 'treeItemIsExpanded'); 1077 }, 1078 1079 /** @private */ 1080 treeItemIsGroupedDidChange: function () { 1081 this.notifyPropertyChange('branchIndexes'); 1082 } 1083 1084 }); 1085 1086