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