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, &currentRange, &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