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 // CHAIN OBSERVER
 10 //
 11 
 12 // This is a private class used by the observable mixin to support chained
 13 // properties.
 14 
 15 // ChainObservers are used to automatically monitor a property several
 16 // layers deep.
 17 // org.plan.name = SC._ChainObserver.create({
 18 //    target: this, property: 'org',
 19 //    next: SC._ChainObserver.create({
 20 //      property: 'plan',
 21 //      next: SC._ChainObserver.create({
 22 //        property: 'name', func: myFunc
 23 //      })
 24 //    })
 25 //  })
 26 //
 27 SC._ChainObserver = function (property, root) {
 28   this.property = property;
 29   this.root = root || this;
 30 };
 31 
 32 /** @private
 33   This is the primary entry point.  Configures the chain.
 34 
 35   @param {String} path The property path for the chain.  Ex. 'propA.propB.propC.@each.propD'
 36   */
 37 SC._ChainObserver.createChain = function (rootObject, path, target, method, context) {
 38   // First we create the chain.
 39   var parts = path.split('.'), // ex. ['propA', 'propB', '@each', 'propC']
 40       root  = new SC._ChainObserver(parts[0]), // ex. _ChainObserver({ property: 'propA' })
 41       tail  = root;
 42 
 43   for (var i = 1, len = parts.length; i < len; i++) {
 44     tail = tail.next = new SC._ChainObserver(parts[i], root);
 45   }
 46 
 47   var tails = root.tails = [tail]; // ex. [_ChainObserver({ property: 'propC' })]
 48 
 49   // Now root has the first observer and tail has the last one.
 50   // Feed the rootObject into the front to setup the chain...
 51   // do this BEFORE we set the target/method so they will not be triggered.
 52   root.objectDidChange(rootObject);
 53 
 54   tails.forEach(function (tail) {
 55     // Finally, set the target/method on the tail so that future changes will trigger.
 56     tail.target = target;
 57     tail.method = method;
 58     tail.context = context;
 59   });
 60 
 61   // no need to hold onto references to the tails; if the underlying
 62   // objects go away, let them get garbage collected
 63   root.tails = null;
 64 
 65   // and return the root to save
 66   return root;
 67 };
 68 
 69 SC._ChainObserver.prototype = {
 70   isChainObserver: true,
 71 
 72   // the object this instance is observing
 73   object: null,
 74 
 75   // the property on the object this link is observing.
 76   property: null,
 77 
 78   // if not null, this is the next link in the chain.  Whenever the
 79   // current property changes, the next observer will be notified.
 80   next: null,
 81 
 82   root: null,
 83 
 84   // if not null, this is the final target observer.
 85   target: null,
 86 
 87   // if not null, this is the final target method
 88   method: null,
 89 
 90   // an accessor method that traverses the list and finds the tail
 91   tail: function () {
 92     if (this._tail) { return this._tail; }
 93 
 94     var tail = this;
 95 
 96     while (tail.next) {
 97       tail = tail.next;
 98     }
 99 
100     this._tail = tail;
101     return tail;
102   },
103 
104   // invoked when the source object changes.  removes observer on old
105   // object, sets up new observer, if needed.
106   objectDidChange: function (newObject) {
107     if (newObject === this.object) return; // nothing to do.
108 
109     // if an old object, remove observer on it.
110     if (this.object) {
111       if (this.property === '@each' && this.object._removeContentObserver) {
112         this.object._removeContentObserver(this);
113       } else if (this.object.removeObserver) {
114         this.object.removeObserver(this.property, this, this.propertyDidChange);
115       }
116     }
117 
118     // if a new object, add observer on it...
119     this.object = newObject;
120 
121     // when [].propName is used, we will want to set up observers on each item
122     // added to the Enumerable, and remove them when the item is removed from
123     // the Enumerable.
124     //
125     // In this case, we invoke addEnumerableObserver, which handles setting up
126     // and tearing down observers as items are added and removed from the
127     // Enumerable.
128     if (this.property === '@each' && this.next) {
129       if (this.object && this.object._addContentObserver) {
130         this.object._addContentObserver(this);
131       }
132     } else {
133       if (this.object && this.object.addObserver) {
134         this.object.addObserver(this.property, this, this.propertyDidChange);
135       }
136 
137       // now, notify myself that my property value has probably changed.
138       this.propertyDidChange();
139     }
140   },
141 
142   // the observer method invoked when the observed property changes.
143   propertyDidChange: function () {
144     // get the new value
145     var object = this.object;
146     var property = this.property;
147     var value = (object && object.get) ? object.get(property) : null;
148 
149     // if we have a next object in the chain, notify it that its object
150     // did change...
151     if (this.next) { this.next.objectDidChange(value); }
152 
153     // if we have a target/method, call it.
154     var target  = this.target,
155         method  = this.method,
156         context = this.context;
157 
158     if (target && method) {
159       var rev = object ? object.propertyRevision : null;
160       if (context) {
161         method.call(target, object, property, value, context, rev);
162       } else {
163         method.call(target, object, property, value, rev);
164       }
165     }
166   },
167 
168   // teardown the chain...
169   destroyChain: function () {
170 
171     // remove observer
172     var obj = this.object;
173     if (obj) {
174       if (this.property === '@each' && this.next && obj._removeContentObserver) {
175         obj._removeContentObserver(this);
176       }
177 
178       if (obj.removeObserver) {
179         obj.removeObserver(this.property, this, this.propertyDidChange);
180       }
181     }
182 
183     // destroy next item in chain
184     if (this.next) this.next.destroyChain();
185 
186     // and clear left overs...
187     this.next = this.target = this.method = this.object = this.context = null;
188     return null;
189   }
190 
191 };
192