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 <ABI48_0_0React/ABI48_0_0RCTBaseTextInputView.h> 9 10#import <ABI48_0_0React/ABI48_0_0RCTBridge.h> 11#import <ABI48_0_0React/ABI48_0_0RCTConvert.h> 12#import <ABI48_0_0React/ABI48_0_0RCTEventDispatcherProtocol.h> 13#import <ABI48_0_0React/ABI48_0_0RCTUIManager.h> 14#import <ABI48_0_0React/ABI48_0_0RCTUtils.h> 15#import <ABI48_0_0React/ABI48_0_0UIView+React.h> 16 17#import <ABI48_0_0React/ABI48_0_0RCTInputAccessoryView.h> 18#import <ABI48_0_0React/ABI48_0_0RCTInputAccessoryViewContent.h> 19#import <ABI48_0_0React/ABI48_0_0RCTTextAttributes.h> 20#import <ABI48_0_0React/ABI48_0_0RCTTextSelection.h> 21 22@implementation ABI48_0_0RCTBaseTextInputView { 23 __weak ABI48_0_0RCTBridge *_bridge; 24 __weak id<ABI48_0_0RCTEventDispatcherProtocol> _eventDispatcher; 25 BOOL _hasInputAccesoryView; 26 NSString *_Nullable _predictedText; 27 BOOL _didMoveToWindow; 28} 29 30- (instancetype)initWithBridge:(ABI48_0_0RCTBridge *)bridge 31{ 32 ABI48_0_0RCTAssertParam(bridge); 33 34 if (self = [super initWithFrame:CGRectZero]) { 35 _bridge = bridge; 36 _eventDispatcher = bridge.eventDispatcher; 37 } 38 39 return self; 40} 41 42ABI48_0_0RCT_NOT_IMPLEMENTED(-(instancetype)init) 43ABI48_0_0RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)decoder) 44ABI48_0_0RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame) 45 46- (UIView<ABI48_0_0RCTBackedTextInputViewProtocol> *)backedTextInputView 47{ 48 ABI48_0_0RCTAssert(NO, @"-[ABI48_0_0RCTBaseTextInputView backedTextInputView] must be implemented in subclass."); 49 return nil; 50} 51 52#pragma mark - ABI48_0_0RCTComponent 53 54- (void)didUpdateABI48_0_0ReactSubviews 55{ 56 // Do nothing. 57} 58 59#pragma mark - Properties 60 61- (void)setTextAttributes:(ABI48_0_0RCTTextAttributes *)textAttributes 62{ 63 _textAttributes = textAttributes; 64 [self enforceTextAttributesIfNeeded]; 65} 66 67- (void)enforceTextAttributesIfNeeded 68{ 69 id<ABI48_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)setABI48_0_0ReactPaddingInsets:(UIEdgeInsets)ABI48_0_0ReactPaddingInsets 80{ 81 _ABI48_0_0ReactPaddingInsets = ABI48_0_0ReactPaddingInsets; 82 // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`. 83 self.backedTextInputView.textContainerInset = ABI48_0_0ReactPaddingInsets; 84 [self setNeedsLayout]; 85} 86 87- (void)setABI48_0_0ReactBorderInsets:(UIEdgeInsets)ABI48_0_0ReactBorderInsets 88{ 89 _ABI48_0_0ReactBorderInsets = ABI48_0_0ReactBorderInsets; 90 // We apply `borderInsets` as `backedTextInputView` layout offset. 91 self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, ABI48_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:ABI48_0_0RCTTextAttributesTagAttributeName 144 range:NSMakeRange(0, backedTextInputViewTextCopy.length)]; 145 146 [attributedTextCopy removeAttribute:ABI48_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 > ABI48_0_0RCTTextUpdateLagWarningThreshold) { 172 ABI48_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- (ABI48_0_0RCTTextSelection *)selection 180{ 181 id<ABI48_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 182 UITextRange *selectedTextRange = backedTextInputView.selectedTextRange; 183 return [[ABI48_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:(ABI48_0_0RCTTextSelection *)selection 191{ 192 if (!selection) { 193 return; 194 } 195 196 id<ABI48_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 > ABI48_0_0RCTTextUpdateLagWarningThreshold) { 209 ABI48_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<ABI48_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 } else { 317 // Hides keyboard, but keeps blinking cursor. 318 self.backedTextInputView.inputView = [UIView new]; 319 } 320} 321 322#pragma mark - ABI48_0_0RCTBackedTextInputDelegate 323 324- (BOOL)textInputShouldBeginEditing 325{ 326 return YES; 327} 328 329- (void)textInputDidBeginEditing 330{ 331 if (_clearTextOnFocus) { 332 self.backedTextInputView.attributedText = [NSAttributedString new]; 333 } 334 335 if (_selectTextOnFocus) { 336 [self.backedTextInputView selectAll:nil]; 337 } 338 339 [_eventDispatcher sendTextEventWithType:ABI48_0_0RCTTextEventTypeFocus 340 ABI48_0_0ReactTag:self.ABI48_0_0ReactTag 341 text:[self.backedTextInputView.attributedText.string copy] 342 key:nil 343 eventCount:_nativeEventCount]; 344} 345 346- (BOOL)textInputShouldEndEditing 347{ 348 return YES; 349} 350 351- (void)textInputDidEndEditing 352{ 353 [_eventDispatcher sendTextEventWithType:ABI48_0_0RCTTextEventTypeEnd 354 ABI48_0_0ReactTag:self.ABI48_0_0ReactTag 355 text:[self.backedTextInputView.attributedText.string copy] 356 key:nil 357 eventCount:_nativeEventCount]; 358 359 [_eventDispatcher sendTextEventWithType:ABI48_0_0RCTTextEventTypeBlur 360 ABI48_0_0ReactTag:self.ABI48_0_0ReactTag 361 text:[self.backedTextInputView.attributedText.string copy] 362 key:nil 363 eventCount:_nativeEventCount]; 364} 365 366- (BOOL)textInputShouldSubmitOnReturn 367{ 368 const BOOL shouldSubmit = 369 [_submitBehavior isEqualToString:@"blurAndSubmit"] || [_submitBehavior isEqualToString:@"submit"]; 370 if (shouldSubmit) { 371 // We send `submit` event here, in `textInputShouldSubmit` 372 // (not in `textInputDidReturn)`, because of semantic of the event: 373 // `onSubmitEditing` is called when "Submit" button 374 // (the blue key on onscreen keyboard) did pressed 375 // (no connection to any specific "submitting" process). 376 [_eventDispatcher sendTextEventWithType:ABI48_0_0RCTTextEventTypeSubmit 377 ABI48_0_0ReactTag:self.ABI48_0_0ReactTag 378 text:[self.backedTextInputView.attributedText.string copy] 379 key:nil 380 eventCount:_nativeEventCount]; 381 } 382 return shouldSubmit; 383} 384 385- (BOOL)textInputShouldReturn 386{ 387 return [_submitBehavior isEqualToString:@"blurAndSubmit"]; 388} 389 390- (void)textInputDidReturn 391{ 392 // Does nothing. 393} 394 395- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range 396{ 397 id<ABI48_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 398 399 if (!backedTextInputView.textWasPasted) { 400 [_eventDispatcher sendTextEventWithType:ABI48_0_0RCTTextEventTypeKeyPress 401 ABI48_0_0ReactTag:self.ABI48_0_0ReactTag 402 text:nil 403 key:text 404 eventCount:_nativeEventCount]; 405 } 406 407 if (_maxLength) { 408 NSInteger allowedLength = MAX( 409 _maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 410 0); 411 412 if (text.length > allowedLength) { 413 // If we typed/pasted more than one character, limit the text inputted. 414 if (text.length > 1) { 415 if (allowedLength > 0) { 416 // make sure unicode characters that are longer than 16 bits (such as emojis) are not cut off 417 NSRange cutOffCharacterRange = [text rangeOfComposedCharacterSequenceAtIndex:allowedLength - 1]; 418 if (cutOffCharacterRange.location + cutOffCharacterRange.length > allowedLength) { 419 // the character at the length limit takes more than 16bits, truncation should end at the character before 420 allowedLength = cutOffCharacterRange.location; 421 } 422 } 423 // Truncate the input string so the result is exactly maxLength 424 NSString *limitedString = [text substringToIndex:allowedLength]; 425 NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy]; 426 // Apply text attributes if original input view doesn't have text. 427 if (backedTextInputView.attributedText.length == 0) { 428 newAttributedText = [[NSMutableAttributedString alloc] 429 initWithString:[self.textAttributes applyTextAttributesToText:limitedString] 430 attributes:self.textAttributes.effectiveTextAttributes]; 431 } else { 432 [newAttributedText replaceCharactersInRange:range withString:limitedString]; 433 } 434 backedTextInputView.attributedText = newAttributedText; 435 _predictedText = newAttributedText.string; 436 437 // Collapse selection at end of insert to match normal paste behavior. 438 UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument 439 offset:(range.location + allowedLength)]; 440 [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd 441 toPosition:insertEnd] 442 notifyDelegate:YES]; 443 444 [self textInputDidChange]; 445 } 446 447 return nil; // Rejecting the change. 448 } 449 } 450 451 NSString *previousText = [backedTextInputView.attributedText.string copy] ?: @""; 452 453 if (range.location + range.length > backedTextInputView.attributedText.string.length) { 454 _predictedText = backedTextInputView.attributedText.string; 455 } else { 456 _predictedText = [backedTextInputView.attributedText.string stringByReplacingCharactersInRange:range 457 withString:text]; 458 } 459 460 if (_onTextInput) { 461 _onTextInput(@{ 462 // We copy the string here because if it's a mutable string it may get released before we stop using it on a 463 // different thread, causing a crash. 464 @"text" : [text copy], 465 @"previousText" : previousText, 466 @"range" : @{@"start" : @(range.location), @"end" : @(range.location + range.length)}, 467 @"eventCount" : @(_nativeEventCount), 468 }); 469 } 470 471 return text; // Accepting the change. 472} 473 474- (void)textInputDidChange 475{ 476 [self updateLocalData]; 477 478 id<ABI48_0_0RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView; 479 480 // Detect when `backedTextInputView` updates happened that didn't invoke `shouldChangeTextInRange` 481 // (e.g. typing simplified Chinese in pinyin will insert and remove spaces without 482 // calling shouldChangeTextInRange). This will cause JS to get out of sync so we 483 // update the mismatched range. 484 NSRange currentRange; 485 NSRange predictionRange; 486 if (findMismatch(backedTextInputView.attributedText.string, _predictedText, ¤tRange, &predictionRange)) { 487 NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange]; 488 [self textInputShouldChangeText:replacement inRange:predictionRange]; 489 // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it. 490 [self textInputDidChangeSelection]; 491 } 492 493 _nativeEventCount++; 494 495 if (_onChange) { 496 _onChange(@{ 497 @"text" : [self.attributedText.string copy], 498 @"target" : self.ABI48_0_0ReactTag, 499 @"eventCount" : @(_nativeEventCount), 500 }); 501 } 502} 503 504- (void)textInputDidChangeSelection 505{ 506 if (!_onSelectionChange) { 507 return; 508 } 509 510 ABI48_0_0RCTTextSelection *selection = self.selection; 511 512 _onSelectionChange(@{ 513 @"selection" : @{ 514 @"start" : @(selection.start), 515 @"end" : @(selection.end), 516 }, 517 }); 518} 519 520- (void)updateLocalData 521{ 522 [self enforceTextAttributesIfNeeded]; 523 524 [_bridge.uiManager setLocalData:[self.backedTextInputView.attributedText copy] forView:self]; 525} 526 527#pragma mark - Layout (in UIKit terms, with all insets) 528 529- (CGSize)intrinsicContentSize 530{ 531 CGSize size = self.backedTextInputView.intrinsicContentSize; 532 size.width += _ABI48_0_0ReactBorderInsets.left + _ABI48_0_0ReactBorderInsets.right; 533 size.height += _ABI48_0_0ReactBorderInsets.top + _ABI48_0_0ReactBorderInsets.bottom; 534 // Returning value DOES include border and padding insets. 535 return size; 536} 537 538- (CGSize)sizeThatFits:(CGSize)size 539{ 540 CGFloat compoundHorizontalBorderInset = _ABI48_0_0ReactBorderInsets.left + _ABI48_0_0ReactBorderInsets.right; 541 CGFloat compoundVerticalBorderInset = _ABI48_0_0ReactBorderInsets.top + _ABI48_0_0ReactBorderInsets.bottom; 542 543 size.width -= compoundHorizontalBorderInset; 544 size.height -= compoundVerticalBorderInset; 545 546 // Note: `paddingInsets` was already included in `backedTextInputView` size 547 // because it was applied as `textContainerInset`. 548 CGSize fittingSize = [self.backedTextInputView sizeThatFits:size]; 549 550 fittingSize.width += compoundHorizontalBorderInset; 551 fittingSize.height += compoundVerticalBorderInset; 552 553 // Returning value DOES include border and padding insets. 554 return fittingSize; 555} 556 557#pragma mark - Accessibility 558 559- (UIView *)ABI48_0_0ReactAccessibilityElement 560{ 561 return self.backedTextInputView; 562} 563 564#pragma mark - Focus Control 565 566- (void)ABI48_0_0ReactFocus 567{ 568 [self.backedTextInputView ABI48_0_0ReactFocus]; 569} 570 571- (void)ABI48_0_0ReactBlur 572{ 573 [self.backedTextInputView ABI48_0_0ReactBlur]; 574} 575 576- (void)didMoveToWindow 577{ 578 if (self.autoFocus && !_didMoveToWindow) { 579 [self.backedTextInputView ABI48_0_0ReactFocus]; 580 } else { 581 [self.backedTextInputView ABI48_0_0ReactFocusIfNeeded]; 582 } 583 584 _didMoveToWindow = YES; 585} 586 587#pragma mark - Custom Input Accessory View 588 589- (void)didSetProps:(NSArray<NSString *> *)changedProps 590{ 591 if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) { 592 [self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID]; 593 } else if (!self.inputAccessoryViewID) { 594 [self setDefaultInputAccessoryView]; 595 } 596} 597 598- (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID 599{ 600 __weak ABI48_0_0RCTBaseTextInputView *weakSelf = self; 601 [_bridge.uiManager rootViewForABI48_0_0ReactTag:self.ABI48_0_0ReactTag 602 withCompletion:^(UIView *rootView) { 603 ABI48_0_0RCTBaseTextInputView *strongSelf = weakSelf; 604 if (rootView) { 605 UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID 606 withRootTag:rootView.ABI48_0_0ReactTag]; 607 if (accessoryView && [accessoryView isKindOfClass:[ABI48_0_0RCTInputAccessoryView class]]) { 608 strongSelf.backedTextInputView.inputAccessoryView = 609 ((ABI48_0_0RCTInputAccessoryView *)accessoryView).inputAccessoryView; 610 [strongSelf reloadInputViewsIfNecessary]; 611 } 612 } 613 }]; 614} 615 616- (void)setDefaultInputAccessoryView 617{ 618 UIView<ABI48_0_0RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView; 619 UIKeyboardType keyboardType = textInputView.keyboardType; 620 621 // These keyboard types (all are number pads) don't have a "Done" button by default, 622 // so we create an `inputAccessoryView` with this button for them. 623 BOOL shouldHaveInputAccesoryView = 624 (keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad || 625 keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) && 626 textInputView.returnKeyType == UIReturnKeyDone; 627 628 if (_hasInputAccesoryView == shouldHaveInputAccesoryView) { 629 return; 630 } 631 632 _hasInputAccesoryView = shouldHaveInputAccesoryView; 633 634 if (shouldHaveInputAccesoryView) { 635 UIToolbar *toolbarView = [UIToolbar new]; 636 [toolbarView sizeToFit]; 637 UIBarButtonItem *flexibleSpace = 638 [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]; 639 UIBarButtonItem *doneButton = 640 [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone 641 target:self 642 action:@selector(handleInputAccessoryDoneButton)]; 643 toolbarView.items = @[ flexibleSpace, doneButton ]; 644 textInputView.inputAccessoryView = toolbarView; 645 } else { 646 textInputView.inputAccessoryView = nil; 647 } 648 [self reloadInputViewsIfNecessary]; 649} 650 651- (void)reloadInputViewsIfNecessary 652{ 653 // We have to call `reloadInputViews` for focused text inputs to update an accessory view. 654 if (self.backedTextInputView.isFirstResponder) { 655 [self.backedTextInputView reloadInputViews]; 656 } 657} 658 659- (void)handleInputAccessoryDoneButton 660{ 661 // Ignore the value of whether we submitted; just make sure the submit event is called if necessary. 662 [self textInputShouldSubmitOnReturn]; 663 if ([self textInputShouldReturn]) { 664 [self.backedTextInputView endEditing:YES]; 665 } 666} 667 668#pragma mark - Helpers 669 670static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange) 671{ 672 NSInteger firstMismatch = -1; 673 for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) { 674 if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) { 675 firstMismatch = ii; 676 break; 677 } 678 } 679 680 if (firstMismatch == -1) { 681 return NO; 682 } 683 684 NSUInteger ii = second.length; 685 NSUInteger lastMismatch = first.length; 686 while (ii > firstMismatch && lastMismatch > firstMismatch) { 687 if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) { 688 break; 689 } 690 ii--; 691 lastMismatch--; 692 } 693 694 *firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch); 695 *secondRange = NSMakeRange(firstMismatch, ii - firstMismatch); 696 return YES; 697} 698 699@end 700