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('mixins/validatable') ;
  9 
 10 /** @class
 11 
 12   Base view for managing a view backed by an input element.  Since the web
 13   browser provides native support for editing input elements, this view
 14   provides basic support for listening to changes on these input elements and
 15   responding to them.
 16 
 17   Generally you will not work with a FieldView directly.  Instead, you should
 18   use one of the subclasses implemented by your target platform such as
 19   SC.CheckboxView, SC.RadioView, SC.TextFieldView, and so on.
 20 
 21   @extends SC.View
 22   @extends SC.Control
 23   @extends SC.Validatable
 24   @since SproutCore 1.0
 25 */
 26 SC.FieldView = SC.View.extend(SC.Control, SC.Validatable,
 27 /** @scope SC.FieldView.prototype */ {
 28 
 29   /**
 30   _field_isMouseDown: NO,
 31 
 32   /**
 33     The raw value of the field itself.  This is computed from the 'value'
 34     property by passing it through any validator you might have set.  This is
 35     the value that will be set on the field itself when the view is updated.
 36 
 37     @type String
 38   */
 39   fieldValue: function() {
 40     var value = this.get('value');
 41     if (SC.typeOf(value) === SC.T_ERROR) value = value.get('errorValue');
 42     return this.fieldValueForObject(value);
 43   }.property('value', 'validator').cacheable(),
 44 
 45   // ..........................................................
 46   // PRIMITIVES
 47   //
 48 
 49   /**
 50     Override to return an CoreQuery object that selects the input elements
 51     for the view.  If this method is defined, the field view will
 52     automatically edit the attrbutes of the input element to reflect the
 53     current isEnabled state among other things.
 54   */
 55   $input: function() {
 56     var elementTagName = this._inputElementTagName(); // usually "input"
 57     return this.$(elementTagName).andSelf().filter(elementTagName);
 58   },
 59 
 60   /** @private
 61     Override to specify the HTML element type to use as the field. For
 62     example, "input" or "textarea".
 63   */
 64   _inputElementTagName: function() {
 65     return 'input';
 66   },
 67 
 68   /**
 69     Override to set the actual value of the field.
 70 
 71     The default implementation will simple copy the newValue to the value
 72     attribute of any input tags in the receiver view.  You can override this
 73     method to provide specific functionality needed by your view.
 74 
 75     @param {Object} newValue the value to display.
 76     @returns {SC.FieldView} receiver
 77   */
 78   setFieldValue: function(newValue) {
 79     if (SC.none(newValue)) newValue = '' ;
 80     var input = this.$input();
 81 
 82     // Don't needlessly set the element if it already has the value, because
 83     // doing so moves the cursor to the end in some browsers.
 84     if (input.val() !== newValue) {
 85       input.val(newValue);
 86     }
 87     return this ;
 88   },
 89 
 90   /**
 91     Override to retrieve the actual value of the field.
 92 
 93     The default implementation will simply retrieve the value attribute from
 94     the first input tag in the receiver view.
 95 
 96     @returns {String} value
 97   */
 98   getFieldValue: function() {
 99     return this.$input().val();
100   },
101 
102   _field_fieldValueDidChange: function(evt) {
103     SC.run(function() {
104       this.fieldValueDidChange(NO);
105     }, this);
106   },
107 
108   /**
109     Your class should call this method anytime you think the value of the
110     input element may have changed.  This will retrieve the value and update
111     the value property of the view accordingly.
112 
113     If this is a partial change (i.e. the user is still editing the field and
114     you expect the value to change further), then be sure to pass YES for the
115     partialChange parameter.  This will change the kind of validation done on
116     the value.  Otherwise, the validator may mark the field as having an error
117     when the user is still in mid-edit.
118 
119     @param partialChange (optional) YES if this is a partial change.
120     @returns {Boolean|SC.Error} result of validation.
121   */
122   fieldValueDidChange: function(partialChange) {
123     // collect the field value and convert it back to a value
124     var fieldValue = this.getFieldValue();
125     var value = this.objectForFieldValue(fieldValue, partialChange);
126     this.setIfChanged('value', value);
127 
128 
129     // ======= [Old code -- left here for concept reminders. Basic validation
130     // API works without it] =======
131 
132     // validate value if needed...
133 
134     // this.notifyPropertyChange('fieldValue');
135     //
136     // // get the field value and set it.
137     // // if ret is an error, use that instead of the field value.
138     // var ret = this.performValidate ? this.performValidate(partialChange) : YES;
139     // if (ret === SC.VALIDATE_NO_CHANGE) return ret ;
140     //
141     // this.propertyWillChange('fieldValue');
142     //
143     // // if the validator says everything is OK, then in addition to posting
144     // // out the value, go ahead and pass the value back through itself.
145     // // This way if you have a formatter applied, it will reformat.
146     // //
147     // // Do this BEFORE we set the value so that the valueObserver will not
148     // // overreact.
149     // //
150     // var ok = SC.$ok(ret);
151     // var value = ok ? this._field_getFieldValue() : ret ;
152     // if (!partialChange && ok) this._field_setFieldValue(value) ;
153     // this.set('value',value) ;
154     //
155     // this.propertyDidChange('fieldValue');
156     //
157     // return ret ;
158   },
159 
160   // ..........................................................
161   // INTERNAL SUPPORT
162   //
163 
164   /** @private
165     invoked when the value property changes.  Sets the field value...
166   */
167   _field_valueDidChange: function() {
168     this.setFieldValue(this.get('fieldValue'));
169   }.observes('fieldValue'),
170 
171   /**
172     SC.View view state callback.
173 
174     After the layer is created, set the field value and begin observing
175     change events on the input field.
176   */
177   didCreateLayer: function() {
178     this.setFieldValue(this.get('fieldValue'));
179     this._addChangeEvent();
180   },
181 
182   /**
183     SC.View state callback.
184 
185     Removes the change event from the input field.
186   */
187   willDestroyLayer: function() {
188     SC.Event.remove(this.$input(), 'change', this, this._field_fieldValueDidChange);
189   },
190 
191   // ACTIONS
192   // You generally do not need to override these but they may be used.
193 
194   /**
195     Called to perform validation on the field just before the form
196     is submitted.  If you have a validator attached, this will get the
197     validators.
198   */
199   // validateSubmit: function() {
200   //   var ret = this.performValidateSubmit ? this.performValidateSubmit() : YES;
201   //   // save the value if needed
202   //   var value = SC.$ok(ret) ? this._field_getFieldValue() : ret ;
203   //   if (value != this.get('value')) this.set('value', value) ;
204   //   return ret ;
205   // },
206 
207   // OVERRIDE IN YOUR SUBCLASS
208   // Override these primitives in your subclass as required.
209 
210   /**
211     SC.RootResponder event handler.
212 
213     Allow the browser to do its normal event handling for the mouse down
214     event.  But first, set isActive to YES.
215   */
216   mouseDown: function(evt) {
217     this._field_isMouseDown = YES;
218     evt.allowDefault();
219     return YES;
220   },
221 
222   /**
223     SC.RootResponder event handler.
224 
225     Remove the active class on mouseExited if mouse is down.
226   */
227   mouseExited: function(evt) {
228     if (this._field_isMouseDown) this.set('isActive', NO);
229     evt.allowDefault();
230     return YES;
231   },
232 
233   /**
234     SC.RootResponder event handler.
235 
236     If mouse was down and we renter the button area, set the active state again.
237   */
238   mouseEntered: function(evt) {
239     this.set('isActive', this._field_isMouseDown);
240     evt.allowDefault();
241     return YES;
242   },
243 
244   /**
245     SC.RootResponder event handler.
246 
247     On mouse up, remove the isActive class and then allow the browser to do
248     its normal thing.
249   */
250   mouseUp: function(evt) {
251     // track independently in case isEnabled has changed
252     if (this._field_isMouseDown) this.set('isActive', NO);
253     this._field_isMouseDown = NO;
254     evt.allowDefault();
255     return YES ;
256   },
257 
258   /**
259     SC.RootResponder event handler.
260 
261     Simply allow keyDown & keyUp to pass through to the default web browser
262     implementation.
263   */
264   keyDown: function(evt) {
265 
266     // handle tab key
267     if (evt.which === 9 || evt.keyCode===9) {
268       var view = evt.shiftKey ? this.get('previousValidKeyView') : this.get('nextValidKeyView');
269       if (view) view.becomeFirstResponder();
270       else evt.allowDefault();
271       return YES ; // handled
272     }
273 
274     // validate keyDown...
275     if (this.performValidateKeyDown(evt)) {
276       this._isKeyDown = YES ;
277       evt.allowDefault();
278     } else {
279       evt.stop();
280     }
281 
282     return YES;
283   },
284 
285   /**
286     Override of SC.Responder.prototype.acceptsFirstResponder.
287 
288     Tied to the `isEnabledInPane` state.
289   */
290   acceptsFirstResponder: function() {
291     if (SC.FOCUS_ALL_CONTROLS) { return this.get('isEnabledInPane'); }
292     return NO;
293   }.property('isEnabledInPane'),
294 
295   /** @private */
296   _addChangeEvent: function() {
297     SC.Event.add(this.$input(), 'change', this, this._field_fieldValueDidChange);
298   },
299 
300   /** @private */
301   // these methods use the validator to convert the raw field value returned
302   // by your subclass into an object and visa versa.
303   _field_setFieldValue: function(newValue) {
304     this.propertyWillChange('fieldValue');
305     if (this.fieldValueForObject) {
306       newValue = this.fieldValueForObject(newValue) ;
307     }
308     var ret = this.setFieldValue(newValue) ;
309     this.propertyDidChange('fieldValue');
310     return ret ;
311   },
312 
313   /** @private */
314   _field_getFieldValue: function() {
315     var ret = this.getFieldValue() ;
316     if (this.objectForFieldValue) ret = this.objectForFieldValue(ret);
317     return ret ;
318   }
319 });
320 
321