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