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