1 // ========================================================================== 2 // Project: SproutCore Costello - Property Observing Library 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 // note: SC.Observable also enhances array. make sure we are called after 9 // SC.Observable so our version of unknownProperty wins. 10 sc_require('ext/function'); 11 sc_require('mixins/observable'); 12 sc_require('mixins/enumerable'); 13 sc_require('system/range_observer'); 14 15 SC.OUT_OF_RANGE_EXCEPTION = "Index out of range"; 16 17 SC.CoreArray = /** @lends SC.Array.prototype */ { 18 19 /** 20 Walk like a duck - use isSCArray to avoid conflicts 21 @type Boolean 22 */ 23 isSCArray: YES, 24 25 /** 26 @field {Number} length 27 28 Your array must support the length property. Your replace methods should 29 set this property whenever it changes. 30 */ 31 // length: 0, 32 33 /** 34 This is one of the primitives you must implement to support SC.Array. You 35 should replace amt objects started at idx with the objects in the passed 36 array. 37 38 Before mutating the underlying data structure, you must call 39 this.arrayContentWillChange(). After the mutation is complete, you must 40 call arrayContentDidChange(). 41 42 NOTE: JavaScript arrays already implement SC.Array and automatically call 43 the correct callbacks. 44 45 @param {Number} idx 46 Starting index in the array to replace. If idx >= length, then append to 47 the end of the array. 48 49 @param {Number} amt 50 Number of elements that should be removed from the array, starting at 51 *idx*. 52 53 @param {Array} objects 54 An array of zero or more objects that should be inserted into the array at 55 *idx* 56 */ 57 replace: function (idx, amt, objects) { 58 throw new Error("replace() must be implemented to support SC.Array"); 59 }, 60 61 /** 62 Returns the index for a particular object in the index. 63 64 @param {Object} object the item to search for 65 @param {Number} startAt optional starting location to search, default 0 66 @returns {Number} index of -1 if not found 67 */ 68 indexOf: function (object, startAt) { 69 var idx, len = this.get('length'); 70 71 if (startAt === undefined) startAt = 0; 72 else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); 73 if (startAt < 0) startAt += len; 74 75 for (idx = startAt; idx < len; idx++) { 76 if (this.objectAt(idx, YES) === object) return idx; 77 } 78 return -1; 79 }, 80 81 /** 82 Returns the last index for a particular object in the index. 83 84 @param {Object} object the item to search for 85 @param {Number} startAt optional starting location to search, default 0 86 @returns {Number} index of -1 if not found 87 */ 88 lastIndexOf: function (object, startAt) { 89 var idx, len = this.get('length'); 90 91 if (startAt === undefined) startAt = len - 1; 92 else startAt = (startAt < 0) ? Math.ceil(startAt) : Math.floor(startAt); 93 if (startAt < 0) startAt += len; 94 95 for (idx = startAt; idx >= 0; idx--) { 96 if (this.objectAt(idx) === object) return idx; 97 } 98 return -1; 99 }, 100 101 /** 102 This is one of the primitives you must implement to support SC.Array. 103 Returns the object at the named index. If your object supports retrieving 104 the value of an array item using get() (i.e. myArray.get(0)), then you do 105 not need to implement this method yourself. 106 107 @param {Number} idx 108 The index of the item to return. If idx exceeds the current length, 109 return null. 110 */ 111 objectAt: function (idx) { 112 if (idx < 0) return undefined; 113 if (idx >= this.get('length')) return undefined; 114 return this.get(idx); 115 }, 116 117 /** 118 @field [] 119 120 This is the handler for the special array content property. If you get 121 this property, it will return this. If you set this property it a new 122 array, it will replace the current content. 123 124 This property overrides the default property defined in SC.Enumerable. 125 */ 126 '[]': function (key, value) { 127 if (value !== undefined) { 128 this.replace(0, this.get('length'), value); 129 } 130 return this; 131 }.property(), 132 133 /** 134 This will use the primitive replace() method to insert an object at the 135 specified index. 136 137 @param {Number} idx index of insert the object at. 138 @param {Object} object object to insert 139 */ 140 insertAt: function (idx, object) { 141 if (idx > this.get('length')) throw new Error(SC.OUT_OF_RANGE_EXCEPTION); 142 this.replace(idx, 0, [object]); 143 return this; 144 }, 145 146 /** 147 Remove an object at the specified index using the replace() primitive 148 method. You can pass either a single index, a start and a length or an 149 index set. 150 151 If you pass a single index or a start and length that is beyond the 152 length this method will throw an SC.OUT_OF_RANGE_EXCEPTION 153 154 @param {Number|SC.IndexSet} start index, start of range, or index set 155 @param {Number} length length of passing range 156 @returns {Object} receiver 157 */ 158 removeAt: function (start, length) { 159 var delta = 0, // used to shift range 160 empty = []; 161 162 if (typeof start === SC.T_NUMBER) { 163 164 if ((start < 0) || (start >= this.get('length'))) { 165 throw new Error(SC.OUT_OF_RANGE_EXCEPTION); 166 } 167 168 // fast case 169 if (length === undefined) { 170 this.replace(start, 1, empty); 171 return this; 172 } else { 173 start = SC.IndexSet.create(start, length); 174 } 175 } 176 177 this.beginPropertyChanges(); 178 start.forEachRange(function (start, length) { 179 start -= delta; 180 delta += length; 181 this.replace(start, length, empty); // remove! 182 }, this); 183 this.endPropertyChanges(); 184 185 return this; 186 }, 187 188 /** 189 Search the array of this object, removing any occurrences of it. 190 @param {object} obj object to remove 191 */ 192 removeObject: function (obj) { 193 var loc = this.get('length') || 0; 194 while (--loc >= 0) { 195 var curObject = this.objectAt(loc); 196 if (curObject === obj) this.removeAt(loc); 197 } 198 return this; 199 }, 200 201 /** 202 Search the array for the passed set of objects and remove any occurrences 203 of the. 204 205 @param {SC.Enumerable} objects the objects to remove 206 @returns {SC.Array} receiver 207 */ 208 removeObjects: function (objects) { 209 this.beginPropertyChanges(); 210 objects.forEach(function (obj) { this.removeObject(obj); }, this); 211 this.endPropertyChanges(); 212 return this; 213 }, 214 215 /** 216 Returns a new array that is a slice of the receiver. This implementation 217 uses the observable array methods to retrieve the objects for the new 218 slice. 219 220 If you don't pass in beginIndex and endIndex, it will act as a copy of the 221 array. 222 223 @param beginIndex {Integer} (Optional) index to begin slicing from. 224 @param endIndex {Integer} (Optional) index to end the slice at. 225 @returns {Array} New array with specified slice 226 */ 227 slice: function (beginIndex, endIndex) { 228 var ret = []; 229 var length = this.get('length'); 230 if (SC.none(beginIndex)) beginIndex = 0; 231 if (SC.none(endIndex) || (endIndex > length)) endIndex = length; 232 while (beginIndex < endIndex) ret[ret.length] = this.objectAt(beginIndex++); 233 return ret; 234 }, 235 236 /** 237 Push the object onto the end of the array. Works just like push() but it 238 is KVO-compliant. 239 240 @param {Object} object the objects to push 241 242 @return {Object} The passed object 243 */ 244 pushObject: function (obj) { 245 this.insertAt(this.get('length'), obj); 246 return obj; 247 }, 248 249 250 /** 251 Add the objects in the passed numerable to the end of the array. Defers 252 notifying observers of the change until all objects are added. 253 254 @param {SC.Enumerable} objects the objects to add 255 @returns {SC.Array} receiver 256 */ 257 pushObjects: function (objects) { 258 this.beginPropertyChanges(); 259 objects.forEach(function (obj) { this.pushObject(obj); }, this); 260 this.endPropertyChanges(); 261 return this; 262 }, 263 264 /** 265 Pop object from array or nil if none are left. Works just like pop() but 266 it is KVO-compliant. 267 268 @return {Object} The popped object 269 */ 270 popObject: function () { 271 var len = this.get('length'); 272 if (len === 0) return null; 273 274 var ret = this.objectAt(len - 1); 275 this.removeAt(len - 1); 276 return ret; 277 }, 278 279 /** 280 Shift an object from start of array or nil if none are left. Works just 281 like shift() but it is KVO-compliant. 282 283 @return {Object} The shifted object 284 */ 285 shiftObject: function () { 286 if (this.get('length') === 0) return null; 287 var ret = this.objectAt(0); 288 this.removeAt(0); 289 return ret; 290 }, 291 292 /** 293 Unshift an object to start of array. Works just like unshift() but it is 294 KVO-compliant. 295 296 @param {Object} obj the object to add 297 @return {Object} The passed object 298 */ 299 unshiftObject: function (obj) { 300 this.insertAt(0, obj); 301 return obj; 302 }, 303 304 /** 305 Adds the named objects to the beginning of the array. Defers notifying 306 observers until all objects have been added. 307 308 @param {SC.Enumerable} objects the objects to add 309 @returns {SC.Array} receiver 310 */ 311 unshiftObjects: function (objects) { 312 this.beginPropertyChanges(); 313 objects.forEach(function (obj) { this.unshiftObject(obj); }, this); 314 this.endPropertyChanges(); 315 return this; 316 }, 317 318 /** 319 Compares each item in the passed array to this one. 320 321 @param {Array} ary The array you want to compare to 322 @returns {Boolean} true if they are equal. 323 */ 324 isEqual: function (ary) { 325 if (!ary) return false; 326 if (ary == this) return true; 327 328 var loc = ary.get('length'); 329 if (loc != this.get('length')) return false; 330 331 while (--loc >= 0) { 332 if (!SC.isEqual(ary.objectAt(loc), this.objectAt(loc))) return false; 333 } 334 return true; 335 }, 336 337 /** 338 Generates a new array with the contents of the old array, sans any null 339 values. 340 341 @returns {Array} The new, compact array 342 */ 343 compact: function () { return this.without(null); }, 344 345 /** 346 Generates a new array with the contents of the old array, sans the passed 347 value. 348 349 @param {Object} value The value you want to be removed 350 @returns {Array} The new, filtered array 351 */ 352 without: function (value) { 353 if (this.indexOf(value) < 0) return this; // value not present. 354 var ret = []; 355 this.forEach(function (k) { 356 if (k !== value) ret[ret.length] = k; 357 }); 358 return ret; 359 }, 360 361 /** 362 Generates a new array with only unique values from the contents of the 363 old array. 364 365 @returns {Array} The new, de-duped array 366 */ 367 uniq: function () { 368 var ret = []; 369 this.forEach(function (k) { 370 if (ret.indexOf(k) < 0) ret[ret.length] = k; 371 }); 372 return ret; 373 }, 374 375 /** 376 Returns a new array that is a one-dimensional flattening of this array, 377 i.e. for every element of this array extract that and it's elements into 378 a new array. 379 380 @returns {Array} 381 */ 382 flatten: function () { 383 var ret = []; 384 this.forEach(function (k) { 385 if (k && k.isEnumerable) { 386 ret = ret.pushObjects(k.flatten()); 387 } else { 388 ret.pushObject(k); 389 } 390 }); 391 return ret; 392 }, 393 394 /** 395 Returns the largest Number in an array of Numbers. Make sure the array 396 only contains values of type Number to get expected result. 397 398 Note: This only works for dense arrays. 399 400 @returns {Number} 401 */ 402 max: function () { 403 return Math.max.apply(Math, this); 404 }, 405 406 /** 407 Returns the smallest Number in an array of Numbers. Make sure the array 408 only contains values of type Number to get expected result. 409 410 Note: This only works for dense arrays. 411 412 @returns {Number} 413 */ 414 min: function () { 415 return Math.min.apply(Math, this); 416 }, 417 418 rangeObserverClass: SC.RangeObserver, 419 420 /** 421 Returns YES if object is in the array 422 423 @param {Object} object to look for 424 @returns {Boolean} 425 */ 426 contains: function (obj) { 427 return this.indexOf(obj) >= 0; 428 }, 429 430 /** 431 Creates a new range observer on the receiver. The target/method callback 432 you provide will be invoked anytime any property on the objects in the 433 specified range changes. It will also be invoked if the objects in the 434 range itself changes also. 435 436 The callback for a range observer should have the signature: 437 438 function rangePropertyDidChange(array, objects, key, indexes, context) 439 440 If the passed key is '[]' it means that the object itself changed. 441 442 The return value from this method is an opaque reference to the 443 range observer object. You can use this reference to destroy the 444 range observer when you are done with it or to update its range. 445 446 @param {SC.IndexSet} indexes indexes to observe 447 @param {Object} target object to invoke on change 448 @param {String|Function} method the method to invoke 449 @param {Object} context optional context 450 @returns {SC.RangeObserver} range observer 451 */ 452 addRangeObserver: function (indexes, target, method, context) { 453 var rangeob = this._array_rangeObservers; 454 if (!rangeob) rangeob = this._array_rangeObservers = SC.CoreSet.create(); 455 456 // The first time a range observer is added, cache the current length so 457 // we can properly notify observers the first time through 458 if (this._array_oldLength === undefined) { 459 this._array_oldLength = this.get('length'); 460 } 461 462 var C = this.rangeObserverClass; 463 var isDeep = NO; //disable this feature for now 464 var ret = C.create(this, indexes, target, method, context, isDeep); 465 rangeob.add(ret); 466 467 // first time a range observer is added, begin observing the [] property 468 if (!this._array_isNotifyingRangeObservers) { 469 this._array_isNotifyingRangeObservers = YES; 470 this.addObserver('[]', this, this._array_notifyRangeObservers); 471 } 472 473 return ret; 474 }, 475 476 /** 477 Moves a range observer so that it observes a new range of objects on the 478 array. You must have an existing range observer object from a call to 479 addRangeObserver(). 480 481 The return value should replace the old range observer object that you 482 pass in. 483 484 @param {SC.RangeObserver} rangeObserver the range observer 485 @param {SC.IndexSet} indexes new indexes to observe 486 @returns {SC.RangeObserver} the range observer (or a new one) 487 */ 488 updateRangeObserver: function (rangeObserver, indexes) { 489 return rangeObserver.update(this, indexes); 490 }, 491 492 /** 493 Removes a range observer from the receiver. The range observer must 494 already be active on the array. 495 496 The return value should replace the old range observer object. It will 497 usually be null. 498 499 @param {SC.RangeObserver} rangeObserver the range observer 500 @returns {SC.RangeObserver} updated range observer or null 501 */ 502 removeRangeObserver: function (rangeObserver) { 503 var ret = rangeObserver.destroy(this); 504 var rangeob = this._array_rangeObservers; 505 if (rangeob) rangeob.remove(rangeObserver); // clear 506 return ret; 507 }, 508 509 addArrayObservers: function (options) { 510 this._modifyObserverSet('add', options); 511 }, 512 513 removeArrayObservers: function (options) { 514 this._modifyObserverSet('remove', options); 515 }, 516 517 _modifyObserverSet: function (method, options) { 518 var willChangeObservers, didChangeObservers; 519 520 var target = options.target || this; 521 var willChange = options.willChange || 'arrayWillChange'; 522 var didChange = options.didChange || 'arrayDidChange'; 523 var context = options.context; 524 525 if (typeof willChange === "string") { 526 willChange = target[willChange]; 527 } 528 529 if (typeof didChange === "string") { 530 didChange = target[didChange]; 531 } 532 533 willChangeObservers = this._kvo_for('_kvo_array_will_change', SC.ObserverSet); 534 didChangeObservers = this._kvo_for('_kvo_array_did_change', SC.ObserverSet); 535 536 willChangeObservers[method](target, willChange, context); 537 didChangeObservers[method](target, didChange, context); 538 }, 539 540 arrayContentWillChange: function (start, removedCount, addedCount) { 541 this._teardownContentObservers(start, removedCount); 542 543 var member, members, membersLen, idx; 544 var target, action; 545 var willChangeObservers = this._kvo_array_will_change; 546 if (willChangeObservers) { 547 members = willChangeObservers.members; 548 membersLen = members.length; 549 550 for (idx = 0; idx < membersLen; idx++) { 551 member = members[idx]; 552 target = member[0]; 553 action = member[1]; 554 action.call(target, start, removedCount, addedCount, this); 555 } 556 } 557 }, 558 559 arrayContentDidChange: function (start, removedCount, addedCount) { 560 var rangeob = this._array_rangeObservers, 561 length, changes; 562 563 this.beginPropertyChanges(); 564 this.notifyPropertyChange('length'); // flush caches 565 566 // schedule info for range observers 567 if (rangeob && rangeob.length > 0) { 568 changes = this._array_rangeChanges; 569 if (!changes) { changes = this._array_rangeChanges = SC.IndexSet.create(); } 570 if (removedCount === addedCount) { 571 length = removedCount; 572 } else { 573 length = this.get('length') - start; 574 575 if (removedCount > addedCount) { 576 length += (removedCount - addedCount); 577 } 578 } 579 changes.add(start, length); 580 } 581 582 this._setupContentObservers(start, addedCount); 583 584 var member, members, membersLen, idx; 585 var target, action; 586 var didChangeObservers = this._kvo_array_did_change; 587 if (didChangeObservers) { 588 // If arrayContentDidChange is called with no parameters, assume the 589 // entire array has changed. 590 if (start === undefined) { 591 start = 0; 592 removedCount = this.get('length'); 593 addedCount = 0; 594 } 595 596 members = didChangeObservers.members; 597 membersLen = members.length; 598 599 for (idx = 0; idx < membersLen; idx++) { 600 member = members[idx]; 601 target = member[0]; 602 action = member[1]; 603 action.call(target, start, removedCount, addedCount, this); 604 } 605 } 606 607 this.enumerableContentDidChange(start, addedCount, addedCount - removedCount); 608 this.endPropertyChanges(); 609 610 return this; 611 }, 612 613 /** 614 @private 615 616 When enumerable content has changed, remove enumerable observers from 617 items that are no longer in the enumerable, and add observers to newly 618 added items. 619 620 @param {Array} addedObjects the array of objects that have been added 621 @param {Array} removedObjects the array of objects that have been removed 622 */ 623 _setupContentObservers: function (start, addedCount) { 624 var observedKeys = this._kvo_for('_kvo_content_observed_keys', SC.CoreSet); 625 var addedObjects; 626 var kvoKey; 627 628 // Only setup and teardown enumerable observers if we have keys to observe 629 if (observedKeys.get('length') > 0) { 630 addedObjects = this.slice(start, start + addedCount); 631 632 var self = this; 633 // added and resume the chain observer. 634 observedKeys.forEach(function (key) { 635 kvoKey = SC.keyFor('_kvo_content_observers', key); 636 637 // Get all original ChainObservers associated with the key 638 self._kvo_for(kvoKey).forEach(function (observer) { 639 addedObjects.forEach(function (item) { 640 self._resumeChainObservingForItemWithChainObserver(item, observer); 641 }); 642 }); 643 }); 644 } 645 }, 646 647 _teardownContentObservers: function (start, removedCount) { 648 var observedKeys = this._kvo_for('_kvo_content_observed_keys', SC.CoreSet); 649 var removedObjects; 650 var kvoKey; 651 652 // Only setup and teardown enumerable observers if we have keys to observe 653 if (observedKeys.get('length') > 0) { 654 removedObjects = this.slice(start, start + removedCount); 655 656 // added and resume the chain observer. 657 observedKeys.forEach(function (key) { 658 kvoKey = SC.keyFor('_kvo_content_observers', key); 659 660 // Loop through removed objects and remove any enumerable observers that 661 // belong to them. 662 removedObjects.forEach(function (item) { 663 item._kvo_for(kvoKey).forEach(function (observer) { 664 observer.destroyChain(); 665 }); 666 }); 667 }); 668 } 669 }, 670 671 teardownEnumerablePropertyChains: function (removedObjects) { 672 var chains = this._kvo_enumerable_property_chains; 673 674 if (chains) { 675 chains.forEach(function (chain) { 676 var idx, len = removedObjects.get('length'), 677 chainGuid = SC.guidFor(chain), 678 clonedChain, item, kvoChainList = '_kvo_enumerable_property_clones'; 679 680 chain.notifyPropertyDidChange(); 681 682 for (idx = 0; idx < len; idx++) { 683 item = removedObjects.objectAt(idx); 684 clonedChain = item[kvoChainList][chainGuid]; 685 clonedChain.deactivate(); 686 delete item[kvoChainList][chainGuid]; 687 } 688 }, this); 689 } 690 return this; 691 }, 692 693 /** 694 For all registered property chains on this object, removed them from objects 695 being removed from the enumerable, and clone them onto newly added objects. 696 697 @param {Object[]} addedObjects the objects being added to the enumerable 698 @param {Object[]} removedObjects the objected being removed from the enumerable 699 @returns {Object} receiver 700 */ 701 setupEnumerablePropertyChains: function (addedObjects) { 702 var chains = this._kvo_enumerable_property_chains; 703 704 if (chains) { 705 chains.forEach(function (chain) { 706 var idx, len = addedObjects.get('length'); 707 708 chain.notifyPropertyDidChange(); 709 710 len = addedObjects.get('length'); 711 for (idx = 0; idx < len; idx++) { 712 this._clonePropertyChainToItem(chain, addedObjects.objectAt(idx)); 713 } 714 }, this); 715 } 716 return this; 717 }, 718 719 /** 720 Register a property chain to propagate to enumerable content. 721 722 This will clone the property chain to each item in the enumerable, 723 then save it so that it is automatically set up and torn down when 724 the enumerable content changes. 725 726 @param {String} property the property being listened for on this object 727 @param {SC._PropertyChain} chain the chain to clone to items 728 */ 729 registerDependentKeyWithChain: function (property, chain) { 730 // Get the set of all existing property chains that should 731 // be propagated to enumerable contents. If that set doesn't 732 // exist yet, _kvo_for() will create it. 733 var kvoChainList = '_kvo_enumerable_property_chains', 734 chains; 735 736 chains = this._kvo_for(kvoChainList, SC.CoreSet); 737 738 // Save a reference to the chain on this object. If new objects 739 // are added to the enumerable, we will clone this chain and add 740 // it to the new object. 741 chains.add(chain); 742 743 this.forEach(function (item) { 744 this._clonePropertyChainToItem(chain, item); 745 }, this); 746 }, 747 748 /** 749 Clones an SC._PropertyChain to a content item. 750 751 @param {SC._PropertyChain} chain 752 @param {Object} item 753 */ 754 _clonePropertyChainToItem: function (chain, item) { 755 var clone = SC.clone(chain), 756 kvoCloneList = '_kvo_enumerable_property_clones', 757 cloneList; 758 759 clone.object = item; 760 761 cloneList = item[kvoCloneList] = item[kvoCloneList] || {}; 762 cloneList[SC.guidFor(chain)] = clone; 763 764 clone.activate(item); 765 }, 766 767 /** 768 Removes a dependent key from the enumerable, and tears it down on 769 all content objects. 770 771 @param {String} property 772 @param {SC._PropertyChain} chain 773 */ 774 removeDependentKeyWithChain: function (property, chain) { 775 var kvoCloneList = '_kvo_enumerable_property_clones', 776 clone, cloneList; 777 778 this.forEach(function (item) { 779 item.removeDependentKeyWithChain(property, chain); 780 781 cloneList = item[kvoCloneList]; 782 clone = cloneList[SC.guidFor(chain)]; 783 784 clone.deactivate(item); 785 }, this); 786 }, 787 788 /** 789 @private 790 791 Clones a segment of an observer chain and applies it 792 to an element of this Enumerable. 793 794 @param {Object} item The element 795 @param {SC._ChainObserver} chainObserver the chain segment to begin from 796 */ 797 _resumeChainObservingForItemWithChainObserver: function (item, chainObserver) { 798 var observer = SC.clone(chainObserver.next); 799 var key = observer.property; 800 801 // The chain observer should create new observers on the child object 802 observer.object = item; 803 item.addObserver(key, observer, observer.propertyDidChange); 804 805 // if we're in the initial chained observer setup phase, add the tail 806 // of the current observer segment to the list of tracked tails. 807 if (chainObserver.root.tails) { 808 chainObserver.root.tails.pushObject(observer.tail()); 809 } 810 811 812 // Maintain a list of observers on the item so we can remove them 813 // if it is removed from the enumerable. 814 item._kvo_for(SC.keyFor('_kvo_content_observers', key)).push(observer); 815 }, 816 817 /** 818 @private 819 820 Adds a content observer. Content observers are able to 821 propagate chain observers to each member item in the enumerable, 822 so that the observer is fired whenever a single item changes. 823 824 You should never call this method directly. Instead, you should 825 call addObserver() with the special '@each' property in the path. 826 827 For example, if you wanted to observe changes to each item's isDone 828 property, you could call: 829 830 arrayController.addObserver('@each.isDone'); 831 832 @param {SC._ChainObserver} chainObserver the chain observer to propagate 833 */ 834 _addContentObserver: function (chainObserver) { 835 var key = chainObserver.next.property; 836 837 // Add the key to a set so we know what we are observing 838 this._kvo_for('_kvo_content_observed_keys', SC.CoreSet).push(key); 839 840 // Add the passed ChainObserver to an ObserverSet for that key 841 var kvoKey = SC.keyFor('_kvo_content_observers', key); 842 this._kvo_for(kvoKey).push(chainObserver); 843 844 // Add an observer on the '[]' property of this array. 845 var observer = chainObserver.tail(); 846 this.addObserver('[]', observer, observer.propertyDidChange); 847 848 // Set up chained observers on the initial content 849 this._setupContentObservers(0, chainObserver.object.get('length')); 850 }, 851 852 /** 853 @private 854 855 Removes a content observer. Pass the same chain observer 856 that was used to add the content observer. 857 858 @param {SC._ChainObserver} chainObserver the chain observer to propagate 859 */ 860 861 _removeContentObserver: function (chainObserver) { 862 var observers, kvoKey; 863 var observedKeys = this._kvo_content_observed_keys; 864 var key = chainObserver.next.property; 865 866 // Clean up the observer on the '[]' property of this array. 867 var observer = chainObserver.tail(); 868 this.removeObserver('[]', observer, observer.propertyDidChange); 869 870 if (observedKeys.contains(key)) { 871 872 kvoKey = SC.keyFor('_kvo_content_observers', key); 873 observers = this._kvo_for(kvoKey); 874 875 observers.removeObject(chainObserver); 876 877 this._teardownContentObservers(0, chainObserver.object.get('length')); 878 879 if (observers.length === 0) { 880 this._kvo_for('_kvo_content_observed_keys').remove(key); 881 } 882 } 883 }, 884 885 /** @private 886 Observer fires whenever the '[]' property changes. If there are 887 range observers, will notify observers of change. 888 */ 889 _array_notifyRangeObservers: function () { 890 var rangeob = this._array_rangeObservers, 891 changes = this._array_rangeChanges, 892 len = rangeob ? rangeob.length : 0, 893 idx; 894 895 if (len > 0 && changes && changes.length > 0) { 896 for (idx = 0; idx < len; idx++) rangeob[idx].rangeDidChange(changes); 897 changes.clear(); // reset for later notifications 898 } 899 } 900 901 }; 902 903 /** 904 @namespace 905 906 This module implements Observer-friendly Array-like behavior. This mixin is 907 picked up by the Array class as well as other controllers, etc. that want to 908 appear to be arrays. 909 910 Unlike SC.Enumerable, this mixin defines methods specifically for 911 collections that provide index-ordered access to their contents. When you 912 are designing code that needs to accept any kind of Array-like object, you 913 should use these methods instead of Array primitives because these will 914 properly notify observers of changes to the array. 915 916 Although these methods are efficient, they do add a layer of indirection to 917 your application so it is a good idea to use them only when you need the 918 flexibility of using both true JavaScript arrays and "virtual" arrays such 919 as controllers and collections. 920 921 You can use the methods defined in this module to access and modify array 922 contents in a KVO-friendly way. You can also be notified whenever the 923 membership if an array changes by changing the syntax of the property to 924 .observes('*myProperty.[]') . 925 926 To support SC.Array in your own class, you must override two 927 primitives to use it: replace() and objectAt(). 928 929 Note that the SC.Array mixin also incorporates the SC.Enumerable mixin. All 930 SC.Array-like objects are also enumerable. 931 932 @extends SC.Enumerable 933 @since SproutCore 0.9.0 934 */ 935 SC.Array = SC.mixin({}, SC.Enumerable, SC.CoreArray); 936