1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2010 Sprout Systems, 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   @namespace
 10 
 11   This mixin allows a view to get its value from a content object based
 12   on the value of its contentValueKey.
 13 
 14       myView = SC.View.create({
 15         content: {prop: "abc123"},
 16 
 17         contentValueKey: 'prop'
 18       });
 19 
 20       // myView.get('value') will be "abc123"
 21 
 22   This is useful if you have a nested record structure and want to have
 23   it be reflected in a nested view structure. If your data structures
 24   only have primitive values, consider using SC.Control instead.
 25 */
 26 SC.ContentValueSupport = {
 27   /**
 28     Walk like a duck.
 29 
 30     @type Boolean
 31     @default YES
 32   */
 33   hasContentValueSupport: YES,
 34 
 35   /** @private */
 36   initMixin: function () {
 37     // setup content observing if needed.
 38     this._control_contentKeysDidChange();
 39   },
 40 
 41   /** @private */
 42   destroyMixin: function () {
 43     // Remove old observers on self.
 44     this._cleanup_old_observers();
 45 
 46     // Remove old observers on content.
 47     this._cleanup_old_content_observers();
 48   },
 49 
 50   /**
 51     The value represented by this control.
 52 
 53     Most controls represent a value of some type, such as a number, string
 54     or image URL.  This property should hold that value.  It is bindable
 55     and observable.  Changing this value will immediately change the
 56     appearance of the control.  Likewise, editing the control
 57     will immediately change this value.
 58 
 59     If instead of setting a single value on a control, you would like to
 60     set a content object and have the control display a single property
 61     of that control, then you should use the content property instead.
 62 
 63     @type Object
 64     @default null
 65   */
 66   value: null,
 67 
 68   /**
 69     The content object represented by this control.
 70 
 71     Often you need to use a control to display some single aspect of an
 72     object, especially if you are using the control as an item view in a
 73     collection view.
 74 
 75     In those cases, you can set the content and contentValueKey for the
 76     control.  This will cause the control to observe the content object for
 77     changes to the value property and then set the value of that property
 78     on the "value" property of this object.
 79 
 80     Note that unless you are using this control as part of a form or
 81     collection view, then it would be better to instead bind the value of
 82     the control directly to a controller property.
 83 
 84     @type SC.Object
 85     @default null
 86   */
 87   content: null,
 88 
 89   /**
 90     Keys that should be observed on the content object and mapped to values on
 91     this object. Should be a hash of local keys that point to keys on the content to
 92     map to local values. For example, the default is {'contentValueKey': 'value'}.
 93     This means that the value of this.contentValueKey will be observed as a key on
 94     the content object and its value will be mapped to this.value.
 95 
 96     @type Hash
 97     @default null
 98   */
 99   contentKeys: null,
100 
101   _default_contentKeys: {
102     contentValueKey: 'value'
103   },
104 
105   /**
106     The property on the content object that would want to represent the
107     value of this control.  This property should only be set before the
108     content object is first set.  If you have a displayDelegate, then
109     you can also use the contentValueKey of the displayDelegate.
110 
111     @type String
112     @default null
113   */
114   contentValueKey: null,
115 
116   /**
117     Invoked whenever any property on the content object changes.
118 
119     The default implementation will update the value property of the view
120     if the contentValueKey property has changed.  You can override this
121     method to implement whatever additional changes you would like.
122 
123     The key will typically contain the name of the property that changed or
124     '*' if the content object itself has changed.  You should generally do
125     a total reset if '*' is changed.
126 
127     @param {Object} target the content object
128     @param {String} key the property that changes
129     @returns {void}
130     @test in content
131   */
132   contentPropertyDidChange: function (target, key) {
133     var contentKeys = this.get('contentKeys');
134 
135     if (contentKeys) {
136       var contentKey;
137 
138       for (contentKey in contentKeys) {
139         // if we found the specific contentKey, then just update that and we're done
140         if (key === this.getDelegateProperty(contentKey, this, this.get('displayDelegate'), contentKeys)) {
141           return this.updatePropertyFromContent(contentKeys[contentKey], key, contentKey, target);
142         }
143 
144         // else if '*' is changed, then update for every contentKey
145         else if (key === '*') {
146           this.updatePropertyFromContent(contentKeys[contentKey], key, contentKey, target);
147         }
148       }
149     }
150 
151     else {
152       return this.updatePropertyFromContent('value', key, 'contentValueKey', target);
153     }
154   },
155 
156   /**
157     Helper method you can use from your own implementation of
158     contentPropertyDidChange().  This method will look up the content key to
159     extract a property and then update the property if needed.  If you do
160     not pass the content key or the content object, they will be computed
161     for you.  It is more efficient, however, for you to compute these values
162     yourself if you expect this method to be called frequently.
163 
164     @param {String} prop local property to update
165     @param {String} key the contentproperty that changed
166     @param {String} contentKey the local property that contains the key
167     @param {Object} content
168     @returns {SC.Control} receiver
169   */
170   updatePropertyFromContent: function (prop, key, contentKey, content) {
171     var del, v;
172 
173     if (contentKey === undefined) contentKey = "content" + prop.capitalize() + "Key";
174 
175     // prefer our own definition of contentKey
176     if (this[contentKey]) contentKey = this.get(contentKey);
177     // if we don't have one defined check the delegate
178     else if ((del = this.get('displayDelegate')) && (v = del[contentKey])) contentKey = del.get ? del.get(contentKey) : v;
179     // if we have no key we can't do anything so just short circuit out
180     else return this;
181 
182     // only bother setting value if the observer triggered for the correct key
183     if (key === '*' || key === contentKey) {
184       if (content === undefined) content = this.get('content');
185 
186       if (content) v = content.get ? content.get(contentKey) : content[contentKey];
187       else v = null;
188 
189       this.setIfChanged(prop, v);
190     }
191 
192     return this;
193   },
194 
195   /**
196     Relays changes to the value back to the content object if you are using
197     a content object.
198 
199     This observer is triggered whenever the value changes.  It will only do
200     something if it finds you are using the content property and
201     contentValueKey and the new value does not match the old value of the
202     content object.
203 
204     If you are using contentValueKey in some other way than typically
205     implemented by this mixin, then you may want to override this method as
206     well.
207 
208     @returns {void}
209   */
210   updateContentWithValueObserver: function (target, key) {
211     var reverseContentKeys = this._reverseContentKeys;
212 
213     // if everything changed, iterate through and update them all
214     if (!key || key === '*') {
215       for (key in reverseContentKeys) {
216         this.updateContentWithValueObserver(this, key);
217       }
218     }
219 
220     // get value -- set on content if changed
221     var value = this.get(key);
222 
223     var content = this.get('content'),
224     // get the key we should be setting on content, asking displayDelegate if
225     // necessary
226     contentKey = this.getDelegateProperty(reverseContentKeys[key], this, this.displayDelegate);
227 
228     // do nothing if disabled
229     if (!contentKey || !content) return this;
230 
231     if (typeof content.setIfChanged === SC.T_FUNCTION) {
232       content.setIfChanged(contentKey, value);
233     }
234 
235     // avoid re-writing inherited props
236     else if (content[contentKey] !== value) {
237       content[contentKey] = value;
238     }
239   },
240 
241   /** @private
242     This should be null so that if content is also null, the
243     _contentDidChange won't do anything on init.
244   */
245   _control_content: null,
246   _old_contentValueKeys: null,
247   _old_contentKeys: null,
248 
249   /** @private
250     Observes when a content object has changed and handles notifying
251     changes to the value of the content object.
252 
253     Optimized for the default case of only observing contentValueKey. If you use
254     a custom value for contentKeys it will switch to using a CoreSet to track
255     observed keys.
256   */
257   _control_contentDidChange: function (target, key) {
258     // remove an observer from the old content if necessary
259     this._cleanup_old_content_observers();
260 
261     var content = this.get('content'),
262     contentKeys = this.get('contentKeys'), contentKey,
263     oldKeys = this._old_contentValueKeys,
264     f = this.contentPropertyDidChange;
265 
266     // add observer to new content if necessary.
267     if (content && content.addObserver) {
268       // set case
269       if (contentKeys) {
270         // lazily create the key set
271         if (!oldKeys) oldKeys = SC.CoreSet.create();
272 
273         // add observers to each key
274         for (contentKey in contentKeys) {
275           contentKey = this.getDelegateProperty(contentKey, this, this.get('displayDelegate'));
276 
277           if (contentKey) {
278             content.addObserver(contentKey, this, f);
279 
280             oldKeys.add(contentKey);
281           }
282         }
283       }
284 
285       // default case hardcoded for contentValueKey
286       else {
287         contentKey = this.getDelegateProperty('contentValueKey', this, this.get('displayDelegate'));
288 
289         if (contentKey) {
290           content.addObserver(contentKey, this, f);
291 
292           // if we had a set before, continue using it
293           if (oldKeys) oldKeys.add(contentKey);
294           // otherwise just use a string
295           else oldKeys = contentKey;
296         }
297       }
298     }
299 
300     // notify that values did change.
301     key = (!key || key === 'content') ? '*' : this.get(key);
302     if (key) this.contentPropertyDidChange(content, key);
303 
304     // Cache values for clean up.
305     this._control_content = content;
306     this._old_contentValueKeys = oldKeys;
307   }.observes('content'),
308 
309   /** @private
310     Observes changes to contentKeys and sets up observers on the local keys to
311     update the observers on the content object.
312   */
313   _control_contentKeysDidChange: function () {
314     var key, reverse = {},
315     // if no hash is present, use the default contentValueKey -> value
316     contentKeys = this.get('contentKeys') || this._default_contentKeys,
317     contentKey,
318     f = this._control_contentDidChange,
319     reverseF = this.updateContentWithValueObserver;
320 
321     // Remove old observers.
322     this._cleanup_old_observers();
323 
324     // add new observers
325     for (key in contentKeys) {
326       contentKey = contentKeys[key];
327 
328       // build reverse mapping to update content with value
329       reverse[contentKey] = key;
330 
331       // add value observer
332       this.addObserver(contentKey, this, reverseF);
333 
334       // add content key observer
335       this.addObserver(key, this, f);
336     }
337 
338     // store reverse map for later use
339     this._reverseContentKeys = reverse;
340 
341     this._old_contentKeys = contentKeys;
342 
343     // call the other observer now to update all the observers
344     this._control_contentDidChange();
345   }.observes('contentKeys'),
346 
347   /** @private */
348   _cleanup_old_content_observers: function () {
349     var old = this._control_content,
350       oldKeys = this._old_contentValueKeys,
351       oldType = SC.typeOf(oldKeys),
352       f = this.contentPropertyDidChange,
353       contentKey;
354 
355     if (old && old.removeObserver && oldKeys) {
356       // default case
357       if (oldType === SC.T_STRING) {
358         old.removeObserver(oldKeys, this, f);
359 
360         this._old_contentValueKeys = oldKeys = null;
361       }
362 
363       // set case
364       else {
365         var i, len = oldKeys.get('length');
366 
367         for (i = 0; i < len; i++) {
368           contentKey = oldKeys[i];
369 
370           old.removeObserver(contentKey, this, f);
371         }
372 
373         oldKeys.clear();
374       }
375     }
376   },
377 
378   /** @private */
379   _cleanup_old_observers: function () {
380     var oldContentKeys = this._old_contentKeys,
381       f = this._control_contentDidChange,
382       reverseF = this.updateContentWithValueObserver,
383       contentKey, key;
384 
385     // remove old observers
386     for (key in oldContentKeys) {
387       contentKey = oldContentKeys[key];
388 
389       this.removeObserver(contentKey, this, reverseF);
390       this.removeObserver(key, this, f);
391     }
392   }
393 };
394 
395