1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2010 Evin Grano
  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 `ChildArray` is used to map an array of `ChildRecord` objects.
 12 
 13   @extends SC.Enumerable
 14   @extends SC.Array
 15   @since SproutCore 1.0
 16 */
 17 
 18 SC.ChildArray = SC.Object.extend(SC.Enumerable, SC.Array,
 19   /** @scope SC.ChildArray.prototype */ {
 20 
 21   //@if(debug)
 22   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
 23 
 24   /* @private */
 25   toString: function () {
 26     var propertyName = this.get('propertyName'),
 27       length = this.get('length');
 28 
 29     return "%@({  propertyName: '%@',  length: %@,  … })".fmt(sc_super(), propertyName, length);
 30   },
 31 
 32   /* END DEBUG ONLY PROPERTIES AND METHODS */
 33   //@endif
 34 
 35   /**
 36     If set, it is the default record `recordType`
 37 
 38     @default null
 39     @type String
 40   */
 41   defaultRecordType: null,
 42 
 43   /**
 44     If set, the parent record will be notified whenever the array changes so that
 45     it can change its own state
 46 
 47     @default null
 48     @type {SC.Record}
 49   */
 50   record: null,
 51 
 52   /**
 53     The name of the attribute in the parent record's datahash that represents
 54     this child array's data.
 55 
 56     @default null
 57     @type String
 58   */
 59   propertyName: null,
 60 
 61   /**
 62     The store that owns this child array's parent record.
 63 
 64     @type SC.Store
 65     @readonly
 66   */
 67   store: function() {
 68     return this.getPath('record.store');
 69   }.property('record').cacheable(),
 70 
 71   /**
 72     The storeKey for the parent record of this child array.
 73 
 74     @type Number
 75     @readonly
 76   */
 77   storeKey: function() {
 78     return this.getPath('record.storeKey');
 79   }.property('record').cacheable(),
 80 
 81   /**
 82     Returns the original child array of JavaScript Objects.
 83 
 84     Note: Avoid modifying this array directly, because changes will not be
 85     reflected by this SC.ChildArray.
 86 
 87     @type SC.Array
 88     @property
 89   */
 90   readOnlyChildren: function() {
 91     return this.get('record').readAttribute(this.get('propertyName'));
 92   }.property(),
 93 
 94   /**
 95     Returns an editable child array of JavaScript Objects.
 96 
 97     Any changes to this array will not affect the parent record's datahash.
 98 
 99     @type {SC.Array}
100     @property
101   */
102   editableChildren: function() {
103     var store    = this.get('store'),
104         storeKey = this.get('storeKey'),
105         pname    = this.get('propertyName'),
106         ret, hash;
107 
108     ret = store.readEditableProperty(storeKey, pname);
109     if (!ret) {
110       hash = store.readEditableDataHash(storeKey);
111       ret = hash[pname] = [];
112     }
113 
114     return ret ;
115   }.property(),
116 
117   // ..........................................................
118   // ARRAY PRIMITIVES
119   //
120 
121   /** @private
122     Returned length is a pass-through to the storeIds array.
123 
124     @type Number
125     @property
126   */
127   length: function() {
128     var children = this.get('readOnlyChildren');
129     return children ? children.length : 0;
130   }.property('readOnlyChildren'),
131 
132   /**
133     Looks up the store id in the store ids array and materializes a
134     records.
135 
136     @param {Number} idx index of the object to retrieve.
137     @returns {SC.Record} The nested record if found or undefined if not.
138   */
139   objectAt: function(idx) {
140     var recs      = this._records,
141         children = this.get('readOnlyChildren'),
142       hash, ret,
143       pname = this.get('propertyName'),
144       parent = this.get('record'),
145       len = children ? children.length : 0;
146 
147     if (!children) return undefined; // nothing to do
148     if (recs && (ret=recs[idx])) return ret ; // cached
149 
150     // If not a good index return undefined
151     if (idx >= len) return undefined;
152     hash = children.objectAt(idx);
153     if (!hash) return undefined;
154 
155     // not in cache, materialize
156     if (!recs) this._records = recs = []; // create cache
157     recs[idx] = ret = parent.registerNestedRecord(hash, pname, pname+'.'+idx);
158 
159     return ret;
160   },
161 
162   /**
163     Pass through to the underlying array.  The passed in objects should be
164     nested SC.Records, which can be converted to JavaScript objects or
165     JavaScript objects themselves.
166 
167     @param {Number} idx index of the object to replace.
168     @param {Number} amt number of objects to replace starting at idx.
169     @param {Number} recs array with records to replace. These may be JavaScript objects or nested SC.Record objects.
170     @returns {SC.ChildArray}
171   */
172   replace: function(idx, amt, recs) {
173     var children = this.get('editableChildren'),
174       recsLen = recs ? (recs.get ? recs.get('length') : recs.length) : 0,
175       parent = this.get('record'),
176       pname    = this.get('propertyName'),
177       store = this.get('store'),
178       removeCount, addCount,
179       defaultRecordType, storeKeysById,
180       newObjects, rec,
181       i, len;
182 
183     // Create the proxy cache, we will need it.
184     if (!this._records) this._records = [];
185 
186     // Convert any SC.Record objects into JavaScript objects.
187     newObjects = this._processRecordsToHashes(recs);
188 
189     // Unregister the records being replaced.
190     // for (i = idx, len = children.length; i < len; ++i) {
191     //  this.unregisterNestedRecord(i);
192     // }
193 
194     // Ensure that all removed objects are pre-registered in case any instances are outstanding.
195     // These objects will improperly reflect being registered to this parent, but
196     // at least they won't conflict with the actual associated records once we
197     // disassociate them from the record type.
198     defaultRecordType = this.get('defaultRecordType');
199     storeKeysById = defaultRecordType.storeKeysById();
200 
201     for (i = idx, len = idx + amt; i < len; i++) {
202       rec = this._records[i];
203 
204       if (!rec) {
205         rec = parent.registerNestedRecord(children[i], pname, pname + '.' + i);
206       } else {
207         // Remove the cached record.
208         this._records[i] = null;
209       }
210 
211       // Now throw away the connection, so that the parent won't retrieve this
212       // same instance. This is a work-around due to the fact that nested records
213       // are proxied through their parent records.
214       storeKeysById[rec.get('id')] = null;
215     }
216 
217     // All materialized nested records after idx + amt to end need to be removed
218     // because the paths will no longer be valid.
219     for (i = idx + amt, len = this._records.length; i < len; i++) {
220       rec = this._records[i];
221 
222       if (rec) {
223         store.unregisterChildFromParent(rec.get('storeKey'));
224 
225         this._records[i] = null;
226       }
227     }
228 
229     // All objects from idx to the end must be removed to do an insert.
230     removeCount = children.length - idx;
231     addCount = children.length - idx - amt + recsLen;
232 
233     this.arrayContentWillChange(idx, removeCount, addCount);
234 
235     // Perform a raw array replace without any KVO checks.
236     if (newObjects.length === 0) {
237       children.splice(idx, amt);
238     } else {
239       var args = [idx, amt].concat(newObjects);
240       children.splice.apply(children, args);
241     }
242 
243     // All current SC.Record instances must be updated to their new backing object.
244     // For example, when passing an SC.Record object in, that instance should
245     // update to reflect its new nested object path.
246     for (i = idx, len = children.length; i < len; i++) {
247       this._records[i] = parent.registerNestedRecord(children[i], pname, pname + '.' + i);
248     }
249 
250     // Update the enumerable, [], property (including firstObject and lastObject)
251     this.arrayContentDidChange(idx, removeCount, addCount);
252 
253     // Update our cache! So when the record property change comes back down we can ignore it.
254     this._sc_prevChildren = children;
255 
256     // We must indicate to the parent that we have been modified, so they can
257     // update their status.
258     parent.recordDidChange(pname);
259 
260     return this;
261   },
262 
263   /**
264     Unregisters a child record from its parent record.
265 
266     Since accessing a child (nested) record creates a new data hash for the
267     child and caches the child record and its relationship to the parent record,
268     it's important to clear those caches when the child record is overwritten
269     or removed.  This function tells the store to remove the child record from
270     the store's various child record caches.
271 
272     You should not need to call this function directly.  Simply setting the
273     child record property on the parent to a different value will cause the
274     previous child record to be unregistered.
275 
276     @param {Number} idx The index of the child record.
277   */
278   // unregisterNestedRecord: function(idx) {
279   //   var childArray, childRecord, csk, store,
280   //       record   = this.get('record'),
281   //       pname    = this.get('propertyName');
282   //
283   //   store = record.get('store');
284   //   childArray = record.getPath(pname);
285   //   childRecord = childArray.objectAt(idx);
286   //   csk = childRecord.get('storeKey');
287   //   store.unregisterChildFromParent(csk);
288   // },
289 
290   /**
291     Calls normalize on each object in the array
292   */
293   normalize: function(){
294     this.forEach(function (rec) {
295       if (rec.normalize) rec.normalize();
296     });
297   },
298 
299   // ..........................................................
300   // INTERNAL SUPPORT
301   //
302 
303   /** @private Converts any SC.Records in the array into an array of hashes.
304 
305     @param {SC.Array} recs records to be converted to hashes.
306     @returns {SC.Array} array of hashes.
307   */
308   _processRecordsToHashes: function (recs) {
309     var store, sk,
310       ret = [];
311 
312     recs.forEach(function (rec, idx) {
313       if (rec.isNestedRecord) {
314         store = rec.get('store');
315         sk = rec.storeKey;
316         ret[idx] = store.readDataHash(sk);
317       } else {
318         ret[idx] = rec;
319       }
320     });
321 
322     return ret;
323   },
324 
325   /** @private
326     This is called by the parent record whenever its properties change. It is
327     also called by the ChildrenAttribute transform when the attribute is set
328     to a new array.
329   */
330   recordPropertyDidChange: function (keys) {
331     var oldLength = this.get('length'),
332       children = this.get('readOnlyChildren'),
333       newLength = children ? children.length : 0,
334       // store = this.get('store'),
335       prevChildren = this._sc_prevChildren;
336 
337     // Fast Path! No actual change to our backing array attribute so we should
338     // not notify any changes.
339     if (prevChildren === children) { return; }
340 
341     // TODO: We can not use this, because removed instances will lose their
342     // connection to their data hashes in the store. There is an ugly hack in
343     // SC.Store#writeDataHash which can't handle this.
344     // All materialized nested records need to be removed. They are no longer valid!
345     // if (this._records) {
346     //   for (var i = 0, len = this._records.length; i < len; i++) {
347     //     var rec = this._records[i];
348 
349     //     // Unregister the nested record.
350     //     if (rec) {
351     //       store.unregisterChildFromParent(rec.get('storeKey'));
352     //     }
353     //   }
354 
355     //   // Throw away our cache.
356     //   this._records.length = 0;
357     // }
358 
359     // Throw away our cache.
360     this._records = null;
361 
362     // this.arrayContentWillChange(0, oldLength, newLength);
363     this.arrayContentDidChange(0, oldLength, newLength);
364 
365     // Cache our backing array so we can avoid updates when we haven't actually
366     // changed. See fast path above.
367     this._sc_prevChildren = children;
368   }
369 
370 }) ;
371