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