RCTUITextView.m 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. /*
  2. * Copyright (c) Facebook, Inc. and its 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. #import <React/RCTUITextView.h>
  8. #import <React/RCTUtils.h>
  9. #import <React/UIView+React.h>
  10. #import <React/RCTBackedTextInputDelegateAdapter.h>
  11. #import <React/RCTTextAttributes.h>
  12. @implementation RCTUITextView
  13. {
  14. UILabel *_placeholderView;
  15. UITextView *_detachedTextView;
  16. RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter;
  17. NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
  18. }
  19. static UIFont *defaultPlaceholderFont()
  20. {
  21. return [UIFont systemFontOfSize:17];
  22. }
  23. static UIColor *defaultPlaceholderColor()
  24. {
  25. // Default placeholder color from UITextField.
  26. return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22];
  27. }
  28. - (instancetype)initWithFrame:(CGRect)frame
  29. {
  30. if (self = [super initWithFrame:frame]) {
  31. [[NSNotificationCenter defaultCenter] addObserver:self
  32. selector:@selector(textDidChange)
  33. name:UITextViewTextDidChangeNotification
  34. object:self];
  35. _placeholderView = [[UILabel alloc] initWithFrame:self.bounds];
  36. _placeholderView.isAccessibilityElement = NO;
  37. _placeholderView.numberOfLines = 0;
  38. [self addSubview:_placeholderView];
  39. _textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
  40. self.backgroundColor = [UIColor clearColor];
  41. self.textColor = [UIColor blackColor];
  42. // This line actually removes 5pt (default value) left and right padding in UITextView.
  43. self.textContainer.lineFragmentPadding = 0;
  44. #if !TARGET_OS_TV
  45. self.scrollsToTop = NO;
  46. #endif
  47. self.scrollEnabled = YES;
  48. }
  49. return self;
  50. }
  51. #pragma mark - Accessibility
  52. - (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement
  53. {
  54. // UITextView is accessible by default (some nested views are) and disabling that is not supported.
  55. // On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view
  56. // (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component.
  57. }
  58. - (NSString *)accessibilityLabel
  59. {
  60. NSMutableString *accessibilityLabel = [NSMutableString new];
  61. NSString *superAccessibilityLabel = [super accessibilityLabel];
  62. if (superAccessibilityLabel.length > 0) {
  63. [accessibilityLabel appendString:superAccessibilityLabel];
  64. }
  65. if (self.placeholder.length > 0 && self.attributedText.string.length == 0) {
  66. if (accessibilityLabel.length > 0) {
  67. [accessibilityLabel appendString:@" "];
  68. }
  69. [accessibilityLabel appendString:self.placeholder];
  70. }
  71. return accessibilityLabel;
  72. }
  73. #pragma mark - Properties
  74. - (void)setPlaceholder:(NSString *)placeholder
  75. {
  76. _placeholder = placeholder;
  77. [self _updatePlaceholder];
  78. }
  79. - (void)setPlaceholderColor:(UIColor *)placeholderColor
  80. {
  81. _placeholderColor = placeholderColor;
  82. [self _updatePlaceholder];
  83. }
  84. - (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
  85. {
  86. if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) {
  87. return;
  88. }
  89. _defaultTextAttributes = defaultTextAttributes;
  90. self.typingAttributes = defaultTextAttributes;
  91. [self _updatePlaceholder];
  92. }
  93. - (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
  94. {
  95. return _defaultTextAttributes;
  96. }
  97. - (void)textDidChange
  98. {
  99. _textWasPasted = NO;
  100. [self _invalidatePlaceholderVisibility];
  101. }
  102. #pragma mark - Overrides
  103. - (void)setFont:(UIFont *)font
  104. {
  105. [super setFont:font];
  106. [self _updatePlaceholder];
  107. }
  108. - (void)setTextAlignment:(NSTextAlignment)textAlignment
  109. {
  110. [super setTextAlignment:textAlignment];
  111. _placeholderView.textAlignment = textAlignment;
  112. }
  113. - (void)setAttributedText:(NSAttributedString *)attributedText
  114. {
  115. // Using `setAttributedString:` while user is typing breaks some internal mechanics
  116. // when entering complex input languages such as Chinese, Korean or Japanese.
  117. // see: https://github.com/facebook/react-native/issues/19339
  118. // We try to avoid calling this method as much as we can.
  119. // If the text has changed, there is nothing we can do.
  120. if (![super.attributedText.string isEqualToString:attributedText.string]) {
  121. [super setAttributedText:attributedText];
  122. } else {
  123. // But if the text is preserved, we just copying the attributes from the source string.
  124. if (![super.attributedText isEqualToAttributedString:attributedText]) {
  125. [self copyTextAttributesFrom:attributedText];
  126. }
  127. }
  128. [self textDidChange];
  129. }
  130. #pragma mark - Overrides
  131. - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate
  132. {
  133. if (!notifyDelegate) {
  134. // We have to notify an adapter that following selection change was initiated programmatically,
  135. // so the adapter must not generate a notification for it.
  136. [_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange];
  137. }
  138. [super setSelectedTextRange:selectedTextRange];
  139. }
  140. - (void)paste:(id)sender
  141. {
  142. [super paste:sender];
  143. _textWasPasted = YES;
  144. }
  145. - (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated
  146. {
  147. // Turning off scroll animation.
  148. // This fixes the problem also known as "flaky scrolling".
  149. [super setContentOffset:contentOffset animated:NO];
  150. }
  151. - (void)selectAll:(id)sender
  152. {
  153. [super selectAll:sender];
  154. // `selectAll:` does not work for UITextView when it's being called inside UITextView's delegate methods.
  155. dispatch_async(dispatch_get_main_queue(), ^{
  156. UITextRange *selectionRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.endOfDocument];
  157. [self setSelectedTextRange:selectionRange notifyDelegate:NO];
  158. });
  159. }
  160. #pragma mark - Layout
  161. - (CGFloat)preferredMaxLayoutWidth
  162. {
  163. // Returning size DOES contain `textContainerInset` (aka `padding`).
  164. return _preferredMaxLayoutWidth ?: self.placeholderSize.width;
  165. }
  166. - (CGSize)placeholderSize
  167. {
  168. UIEdgeInsets textContainerInset = self.textContainerInset;
  169. NSString *placeholder = self.placeholder ?: @"";
  170. CGSize maxPlaceholderSize = CGSizeMake(UIEdgeInsetsInsetRect(self.bounds, textContainerInset).size.width, CGFLOAT_MAX);
  171. CGSize placeholderSize = [placeholder boundingRectWithSize:maxPlaceholderSize options:NSStringDrawingUsesLineFragmentOrigin attributes:[self _placeholderTextAttributes] context:nil].size;
  172. placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width), RCTCeilPixelValue(placeholderSize.height));
  173. placeholderSize.width += textContainerInset.left + textContainerInset.right;
  174. placeholderSize.height += textContainerInset.top + textContainerInset.bottom;
  175. // Returning size DOES contain `textContainerInset` (aka `padding`; as `sizeThatFits:` does).
  176. return placeholderSize;
  177. }
  178. - (CGSize)contentSize
  179. {
  180. CGSize contentSize = super.contentSize;
  181. CGSize placeholderSize = _placeholderView.isHidden ? CGSizeZero : self.placeholderSize;
  182. // When a text input is empty, it actually displays a placehoder.
  183. // So, we have to consider `placeholderSize` as a minimum `contentSize`.
  184. // Returning size DOES contain `textContainerInset` (aka `padding`).
  185. return CGSizeMake(
  186. MAX(contentSize.width, placeholderSize.width),
  187. MAX(contentSize.height, placeholderSize.height));
  188. }
  189. - (void)layoutSubviews
  190. {
  191. [super layoutSubviews];
  192. CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset);
  193. CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height;
  194. textFrame.size.height = MIN(placeholderHeight, textFrame.size.height);
  195. _placeholderView.frame = textFrame;
  196. }
  197. - (CGSize)intrinsicContentSize
  198. {
  199. // Returning size DOES contain `textContainerInset` (aka `padding`).
  200. return [self sizeThatFits:CGSizeMake(self.preferredMaxLayoutWidth, CGFLOAT_MAX)];
  201. }
  202. - (CGSize)sizeThatFits:(CGSize)size
  203. {
  204. // Returned fitting size depends on text size and placeholder size.
  205. CGSize textSize = [super sizeThatFits:size];
  206. CGSize placeholderSize = self.placeholderSize;
  207. // Returning size DOES contain `textContainerInset` (aka `padding`).
  208. return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height));
  209. }
  210. #pragma mark - Context Menu
  211. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
  212. {
  213. if (_contextMenuHidden) {
  214. return NO;
  215. }
  216. return [super canPerformAction:action withSender:sender];
  217. }
  218. #pragma mark - Placeholder
  219. - (void)_invalidatePlaceholderVisibility
  220. {
  221. BOOL isVisible = _placeholder.length != 0 && self.attributedText.length == 0;
  222. _placeholderView.hidden = !isVisible;
  223. }
  224. - (void)_updatePlaceholder
  225. {
  226. _placeholderView.attributedText = [[NSAttributedString alloc] initWithString:_placeholder ?: @"" attributes:[self _placeholderTextAttributes]];
  227. }
  228. - (NSDictionary<NSAttributedStringKey, id> *)_placeholderTextAttributes
  229. {
  230. NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new];
  231. [textAttributes setValue:self.placeholderColor ?: defaultPlaceholderColor() forKey:NSForegroundColorAttributeName];
  232. if (![textAttributes objectForKey:NSFontAttributeName]) {
  233. [textAttributes setValue:defaultPlaceholderFont() forKey:NSFontAttributeName];
  234. }
  235. return textAttributes;
  236. }
  237. #pragma mark - Utility Methods
  238. - (void)copyTextAttributesFrom:(NSAttributedString *)sourceString
  239. {
  240. [self.textStorage beginEditing];
  241. NSTextStorage *textStorage = self.textStorage;
  242. [sourceString enumerateAttributesInRange:NSMakeRange(0, sourceString.length)
  243. options:NSAttributedStringEnumerationReverse
  244. usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
  245. [textStorage setAttributes:attrs range:range];
  246. }];
  247. [self.textStorage endEditing];
  248. }
  249. @end