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('views/field');
  9 sc_require('system/text_selection');
 10 sc_require('mixins/editable');
 11 
 12 SC.AUTOCAPITALIZE_NONE = 'none';
 13 SC.AUTOCAPITALIZE_SENTENCES = 'sentences';
 14 SC.AUTOCAPITALIZE_WORDS = 'words';
 15 SC.AUTOCAPITALIZE_CHARACTERS = 'characters';
 16 
 17 /**
 18   @class
 19 
 20   A text field is an input element with type "text".  This view adds support
 21   for hinted values, etc.
 22 
 23   @extends SC.FieldView
 24   @extends SC.Editable
 25   @author Charles Jolley
 26  */
 27 SC.TextFieldView = SC.FieldView.extend(SC.Editable,
 28   /** @scope SC.TextFieldView.prototype */ {
 29 
 30   classNames: ['sc-text-field-view'],
 31 
 32   /**
 33     Walk like a duck.
 34 
 35     @type Boolean
 36     @default true
 37     @readOnly
 38    */
 39   isTextField: true,
 40 
 41   // ..........................................................
 42   // PROPERTIES
 43   //
 44 
 45   /**
 46     When `applyImmediately` is turned on, every keystroke will set the value
 47     of the underlying object. Turning it off will only set the value on blur.
 48 
 49     @type Boolean
 50     @default true
 51    */
 52   applyImmediately: true,
 53 
 54   /**
 55     Flag indicating whether the editor should automatically commit if you click
 56     outside it.
 57 
 58     @type Boolean
 59     @default true
 60    */
 61   commitOnBlur: true,
 62 
 63   /**
 64     If `true` then allow multi-line input.  This will also change the default
 65     tag type from "input" to "textarea".  Otherwise, pressing return will
 66     trigger the default insertion handler.
 67 
 68     @type Boolean
 69     @default false
 70    */
 71   isTextArea: false,
 72 
 73   /**
 74     Whether the text field is currently focused.
 75 
 76     @type Boolean
 77     @default false
 78    */
 79   focused: false,
 80 
 81   /**
 82     The hint to display while the field is not active.
 83 
 84     @type String
 85     @default ""
 86    */
 87   hint: '',
 88 
 89   /**
 90     The type attribute of the input.
 91 
 92     @type String
 93     @default "text"
 94    */
 95   type: 'text',
 96 
 97   /**
 98     This property will set a tabindex="-1" on your view if set to false.
 99 
100     This gives us control over the native tabbing behavior. When nextValidKeyView
101     reaches the end of the views in the pane views tree, it won't go to a textfield
102     that can accept the default tabbing behavior in any other pane. This was a
103     problem when you had an alert on top of a mainPane with textfields.
104 
105     Modal panes set this to false on all textfields that don't belong to itself.
106     @type Boolean
107     @default true
108    */
109   isBrowserFocusable: true,
110 
111   /**
112     Whether the browser should automatically correct the input.
113 
114     When `autoCorrect` is set to `null`, the browser will use
115     the system defaults.
116 
117     @type Boolean
118     @default true
119    */
120   autoCorrect: true,
121 
122   /**
123     Specifies the autocapitalization behavior.
124 
125     Possible values are:
126 
127      - `SC.AUTOCAPITALIZE_NONE` -- Do not autocapitalize.
128      - `SC.AUTOCAPITALIZE_SENTENCES` -- Autocapitalize the first letter of each
129        sentence.
130      - `SC.AUTOCAPITALIZE_WORDS` -- Autocapitalize the first letter of each word.
131      - `SC.AUTOCAPITALIZE_CHARACTERS` -- Autocapitalize all characters.
132 
133     Boolean values are also supported, with `true` interpreted as
134     `SC.AUTOCAPITALIZE_NONE` and `false` as `SC.AUTOCAPITALIZE_SENTENCES`.
135 
136     When `autoCapitalize` is set to `null`, the browser will use
137     the system defaults.
138 
139     @type String SC.AUTOCAPITALIZE_NONE|SC.AUTOCAPITALIZE_SENTENCES|SC.AUTOCAPITALIZE_WORDS|SC.AUTOCAPITALIZE_CHARACTERS
140     @default SC.CAPITALIZE_SENTENCES
141    */
142   autoCapitalize: SC.CAPITALIZE_SENTENCES,
143 
144   /**
145     Whether the browser should automatically complete the input.
146 
147     When `autoComplete` is set to `null`, the browser will use
148     the system defaults.
149 
150     @type Boolean
151     @default null
152    */
153   autoComplete: null,
154 
155   /**
156     Localizes and escapes the hint if necessary.
157 
158     @field
159     @type String
160    */
161   formattedHint: function () {
162     var hint = this.get('hint');
163     hint = typeof(hint) === 'string' && this.get('localize') ? SC.String.loc(hint) : hint;
164 
165     // If the hint is appended via an overlay, ensure that the text is escaped in order to avoid XSS attacks.
166     if (this.get('useHintOverlay')) {
167       hint = this.get('escapeHTML') ? SC.RenderContext.escapeHTML(hint) : hint;
168     }
169 
170     return hint;
171   }.property('hint', 'localize').cacheable(),
172 
173   /**
174     Whether to show the hint while the field still has focus.
175 
176     While newer versions of Safari, Firefox and Chrome will act this way using the
177     placeholder attribute, other browsers will not. By setting this property
178     to true, we can ensure that the hint will always appear even when the
179     field has focus.
180 
181     Note: If `hintOnFocus` is false, this doesn't necessarily mean that the
182     hint will disappear on focus, because some browsers will still not remove
183     the placeholder on focus when empty.
184 
185     *Important:* You can not modify this property once the view has been rendered.
186 
187     @type Boolean
188     @default true
189    */
190   hintOnFocus: true,
191 
192   /**
193     Whether the hint should be localized or not.
194 
195     @type Boolean
196     @default true
197    */
198   localize: true,
199 
200   /**
201     If `true` then the text field is currently editing.
202 
203     @type Boolean
204     @default false
205    */
206   isEditing: false,
207 
208   /**
209     If you set this property to false the tab key won't trigger its default
210     behavior (tabbing to the next field).
211 
212     @type Boolean
213     @default true
214    */
215   defaultTabbingEnabled: true,
216 
217   /**
218     Enabled context menu for textfields.
219 
220     @type Boolean
221     @default true
222    */
223   isContextMenuEnabled: true,
224 
225   /**
226     If no, will not allow transform or validation errors (SC.Error objects)
227     to be passed to `value`.  Upon focus lost, the text field will revert
228     to its previous value.
229 
230     @type Boolean
231     @default true
232    */
233   allowsErrorAsValue: true,
234 
235   /**
236     An optional view instance, or view class reference, which will be visible
237     on the left side of the text field.  Visually the accessory view will look
238     to be inside the field but the text editing will not overlap the accessory
239     view.
240 
241     The view will be rooted to the top-left of the text field.  You should use
242     a layout with 'left' and/or 'top' specified if you would like to adjust
243     the offset from the top-left.
244 
245     One example use would be for a web site's icon, found to the left of the
246     URL field, in many popular web browsers.
247 
248     Note:  If you set a left accessory view, the left padding of the text
249     field (really, the left offset of the padding element) will automatically
250     be set to the width of the accessory view, overriding any CSS you may have
251     defined on the "padding" element.  If you would like to customize the
252     amount of left padding used when the accessory view is visible, make the
253     accessory view wider, with empty space on the right.
254 
255     @type SC.View
256     @default null
257    */
258   leftAccessoryView: null,
259 
260   /**
261     An optional view instance, or view class reference, which will be visible
262     on the right side of the text field.  Visually the accessory view will
263     look to be inside the field but the text editing will not overlap the
264     accessory view.
265 
266     The view will be rooted to the top-right of the text field.  You should
267     use a layout with 'right' and/or 'top' specified if you would like to
268     adjust the offset from the top-right.  If 'left' is specified in the
269     layout it will be cleared.
270 
271     One example use would be for a button to clear the contents of the text
272     field.
273 
274     Note:  If you set a right accessory view, the right padding of the text
275     field (really, the right offset of the padding element) will automatically
276     be set to the width of the accessory view, overriding any CSS you may have
277     defined on the "padding" element.  If you would like to customize the
278     amount of right padding used when the accessory view is visible, make the
279     accessory view wider, with empty space on the left.
280 
281     @type SC.View
282     @default null
283    */
284   rightAccessoryView: null,
285 
286   /**
287     This property will enable disable HTML5 spell checking if available on the
288     browser. As of today Safari 4+, Chrome 3+ and Firefox 3+ support it.
289 
290     @type Boolean
291     @default true
292    */
293   spellCheckEnabled: true,
294 
295   /**
296     Maximum amount of characters this field will allow.
297 
298     @type Number
299     @default 5096
300    */
301   maxLength: 5096,
302 
303   /**
304     Whether to render a border or not.
305 
306     @type Boolean
307     @default true
308    */
309   shouldRenderBorder: true,
310 
311   // ..........................................................
312   // SUPPORT FOR AUTOMATIC RESIZING
313   //
314 
315   /**
316     Text fields support auto resizing.
317     @type Boolean
318     @default true
319     @see SC.AutoResize#supportsAutoResize
320    */
321   supportsAutoResize: true,
322 
323   /**
324     The layer to automatically resize.
325 
326     @type DOMElement
327     @see SC.AutoResize#autoResizeLayer
328    */
329   autoResizeLayer: function () {
330     return this.$input()[0];
331   }.property('layer').cacheable(),
332 
333   /**
334     The text to be used when automatically resizing the text field.
335 
336     @type String
337     @see SC.AutoResize#autoResizeText
338    */
339   autoResizeText: function () {
340     return this.get('value');
341   }.property('value').cacheable(),
342 
343   /**
344     How much padding should be used when automatically resizing.
345     @type Number
346     @default 20
347     @see SC.AutoResize#autoResizePadding
348    */
349   autoResizePadding: SC.propertyFromRenderDelegate('autoResizePadding', 20),
350 
351   /**
352     This property indicates if the value in the text field can be changed.
353     If set to `false`, a `readOnly` attribute will be added to the DOM Element.
354 
355     Note if `isEnabledInPane` is `false` this property will have no effect.
356 
357     @type Boolean
358     @default true
359    */
360   isEditable: true,
361 
362   /**
363     The current selection of the text field, returned as an SC.TextSelection
364     object.
365 
366     Note that if the selection changes a new object will be returned -- it is
367     not the case that a previously-returned SC.TextSelection object will
368     simply have its properties mutated.
369 
370     @field
371     @type SC.TextSelection
372    */
373   selection: function (key, value) {
374     var element = this.$input()[0],
375         direction = 'none',
376         range, start, end;
377 
378     // Are we being asked to set the value, or return the current value?
379     if (value === undefined) {
380       // The client is retrieving the value.
381       if (element) {
382         start = null;
383         end = null;
384 
385         if (!element.value) {
386           start = end = 0;
387         } else {
388           // In IE8, input elements don't have hasOwnProperty() defined.
389           try {
390             if (SC.platform.input.selectionStart) {
391               start = element.selectionStart;
392             }
393             if (SC.platform.input.selectionEnd) {
394               end = element.selectionEnd;
395             }
396             if (SC.platform.input.selectionDirection) {
397               direction = element.selectionDirection;
398             }
399           }
400           // In Firefox when you ask the selectionStart or End of a hidden
401           // input, sometimes it throws a weird error.
402           // Adding this to just ignore it.
403           catch (e) {
404             return null;
405           }
406 
407           // Support Internet Explorer.
408           if (start === null  ||  end === null) {
409             var selection = document.selection;
410             if (selection) {
411               var type = selection.type;
412               if (type  &&  (type === 'None'  ||  type === 'Text')) {
413                 range = selection.createRange();
414 
415                 if (!this.get('isTextArea')) {
416                   // Input tag support.  Figure out the starting position by
417                   // moving the range's start position as far left as possible
418                   // and seeing how many characters it actually moved over.
419                   var length = range.text.length;
420                   start = Math.abs(range.moveStart('character', 0 - (element.value.length + 1)));
421                   end = start + length;
422                 } else {
423                   // Textarea support.  Unfortunately, this case is a bit more
424                   // complicated than the input tag case.  We need to create a
425                   // "dummy" range to help in the calculations.
426                   var dummyRange = range.duplicate();
427                   dummyRange.moveToElementText(element);
428                   dummyRange.setEndPoint('EndToStart', range);
429                   start = dummyRange.text.length;
430                   end = start + range.text.length;
431                 }
432               }
433             }
434           }
435         }
436 
437         return SC.TextSelection.create({ start: start, end: end, direction: direction });
438       } else {
439         return null;
440       }
441     } else {
442       // The client is setting the value.  Make sure the new value is a text
443       // selection object.
444       if (!value  ||  !value.kindOf  ||  !value.kindOf(SC.TextSelection)) {
445         throw new Error("When setting the selection, you must specify an SC.TextSelection instance.");
446       }
447 
448       if (element) {
449         if (element.setSelectionRange) {
450           try {
451             element.setSelectionRange(value.get('start'), value.get('end'), value.get('direction'));
452           } catch (e) {
453             // In Firefox & IE when you call setSelectionRange on a hidden input it will throw weird
454             // errors. Adding this to just ignore it.
455             return null;
456           }
457 
458           if (!SC.platform.input.selectionDirection) {
459             // Browser doesn't support selectionDirection, set it to 'none' so the wrong value is not cached.
460             value.set('direction', 'none');
461           }
462         } else {
463           // Support Internet Explorer.
464           range = element.createTextRange();
465           start = value.get('start');
466           range.move('character', start);
467           range.moveEnd('character', value.get('end') - start);
468           range.select();
469         }
470       }
471 
472       return value;
473     }
474 
475     // Implementation note:
476     // There are certain ways users can add/remove text that we can't identify
477     // via our key/mouse down/up handlers (such as the user choosing Paste
478     // from a menu).  So that's why we need to update our 'selection' property
479     // whenever the field's value changes.
480   }.property('fieldValue').cacheable(),
481 
482   /**
483     Whether or not the text field view will use an overlaid label for the hint.
484 
485     There are two conditions that will result in the text field adding an
486     overlaid label for the hint. The first is when the `hintOnFocus` property is
487     true. This allows the user to focus the text field and still see the hint
488     text while there is no value in the field. Since some browsers clear the
489     placeholder when the field has text, this is a way to ensure the same
490     behavior across all browsers.
491 
492     The second is when the browser doesn't support the placeholder attribute
493     (i.e. < IE 10). By using an overlaid label rather than inserting the hint
494     into the input, we are able to show clear text hints over password fields.
495 
496     @field
497     @type Boolean
498     @default true
499     @readonly
500   */
501   useHintOverlay: function () {
502     return this.get('hintOnFocus') || !SC.platform.input.placeholder;
503   }.property().cacheable(),
504 
505   // ..........................................................
506   // INTERNAL SUPPORT
507   //
508 
509   // Note: isEnabledInPane is required here because it is used in the renderMixin function of
510   // SC.Control. It is not a display property directly in SC.Control, because the use of it in
511   // SC.Control is only applied to input fields, which very few consumers of SC.Control have.
512   // TODO: Pull the disabled attribute updating out of SC.Control.
513   displayProperties: ['isBrowserFocusable', 'formattedHint', 'fieldValue', 'isEditing', 'isEditable', 'isEnabledInPane',
514                       'leftAccessoryView', 'rightAccessoryView', 'isTextArea', 'maxLength'],
515 
516   createChildViews: function () {
517     sc_super();
518     this.accessoryViewObserver();
519   },
520 
521   acceptsFirstResponder: function () {
522     return this.get('isEnabledInPane');
523   }.property('isEnabledInPane'),
524 
525   accessoryViewObserver: function () {
526     var classNames,
527         viewProperties = ['leftAccessoryView', 'rightAccessoryView'],
528         len = viewProperties.length, i, viewProperty, previousView,
529         accessoryView;
530 
531     for (i = 0; i < len; i++) {
532       viewProperty = viewProperties[i];
533 
534       // Is there an accessory view specified?
535       previousView = this['_' + viewProperty];
536       accessoryView = this.get(viewProperty);
537 
538       // If the view is the same, there's nothing to do.  Otherwise, remove
539       // the old one (if any) and add the new one.
540       if (! (previousView &&
541              accessoryView &&
542              (previousView === accessoryView))) {
543 
544         // If there was a previous previous accessory view, remove it now.
545         if (previousView) {
546           // Remove the "sc-text-field-accessory-view" class name that we had
547           // added earlier.
548           classNames = previousView.get('classNames');
549           classNames = classNames.without('sc-text-field-accessory-view');
550           previousView.set('classNames', classNames);
551 
552           if (previousView.createdByParent) {
553             this.removeChildAndDestroy(previousView);
554           } else {
555             this.removeChild(previousView);
556           }
557 
558           // Tidy up.
559           previousView = this['_' + viewProperty] = this['_created' + viewProperty] = null;
560         }
561 
562         // If there's a new accessory view to add, do so now.
563         if (accessoryView) {
564           // If the user passed in a class rather than an instance, create an
565           // instance now.
566           accessoryView = this.createChildView(accessoryView);
567 
568           // Fix up right accessory views to be right positioned.
569           if (viewProperty === 'rightAccessoryView') {
570             var layout = accessoryView.get('layout');
571 
572             accessoryView.adjust({ left: null, right: layout.right || 0 });
573           }
574 
575           // Add in the "sc-text-field-accessory-view" class name so that the
576           // z-index gets set correctly.
577           classNames = accessoryView.get('classNames');
578           var className = 'sc-text-field-accessory-view';
579           if (classNames.indexOf(className) < 0) {
580             classNames = SC.clone(classNames);
581             classNames.push(className);
582             accessoryView.set('classNames', classNames);
583           }
584 
585           // Actually add the view to our hierarchy and cache a reference.
586           this.appendChild(accessoryView);
587           this['_' + viewProperty] = accessoryView;
588         }
589       }
590     }
591   }.observes('leftAccessoryView', 'rightAccessoryView'),
592 
593   render: function (context, firstTime) {
594     var v, accessoryViewWidths, leftAdjustment, rightAdjustment;
595 
596     // always have at least an empty string
597     v = this.get('fieldValue');
598     if (SC.none(v)) v = '';
599     v = String(v);
600 
601     // update layer classes always
602     context.setClass('not-empty', v.length > 0);
603 
604     // If we have accessory views, we'll want to update the padding on the
605     // hint to compensate for the width of the accessory view.  (It'd be nice
606     // if we could add in the original padding, too, but there's no efficient
607     // way to do that without first rendering the element somewhere on/off-
608     // screen, and we don't want to take the performance hit.)
609     accessoryViewWidths = this._getAccessoryViewWidths();
610     leftAdjustment  = accessoryViewWidths.left;
611     rightAdjustment = accessoryViewWidths.right;
612 
613     if (leftAdjustment)  leftAdjustment  += 'px';
614     if (rightAdjustment) rightAdjustment += 'px';
615 
616     this._renderField(context, firstTime, v, leftAdjustment, rightAdjustment);
617   },
618 
619   /** @private
620     If isTextArea is changed (this might happen in inlineeditor constantly)
621     force the field render to render like the firsttime to avoid writing extra
622     code. This can be useful also
623    */
624   _forceRenderFirstTime: false,
625 
626   /** @private */
627   _renderFieldLikeFirstTime: function () {
628     this.set('_forceRenderFirstTime', true);
629   }.observes('isTextArea'),
630 
631   /** @private */
632   _renderField: function (context, firstTime, value, leftAdjustment, rightAdjustment) {
633     // TODO:  The cleanest thing might be to create a sub- rendering context
634     //        here, but currently SC.RenderContext will render sibling
635     //        contexts as parent/child.
636     var hint = this.get('formattedHint'),
637       hintAttr = '',
638       maxLength = this.get('maxLength'),
639       isTextArea = this.get('isTextArea'),
640       isEnabledInPane = this.get('isEnabledInPane'),
641       isEditable = this.get('isEditable'),
642       autoCorrect = this.get('autoCorrect'),
643       autoCapitalize = this.get('autoCapitalize'),
644       autoComplete = this.get('autoComplete'),
645       isBrowserFocusable = this.get('isBrowserFocusable'),
646       spellCheckString = '', autocapitalizeString = '', autocorrectString = '',
647       autocompleteString = '', activeStateString = '', browserFocusableString = '',
648       name, adjustmentStyle, type, paddingElementStyle,
649       fieldClassNames, isOldSafari;
650 
651     context.setClass('text-area', isTextArea);
652 
653     //Adding this to differentiate between older and newer versions of safari
654     //since the internal default field padding changed
655     isOldSafari = SC.browser.isWebkit &&
656         SC.browser.compare(SC.browser.engineVersion, '532') < 0;
657     context.setClass('oldWebKitFieldPadding', isOldSafari);
658 
659 
660     if (firstTime || this._forceRenderFirstTime) {
661       this._forceRenderFirstTime = false;
662       activeStateString = isEnabledInPane ? (isEditable ? '' : ' readonly="readonly"') : ' disabled="disabled"';
663       name = this.get('layerId');
664 
665       spellCheckString = this.get('spellCheckEnabled') ? ' spellcheck="true"' : ' spellcheck="false"';
666 
667       if (!SC.none(autoCorrect)) {
668         autocorrectString = ' autocorrect=' + (!autoCorrect ? '"off"' : '"on"');
669       }
670 
671       if (!SC.none(autoCapitalize)) {
672         if (SC.typeOf(autoCapitalize) === 'boolean') {
673           autocapitalizeString = ' autocapitalize=' + (!autoCapitalize ? '"none"' : '"sentences"');
674         } else {
675           autocapitalizeString = ' autocapitalize=' + autoCapitalize;
676         }
677       }
678 
679       if (!SC.none(autoComplete)) {
680         autocompleteString = ' autocomplete=' + (!autoComplete ? '"off"' : '"on"');
681       }
682 
683       if (!isBrowserFocusable) {
684         browserFocusableString = ' tabindex="-1"';
685       }
686 
687       if (this.get('shouldRenderBorder')) context.push('<div class="border"></div>');
688 
689       // Render the padding element, with any necessary positioning
690       // adjustments to accommodate accessory views.
691       adjustmentStyle = '';
692       if (leftAdjustment || rightAdjustment) {
693         adjustmentStyle = 'style="';
694         if (leftAdjustment)  adjustmentStyle += 'left:'  + leftAdjustment  + ';';
695         if (rightAdjustment) adjustmentStyle += 'right:' + rightAdjustment + ';';
696         adjustmentStyle += '"';
697       }
698       context.push('<div class="padding" ' + adjustmentStyle + '>');
699 
700       value = this.get('escapeHTML') ? SC.RenderContext.escapeHTML(value) : value;
701 
702       // When hintOnFocus is true or the field doesn't support placeholders, ensure that a hint appears by adding an overlay hint element.
703       if (this.get('useHintOverlay')) {
704         var hintOverlay = '<div aria-hidden="true" class="hint ' +
705                       (isTextArea ? '':'ellipsis') + '%@">' + hint + '</div>';
706         context.push(hintOverlay.fmt(value ? ' sc-hidden': ''));
707 
708       // Use the input placeholder attribute for the hint.
709       } else {
710         hintAttr = ' placeholder="' + hint + '"';
711       }
712 
713       fieldClassNames = "field";
714 
715       // Render the input/textarea field itself, and close off the padding.
716       if (isTextArea) {
717         context.push('<textarea aria-label="' + hint + '" class="' + fieldClassNames + '" aria-multiline="true"' +
718                       '" name="' + name + '"' + activeStateString + hintAttr +
719                       spellCheckString + autocorrectString + autocapitalizeString +
720                       browserFocusableString + ' maxlength="' + maxLength +
721                       '">' + value + '</textarea></div>');
722       } else {
723         type = this.get('type');
724         context.push('<input aria-label="' + hint + '" class="' + fieldClassNames + '" type="' + type +
725                       '" name="' + name + '"' + activeStateString + hintAttr +
726                       spellCheckString + autocorrectString + autocapitalizeString +
727                       autocompleteString + browserFocusableString + ' maxlength="' + maxLength +
728                       '" value="' + value + '"' + '/></div>');
729       }
730     } else {
731       var input = this.$input(),
732         element = input[0];
733 
734       // Update the hint. If the overlay hint was used, update it.
735       if (this.get('useHintOverlay')) {
736         context.$('.hint')[0].innerHTML = hint;
737       } else {
738         input.attr('placeholder', hint);
739       }
740 
741       input.attr('maxLength', maxLength);
742 
743       // IE8 has problems aligning the input text in the center
744       // This is a workaround for centering it.
745       if (SC.browser.name === SC.BROWSER.ie && SC.browser.version <= 8 && !isTextArea) {
746         input.css('line-height', this.get('frame').height + 'px');
747       }
748 
749       if (!SC.none(autoCorrect)) {
750         input.attr('autocorrect', !autoCorrect ? 'off' : 'on');
751       } else {
752         input.attr('autocorrect', null);
753       }
754 
755       if (!SC.none(autoCapitalize)) {
756         if (SC.typeOf(autoCapitalize) === 'boolean') {
757           input.attr('autocapitalize', !autoCapitalize ? 'none' : 'sentences');
758         } else {
759           input.attr('autocapitalize', autoCapitalize);
760         }
761       } else {
762         input.attr('autocapitalize', null);
763       }
764 
765       if (!SC.none(autoComplete)) {
766         input.attr('autoComplete', !autoComplete ? 'off' : 'on');
767       } else {
768         input.attr('autoComplete', null);
769       }
770 
771       if (isBrowserFocusable) {
772         input.removeAttr('tabindex');
773       } else {
774         input.attr('tabindex', '-1');
775       }
776 
777       // Enable/disable the actual input/textarea as appropriate.
778       if (!isEditable) {
779         input.attr('readOnly', true);
780       } else {
781         input.attr('readOnly', null);
782       }
783 
784       if (element) {
785         // Adjust the padding element to accommodate any accessory views.
786         paddingElementStyle = element.parentNode.style;
787         if (leftAdjustment) {
788           if (paddingElementStyle.left !== leftAdjustment) {
789             paddingElementStyle.left = leftAdjustment;
790           }
791         } else {
792           paddingElementStyle.left = null;
793         }
794 
795         if (rightAdjustment) {
796           if (paddingElementStyle.right !== rightAdjustment) {
797             paddingElementStyle.right = rightAdjustment;
798           }
799         } else {
800           paddingElementStyle.right = null;
801         }
802       }
803     }
804   },
805 
806   _getAccessoryViewWidths: function () {
807     var widths = {},
808         accessoryViewPositions = ['left', 'right'],
809         numberOfAccessoryViewPositions = accessoryViewPositions.length, i,
810         position, accessoryView, width, layout, offset, frame;
811     for (i = 0;  i < numberOfAccessoryViewPositions;  i++) {
812       position = accessoryViewPositions[i];
813       accessoryView = this['_' + position + 'AccessoryView'];
814       if (accessoryView && accessoryView.isObservable) {
815         frame = accessoryView.get('frame');
816         if (frame) {
817           width = frame.width;
818           if (width) {
819             // Also account for the accessory view's inset.
820             layout = accessoryView.get('layout');
821             if (layout) {
822               offset = layout[position];
823               width += offset;
824             }
825             widths[position] = width;
826           }
827         }
828       }
829     }
830     return widths;
831   },
832 
833   // ..........................................................
834   // HANDLE NATIVE CONTROL EVENTS
835   //
836 
837   /**
838     Override of SC.FieldView.prototype.didCreateLayer.
839   */
840   didCreateLayer: function () {
841     sc_super();
842 
843     // For some strange reason if we add focus/blur events to textarea
844     // inmediately they won't work. However if I add them at the end of the
845     // runLoop it works fine.
846     if (this.get('isTextArea')) {
847       this.invokeLast(this._addTextAreaEvents);
848     } else {
849       this._addTextAreaEvents();
850 
851       // In Firefox, for input fields only (that is, not textarea elements),
852       // if the cursor is at the end of the field, the "down" key will not
853       // result in a "keypress" event for the document (only for the input
854       // element), although it will be bubbled up in other contexts.  Since
855       // SproutCore's event dispatching requires the document to see the
856       // event, we'll manually forward the event along.
857       if (SC.browser.isMozilla) {
858         var input = this.$input();
859         SC.Event.add(input, 'keypress', this, this._firefox_dispatch_keypress);
860       }
861     }
862   },
863 
864   /**
865     SC.View view state callback.
866 
867     Once the view is appended, fix up the text layout to hint and input.
868   */
869   didAppendToDocument: function () {
870     this._fixupTextLayout();
871   },
872 
873   /** @private
874     Apply proper text layout to hint and input.
875    */
876   _fixupTextLayout: function () {
877     var height = this.get('frame').height;
878 
879     if (SC.browser.name === SC.BROWSER.ie && SC.browser.version <= 8 &&
880         !this.get('isTextArea')) {
881       this.$input().css('line-height', height + 'px');
882     }
883 
884     if (this.get('useHintOverlay') && !this.get('isTextArea')) {
885       var hintJQ = this.$('.hint');
886 
887       hintJQ.css('line-height', hintJQ.outerHeight() + 'px');
888     }
889   },
890 
891   /** @private
892     Adds all the textarea events. This functions is called by didCreateLayer
893     at different moments depending if it is a textarea or not. Appending
894     events to text areas is not reliable unless the element is already added
895     to the DOM.
896    */
897   _addTextAreaEvents: function () {
898     var input = this.$input();
899     SC.Event.add(input, 'focus', this, this._textField_fieldDidFocus);
900     SC.Event.add(input, 'blur',  this, this._textField_fieldDidBlur);
901 
902     // There are certain ways users can select text that we can't identify via
903     // our key/mouse down/up handlers (such as the user choosing Select All
904     // from a menu).
905     SC.Event.add(input, 'select', this, this._textField_selectionDidChange);
906 
907     // handle a "paste" from app menu and context menu
908     SC.Event.add(input, 'input', this, this._textField_inputDidChange);
909   },
910 
911   /**
912     Removes all the events attached to the textfield
913    */
914   willDestroyLayer: function () {
915     sc_super();
916 
917     var input = this.$input();
918     SC.Event.remove(input, 'focus',  this, this._textField_fieldDidFocus);
919     SC.Event.remove(input, 'blur',   this, this._textField_fieldDidBlur);
920     SC.Event.remove(input, 'select', this, this._textField_selectionDidChange);
921     SC.Event.remove(input, 'keypress',  this, this._firefox_dispatch_keypress);
922     SC.Event.remove(input, 'input', this, this._textField_inputDidChange);
923   },
924 
925   /** @private
926      This function is called by the event when the textfield gets focus
927   */
928   _textField_fieldDidFocus: function (evt) {
929     SC.run(function () {
930       this.set('focused', true);
931       this.fieldDidFocus(evt);
932     }, this);
933   },
934 
935   /** @private
936     This function is called by the event when the textfield blurs
937    */
938   _textField_fieldDidBlur: function (evt) {
939     SC.run(function () {
940       this.set('focused', false);
941       // passing the original event here instead that was potentially set from
942       // losing the responder on the inline text editor so that we can
943       // use it for the delegate to end editing
944       this.fieldDidBlur(this._origEvent || evt);
945     }, this);
946   },
947 
948   fieldDidFocus: function (evt) {
949     this.becomeFirstResponder();
950 
951     this.beginEditing(evt);
952 
953     // We have to hide the intercept pane, as it blocks the events.
954     // However, show any that we previously hid, first just in case something wacky happened.
955     if (this._didHideInterceptForPane) {
956       this._didHideInterceptForPane.showTouchIntercept();
957       this._didHideInterceptForPane = null;
958     }
959 
960     // now, hide the intercept on this pane if it has one
961     var pane = this.get('pane');
962     if (pane && pane.get('hasTouchIntercept')) {
963       // hide
964       pane.hideTouchIntercept();
965 
966       // and set our internal one so we can unhide it (even if the pane somehow changes)
967       this._didHideInterceptForPane = this.get("pane");
968     }
969   },
970 
971   fieldDidBlur: function (evt) {
972     this.resignFirstResponder(evt);
973 
974     if (this.get('commitOnBlur')) this.commitEditing(evt);
975 
976     // get the pane we hid intercept pane for (if any)
977     var touchPane = this._didHideInterceptForPane;
978     if (touchPane) {
979       touchPane.showTouchIntercept();
980       touchPane = null;
981     }
982   },
983 
984   /** @private */
985   _field_fieldValueDidChange: function (evt) {
986     if (this.get('focused')) {
987       SC.run(function () {
988         this.fieldValueDidChange(false);
989       }, this);
990     }
991 
992     this.updateHintOnFocus();
993   },
994 
995   /** @private
996     Context-menu paste does not trigger fieldValueDidChange normally. To do so, we'll capture the
997     input event and avoid duplicating the "fieldValueDidChange" call if it was already issued elsewhere.
998 
999     I welcome someone else to find a better solution to this problem. However, please make sure that it
1000     works with pasting via shortcut, context menu and the application menu on *All Browsers*.
1001    */
1002   _textField_inputDidChange: function () {
1003     var timerNotPending = SC.empty(this._fieldValueDidChangeTimer) || !this._fieldValueDidChangeTimer.get('isValid');
1004     if (this.get('applyImmediately') && timerNotPending) {
1005       this.invokeLater(this.fieldValueDidChange, 10);
1006     }
1007   },
1008 
1009   /** @private
1010     Make sure to update visibility of hint if it changes
1011    */
1012   updateHintOnFocus: function () {
1013     // Fast path. If we aren't using the hind overlay, do nothing.
1014     if (!this.get('useHintOverlay')) return;
1015 
1016     // If there is a value in the field, hide the hint.
1017     if (this.getFieldValue()) {
1018       this.$('.hint').addClass('sc-hidden');
1019     } else {
1020       this.$('.hint').removeClass('sc-hidden');
1021       this._fixupTextLayout();
1022     }
1023   }.observes('value'),
1024 
1025   /** @private
1026     Move magic number out so it can be over-written later in inline editor
1027    */
1028   _topOffsetForFirefoxCursorFix: 3,
1029 
1030   /** @private
1031     In Firefox, as of 3.6 -- including 3.0 and 3.5 -- for input fields only
1032     (that is, not textarea elements), if the cursor is at the end of the
1033     field, the "down" key will not result in a "keypress" event for the
1034     document (only for the input element), although it will be bubbled up in
1035     other contexts.  Since SproutCore's event dispatching requires the
1036     document to see the event, we'll manually forward the event along.
1037    */
1038   _firefox_dispatch_keypress: function (evt) {
1039     var selection = this.get('selection'),
1040         value     = this.get('value'),
1041         valueLen  = value ? value.length : 0,
1042         responder;
1043 
1044     if (!selection || ((selection.get('length') === 0 && (selection.get('start') === 0) || selection.get('end') === valueLen))) {
1045       responder = SC.RootResponder.responder;
1046       if (evt.keyCode === 9) return;
1047       responder.keypress.call(responder, evt);
1048       evt.stopPropagation();
1049     }
1050   },
1051 
1052   /** @private */
1053   _textField_selectionDidChange: function () {
1054     this.notifyPropertyChange('selection');
1055   },
1056 
1057   // ..........................................................
1058   // FIRST RESPONDER SUPPORT
1059   //
1060   // When we become first responder, make sure the field gets focus and
1061   // the hint value is hidden if needed.
1062 
1063   /** @private
1064     When we become first responder, focus the text field if needed and
1065     hide the hint text.
1066    */
1067   didBecomeKeyResponderFrom: function (keyView) {
1068     if (this.get('isVisibleInWindow')) {
1069       var inp = this.$input()[0];
1070       try {
1071         if (inp) inp.focus();
1072       } catch (e) {}
1073 
1074       if (!this._txtFieldMouseDown) {
1075         this.invokeLast(this._selectRootElement);
1076       }
1077     }
1078   },
1079 
1080   /** @private
1081     In IE, you can't modify functions on DOM elements so we need to wrap the
1082     call to select() like this.
1083    */
1084   _selectRootElement: function () {
1085     var inputElem = this.$input()[0],
1086         isLion;
1087     // Make sure input element still exists, as a redraw could have remove it
1088     // already.
1089     if (inputElem) {
1090       // Determine if the OS is OS 10.7 "Lion"
1091       isLion = SC.browser.os === SC.OS.mac &&
1092           SC.browser.compare(SC.browser.osVersion, '10.7') === 0;
1093 
1094       if (!(SC.browser.name === SC.BROWSER.safari &&
1095             isLion && SC.buildLocale === 'ko-kr')) {
1096         inputElem.select();
1097       }
1098     }
1099     else this._textField_selectionDidChange();
1100   },
1101 
1102   /** @private
1103     When we lose first responder, blur the text field if needed and show
1104     the hint text if needed.
1105    */
1106   didLoseKeyResponderTo: function (keyView) {
1107     var el = this.$input()[0];
1108     if (el) el.blur();
1109     this.invokeLater("scrollToOriginIfNeeded", 100);
1110   },
1111 
1112   /** @private
1113     Scrolls to origin if necessary (if the pane's current firstResponder is not a text field).
1114    */
1115   scrollToOriginIfNeeded: function () {
1116     var pane = this.get("pane");
1117     if (!pane) return;
1118 
1119     var first = pane.get("firstResponder");
1120     if (!first || !first.get("isTextField")) {
1121       document.body.scrollTop = document.body.scrollLeft = 0;
1122     }
1123   },
1124 
1125   /** @private */
1126   keyDown: function (evt) {
1127     return this.interpretKeyEvents(evt) || false;
1128   },
1129 
1130   /** @private */
1131   insertText: function (chr, evt) {
1132     var which = evt.which,
1133         keyCode = evt.keyCode,
1134         maxLengthReached = false;
1135 
1136     // maxlength for textareas
1137     if (!SC.platform.input.maxlength && this.get('isTextArea')) {
1138       var val = this.get('value');
1139 
1140       // This code is nasty. It's thanks to Gecko .keycode table that has characters like '&' with the same keycode as up arrow key
1141       if (val && ((!SC.browser.isMozilla && which > 47) ||
1142                   (SC.browser.isMozilla && ((which > 32 && which < 43) || which > 47) && !(keyCode > 36 && keyCode < 41))) &&
1143           (val.length >= this.get('maxLength'))) {
1144         maxLengthReached = true;
1145       }
1146     }
1147 
1148     // Validate keyDown...
1149     if (this.performValidateKeyDown(evt) && !maxLengthReached) {
1150       evt.allowDefault();
1151     } else {
1152       evt.stop();
1153     }
1154 
1155     if (this.get('applyImmediately')) {
1156       // This has gone back and forth several times between invokeLater and setTimeout.
1157       // Now we're back to invokeLater, please read the code comment above
1158       // this._textField_inputDidChange before changing it again.
1159       this._fieldValueDidChangeTimer = this.invokeLater(this.fieldValueDidChange, 10);
1160     }
1161 
1162     return true;
1163   },
1164 
1165   /** @private */
1166   insertTab: function (evt) {
1167     // Don't handle if default tabbing hasn't been enabled.
1168     if (!this.get('defaultTabbingEnabled')) {
1169       evt.preventDefault();
1170       return false;
1171     }
1172 
1173     // Otherwise, handle.
1174     var view = this.get('nextValidKeyView');
1175     if (view) view.becomeFirstResponder();
1176     else evt.allowDefault();
1177     return true; // handled
1178   },
1179 
1180   /** @private */
1181   insertBacktab: function (evt) {
1182     // Don't handle if default tabbing hasn't been enabled.
1183     if (!this.get('defaultTabbingEnabled')) {
1184       evt.preventDefault();
1185       return false;
1186     }
1187 
1188     // Otherwise, handle.
1189     var view = this.get('previousValidKeyView');
1190     if (view) view.becomeFirstResponder();
1191     else evt.allowDefault();
1192     return true; // handled
1193   },
1194 
1195   /**
1196     @private
1197 
1198     Invoked when the user presses return.  If this is a multi-line field,
1199     then allow the newline to proceed.  Otherwise, try to commit the
1200     edit.
1201   */
1202   insertNewline: function (evt) {
1203     if (this.get('isTextArea') || evt.isIMEInput) {
1204       evt.allowDefault();
1205       return true; // handled
1206     }
1207     return false;
1208   },
1209 
1210   /** @private */
1211   deleteForward: function (evt) {
1212     evt.allowDefault();
1213     return true;
1214   },
1215 
1216   /** @private */
1217   deleteBackward: function (evt) {
1218     evt.allowDefault();
1219     return true;
1220   },
1221 
1222   /** @private */
1223   moveLeft: function (evt) {
1224     evt.allowDefault();
1225     return true;
1226   },
1227 
1228   /** @private */
1229   moveRight: function (evt) {
1230     evt.allowDefault();
1231     return true;
1232   },
1233 
1234   /** @private */
1235   selectAll: function (evt) {
1236     evt.allowDefault();
1237     return true;
1238   },
1239 
1240   /** @private */
1241   moveUp: function (evt) {
1242     if (this.get('isTextArea')) {
1243       evt.allowDefault();
1244       return true;
1245     }
1246     return false;
1247   },
1248 
1249   /** @private */
1250   moveDown: function (evt) {
1251     if (this.get('isTextArea')) {
1252       evt.allowDefault();
1253       return true;
1254     }
1255     return false;
1256   },
1257 
1258   keyUp: function (evt) {
1259     if (SC.browser.isMozilla &&
1260         evt.keyCode === SC.Event.KEY_RETURN) { this.fieldValueDidChange(); }
1261 
1262     // The caret/selection may have changed.
1263     // This cannot notify immediately, because in some browsers (tested Chrome 39.0 on OS X), the
1264     // value of `selectionStart` and `selectionEnd` won't have updated yet. Thus if we notified
1265     // immediately, observers of this view's `selection` property would get the old value.
1266     this.invokeNext(this._textField_selectionDidChange);
1267 
1268     evt.allowDefault();
1269     return true;
1270   },
1271 
1272   mouseDown: function (evt) {
1273     if (!this.get('isEnabledInPane')) {
1274       evt.stop();
1275       return true;
1276     } else {
1277       this._txtFieldMouseDown = true;
1278       this.becomeFirstResponder();
1279 
1280       return sc_super();
1281     }
1282   },
1283 
1284   mouseUp: function (evt) {
1285     this._txtFieldMouseDown = false;
1286 
1287     if (!this.get('isEnabledInPane')) {
1288       evt.stop();
1289       return true;
1290     }
1291 
1292     // The caret/selection may have changed.
1293     // This cannot notify immediately, because in some browsers (tested Chrome 39.0 on OS X), the
1294     // value of `selectionStart` and `selectionEnd` won't have updated yet. Thus if we notified
1295     // immediately, observers of this view's `selection` property would get the old value.
1296     this.invokeNext(this._textField_selectionDidChange);
1297 
1298     return sc_super();
1299   },
1300 
1301   touchStart: function (evt) {
1302     return this.mouseDown(evt);
1303   },
1304 
1305   touchEnd: function (evt) {
1306     return this.mouseUp(evt);
1307   },
1308 
1309   /**
1310     Adds mouse wheel support for textareas.
1311    */
1312   mouseWheel: function (evt) {
1313     if (this.get('isTextArea')) {
1314       evt.allowDefault();
1315       return true;
1316     } else return false;
1317   },
1318 
1319   /**
1320     Allows text selection in IE. We block the IE only event selectStart to
1321     block text selection in all other views.
1322    */
1323   selectStart: function (evt) {
1324     return true;
1325   },
1326 
1327   /** @private
1328     Overridden from SC.FieldView. Provides correct tag name based on the
1329     `isTextArea` property.
1330    */
1331   _inputElementTagName: function () {
1332     if (this.get('isTextArea')) {
1333       return 'textarea';
1334     } else {
1335       return 'input';
1336     }
1337   },
1338 
1339   /** @private
1340     This observer makes sure to hide the hint when a value is entered, or
1341     show it if it becomes empty.
1342    */
1343   _valueObserver: function () {
1344     var val = this.get('value'), max;
1345 
1346     if (val && val.length > 0) {
1347       max = this.get('maxLength');
1348 
1349       if (!SC.platform.input.maxlength && val.length > max) {
1350         this.set('value', val.substr(0, max));
1351       }
1352     }
1353   }.observes('value')
1354 
1355 });
1356