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