1 sc_require('views/template');
  2 
  3 /** @class
  4 
  5   @author Tom Dale
  6   @author Yehuda Katz
  7   @extends SC.TemplateView
  8   @since SproutCore 1.5
  9 */
 10 SC.TemplateCollectionView = SC.TemplateView.extend(
 11   /** @scope SC.TemplateCollectionView.prototype */{
 12   /**
 13     Name of the tag that is used for the collection
 14 
 15     If the tag is a list ('ul' or 'ol') each item will be wrapped into a 'li' tag.
 16     If the tag is a table ('table', 'thead', 'tbody') each item will be wrapped into a 'tr' tag.
 17 
 18     @type String
 19     @default ul
 20   */
 21   tagName: 'ul',
 22 
 23   /**
 24     A list of items to be displayed by the TemplateCollectionView.
 25 
 26     @type SC.Array
 27     @default null
 28   */
 29   content: null,
 30 
 31   template: SC.Handlebars.compile(''),
 32 
 33   /**
 34     An optional view to display if content is set to an empty array.
 35 
 36     @type SC.TemplateView
 37     @default null
 38   */
 39   emptyView: null,
 40 
 41   /**
 42     @private
 43     When the view is destroyed, remove array observers on the content array.
 44   */
 45   destroy: function() {
 46     var content = this.get('content');
 47     if(content) {
 48       content.removeArrayObservers({
 49         target: this,
 50         willChange: 'arrayContentWillChange',
 51         didChange: 'arrayContentDidChange'
 52       });
 53     }
 54     this.removeObserver('content', this, this._sctcv_contentDidChange);
 55     return sc_super();
 56   },
 57 
 58   // In case a default content was set, trigger the child view creation
 59   // as soon as the empty layer was created
 60   didCreateLayer: function() {
 61     // FIXME: didCreateLayer gets called multiple times when template collection
 62     // views are nested - this is a hack to avoid rendering the content more
 63     // than once.
 64     if (this._sctcv_layerCreated) { return; }
 65     this._sctcv_layerCreated = true;
 66 
 67     //set up array observers on the content array.
 68     this.addObserver('content', this, this._sctcv_contentDidChange);
 69     this._sctcv_contentDidChange();
 70   },
 71 
 72   /**
 73     @type SC.TemplateView
 74     @default SC.TemplateView
 75   */
 76   itemView: 'SC.TemplateView',
 77 
 78   /**
 79     The template used to render each item in the collection.
 80 
 81     This should be a function that takes a content object and returns
 82     a string of HTML that will be inserted into the DOM.
 83 
 84     In general, you should set the `itemViewTemplateName` property instead of
 85     setting the `itemViewTemplate` property yourself. If you created the
 86     SC.TemplateCollectionView using the Handlebars {{#collection}} helper, this
 87     will be set for you automatically.
 88 
 89     @type Function
 90   */
 91   itemViewTemplate: null,
 92 
 93   /**
 94     The name of the template to lookup if no item view template is provided.
 95 
 96     The collection will look for a template with this name in the global
 97     `SC.TEMPLATES` hash. Usually this hash will be populated for you
 98     automatically when you include `.handlebars` files in your project.
 99 
100     @type String
101   */
102   itemViewTemplateName: null,
103 
104   /**
105     A template to render when there is no content or the content length is 0.
106   */
107   inverseTemplate: function(key, value) {
108     if (value !== undefined) {
109       return value;
110     }
111 
112     var templateName = this.get('inverseTemplateName'),
113         template = this.get('templates').get(templateName);
114 
115     if (!template) {
116       //@if(debug)
117       if (templateName) {
118         SC.Logger.warn('%@ - Unable to find template "%@".'.fmt(this, templateName));
119       }
120       //@endif
121 
122       return function() { return ''; };
123     }
124 
125     return template;
126   }.property('inverseTemplateName').cacheable(),
127 
128   /**
129     The name of a template to lookup if no inverse template is provided.
130 
131     @type String
132   */
133   inverseTemplateName: null,
134 
135   itemContext: null,
136 
137   itemViewClass: function() {
138     var itemView = this.get('itemView');
139     var itemViewTemplate = this.get('itemViewTemplate');
140     var itemViewTemplateName = this.get('itemViewTemplateName');
141 
142     // hash of properties to override in our
143     // item view class
144     var extensions = {};
145 
146     if(SC.typeOf(itemView) === SC.T_STRING) {
147       itemView = SC.objectForPropertyPath(itemView);
148     }
149 
150     if (!itemViewTemplate && itemViewTemplateName) {
151       itemViewTemplate = this.get('templates').get(itemViewTemplateName);
152     }
153 
154     if (itemViewTemplate) {
155       extensions.template = itemViewTemplate;
156     }
157 
158     // If the itemView has not defined a unique tagName, then check for a unique item tagName
159     // to match the given collection tagName.  This is safe, since the unique item tagNames
160     // are required by HTML to be children of the special collection tagName.  If the collection
161     // doesn't have a special tagName, then the default value of SC.TemplateView is still
162     // used.
163     if (itemView.prototype.tagName === SC.TemplateView.prototype.tagName) {
164       extensions.tagName = this.get('itemTagName');
165     }
166 
167     return itemView.extend(extensions);
168   }.property('itemView').cacheable(),
169 
170   /**
171     @private
172 
173     When the content property of the collection changes, remove any existing
174     child views and observers, then set up an observer on the new content, if
175     needed.
176   */
177   _sctcv_contentDidChange: function() {
178 
179     var oldContent = this._content, oldLen = 0;
180     var content = this.get('content'), newLen = 0;
181 
182     if (oldContent) {
183       oldContent.removeArrayObservers({
184         target: this,
185         willChange: 'arrayContentWillChange',
186         didChange: 'arrayContentDidChange'
187       });
188 
189       oldLen = oldContent.get('length');
190     }
191 
192     if (content) {
193       content.addArrayObservers({
194         target: this,
195         willChange: 'arrayContentWillChange',
196         didChange: 'arrayContentDidChange'
197       });
198 
199       newLen = content.get('length');
200     }
201 
202     this.arrayContentWillChange(0, oldLen, newLen);
203     this._content = this.get('content');
204     this.arrayContentDidChange(0, oldLen, newLen);
205   },
206 
207   arrayContentWillChange: function(start, removedCount, addedCount) {
208     // If the contents were empty before and this template collection has an empty view
209     // remove it now.
210     var emptyView = this.get('emptyView');
211     if (emptyView) { emptyView.destroy(); }
212 
213     // Loop through child views that correspond with the removed items.
214     // Note that we loop from the end of the array to the beginning because
215     // we are mutating it as we go.
216     var childViews = this.get('childViews'), childView, idx, len;
217 
218     len = childViews.get('length');
219     for (idx = start+removedCount-1; idx >= start; idx--) {
220       childView = childViews[idx];
221       if(childView) {
222         childView.destroy();
223       }
224     }
225   },
226 
227   /**
228     Called when a mutation to the underlying content array occurs.
229 
230     This method will replay that mutation against the views that compose the
231     SC.TemplateCollectionView, ensuring that the view reflects the model.
232 
233     This enumerable observer is added in contentDidChange.
234 
235     @param {Array} addedObjects the objects that were added to the content
236     @param {Array} removedObjects the objects that were removed from the content
237     @param {Number} changeIndex the index at which the changes occurred
238   */
239   arrayContentDidChange: function(start, removedCount, addedCount) {
240     if (!this.get('layer')) { return; }
241 
242     var content       = this.get('content'),
243         itemViewClass = this.get('itemViewClass'),
244         childViews    = this.get('childViews'),
245         addedViews    = [],
246         renderFunc, childView, itemOptions, elem = this.$(), insertAtElement, item, itemElem, idx, len;
247 
248     if (content) {
249       var addedObjects = content.slice(start, start+addedCount);
250 
251       // If we have content to display, create a view for
252       // each item.
253       itemOptions = this.get('itemViewOptions') || {};
254 
255       insertAtElement = elem.find('li')[start-1] || null;
256       len = addedObjects.get('length');
257 
258       // TODO: This logic is duplicated from the view helper. Refactor
259       // it so we can share logic.
260       var itemAttrs = {
261         "id": itemOptions.id,
262         "class": itemOptions['class'],
263         "classBinding": itemOptions.classBinding
264       };
265 
266       renderFunc = function(context) {
267         sc_super();
268         SC.Handlebars.ViewHelper.applyAttributes(itemAttrs, this, context);
269       };
270 
271       itemOptions = SC.clone(itemOptions);
272       delete itemOptions.id;
273       delete itemOptions['class'];
274       delete itemOptions.classBinding;
275 
276       for (idx = 0; idx < len; idx++) {
277         item = addedObjects.objectAt(idx);
278         childView = this.createChildView(itemViewClass.extend(itemOptions, {
279           content: item,
280           render: renderFunc,
281           // Use the itemTagName property if it is set, over the tagName of the itemViewClass which is 'div' by default
282           tagName: itemOptions.tagName || itemViewClass.prototype.tagName
283         }));
284 
285         var contextProperty = childView.get('contextProperty');
286         if (contextProperty) {
287           childView.set('context', childView.get(contextProperty));
288         }
289 
290         itemElem = childView.createLayer().$();
291         if (!insertAtElement) {
292           elem.append(itemElem);
293         } else {
294           itemElem.insertAfter(insertAtElement);
295         }
296         insertAtElement = itemElem;
297 
298         addedViews.push(childView);
299       }
300 
301       childViews.replace(start, 0, addedViews);
302     }
303 
304     var inverseTemplate = this.get('inverseTemplate');
305     if (childViews.get('length') === 0 && inverseTemplate) {
306       childView = this.createChildView(SC.TemplateView.extend({
307         template: inverseTemplate,
308         content: this
309       }));
310       this.set('emptyView', childView);
311       childView.createLayer().$().appendTo(elem);
312       this.childViews = [childView];
313     }
314 
315     // Because the layer has been modified, we need to invalidate the frame
316     // property, if it exists, at the end of the run loop. This allows it to
317     // be used inside of SC.ScrollView.
318     this.invokeLast('invalidateFrame');
319   },
320 
321   itemTagName: function() {
322     switch(this.get('tagName')) {
323       case 'dl':
324         return 'dt';
325       case 'ul':
326       case 'ol':
327         return 'li';
328       case 'table':
329       case 'thead':
330       case 'tbody':
331       case 'tfoot':
332         return 'tr';
333       case 'select':
334         return 'option';
335       default:
336         return SC.TemplateView.prototype.tagName;
337     }
338   }.property('tagName'),
339 
340   invalidateFrame: function() {
341     this.notifyPropertyChange('frame');
342   }
343 });
344 
345