1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 sc_require('views/template');
  9 
 10 /**
 11   @class
 12 */
 13 SC.TextFieldSupport = /** @scope SC.TextFieldSupport */{
 14 
 15   $input: function() {
 16     return this.$('input');
 17   },
 18 
 19   /** @private
 20     Used internally to store value because the layer may not exist
 21   */
 22   _value: null,
 23 
 24   /**
 25     The problem this property is trying to solve is twofold:
 26 
 27     1. Make it possible to set the value of a text field that has
 28        not yet been inserted into the DOM
 29     2. Make sure that `value` properly reflects changes made directly
 30        to the element's `value` property.
 31 
 32     In order to achieve (2), we need to make the property volatile,
 33     so that SproutCore will call the getter no matter what if get()
 34     is called.
 35 
 36     In order to achieve (1), we need to store a local cache of the
 37     value, so that SproutCore can set the proper value as soon as
 38     the underlying DOM element is created.
 39 
 40     @type String
 41     @default  null
 42   */
 43   value: function(key, value) {
 44     var input = this.$input();
 45 
 46     if (value !== undefined) {
 47       // We don't want to unnecessarily set the value.
 48       // Doing that could cause the selection to be lost.
 49       if (this._value !== value) { this._value = value; }
 50       if (input.val() !== value) { input.val(value); }
 51     } else {
 52       if (input.length > 0) {
 53         value = this._value = input.val();
 54       } else {
 55         value = this._value;
 56       }
 57     }
 58 
 59     return value;
 60   }.property().idempotent(),
 61 
 62   didCreateLayer: function() {
 63     var input = this.$input(),
 64         self = this;
 65 
 66     input.val(this._value);
 67 
 68     if (SC.browser.isIE) {
 69       SC.Event.add(input, 'focusin', this, this.focusIn);
 70       SC.Event.add(input, 'focusout', this, this.focusOut);
 71     } else {
 72       SC.Event.add(input, 'focus', this, this.focusIn);
 73       SC.Event.add(input, 'blur', this, this.focusOut);
 74     }
 75   },
 76 
 77   willDestroyLayerMixin: function() {
 78     var input = this.$input();
 79 
 80     if (SC.browser.isIE) {
 81       SC.Event.remove(input, 'focusin', this, this.focusIn);
 82       SC.Event.remove(input, 'focusout', this, this.focusOut);
 83     } else {
 84       SC.Event.remove(input, 'focus', this, this.focusIn);
 85       SC.Event.remove(input, 'blur', this, this.focusOut);
 86     }
 87   },
 88 
 89   focusIn: function(event) {
 90     this.becomeFirstResponder();
 91     this.tryToPerform('focus', event);
 92   },
 93 
 94   focusOut: function(event) {
 95     this.resignFirstResponder();
 96     this.tryToPerform('blur', event);
 97   },
 98 
 99   touchStart: function(evt) {
100     evt.allowDefault();
101     return YES;
102   },
103 
104   touchEnd: function(evt) {
105     evt.allowDefault();
106     return YES;
107   },
108 
109   /** @private
110     Make sure our input value is synced with any bindings.
111     In some cases, such as auto-filling, a value can get
112     changed without an event firing. We could do this
113     on focusOut, but blur can potentially get called
114     after other events.
115   */
116   willLoseFirstResponder: function() {
117     this.notifyPropertyChange('value');
118   },
119 
120   domValueDidChange: function(jquery) {
121     this.set('value', jquery.val());
122   },
123 
124   keyUp: function(event) {
125     this.domValueDidChange(this.$input());
126 
127     if (event.keyCode === SC.Event.KEY_RETURN) {
128       return this.tryToPerform('insertNewline', event);
129     } else if (event.keyCode === SC.Event.KEY_ESC) {
130       return this.tryToPerform('cancel', event);
131     }
132   },
133 
134   /** @private
135     RootResponder will call this function whenever a selection
136     event has occurred, for instance a select all. Simply return
137     true so that all selection events bubble up to the browser,
138     triggering the default browser behavior.
139   */
140   selectStart: function() {
141     return true;
142   }
143 
144 };
145 
146 /**
147   @class
148   @extends SC.TemplateView
149   @extends SC.TextFieldSupport
150 */
151 SC.TextField = SC.TemplateView.extend(SC.TextFieldSupport,
152 /** @scope SC.TextField.prototype */ {
153 
154   classNames: ['sc-text-field'],
155 
156   /**
157     If set to `YES` uses textarea tag instead of input to
158     accommodate multi-line strings.
159 
160     @type Boolean
161     @default NO
162   */
163   isMultiline: NO,
164 
165   // we can't use bindAttr because of a race condition:
166   //
167   // when `value` is set, the bindAttr observer immediately calls
168   // `get` in order to persist it to the DOM, but because we made
169   // the `value` property idempotent, when it gets called by
170   // bindAttr, it fetches the not-yet-updated value from the DOM
171   // and returns it.
172   //
173   // In short, because we need to be able to catch changes to the
174   // DOM made directly, we cannot also rely on bindAttr to update
175   // the property: a chicken-and-egg problem.
176   template: function(){
177     return SC.Handlebars.compile(this.get('isMultiline') ? '<textarea></textarea>' : '<input type="text">');
178   }.property('isMultiline').cacheable(),
179 
180   $input: function() {
181     var tagName = this.get('isMultiline') ? 'textarea' : 'input';
182     return this.$(tagName);
183   }
184 
185 });
186 
187