UNPKG

blossom

Version:

Modern, Cross-Platform Application Framework

434 lines (345 loc) 12.3 kB
// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2011 Strobe Inc. and contributors. // Portions ©2008-2011 Apple Inc. All rights reserved. // License: Licensed under MIT license (see license.js) // ========================================================================== /** @class A `ManyArray` is used to map an array of record ids back to their record objects which will be materialized from the owner store on demand. Whenever you create a `toMany()` relationship, the value returned from the property will be an instance of `ManyArray`. You can generally customize the behavior of ManyArray by passing settings to the `toMany()` helper. @extends SC.Enumerable @extends SC.Array @since SproutCore 1.0 */ SC.ManyArray = SC.Object.extend(SC.Enumerable, SC.Array, /** @scope SC.ManyArray.prototype */ { /** `recordType` will tell what type to transform the record to when materializing the record. @default null @type String */ recordType: null, /** If set, the record will be notified whenever the array changes so that it can change its own state @default null @type SC.Record */ record: null, /** If set will be used by the many array to get an editable version of the storeIds from the owner. @default null @type String */ propertyName: null, /** The `ManyAttribute` that created this array. @default null @type SC.ManyAttribute */ manyAttribute: null, /** The store that owns this record array. All record arrays must have a store to function properly. @type SC.Store @property */ store: function() { return this.get('record').get('store'); }.property('record').cacheable(), /** The `storeKey` for the parent record of this many array. Editing this array will place the parent record into a `READY_DIRTY` state. @type Number @property */ storeKey: function() { return this.get('record').get('storeKey'); }.property('record').cacheable(), /** Returns the `storeId`s in read-only mode. Avoids modifying the record unnecessarily. @type SC.Array @property */ readOnlyStoreIds: function() { return this.get('record').readAttribute(this.get('propertyName')); }.property(), /** Returns an editable array of `storeId`s. Marks the owner records as modified. @type {SC.Array} @property */ editableStoreIds: function() { var store = this.get('store'), storeKey = this.get('storeKey'), pname = this.get('propertyName'), ret, hash; ret = store.readEditableProperty(storeKey, pname); if (!ret) { hash = store.readEditableDataHash(storeKey); ret = hash[pname] = []; } if (ret !== this._prevStoreIds) this.recordPropertyDidChange(); return ret ; }.property(), // .......................................................... // COMPUTED FROM OWNER // /** Computed from owner many attribute @type Boolean @property */ isEditable: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? attr.get('isEditable') : false; }.property('manyAttribute').cacheable(), /** Computed from owner many attribute @type String @property */ inverse: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? attr.get('inverse') : null; }.property('manyAttribute').cacheable(), /** Computed from owner many attribute @type Boolean @property */ isMaster: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? attr.get('isMaster') : null; }.property("manyAttribute").cacheable(), /** Computed from owner many attribute @type Array @property */ orderBy: function() { // NOTE: can't use get() b/c manyAttribute looks like a computed prop var attr = this.manyAttribute; return attr ? attr.get('orderBy') : null; }.property("manyAttribute").cacheable(), // .......................................................... // ARRAY PRIMITIVES // /** @private Returned length is a pass-through to the `storeIds` array. @type Number @property */ length: function() { var storeIds = this.get('readOnlyStoreIds'); return storeIds ? storeIds.get('length') : 0; }.property('readOnlyStoreIds'), /** @private Looks up the store id in the store ids array and materializes a records. */ objectAt: function(idx) { var recs = this._records, storeIds = this.get('readOnlyStoreIds'), store = this.get('store'), recordType = this.get('recordType'), storeKey, ret, storeId ; if (!storeIds || !store) return undefined; // nothing to do if (recs && (ret=recs[idx])) return ret ; // cached // not in cache, materialize if (!recs) this._records = recs = [] ; // create cache storeId = storeIds.objectAt(idx); if (storeId) { // if record is not loaded already, then ask the data source to // retrieve it storeKey = store.storeKeyFor(recordType, storeId); if (store.readStatus(storeKey) === SC.Record.EMPTY) { store.retrieveRecord(recordType, null, storeKey); } recs[idx] = ret = store.materializeRecord(storeKey); } return ret ; }, /** @private Pass through to the underlying array. The passed in objects must be records, which can be converted to `storeId`s. */ replace: function(idx, amt, recs) { if (!this.get('isEditable')) { throw "%@.%@[] is not editable".fmt(this.get('record'), this.get('propertyName')); } var storeIds = this.get('editableStoreIds'), len = recs ? (recs.get ? recs.get('length') : recs.length) : 0, record = this.get('record'), pname = this.get('propertyName'), i, keys, ids, toRemove, inverse, attr, inverseRecord; // map to store keys ids = [] ; for(i=0;i<len;i++) ids[i] = recs.objectAt(i).get('id'); // if we have an inverse - collect the list of records we are about to // remove inverse = this.get('inverse'); if (inverse && amt>0) { toRemove = SC.ManyArray._toRemove; if (toRemove) SC.ManyArray._toRemove = null; // reuse if possible else toRemove = []; for(i=0;i<amt;i++) toRemove[i] = this.objectAt(idx + i); } // pass along - if allowed, this should trigger the content observer storeIds.replace(idx, amt, ids); // ok, notify records that were removed then added; this way reordered // objects are added and removed if (inverse) { // notive removals for(i=0;i<amt;i++) { inverseRecord = toRemove[i]; attr = inverseRecord ? inverseRecord[inverse] : null; if (attr && attr.inverseDidRemoveRecord) { attr.inverseDidRemoveRecord(inverseRecord, inverse, record, pname); } } if (toRemove) { toRemove.length = 0; // cleanup if (!SC.ManyArray._toRemove) SC.ManyArray._toRemove = toRemove; } // notify additions for(i=0;i<len;i++) { inverseRecord = recs.objectAt(i); attr = inverseRecord ? inverseRecord[inverse] : null; if (attr && attr.inverseDidAddRecord) { attr.inverseDidAddRecord(inverseRecord, inverse, record, pname); } } } // only mark record dirty if there is no inverse or we are master if (record && (!inverse || this.get('isMaster'))) { record.recordDidChange(pname); } this.enumerableContentDidChange(idx, amt, len - amt); return this; }, // .......................................................... // INVERSE SUPPORT // /** Called by the `ManyAttribute` whenever a record is removed on the inverse of the relationship. @param {SC.Record} inverseRecord the record that was removed @returns {SC.ManyArray} receiver */ removeInverseRecord: function(inverseRecord) { if (!inverseRecord) return this; // nothing to do var id = inverseRecord.get('id'), storeIds = this.get('editableStoreIds'), idx = (storeIds && id) ? storeIds.indexOf(id) : -1, record; if (idx >= 0) { storeIds.removeAt(idx); if (this.get('isMaster') && (record = this.get('record'))) { record.recordDidChange(this.get('propertyName')); } } return this; }, /** Called by the `ManyAttribute` whenever a record is added on the inverse of the relationship. @param {SC.Record} inverseRecord the record this array is a part of @returns {SC.ManyArray} receiver */ addInverseRecord: function(inverseRecord) { if (!inverseRecord) return this; var id = inverseRecord.get('id'), storeIds = this.get('editableStoreIds'), orderBy = this.get('orderBy'), len = storeIds.get('length'), idx, record; // find idx to insert at. if (orderBy) { idx = this._findInsertionLocation(inverseRecord, 0, len, orderBy); } else idx = len; storeIds.insertAt(idx, inverseRecord.get('id')); if (this.get('isMaster') && (record = this.get('record'))) { record.recordDidChange(this.get('propertyName')); } return this; }, /** @private binary search to find insertion location */ _findInsertionLocation: function(rec, min, max, orderBy) { var idx = min+Math.floor((max-min)/2), cur = this.objectAt(idx), order = this._compare(rec, cur, orderBy); if (order < 0) { if (idx===0) return idx; else return this._findInsertionLocation(rec, 0, idx, orderBy); } else if (order > 0) { if (idx >= max) return idx; else return this._findInsertionLocation(rec, idx, max, orderBy); } else return idx; }, /** @private function to compare to objects */ _compare: function(a, b, orderBy) { var t = SC.typeOf(orderBy), ret, idx, len; if (t === SC.T_FUNCTION) ret = orderBy(a, b); else if (t === SC.T_STRING) ret = SC.compare(a,b); else { len = orderBy.get('length'); ret = 0; for(idx=0;(ret===0) && (idx<len);idx++) ret = SC.compare(a,b); } return ret ; }, // .......................................................... // INTERNAL SUPPORT // /** @private Invoked whenever the `storeIds` array changes. Observes changes. */ recordPropertyDidChange: function(keys) { if (keys && !keys.contains(this.get('propertyName'))) return this; var storeIds = this.get('readOnlyStoreIds'); var prev = this._prevStoreIds, f = this._storeIdsContentDidChange; if (storeIds === prev) return this; // nothing to do if (prev) prev.removeObserver('[]', this, f); this._prevStoreIds = storeIds; if (storeIds) storeIds.addObserver('[]', this, f); var rev = (storeIds) ? storeIds.propertyRevision : -1 ; this._storeIdsContentDidChange(storeIds, '[]', storeIds, rev); }, /** @private Invoked whenever the content of the storeIds array changes. This will dump any cached record lookup and then notify that the enumerable content has changed. */ _storeIdsContentDidChange: function(target, key, value, rev) { this._records = null ; // clear cache this.enumerableContentDidChange(); }, /** @private */ unknownProperty: function(key, value) { var ret; if (SC.typeOf(key) === SC.T_STRING) ret = this.reducedProperty(key, value); return ret === undefined ? arguments.callee.base.apply(this, arguments) : ret; }, /** @private */ init: function() { arguments.callee.base.apply(this, arguments); this.recordPropertyDidChange(); } });