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