1/* 2 * Copyright (c) Meta Platforms, Inc. and affiliates. 3 * 4 * This source code is licensed under the MIT license found in the 5 * LICENSE file in the root directory of this source tree. 6 */ 7 8#import <ABI49_0_0React/ABI49_0_0RCTBaseTextInputView.h> 9 10#import <ABI49_0_0React/ABI49_0_0RCTBridge.h> 11#import <ABI49_0_0React/ABI49_0_0RCTConvert.h> 12#import <ABI49_0_0React/ABI49_0_0RCTEventDispatcherProtocol.h> 13#import <ABI49_0_0React/ABI49_0_0RCTUIManager.h> 14#import <ABI49_0_0React/ABI49_0_0RCTUtils.h> 15#import <ABI49_0_0React/ABI49_0_0UIView+React.h> 16 17#import <ABI49_0_0React/ABI49_0_0RCTInputAccessoryView.h> 18#import <ABI49_0_0React/ABI49_0_0RCTInputAccessoryViewContent.h> 19#import <ABI49_0_0React/ABI49_0_0RCTTextAttributes.h> 20#import <ABI49_0_0React/ABI49_0_0RCTTextSelection.h> 21 22@implementation ABI49_0_0RCTBaseTextInputView { 23 __weak ABI49_0_0RCTBridge *_bridge; 24 __weak id<ABI49_0_0RCTEventDispatcherProtocol> _eventDispatcher; 25 BOOL _hasInputAccessoryView; 26 NSString *_Nullable _predictedText; 27 BOOL _didMoveToWindow; 28} 29 30- (instancetype)initWithBridge:(ABI49_0_0RCTBridge *)bridge 31{ 32 ABI49_0_0RCTAssertParam(bridge); 33 34 if (self = [super initWithFrame:CGRectZero]) { 35 _bridge = bridge; 36 _eventDispatcher = bridge.eventDispatcher; 37 } 38 39 return self; 40} 41 42ABI49_0_0RCT_NOT_IMPLEMENTED(-(instancetype)init) 43ABI49_0_0RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)decoder) 44ABI49_0_0RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) 45 46- (UIView<ABI49_0_0RCTBackedTextInputViewProtocol> *)backedTextInputView 47{ 48 ABI49_0_0RCTAssert(NO, @"-[ABI49_0_0RCTBaseTextInputView backedTextInputView] must be implemented in subclass."); 49 return nil; 50} 51 52#pragma mark - ABI49_0_0RCTComponent 53 54- (void)didUpdateABI49_0_0ReactSubviews 55{ 56 // Do nothing. 57} 58 59#pragma mark - Properties 60 61- (void)setTextAttributes:(ABI49_0_0RCTTextAttributes *)textAttributes 62{ 63 _textAttributes = textAttributes; 64 [self enforceTextAttributesIfNeeded]; 65} 66 67- (void)enforceTextAttributesIfNeeded 68{ 69 id<ABI49_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 70 71 NSDictionary<NSAttributedStringKey, id> *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy]; 72 if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) { 73 [textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName]; 74 } 75 76 backedTextInputView.defaultTextAttributes = textAttributes; 77} 78 79- (void)setABI49_0_0ReactPaddingInsets:(UIEdgeInsets)ABI49_0_0ReactPaddingInsets 80{ 81 _ABI49_0_0ReactPaddingInsets = ABI49_0_0ReactPaddingInsets; 82 // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`. 83 self.backedTextInputView.textContainerInset = ABI49_0_0ReactPaddingInsets; 84 [self setNeedsLayout]; 85} 86 87- (void)setABI49_0_0ReactBorderInsets:(UIEdgeInsets)ABI49_0_0ReactBorderInsets 88{ 89 _ABI49_0_0ReactBorderInsets = ABI49_0_0ReactBorderInsets; 90 // We apply `borderInsets` as `backedTextInputView` layout offset. 91 self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, ABI49_0_0ReactBorderInsets); 92 [self setNeedsLayout]; 93} 94 95- (NSAttributedString *)attributedText 96{ 97 return self.backedTextInputView.attributedText; 98} 99 100- (BOOL)textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText 101{ 102 // When the dictation is running we can't update the attributed text on the backed up text view 103 // because setting the attributed string will kill the dictation. This means that we can't impose 104 // the settings on a dictation. 105 // Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the 106 // text that we should disregard. See 107 // https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc for more info. 108 // Also, updating the attributed text while inputting Korean language will break input mechanism. 109 // If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in 110 // NSOriginalFont. Lastly, when entering a password, etc., there will be additional styling on the field as the native 111 // text view handles showing the last character for a split second. 112 __block BOOL fontHasBeenUpdatedBySystem = false; 113 [oldText enumerateAttribute:@"NSOriginalFont" 114 inRange:NSMakeRange(0, oldText.length) 115 options:0 116 usingBlock:^(id value, NSRange range, BOOL *stop) { 117 if (value) { 118 fontHasBeenUpdatedBySystem = true; 119 } 120 }]; 121 122 BOOL shouldFallbackToBareTextComparison = 123 [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] || 124 [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"ko-KR"] || 125 self.backedTextInputView.markedTextRange || self.backedTextInputView.isSecureTextEntry || 126 fontHasBeenUpdatedBySystem; 127 128 if (shouldFallbackToBareTextComparison) { 129 return ([newText.string isEqualToString:oldText.string]); 130 } else { 131 return ([newText isEqualToAttributedString:oldText]); 132 } 133} 134 135- (void)setAttributedText:(NSAttributedString *)attributedText 136{ 137 NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; 138 BOOL textNeedsUpdate = NO; 139 // Remove tag attribute to ensure correct attributed string comparison. 140 NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy]; 141 NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy]; 142 143 [backedTextInputViewTextCopy removeAttribute:ABI49_0_0RCTTextAttributesTagAttributeName 144 range:NSMakeRange(0, backedTextInputViewTextCopy.length)]; 145 146 [attributedTextCopy removeAttribute:ABI49_0_0RCTTextAttributesTagAttributeName 147 range:NSMakeRange(0, attributedTextCopy.length)]; 148 149 textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO); 150 151 if (eventLag == 0 && textNeedsUpdate) { 152 UITextRange *selection = self.backedTextInputView.selectedTextRange; 153 NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length; 154 155 self.backedTextInputView.attributedText = attributedText; 156 157 if (selection.empty) { 158 // Maintaining a cursor position relative to the end of the old text. 159 NSInteger offsetStart = [self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument 160 toPosition:selection.start]; 161 NSInteger offsetFromEnd = oldTextLength - offsetStart; 162 NSInteger newOffset = attributedText.string.length - offsetFromEnd; 163 UITextPosition *position = 164 [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument offset:newOffset]; 165 [self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position 166 toPosition:position] 167 notifyDelegate:YES]; 168 } 169 170 [self updateLocalData]; 171 } else if (eventLag > ABI49_0_0RCTTextUpdateLagWarningThreshold) { 172 ABI49_0_0RCTLog( 173 @"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", 174 self.backedTextInputView.attributedText.string, 175 (long long)eventLag); 176 } 177} 178 179- (ABI49_0_0RCTTextSelection *)selection 180{ 181 id<ABI49_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 182 UITextRange *selectedTextRange = backedTextInputView.selectedTextRange; 183 return [[ABI49_0_0RCTTextSelection new] 184 initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument 185 toPosition:selectedTextRange.start] 186 end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument 187 toPosition:selectedTextRange.end]]; 188} 189 190- (void)setSelection:(ABI49_0_0RCTTextSelection *)selection 191{ 192 if (!selection) { 193 return; 194 } 195 196 id<ABI49_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 197 198 UITextRange *previousSelectedTextRange = backedTextInputView.selectedTextRange; 199 UITextPosition *start = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument 200 offset:selection.start]; 201 UITextPosition *end = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument 202 offset:selection.end]; 203 UITextRange *selectedTextRange = [backedTextInputView textRangeFromPosition:start toPosition:end]; 204 205 NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; 206 if (eventLag == 0 && ![previousSelectedTextRange isEqual:selectedTextRange]) { 207 [backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO]; 208 } else if (eventLag > ABI49_0_0RCTTextUpdateLagWarningThreshold) { 209 ABI49_0_0RCTLog( 210 @"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", 211 backedTextInputView.attributedText.string, 212 (long long)eventLag); 213 } 214} 215 216- (void)setSelectionStart:(NSInteger)start selectionEnd:(NSInteger)end 217{ 218 UITextPosition *startPosition = 219 [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument offset:start]; 220 UITextPosition *endPosition = 221 [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument offset:end]; 222 if (startPosition && endPosition) { 223 UITextRange *range = [self.backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition]; 224 [self.backedTextInputView setSelectedTextRange:range notifyDelegate:NO]; 225 } 226} 227 228- (void)setTextContentType:(NSString *)type 229{ 230#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) 231 static dispatch_once_t onceToken; 232 static NSDictionary<NSString *, NSString *> *contentTypeMap; 233 234 dispatch_once(&onceToken, ^{ 235 contentTypeMap = @{ 236 @"none" : @"", 237 @"URL" : UITextContentTypeURL, 238 @"addressCity" : UITextContentTypeAddressCity, 239 @"addressCityAndState" : UITextContentTypeAddressCityAndState, 240 @"addressState" : UITextContentTypeAddressState, 241 @"countryName" : UITextContentTypeCountryName, 242 @"creditCardNumber" : UITextContentTypeCreditCardNumber, 243 @"emailAddress" : UITextContentTypeEmailAddress, 244 @"familyName" : UITextContentTypeFamilyName, 245 @"fullStreetAddress" : UITextContentTypeFullStreetAddress, 246 @"givenName" : UITextContentTypeGivenName, 247 @"jobTitle" : UITextContentTypeJobTitle, 248 @"location" : UITextContentTypeLocation, 249 @"middleName" : UITextContentTypeMiddleName, 250 @"name" : UITextContentTypeName, 251 @"namePrefix" : UITextContentTypeNamePrefix, 252 @"nameSuffix" : UITextContentTypeNameSuffix, 253 @"nickname" : UITextContentTypeNickname, 254 @"organizationName" : UITextContentTypeOrganizationName, 255 @"postalCode" : UITextContentTypePostalCode, 256 @"streetAddressLine1" : UITextContentTypeStreetAddressLine1, 257 @"streetAddressLine2" : UITextContentTypeStreetAddressLine2, 258 @"sublocality" : UITextContentTypeSublocality, 259 @"telephoneNumber" : UITextContentTypeTelephoneNumber, 260 @"username" : UITextContentTypeUsername, 261 @"password" : UITextContentTypePassword, 262 }; 263 264#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 /* __IPHONE_12_0 */ 265 if (@available(iOS 12.0, *)) { 266 NSDictionary<NSString *, NSString *> *iOS12extras = 267 @{@"newPassword" : UITextContentTypeNewPassword, @"oneTimeCode" : UITextContentTypeOneTimeCode}; 268 269 NSMutableDictionary<NSString *, NSString *> *iOS12baseMap = [contentTypeMap mutableCopy]; 270 [iOS12baseMap addEntriesFromDictionary:iOS12extras]; 271 272 contentTypeMap = [iOS12baseMap copy]; 273 } 274#endif 275 }); 276 277 // Setting textContentType to an empty string will disable any 278 // default behaviour, like the autofill bar for password inputs 279 self.backedTextInputView.textContentType = contentTypeMap[type] ?: type; 280#endif 281} 282 283- (void)setPasswordRules:(NSString *)descriptor 284{ 285#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0 286 if (@available(iOS 12.0, *)) { 287 self.backedTextInputView.passwordRules = [UITextInputPasswordRules passwordRulesWithDescriptor:descriptor]; 288 } 289#endif 290} 291 292- (UIKeyboardType)keyboardType 293{ 294 return self.backedTextInputView.keyboardType; 295} 296 297- (void)setKeyboardType:(UIKeyboardType)keyboardType 298{ 299 UIView<ABI49_0_0RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView; 300 if (textInputView.keyboardType != keyboardType) { 301 textInputView.keyboardType = keyboardType; 302 // Without the call to reloadInputViews, the keyboard will not change until the textview field (the first responder) 303 // loses and regains focus. 304 if (textInputView.isFirstResponder) { 305 [textInputView reloadInputViews]; 306 } 307 } 308} 309 310- (void)setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus 311{ 312 (void)_showSoftInputOnFocus; 313 if (showSoftInputOnFocus) { 314 // Resets to default keyboard. 315 self.backedTextInputView.inputView = nil; 316 317 // Without the call to reloadInputViews, the keyboard will not change until the textInput field (the first 318 // responder) loses and regains focus. 319 if (self.backedTextInputView.isFirstResponder) { 320 [self.backedTextInputView reloadInputViews]; 321 } 322 } else { 323 // Hides keyboard, but keeps blinking cursor. 324 self.backedTextInputView.inputView = [UIView new]; 325 } 326} 327 328#pragma mark - ABI49_0_0RCTBackedTextInputDelegate 329 330- (BOOL)textInputShouldBeginEditing 331{ 332 return YES; 333} 334 335- (void)textInputDidBeginEditing 336{ 337 if (_clearTextOnFocus) { 338 self.backedTextInputView.attributedText = [NSAttributedString new]; 339 } 340 341 if (_selectTextOnFocus) { 342 [self.backedTextInputView selectAll:nil]; 343 } 344 345 [_eventDispatcher sendTextEventWithType:ABI49_0_0RCTTextEventTypeFocus 346 ABI49_0_0ReactTag:self.ABI49_0_0ReactTag 347 text:[self.backedTextInputView.attributedText.string copy] 348 key:nil 349 eventCount:_nativeEventCount]; 350} 351 352- (BOOL)textInputShouldEndEditing 353{ 354 return YES; 355} 356 357- (void)textInputDidEndEditing 358{ 359 [_eventDispatcher sendTextEventWithType:ABI49_0_0RCTTextEventTypeEnd 360 ABI49_0_0ReactTag:self.ABI49_0_0ReactTag 361 text:[self.backedTextInputView.attributedText.string copy] 362 key:nil 363 eventCount:_nativeEventCount]; 364 365 [_eventDispatcher sendTextEventWithType:ABI49_0_0RCTTextEventTypeBlur 366 ABI49_0_0ReactTag:self.ABI49_0_0ReactTag 367 text:[self.backedTextInputView.attributedText.string copy] 368 key:nil 369 eventCount:_nativeEventCount]; 370} 371 372- (BOOL)textInputShouldSubmitOnReturn 373{ 374 const BOOL shouldSubmit = 375 [_submitBehavior isEqualToString:@"blurAndSubmit"] || [_submitBehavior isEqualToString:@"submit"]; 376 if (shouldSubmit) { 377 // We send `submit` event here, in `textInputShouldSubmit` 378 // (not in `textInputDidReturn)`, because of semantic of the event: 379 // `onSubmitEditing` is called when "Submit" button 380 // (the blue key on onscreen keyboard) did pressed 381 // (no connection to any specific "submitting" process). 382 [_eventDispatcher sendTextEventWithType:ABI49_0_0RCTTextEventTypeSubmit 383 ABI49_0_0ReactTag:self.ABI49_0_0ReactTag 384 text:[self.backedTextInputView.attributedText.string copy] 385 key:nil 386 eventCount:_nativeEventCount]; 387 } 388 return shouldSubmit; 389} 390 391- (BOOL)textInputShouldReturn 392{ 393 return [_submitBehavior isEqualToString:@"blurAndSubmit"]; 394} 395 396- (void)textInputDidReturn 397{ 398 // Does nothing. 399} 400 401- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range 402{ 403 id<ABI49_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 404 405 if (!backedTextInputView.textWasPasted) { 406 [_eventDispatcher sendTextEventWithType:ABI49_0_0RCTTextEventTypeKeyPress 407 ABI49_0_0ReactTag:self.ABI49_0_0ReactTag 408 text:nil 409 key:text 410 eventCount:_nativeEventCount]; 411 } 412 413 if (_maxLength) { 414 NSInteger allowedLength = MAX( 415 _maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 416 0); 417 418 if (text.length > allowedLength) { 419 // If we typed/pasted more than one character, limit the text inputted. 420 if (text.length > 1) { 421 if (allowedLength > 0) { 422 // make sure unicode characters that are longer than 16 bits (such as emojis) are not cut off 423 NSRange cutOffCharacterRange = [text rangeOfComposedCharacterSequenceAtIndex:allowedLength - 1]; 424 if (cutOffCharacterRange.location + cutOffCharacterRange.length > allowedLength) { 425 // the character at the length limit takes more than 16bits, truncation should end at the character before 426 allowedLength = cutOffCharacterRange.location; 427 } 428 } 429 // Truncate the input string so the result is exactly maxLength 430 NSString *limitedString = [text substringToIndex:allowedLength]; 431 NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; 432 // Apply text attributes if original input view doesn't have text. 433 if (backedTextInputView.attributedText.length == 0) { 434 newAttributedText = [[NSMutableAttributedString alloc] 435 initWithString:[self.textAttributes applyTextAttributesToText:limitedString] 436 attributes:self.textAttributes.effectiveTextAttributes]; 437 } else { 438 [newAttributedText replaceCharactersInRange:range withString:limitedString]; 439 } 440 backedTextInputView.attributedText = newAttributedText; 441 _predictedText = newAttributedText.string; 442 443 // Collapse selection at end of insert to match normal paste behavior. 444 UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument 445 offset:(range.location + allowedLength)]; 446 [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd 447 toPosition:insertEnd] 448 notifyDelegate:YES]; 449 450 [self textInputDidChange]; 451 } 452 453 return nil; // Rejecting the change. 454 } 455 } 456 457 NSString *previousText = [backedTextInputView.attributedText.string copy] ?: @""; 458 459 if (range.location + range.length > backedTextInputView.attributedText.string.length) { 460 _predictedText = backedTextInputView.attributedText.string; 461 } else if (text != nil) { 462 _predictedText = [backedTextInputView.attributedText.string stringByReplacingCharactersInRange:range 463 withString:text]; 464 } 465 466 if (_onTextInput) { 467 _onTextInput(@{ 468 // We copy the string here because if it's a mutable string it may get released before we stop using it on a 469 // different thread, causing a crash. 470 @"text" : [text copy], 471 @"previousText" : previousText, 472 @"range" : @{@"start" : @(range.location), @"end" : @(range.location + range.length)}, 473 @"eventCount" : @(_nativeEventCount), 474 }); 475 } 476 477 return text; // Accepting the change. 478} 479 480- (void)textInputDidChange 481{ 482 [self updateLocalData]; 483 484 id<ABI49_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 485 486 // Detect when `backedTextInputView` updates happened that didn't invoke `shouldChangeTextInRange` 487 // (e.g. typing simplified Chinese in pinyin will insert and remove spaces without 488 // calling shouldChangeTextInRange). This will cause JS to get out of sync so we 489 // update the mismatched range. 490 NSRange currentRange; 491 NSRange predictionRange; 492 if (findMismatch(backedTextInputView.attributedText.string, _predictedText, ¤tRange, &predictionRange)) { 493 NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange]; 494 [self textInputShouldChangeText:replacement inRange:predictionRange]; 495 // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it. 496 [self textInputDidChangeSelection]; 497 } 498 499 _nativeEventCount++; 500 501 if (_onChange) { 502 _onChange(@{ 503 @"text" : [self.attributedText.string copy], 504 @"target" : self.ABI49_0_0ReactTag, 505 @"eventCount" : @(_nativeEventCount), 506 }); 507 } 508} 509 510- (void)textInputDidChangeSelection 511{ 512 if (!_onSelectionChange) { 513 return; 514 } 515 516 ABI49_0_0RCTTextSelection *selection = self.selection; 517 518 _onSelectionChange(@{ 519 @"selection" : @{ 520 @"start" : @(selection.start), 521 @"end" : @(selection.end), 522 }, 523 }); 524} 525 526- (void)updateLocalData 527{ 528 [self enforceTextAttributesIfNeeded]; 529 530 [_bridge.uiManager setLocalData:[self.backedTextInputView.attributedText copy] forView:self]; 531} 532 533#pragma mark - Layout (in UIKit terms, with all insets) 534 535- (CGSize)intrinsicContentSize 536{ 537 CGSize size = self.backedTextInputView.intrinsicContentSize; 538 size.width += _ABI49_0_0ReactBorderInsets.left + _ABI49_0_0ReactBorderInsets.right; 539 size.height += _ABI49_0_0ReactBorderInsets.top + _ABI49_0_0ReactBorderInsets.bottom; 540 // Returning value DOES include border and padding insets. 541 return size; 542} 543 544- (CGSize)sizeThatFits:(CGSize)size 545{ 546 CGFloat compoundHorizontalBorderInset = _ABI49_0_0ReactBorderInsets.left + _ABI49_0_0ReactBorderInsets.right; 547 CGFloat compoundVerticalBorderInset = _ABI49_0_0ReactBorderInsets.top + _ABI49_0_0ReactBorderInsets.bottom; 548 549 size.width -= compoundHorizontalBorderInset; 550 size.height -= compoundVerticalBorderInset; 551 552 // Note: `paddingInsets` was already included in `backedTextInputView` size 553 // because it was applied as `textContainerInset`. 554 CGSize fittingSize = [self.backedTextInputView sizeThatFits:size]; 555 556 fittingSize.width += compoundHorizontalBorderInset; 557 fittingSize.height += compoundVerticalBorderInset; 558 559 // Returning value DOES include border and padding insets. 560 return fittingSize; 561} 562 563#pragma mark - Accessibility 564 565- (UIView *)ABI49_0_0ReactAccessibilityElement 566{ 567 return self.backedTextInputView; 568} 569 570#pragma mark - Focus Control 571 572- (void)ABI49_0_0ReactFocus 573{ 574 [self.backedTextInputView ABI49_0_0ReactFocus]; 575} 576 577- (void)ABI49_0_0ReactBlur 578{ 579 [self.backedTextInputView ABI49_0_0ReactBlur]; 580} 581 582- (void)didMoveToWindow 583{ 584 if (self.autoFocus && !_didMoveToWindow) { 585 [self.backedTextInputView ABI49_0_0ReactFocus]; 586 } else { 587 [self.backedTextInputView ABI49_0_0ReactFocusIfNeeded]; 588 } 589 590 _didMoveToWindow = YES; 591} 592 593#pragma mark - Custom Input Accessory View 594 595- (void)didSetProps:(NSArray<NSString *> *)changedProps 596{ 597 if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) { 598 [self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID]; 599 } else if (!self.inputAccessoryViewID) { 600 [self setDefaultInputAccessoryView]; 601 } 602} 603 604- (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID 605{ 606 __weak ABI49_0_0RCTBaseTextInputView *weakSelf = self; 607 [_bridge.uiManager rootViewForABI49_0_0ReactTag:self.ABI49_0_0ReactTag 608 withCompletion:^(UIView *rootView) { 609 ABI49_0_0RCTBaseTextInputView *strongSelf = weakSelf; 610 if (rootView) { 611 UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID 612 withRootTag:rootView.ABI49_0_0ReactTag]; 613 if (accessoryView && [accessoryView isKindOfClass:[ABI49_0_0RCTInputAccessoryView class]]) { 614 strongSelf.backedTextInputView.inputAccessoryView = 615 ((ABI49_0_0RCTInputAccessoryView *)accessoryView).inputAccessoryView; 616 [strongSelf reloadInputViewsIfNecessary]; 617 } 618 } 619 }]; 620} 621 622- (void)setDefaultInputAccessoryView 623{ 624 UIView<ABI49_0_0RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView; 625 UIKeyboardType keyboardType = textInputView.keyboardType; 626 627 // These keyboard types (all are number pads) don't have a "Done" button by default, 628 // so we create an `inputAccessoryView` with this button for them. 629 BOOL shouldHaveInputAccessoryView = 630 (keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad || 631 keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) && 632 textInputView.returnKeyType == UIReturnKeyDone; 633 634 if (_hasInputAccessoryView == shouldHaveInputAccessoryView) { 635 return; 636 } 637 638 _hasInputAccessoryView = shouldHaveInputAccessoryView; 639 640 if (shouldHaveInputAccessoryView) { 641 UIToolbar *toolbarView = [UIToolbar new]; 642 [toolbarView sizeToFit]; 643 UIBarButtonItem *flexibleSpace = 644 [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; 645 UIBarButtonItem *doneButton = 646 [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone 647 target:self 648 action:@selector(handleInputAccessoryDoneButton)]; 649 toolbarView.items = @[ flexibleSpace, doneButton ]; 650 textInputView.inputAccessoryView = toolbarView; 651 } else { 652 textInputView.inputAccessoryView = nil; 653 } 654 [self reloadInputViewsIfNecessary]; 655} 656 657- (void)reloadInputViewsIfNecessary 658{ 659 // We have to call `reloadInputViews` for focused text inputs to update an accessory view. 660 if (self.backedTextInputView.isFirstResponder) { 661 [self.backedTextInputView reloadInputViews]; 662 } 663} 664 665- (void)handleInputAccessoryDoneButton 666{ 667 // Ignore the value of whether we submitted; just make sure the submit event is called if necessary. 668 [self textInputShouldSubmitOnReturn]; 669 if ([self textInputShouldReturn]) { 670 [self.backedTextInputView endEditing:YES]; 671 } 672} 673 674#pragma mark - Helpers 675 676static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange) 677{ 678 NSInteger firstMismatch = -1; 679 for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) { 680 if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) { 681 firstMismatch = ii; 682 break; 683 } 684 } 685 686 if (firstMismatch == -1) { 687 return NO; 688 } 689 690 NSUInteger ii = second.length; 691 NSUInteger lastMismatch = first.length; 692 while (ii > firstMismatch && lastMismatch > firstMismatch) { 693 if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) { 694 break; 695 } 696 ii--; 697 lastMismatch--; 698 } 699 700 *firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch); 701 *secondRange = NSMakeRange(firstMismatch, ii - firstMismatch); 702 return YES; 703} 704 705@end 706