1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  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 sc_require('controllers/controller') ;
  9 
 10 /** @class
 11 
 12   An ObjectController gives you a simple way to manage the editing state of
 13   an object.  You can use an ObjectController instance as a "proxy" for your
 14   model objects.
 15 
 16   Any properties you get or set on the object controller, will be passed
 17   through to its content object.  This allows you to setup bindings to your
 18   object controller one time for all of your views and then swap out the
 19   content as needed.
 20 
 21   ## Working with Arrays
 22 
 23   An ObjectController can accept both arrays and single objects as content.
 24   If the content is an array, the ObjectController will do its best to treat
 25   the array as a single object.  For example, if you set the content of an
 26   ObjectController to an array of Contact records and then call:
 27 
 28       contactController.get('name');
 29 
 30   The controller will check the name property of each Contact in the array.
 31   If the value of the property for each Contact is the same, that value will
 32   be returned.  If the any values are different, then an array will be
 33   returned with the values from each Contact in them.
 34 
 35   Most SproutCore views can work with both arrays and single content, which
 36   means that most of the time, you can simply hook up your views and this will
 37   work.
 38 
 39   If you would prefer to make sure that your ObjectController is always
 40   working with a single object and you are using bindings, you can always
 41   setup your bindings so that they will convert the content to a single object
 42   like so:
 43 
 44       contentBinding: SC.Binding.single('MyApp.listController.selection') ;
 45 
 46   This will ensure that your content property is always a single object
 47   instead of an array.
 48 
 49   @extends SC.Controller
 50   @since SproutCore 1.0
 51 */
 52 SC.ObjectController = SC.Controller.extend(
 53 /** @scope SC.ObjectController.prototype */ {
 54 
 55   //@if(debug)
 56   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
 57 
 58   /* @private */
 59   toString: function () {
 60     var content = this.get('content'),
 61       ret = sc_super();
 62 
 63     return content ? "%@:\n  ↳ %@".fmt(ret, content) : ret;
 64   },
 65 
 66   /* END DEBUG ONLY PROPERTIES AND METHODS */
 67   //@endif
 68 
 69   // ..........................................................
 70   // PROPERTIES
 71   //
 72 
 73   /**
 74     Set to the object you want this controller to manage.  The object should
 75     usually be a single value; not an array or enumerable.  If you do supply
 76     an array or enumerable with a single item in it, the ObjectController
 77     will manage that single item.
 78 
 79     Usually your content object should implement the SC.Observable mixin, but
 80     this is not required.  All SC.Object-based objects support SC.Observable
 81 
 82     @type Object
 83   */
 84   content: null,
 85 
 86   /**
 87     If YES, then setting the content to an enumerable or an array with more
 88     than one item will cause the Controller to attempt to treat the array as
 89     a single object.  Use of get(), for example, will get every property on
 90     the enumerable and return it.  set() will set the property on every item
 91     in the enumerable.
 92 
 93     If NO, then setting content to an enumerable with multiple items will be
 94     treated like setting a null value.  hasContent will be NO.
 95 
 96     @type Boolean
 97   */
 98   allowsMultipleContent: NO,
 99 
100   /**
101     Becomes YES whenever this object is managing content.  Usually this means
102     the content property contains a single object or an array or enumerable
103     with a single item.  Array's or enumerables with multiple items will
104     normally make this property NO unless allowsMultipleContent is YES.
105 
106     @type Boolean
107   */
108   hasContent: function() {
109     return !SC.none(this.get('observableContent'));
110   }.property('observableContent'),
111 
112   /**
113     Makes a controller editable or not editable.  The SC.Controller class
114     itself does not do anything with this property but subclasses will
115     respect it when modifying content.
116 
117     @type Boolean
118   */
119   isEditable: YES,
120 
121   /**
122     Primarily for internal use.  Normally you should not access this property
123     directly.
124 
125     Returns the actual observable object proxied by this controller.  Usually
126     this property will mirror the content property.  In some cases - notably
127     when setting content to an enumerable, this may return a different object.
128 
129     Note that if you set the content to an enumerable which itself contains
130     enumerables and allowsMultipleContent is NO, this will become null.
131 
132     @type Object
133   */
134   observableContent: function() {
135     var content = this.get('content'),
136         len, allowsMultiple;
137 
138     // if enumerable, extract the first item or possibly become null
139     if (content && content.isEnumerable) {
140       len = content.get('length');
141       allowsMultiple = this.get('allowsMultipleContent');
142 
143       if (len === 1) content = content.firstObject();
144       else if (len===0 || !allowsMultiple) content = null;
145 
146       // if we got some new content, it better not be enum also...
147       if (content && !allowsMultiple && content.isEnumerable) content=null;
148     }
149 
150     return content;
151   }.property('content', 'allowsMultipleContent').cacheable(),
152 
153   // ..........................................................
154   // METHODS
155   //
156 
157   /**
158     Override this method to destroy the selected object.
159 
160     The default just passes this call onto the content object if it supports
161     it, and then sets the content to null.
162 
163     Unlike most calls to destroy() this will not actually destroy the
164     controller itself; only the the content.  You continue to use the
165     controller by setting the content to a new value.
166 
167     @returns {SC.ObjectController} receiver
168   */
169   destroy: function() {
170     var content = this.get('observableContent') ;
171     if (content && SC.typeOf(content.destroy) === SC.T_FUNCTION) {
172       content.destroy();
173     }
174     this.set('content', null) ;
175     return this;
176   },
177 
178   /**
179     Invoked whenever any property on the content object changes.
180 
181     The default implementation will simply notify any observers that the
182     property has changed.  You can override this method if you need to do
183     some custom work when the content property changes.
184 
185     If you have set the content property to an enumerable with multiple
186     objects and you set allowsMultipleContent to YES, this method will be
187     called anytime any property in the set changes.
188 
189     If all properties have changed on the content or if the content itself
190     has changed, this method will be called with a key of "*".
191 
192     @param {Object} target the content object
193     @param {String} key the property that changes
194     @returns {void}
195   */
196   contentPropertyDidChange: function(target, key) {
197     if (key === '*') this.allPropertiesDidChange();
198     else this.notifyPropertyChange(key);
199   },
200 
201   /**
202     Called whenver you try to get/set an unknown property.  The default
203     implementation will pass through to the underlying content object but
204     you can override this method to do some other kind of processing if
205     needed.
206 
207     @param {String} key key being retrieved
208     @param {Object} value value to set or undefined if reading only
209     @returns {Object} property value
210   */
211   unknownProperty: function(key,value) {
212 
213     // avoid circular references
214     if (key==='content') {
215       if (value !== undefined) this.content = value;
216       return this.content;
217     }
218 
219     // for all other keys, just pass through to the observable object if
220     // there is one.  Use getEach() and setEach() on enumerable objects.
221     var content = this.get('observableContent'), loc, cur, isSame;
222     if (content===null || content===undefined) return undefined; // empty
223 
224     // getter...
225     if (value === undefined) {
226       if (content.isEnumerable) {
227         value = content.getEach(key);
228 
229         // iterate over array to see if all values are the same. if so, then
230         // just return that value
231         loc = value.get('length');
232         if (loc>0) {
233           isSame = YES;
234           cur = value.objectAt(0);
235           while((--loc > 0) && isSame) {
236             if (cur !== value.objectAt(loc)) isSame = NO ;
237           }
238           if (isSame) value = cur;
239         } else value = undefined; // empty array.
240 
241       } else value = (content.isObservable) ? content.get(key) : content[key];
242 
243     // setter
244     } else {
245       if (!this.get('isEditable')) {
246         throw new Error("%@.%@ is not editable".fmt(this,key));
247       }
248 
249       if (content.isEnumerable) content.setEach(key, value);
250       else if (content.isObservable) content.set(key, value);
251       else content[key] = value;
252     }
253 
254     return value;
255   },
256 
257   // ...............................
258   // INTERNAL SUPPORT
259   //
260 
261   /** @private - setup observer on init if needed. */
262   init: function() {
263     sc_super();
264     if (this.get('content')) this._scoc_contentDidChange();
265     if (this.get('observableContent')) this._scoc_observableContentDidChange();
266   },
267 
268   _scoc_contentDidChange: function () {
269     var last = this._scoc_content,
270         cur  = this.get('content');
271 
272     if (last !== cur) {
273       this._scoc_content = cur;
274       var func = this._scoc_enumerableContentDidChange;
275       if (last && last.isEnumerable) {
276         last.removeObserver('[]', this, func);
277       }
278       if (cur && cur.isEnumerable) {
279         cur.addObserver('[]', this, func);
280       }
281     }
282   }.observes("content"),
283 
284   /**  @private
285 
286     Called whenever the observable content property changes.  This will setup
287     observers on the content if needed.
288   */
289   _scoc_observableContentDidChange: function() {
290     var last = this._scoc_observableContent,
291         cur  = this.get('observableContent'),
292         func = this.contentPropertyDidChange,
293         efunc= this._scoc_enumerableContentDidChange;
294 
295     if (last === cur) return this; // nothing to do
296     //console.log('observableContentDidChange');
297 
298     this._scoc_observableContent = cur; // save old content
299 
300     // stop observing last item -- if enumerable stop observing set
301     if (last) {
302       if (last.isEnumerable) last.removeObserver('[]', this, efunc);
303       else if (last.isObservable) last.removeObserver('*', this, func);
304     }
305 
306     if (cur) {
307       if (cur.isEnumerable) cur.addObserver('[]', this, efunc);
308       else if (cur.isObservable) cur.addObserver('*', this, func);
309     }
310 
311     // notify!
312     if ((last && last.isEnumerable) || (cur && cur.isEnumerable)) {
313       this._scoc_enumerableContentDidChange();
314     } else {
315       this.contentPropertyDidChange(cur, '*');
316     }
317 
318   }.observes("observableContent"),
319 
320   /** @private
321     Called when observed enumerable content has changed.  This will teardown
322     and setup observers on the enumerable content items and then calls
323     contentPropertyDidChange().  This method may be called even if the new
324     'cur' is not enumerable but the last content was enumerable.
325   */
326   _scoc_enumerableContentDidChange: function() {
327     var cur  = this.get('observableContent'),
328         set  = this._scoc_observableContentItems,
329         func = this.contentPropertyDidChange;
330 
331     // stop observing each old item
332     if (set) {
333       set.forEach(function(item) {
334         if (item.isObservable) item.removeObserver('*', this, func);
335       }, this);
336       set.clear();
337     }
338 
339     // start observing new items if needed
340     if (cur && cur.isEnumerable) {
341       if (!set) set = SC.Set.create();
342       cur.forEach(function(item) {
343         if (set.contains(item)) return ; // nothing to do
344         set.add(item);
345         if (item.isObservable) item.addObserver('*', this, func);
346       }, this);
347     } else set = null;
348 
349     this._scoc_observableContentItems = set; // save for later cleanup
350 
351     // notify
352     this.contentPropertyDidChange(cur, '*');
353     return this ;
354   }
355 
356 }) ;
357