1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2009 Alex Iskander and TPSi
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 /*globals Forms */
  8 
  9 sc_require("mixins/emptiness");
 10 sc_require("mixins/edit_mode");
 11 sc_require("views/form_row");
 12 
 13 /** 
 14   @class
 15   FormView lays out rows, manages their label widths, binds their
 16   content properties, and sets up their contentValueKeys as needed. This class is 
 17   experimental and not available out of the box. 
 18 
 19   Usually, you will place rows into the FormView:
 20   
 21       childViews: "fullName gender".w(),
 22       contentBinding: 'MyApp.personController',
 23 
 24       fullName: SC.FormView.row("Name:", SC.TextFieldView.extend({
 25         layout: {height: 20, width: 150}
 26       })),
 27 
 28       gender: SC.FormView.row("Gender:", SC.RadioView.design({
 29         layout: {width: 150, height: 40, centerY: 0},
 30         items: ["male", "female"]
 31       }))
 32 
 33   The name of the row (ie. 'fullName'), is passed down to the fields, and used as the key
 34   to bind the value property to the content. In this case it will bind content.fullName to the
 35   value property of the textFieldView.
 36 
 37 
 38   @extends SC.View
 39   @implements SC.FlowedLayout, SC.CalculatesEmptiness, SC.FormsEditMode
 40 */
 41 
 42 SC.FormView = SC.View.extend(SC.FlowedLayout, SC.CalculatesEmptiness, SC.FormsEditMode, /** @scope SC.FormView.prototype */ {
 43   // We lay out forms vertically. Each item gets its own "row". Wrapping makes
 44   // no sense, as the FormView should grow with each row.
 45   layoutDirection: SC.LAYOUT_VERTICAL,
 46   canWrap: NO,
 47 
 48   renderDelegateName: 'formRenderDelegate',
 49 
 50   /**
 51     The default padding around items in the form. By default, this comes from the theme.
 52     You can supply your own directly, or override the formRenderDelegate:
 53 
 54         // base it on the existing render delegate
 55         MyTheme.formRenderDelegate = SC.AceTheme.formRenderDelegate.create({
 56           flowSpacing: { left: 5, top: 5, right: 5, bottom: 5 }
 57         });
 58   */
 59   defaultFlowSpacing: SC.propertyFromRenderDelegate('flowSpacing', {}),
 60 
 61   classNames: ["sc-form-view"],
 62 
 63   /**
 64     Whether to automatically start editing.
 65   */
 66   editsByDefault: YES,
 67 
 68   /**
 69     The content to bind the form to. This content object is passed to all children.
 70   
 71     All child views, if added at design time via string-based childViews array, will get their
 72     contentValueKey set to their own key. Note that SC.RowView passes on its contentValueKey to its
 73     child field, and if its isNested property is YES, uses it to find its own content object.
 74   */
 75   content: null,
 76   
 77   /**
 78     Rows in the form do not have to be full SC.FormRowView at design time. They can also be hashes
 79     that get loaded into rows.
 80   */
 81   exampleRow: SC.FormRowView.extend({
 82     labelView: SC.FormRowView.LabelView.extend({ textAlign: SC.ALIGN_RIGHT })
 83   }),
 84 
 85   /**
 86      @private
 87   */
 88   init: function() {
 89     if (this.get("editsByDefault")) this.set("isEditing", YES);
 90     sc_super();
 91   },
 92 
 93   /**
 94   */
 95   createChildViews: function() {
 96     var childViews = this.get('childViews'),
 97         len        = childViews.length,
 98         idx, key, views, view,
 99         attrs, exampleRow = this.get('exampleRow');
100 
101     this.beginPropertyChanges() ;
102 
103     // swap the array
104     for (idx=0; idx<len; ++idx) {
105       if (key = (view = childViews[idx])) {
106 
107         // is this is a key name, lookup view class
108         if (typeof key === SC.T_STRING) {
109           view = this[key];
110         } else {
111           key = null ;
112         }
113 
114         if (!view) {
115           SC.Logger.error ("No view with name "+key+" has been found in "+this.toString());
116           // skip this one.
117           continue;
118         }
119 
120         if(!view.isClass && SC.typeOf(view) === SC.T_HASH) {
121           attrs = view;
122           view = exampleRow;
123         }
124         else {
125           attrs = {};
126         }
127 
128         if((attrs.isFormField || view.prototype.isFormField) && (attrs.hasContentValueSupport || view.prototype.hasContentValueSupport)) {
129           attrs.contentValueKey = key;
130         }
131 
132         attrs.formKey = key;
133 
134         // createChildView creates the view if necessary, but also sets
135         // important properties, such as parentView
136         view = this.createChildView(view, attrs) ;
137 
138         if(!view.get('content')) {
139           view.bind('content', this, 'content');
140         }
141 
142         // for form rows, set up label measuring and the label itself.
143         if (view.isFormRow) {
144           // set label (if possible).
145           if (SC.none(view.get('label'))) {
146               view.set("label", key.humanize().titleize());
147           }
148 
149           // set the label size measuring stuff
150           if (this.get('labelWidth') !== null) {
151             view.set("shouldMeasureLabel", NO);
152           }
153         }
154 
155         if (key) { this[key] = view ; } // save on key name if passed
156       }
157       childViews[idx] = view;
158     }
159 
160     this.endPropertyChanges() ;
161 
162     this._hasCreatedRows = YES;
163     this.recalculateLabelWidth();
164 
165     return this ;
166   },
167 
168   
169   /**
170     Allows rows to use this to track label width.
171   */
172   isRowDelegate: YES,
173   
174   /**
175     Supply a label width to avoid automatically calculating the widths of the labels
176     in the form. Leave null to let SproutCore automatically determine the proper width
177     for the label.
178 
179     @type Number
180     @default null
181   */
182   labelWidth: null,
183   
184   /**
185     Tells the child rows whether they should measure their labels or not.
186   */
187   labelWidthDidChange: function() {
188     var childViews = this.get('childViews'), i, len = childViews.length,
189     shouldMeasure = SC.none(this.get('labelWidth'));
190     
191     for(i = 0; i < len; i++) {
192       childViews[i].set('shouldMeasureLabel', shouldMeasure);
193     }
194     
195     this.recalculateLabelWidth();
196   }.observes('labelWidth'),
197   
198   /**
199     Propagates the label width to the child rows, finding the measured size if necessary.
200   */
201   recalculateLabelWidth: function() {
202     if (!this._hasCreatedRows) {
203       return;
204     }
205     
206     var ret = this.get("labelWidth"), children = this.get("childViews"), idx, len = children.length, child;
207     
208     // calculate by looping through child views and getting size (if possible and if
209     // no label width is explicitly set)
210     if (ret === null) {
211       ret = 0;
212       for (idx = 0; idx < len; idx++) {
213         child = children[idx];
214       
215         // if it has a measurable row label
216         if (child.get("rowLabelMeasuredSize")) {
217           ret = Math.max(child.get("rowLabelMeasuredSize"), ret);
218         }
219       }
220     }
221     
222     // now set for all children
223     if (this._rowLabelSize !== ret) {
224       this._rowLabelSize = ret;
225       
226       // set by looping through child views
227       for (idx = 0; idx < len; idx++) {
228         child = children[idx];
229 
230         // if it has a measurable row label
231         if (child.get("hasRowLabel")) {
232           child.set("rowLabelSize", ret);
233         }
234       }
235       
236     }
237   },
238   
239   /**
240     Rows call this when their label width changes.
241   */
242   rowLabelMeasuredSizeDidChange: function(row, labelSize) {
243     this.invokeOnce("recalculateLabelWidth");
244   }
245 
246 
247 });
248 
249 SC.mixin(SC.FormView, {
250   /**
251   Creates a form row.
252 
253   Can be called in two ways: `row(optionalClass, properties)`, which creates
254   a field with the properties, and puts it in a new row;
255   and `row(properties)`, which creates a new row—and it is up to you to add
256   any fields you want in the row.
257   
258   You can also supply some properties to extend the row itself with.
259   */
260   row: function(optionalClass, properties, rowExt)
261   {
262     return SC.FormRowView.row(optionalClass, properties, rowExt);
263   }
264 });
265