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 
  9 /** @class
 10 
 11   A RangeObserver is used by Arrays to automatically observe all of the
 12   objects in a particular range on the array.  Whenever any property on one
 13   of those objects changes, it will notify its delegate.  Likewise, whenever
 14   the contents of the array itself changes, it will notify its delegate and
 15   possibly update its own registration.
 16 
 17   This implementation uses only SC.Array methods.  It can be used on any
 18   object that complies with SC.Array.  You may, however, choose to subclass
 19   this object in a way that is more optimized for your particular design.
 20 
 21   @since SproutCore 1.0
 22 */
 23 SC.RangeObserver = /** @scope SC.RangeObserver.prototype */{
 24 
 25   /**
 26     Walk like a duck.
 27 
 28     @type Boolean
 29   */
 30   isRangeObserver: YES,
 31 
 32   /** @private */
 33   toString: function() {
 34     var base = this.indexes ? this.indexes.toString() : "SC.IndexSet<..>";
 35     return base.replace('IndexSet', 'RangeObserver(%@)'.fmt(SC.guidFor(this)));
 36   },
 37 
 38   /**
 39     Creates a new range observer owned by the source.  The indexSet you pass
 40     must identify the indexes you are interested in observing.  The passed
 41     target/method will be invoked whenever the observed range changes.
 42 
 43     Note that changes to a range are buffered until the end of a run loop
 44     unless a property on the record itself changes.
 45 
 46     @param {SC.Array} source the source array
 47     @param {SC.IndexSet} indexSet set of indexes to observer
 48     @param {Object} target the target
 49     @param {Function|String} method the method to invoke
 50     @param {Object} context optional context to include in callback
 51     @param {Boolean} isDeep if YES, observe property changes as well
 52     @returns {SC.RangeObserver} instance
 53   */
 54   create: function(source, indexSet, target, method, context, isDeep) {
 55     var ret = SC.beget(this);
 56     ret.source = source;
 57     ret.indexes = indexSet ? indexSet.frozenCopy() : null;
 58     ret.target = target;
 59     ret.method = (typeof method === 'string') ? target[method] : method;
 60     ret.context = context ;
 61     ret.isDeep  = isDeep || false ;
 62     ret.beginObserving();
 63     return ret ;
 64   },
 65 
 66   /**
 67     Create subclasses for the RangeObserver.  Pass one or more attribute
 68     hashes.  Use this to create customized RangeObservers if needed for your
 69     classes.
 70 
 71     @param {Hash} attrs one or more attribute hashes
 72     @returns {SC.RangeObserver} extended range observer class
 73   */
 74   extend: function(attrs) {
 75     var ret = SC.beget(this), args = arguments;
 76     for(var i=0, l=args.length; i<l; i++) { SC.mixin(ret, args[i]); }
 77     return ret ;
 78   },
 79 
 80   /**
 81     Destroys an active ranger observer, cleaning up first.
 82 
 83     @param {SC.Array} source the source array
 84     @returns {SC.RangeObserver} receiver
 85   */
 86   destroy: function(source) {
 87     this.endObserving();
 88     return this;
 89   },
 90 
 91   /**
 92     Updates the set of indexes the range observer applies to.  This will
 93     stop observing the old objects for changes and start observing the
 94     new objects instead.
 95 
 96     @param {SC.Array} source the source array
 97     @param {SC.IndexSet} indexSet The index set representing the change
 98     @returns {SC.RangeObserver} receiver
 99   */
100   update: function(source, indexSet) {
101     if (this.indexes && this.indexes.isEqual(indexSet)) { return this ; }
102 
103     this.indexes = indexSet ? indexSet.frozenCopy() : null ;
104     this.endObserving().beginObserving();
105     return this;
106   },
107 
108   /**
109     Configures observing for each item in the current range.  Should update
110     the observing array with the list of observed objects so they can be
111     torn down later
112 
113     @returns {SC.RangeObserver} receiver
114   */
115   beginObserving: function() {
116     if ( !this.isDeep ) { return this; } // nothing to do
117 
118     var observing = this.observing = this.observing || SC.CoreSet.create();
119 
120     // cache iterator function to keep things fast
121     var func = this._beginObservingForEach, source = this.source;
122 
123     if( !func ) {
124       func = this._beginObservingForEach = function(idx) {
125         var obj = source.objectAt(idx);
126         if (obj && obj.addObserver) {
127           observing.push(obj);
128           obj._kvo_needsRangeObserver = true ;
129         }
130       };
131     }
132 
133     this.indexes.forEach(func);
134 
135     // add to pending range observers queue so that if any of these objects
136     // change we will have a chance to setup observing on them.
137     this.isObserving = false ;
138     SC.Observers.addPendingRangeObserver(this);
139 
140     return this;
141   },
142 
143   /** @private
144     Called when an object that appears to need range observers has changed.
145     Check to see if the range observer contains this object in its list.  If
146     it does, go ahead and setup observers on all objects and remove ourself
147     from the queue.
148   */
149   setupPending: function(object) {
150     var observing = this.observing ;
151 
152     if ( this.isObserving || !observing || (observing.get('length')===0) ) {
153       return true ;
154     }
155 
156     if (observing.contains(object)) {
157       this.isObserving = true ;
158 
159       // cache iterator function to keep things fast
160       var func = this._setupPendingForEach;
161       if (!func) {
162         var source = this.source,
163             method = this.objectPropertyDidChange,
164             self   = this;
165 
166         func = this._setupPendingForEach = function(idx) {
167           var obj = source.objectAt(idx),
168               guid = SC.guidFor(obj),
169               key ;
170 
171           if (obj && obj.addObserver) {
172             observing.push(obj);
173             obj.addObserver('*', self, method);
174 
175             // also save idx of object on range observer itself.  If there is
176             // more than one idx, convert to IndexSet.
177             key = self[guid];
178             if ( key == null ) {
179               self[guid] = idx ;
180             } else if (key.isIndexSet) {
181               key.add(idx);
182             } else {
183               self[guid] = SC.IndexSet.create(key).add(idx);
184             }
185 
186           }
187         };
188       }
189       this.indexes.forEach(func);
190       return true ;
191     } else {
192       return false ;
193     }
194   },
195 
196   /**
197     Remove observers for any objects currently begin observed.  This is
198     called whenever the observed range changes due to an array change or
199     due to destroying the observer.
200 
201     @returns {SC.RangeObserver} receiver
202   */
203   endObserving: function() {
204     if ( !this.isDeep ) return this; // nothing to do
205 
206     var observing = this.observing;
207 
208     if (this.isObserving) {
209       var meth      = this.objectPropertyDidChange,
210           source    = this.source,
211           idx, lim, obj;
212 
213       if (observing) {
214         lim = observing.length;
215         for(idx=0;idx<lim;idx++) {
216           obj = observing[idx];
217           obj.removeObserver('*', this, meth);
218           this[SC.guidFor(obj)] = null;
219         }
220         observing.length = 0 ; // reset
221       }
222 
223       this.isObserving = false ;
224     }
225 
226     if (observing) { observing.clear(); } // empty set.
227     return this ;
228   },
229 
230   /**
231     Whenever the actual objects in the range changes, notify the delegate
232     then begin observing again.  Usually this method will be passed an
233     IndexSet with the changed indexes.  The range observer will only notify
234     its delegate if the changed indexes include some of all of the indexes
235     this range observer is monitoring.
236 
237     @param {SC.IndexSet} changes optional set of changed indexes
238     @returns {SC.RangeObserver} receiver
239   */
240   rangeDidChange: function(changes) {
241     var indexes = this.indexes;
242     if ( !changes || !indexes || indexes.intersects(changes) ) {
243       this.endObserving(); // remove old observers
244       this.method.call(this.target, this.source, null, '[]', changes, this.context);
245       this.beginObserving(); // setup new ones
246     }
247     return this ;
248   },
249 
250   /**
251     Whenever an object changes, notify the delegate
252 
253     @param {Object} the object that changed
254     @param {String} key the property that changed
255     @param {Null} value No longer used
256     @param {Number} rev The revision of the change
257     @returns {SC.RangeObserver} receiver
258   */
259   objectPropertyDidChange: function(object, key, value, rev) {
260     var context = this.context,
261         method  = this.method,
262         guid    = SC.guidFor(object),
263         index   = this[guid];
264 
265     // lazily convert index to IndexSet.
266     if ( index && !index.isIndexSet ) {
267       index = this[guid] = SC.IndexSet.create(index).freeze();
268     }
269 
270     method.call(this.target, this.source, object, key, index, context || rev, rev);
271   }
272 
273 };
274