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