This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTShadowView.h>
#import "RCTTextAttributes.h"
NS_ASSUME_NONNULL_BEGIN
extern NSString *const RCTBaseTextShadowViewEmbeddedShadowViewAttributeName;
@interface RCTBaseTextShadowView : RCTShadowView {
@protected NSAttributedString *_Nullable cachedAttributedText;
@protected RCTTextAttributes *_Nullable cachedTextAttributes;
}
@property (nonatomic, strong) RCTTextAttributes *textAttributes;
- (NSAttributedString *)attributedTextWithBaseTextAttributes:(nullable RCTTextAttributes *)baseTextAttributes;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,157 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBaseTextShadowView.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTRawTextShadowView.h>
#import <React/RCTVirtualTextShadowView.h>
NSString *const RCTBaseTextShadowViewEmbeddedShadowViewAttributeName = @"RCTBaseTextShadowViewEmbeddedShadowViewAttributeName";
static void RCTInlineViewYogaNodeDirtied(YGNodeRef node)
{
// An inline view (a view nested inside of a text node) does not have a parent
// in the Yoga tree. Consequently, we have to manually propagate the inline
// view's dirty signal up through the text nodes. At some point, it'll reach
// the outermost text node which has a Yoga node and then Yoga will take over
// the dirty signal propagation.
RCTShadowView *inlineView = (__bridge RCTShadowView *)YGNodeGetContext(node);
RCTBaseTextShadowView *baseTextShadowView =
(RCTBaseTextShadowView *)inlineView.reactSuperview;
[baseTextShadowView dirtyLayout];
}
@implementation RCTBaseTextShadowView
- (instancetype)init
{
if (self = [super init]) {
_textAttributes = [RCTTextAttributes new];
}
return self;
}
- (void)setReactTag:(NSNumber *)reactTag
{
[super setReactTag:reactTag];
_textAttributes.tag = reactTag;
}
#pragma mark - Life Cycle
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[self dirtyLayout];
if (![subview isKindOfClass:[RCTVirtualTextShadowView class]]) {
YGNodeSetDirtiedFunc(subview.yogaNode, RCTInlineViewYogaNodeDirtied);
}
}
- (void)removeReactSubview:(RCTShadowView *)subview
{
if (![subview isKindOfClass:[RCTVirtualTextShadowView class]]) {
YGNodeSetDirtiedFunc(subview.yogaNode, NULL);
}
[self dirtyLayout];
[super removeReactSubview:subview];
}
#pragma mark - attributedString
- (NSAttributedString *)attributedTextWithBaseTextAttributes:(nullable RCTTextAttributes *)baseTextAttributes
{
RCTTextAttributes *textAttributes;
if (baseTextAttributes) {
textAttributes = [baseTextAttributes copy];
[textAttributes applyTextAttributes:self.textAttributes];
} else {
textAttributes = [self.textAttributes copy];
}
if (cachedAttributedText && [cachedTextAttributes isEqual:textAttributes]) {
return cachedAttributedText;
}
NSMutableAttributedString *attributedText = [NSMutableAttributedString new];
[attributedText beginEditing];
for (RCTShadowView *shadowView in self.reactSubviews) {
// Special Case: RCTRawTextShadowView
if ([shadowView isKindOfClass:[RCTRawTextShadowView class]]) {
RCTRawTextShadowView *rawTextShadowView = (RCTRawTextShadowView *)shadowView;
NSString *text = rawTextShadowView.text;
if (text) {
NSAttributedString *rawTextAttributedString =
[[NSAttributedString alloc] initWithString:[textAttributes applyTextAttributesToText:text]
attributes:textAttributes.effectiveTextAttributes];
[attributedText appendAttributedString:rawTextAttributedString];
}
continue;
}
// Special Case: RCTBaseTextShadowView
if ([shadowView isKindOfClass:[RCTBaseTextShadowView class]]) {
RCTBaseTextShadowView *baseTextShadowView = (RCTBaseTextShadowView *)shadowView;
NSAttributedString *baseTextAttributedString =
[baseTextShadowView attributedTextWithBaseTextAttributes:textAttributes];
[attributedText appendAttributedString:baseTextAttributedString];
continue;
}
// Generic Case: Any RCTShadowView
NSTextAttachment *attachment = [NSTextAttachment new];
NSMutableAttributedString *embeddedShadowViewAttributedString = [NSMutableAttributedString new];
[embeddedShadowViewAttributedString beginEditing];
[embeddedShadowViewAttributedString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
[embeddedShadowViewAttributedString addAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName
value:shadowView
range:(NSRange){0, embeddedShadowViewAttributedString.length}];
[embeddedShadowViewAttributedString endEditing];
[attributedText appendAttributedString:embeddedShadowViewAttributedString];
}
[attributedText endEditing];
[self clearLayout];
cachedAttributedText = [attributedText copy];
cachedTextAttributes = textAttributes;
return cachedAttributedText;
}
- (void)dirtyLayout
{
[super dirtyLayout];
cachedAttributedText = nil;
cachedTextAttributes = nil;
}
- (void)didUpdateReactSubviews
{
[super didUpdateReactSubviews];
[self dirtyLayout];
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
[self dirtyLayout];
}
@end

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTBaseTextViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,57 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBaseTextViewManager.h>
@implementation RCTBaseTextViewManager
RCT_EXPORT_MODULE(RCTBaseText)
- (UIView *)view
{
RCTAssert(NO, @"The `-[RCTBaseTextViewManager view]` property must be overridden in subclass.");
return nil;
}
- (RCTShadowView *)shadowView
{
RCTAssert(NO, @"The `-[RCTBaseTextViewManager shadowView]` property must be overridden in subclass.");
return nil;
}
#pragma mark - Text Attributes
// Color
RCT_REMAP_SHADOW_PROPERTY(color, textAttributes.foregroundColor, UIColor)
RCT_REMAP_SHADOW_PROPERTY(backgroundColor, textAttributes.backgroundColor, UIColor)
RCT_REMAP_SHADOW_PROPERTY(opacity, textAttributes.opacity, CGFloat)
// Font
RCT_REMAP_SHADOW_PROPERTY(fontFamily, textAttributes.fontFamily, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontSize, textAttributes.fontSize, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(fontWeight, textAttributes.fontWeight, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontStyle, textAttributes.fontStyle, NSString)
RCT_REMAP_SHADOW_PROPERTY(fontVariant, textAttributes.fontVariant, NSArray)
RCT_REMAP_SHADOW_PROPERTY(allowFontScaling, textAttributes.allowFontScaling, BOOL)
RCT_REMAP_SHADOW_PROPERTY(maxFontSizeMultiplier, textAttributes.maxFontSizeMultiplier, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(letterSpacing, textAttributes.letterSpacing, CGFloat)
// Paragraph Styles
RCT_REMAP_SHADOW_PROPERTY(lineHeight, textAttributes.lineHeight, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(textAlign, textAttributes.alignment, NSTextAlignment)
RCT_REMAP_SHADOW_PROPERTY(writingDirection, textAttributes.baseWritingDirection, NSWritingDirection)
// Decoration
RCT_REMAP_SHADOW_PROPERTY(textDecorationColor, textAttributes.textDecorationColor, UIColor)
RCT_REMAP_SHADOW_PROPERTY(textDecorationStyle, textAttributes.textDecorationStyle, NSUnderlineStyle)
RCT_REMAP_SHADOW_PROPERTY(textDecorationLine, textAttributes.textDecorationLine, RCTTextDecorationLineType)
// Shadow
RCT_REMAP_SHADOW_PROPERTY(textShadowOffset, textAttributes.textShadowOffset, CGSize)
RCT_REMAP_SHADOW_PROPERTY(textShadowRadius, textAttributes.textShadowRadius, CGFloat)
RCT_REMAP_SHADOW_PROPERTY(textShadowColor, textAttributes.textShadowColor, UIColor)
// Special
RCT_REMAP_SHADOW_PROPERTY(isHighlighted, textAttributes.isHighlighted, BOOL)
RCT_REMAP_SHADOW_PROPERTY(textTransform, textAttributes.textTransform, RCTTextTransform)
@end

View File

@ -0,0 +1,22 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTConvert.h>
#import "RCTTextTransform.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTConvert (Text)
+ (UITextAutocorrectionType)UITextAutocorrectionType:(nullable id)json;
+ (UITextSpellCheckingType)UITextSpellCheckingType:(nullable id)json;
+ (RCTTextTransform)RCTTextTransform:(nullable id)json;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTConvert+Text.h>
@implementation RCTConvert (Text)
+ (UITextAutocorrectionType)UITextAutocorrectionType:(id)json
{
return
json == nil ? UITextAutocorrectionTypeDefault :
[RCTConvert BOOL:json] ? UITextAutocorrectionTypeYes :
UITextAutocorrectionTypeNo;
}
+ (UITextSpellCheckingType)UITextSpellCheckingType:(id)json
{
return
json == nil ? UITextSpellCheckingTypeDefault :
[RCTConvert BOOL:json] ? UITextSpellCheckingTypeYes :
UITextSpellCheckingTypeNo;
}
RCT_ENUM_CONVERTER(RCTTextTransform, (@{
@"none": @(RCTTextTransformNone),
@"capitalize": @(RCTTextTransformCapitalize),
@"uppercase": @(RCTTextTransformUppercase),
@"lowercase": @(RCTTextTransformLowercase),
}), RCTTextTransformUndefined, integerValue)
@end

View File

@ -0,0 +1,97 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTTextDecorationLineType.h>
#import "RCTTextTransform.h"
NS_ASSUME_NONNULL_BEGIN
extern NSString *const RCTTextAttributesIsHighlightedAttributeName;
extern NSString *const RCTTextAttributesTagAttributeName;
/**
* Represents knowledge about all supported *text* attributes
* assigned to some text component such as <Text>, <VirtualText>,
* and <TextInput>.
*/
@interface RCTTextAttributes : NSObject <NSCopying>
// Color
@property (nonatomic, strong, nullable) UIColor *foregroundColor;
@property (nonatomic, strong, nullable) UIColor *backgroundColor;
@property (nonatomic, assign) CGFloat opacity;
// Font
@property (nonatomic, copy, nullable) NSString *fontFamily;
@property (nonatomic, assign) CGFloat fontSize;
@property (nonatomic, assign) CGFloat fontSizeMultiplier;
@property (nonatomic, assign) CGFloat maxFontSizeMultiplier;
@property (nonatomic, copy, nullable) NSString *fontWeight;
@property (nonatomic, copy, nullable) NSString *fontStyle;
@property (nonatomic, copy, nullable) NSArray<NSString *> *fontVariant;
@property (nonatomic, assign) BOOL allowFontScaling;
@property (nonatomic, assign) CGFloat letterSpacing;
// Paragraph Styles
@property (nonatomic, assign) CGFloat lineHeight;
@property (nonatomic, assign) NSTextAlignment alignment;
@property (nonatomic, assign) NSWritingDirection baseWritingDirection;
// Decoration
@property (nonatomic, strong, nullable) UIColor *textDecorationColor;
@property (nonatomic, assign) NSUnderlineStyle textDecorationStyle;
@property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine;
// Shadow
@property (nonatomic, assign) CGSize textShadowOffset;
@property (nonatomic, assign) CGFloat textShadowRadius;
@property (nonatomic, strong, nullable) UIColor *textShadowColor;
// Special
@property (nonatomic, assign) BOOL isHighlighted;
@property (nonatomic, strong, nullable) NSNumber *tag;
@property (nonatomic, assign) UIUserInterfaceLayoutDirection layoutDirection;
@property (nonatomic, assign) RCTTextTransform textTransform;
#pragma mark - Inheritance
- (void)applyTextAttributes:(RCTTextAttributes *)textAttributes;
#pragma mark - Adapters
/**
* Text attributes in NSAttributedString terms.
*/
- (NSDictionary<NSAttributedStringKey, id> *)effectiveTextAttributes;
/**
* Constructed paragraph style.
*/
- (NSParagraphStyle *_Nullable)effectiveParagraphStyle;
/**
* Constructed font.
*/
- (UIFont *)effectiveFont;
/**
* Font size multiplier reflects `allowFontScaling`, `fontSizeMultiplier`, and `maxFontSizeMultiplier`.
*/
- (CGFloat)effectiveFontSizeMultiplier;
/**
* Foreground and background colors with opacity and right defaults.
*/
- (UIColor *)effectiveForegroundColor;
- (UIColor *)effectiveBackgroundColor;
/**
* Text transformed per 'none', 'uppercase', 'lowercase', 'capitalize'
*/
- (NSString *)applyTextAttributesToText:(NSString *)text;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,312 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTTextAttributes.h>
#import <React/RCTAssert.h>
#import <React/RCTFont.h>
#import <React/RCTLog.h>
NSString *const RCTTextAttributesIsHighlightedAttributeName = @"RCTTextAttributesIsHighlightedAttributeName";
NSString *const RCTTextAttributesTagAttributeName = @"RCTTextAttributesTagAttributeName";
@implementation RCTTextAttributes
- (instancetype)init
{
if (self = [super init]) {
_fontSize = NAN;
_letterSpacing = NAN;
_lineHeight = NAN;
_textDecorationStyle = NSUnderlineStyleSingle;
_fontSizeMultiplier = NAN;
_maxFontSizeMultiplier = NAN;
_alignment = NSTextAlignmentNatural;
_baseWritingDirection = NSWritingDirectionNatural;
_textShadowRadius = NAN;
_opacity = NAN;
_textTransform = RCTTextTransformUndefined;
}
return self;
}
- (void)applyTextAttributes:(RCTTextAttributes *)textAttributes
{
// Note: All lines marked with `*` does not use explicit/correct rules to compare old and new values because
// their types do not have special designated value representing undefined/unspecified/inherit meaning.
// We will address this in the future.
// Color
_foregroundColor = textAttributes->_foregroundColor ?: _foregroundColor;
_backgroundColor = textAttributes->_backgroundColor ?: _backgroundColor;
_opacity = !isnan(textAttributes->_opacity) ? (isnan(_opacity) ? 1.0 : _opacity) * textAttributes->_opacity : _opacity;
// Font
_fontFamily = textAttributes->_fontFamily ?: _fontFamily;
_fontSize = !isnan(textAttributes->_fontSize) ? textAttributes->_fontSize : _fontSize;
_fontSizeMultiplier = !isnan(textAttributes->_fontSizeMultiplier) ? textAttributes->_fontSizeMultiplier : _fontSizeMultiplier;
_maxFontSizeMultiplier = !isnan(textAttributes->_maxFontSizeMultiplier) ? textAttributes->_maxFontSizeMultiplier : _maxFontSizeMultiplier;
_fontWeight = textAttributes->_fontWeight ?: _fontWeight;
_fontStyle = textAttributes->_fontStyle ?: _fontStyle;
_fontVariant = textAttributes->_fontVariant ?: _fontVariant;
_allowFontScaling = textAttributes->_allowFontScaling || _allowFontScaling; // *
_letterSpacing = !isnan(textAttributes->_letterSpacing) ? textAttributes->_letterSpacing : _letterSpacing;
// Paragraph Styles
_lineHeight = !isnan(textAttributes->_lineHeight) ? textAttributes->_lineHeight : _lineHeight;
_alignment = textAttributes->_alignment != NSTextAlignmentNatural ? textAttributes->_alignment : _alignment; // *
_baseWritingDirection = textAttributes->_baseWritingDirection != NSWritingDirectionNatural ? textAttributes->_baseWritingDirection : _baseWritingDirection; // *
// Decoration
_textDecorationColor = textAttributes->_textDecorationColor ?: _textDecorationColor;
_textDecorationStyle = textAttributes->_textDecorationStyle != NSUnderlineStyleSingle ? textAttributes->_textDecorationStyle : _textDecorationStyle; // *
_textDecorationLine = textAttributes->_textDecorationLine != RCTTextDecorationLineTypeNone ? textAttributes->_textDecorationLine : _textDecorationLine; // *
// Shadow
_textShadowOffset = !CGSizeEqualToSize(textAttributes->_textShadowOffset, CGSizeZero) ? textAttributes->_textShadowOffset : _textShadowOffset; // *
_textShadowRadius = !isnan(textAttributes->_textShadowRadius) ? textAttributes->_textShadowRadius : _textShadowRadius;
_textShadowColor = textAttributes->_textShadowColor ?: _textShadowColor;
// Special
_isHighlighted = textAttributes->_isHighlighted || _isHighlighted; // *
_tag = textAttributes->_tag ?: _tag;
_layoutDirection = textAttributes->_layoutDirection != UIUserInterfaceLayoutDirectionLeftToRight ? textAttributes->_layoutDirection : _layoutDirection;
_textTransform = textAttributes->_textTransform != RCTTextTransformUndefined ? textAttributes->_textTransform : _textTransform;
}
- (NSParagraphStyle *)effectiveParagraphStyle
{
// Paragraph Style
NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
BOOL isParagraphStyleUsed = NO;
if (_alignment != NSTextAlignmentNatural) {
NSTextAlignment alignment = _alignment;
if (_layoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
if (alignment == NSTextAlignmentRight) {
alignment = NSTextAlignmentLeft;
} else if (alignment == NSTextAlignmentLeft) {
alignment = NSTextAlignmentRight;
}
}
paragraphStyle.alignment = alignment;
isParagraphStyleUsed = YES;
}
if (_baseWritingDirection != NSWritingDirectionNatural) {
paragraphStyle.baseWritingDirection = _baseWritingDirection;
isParagraphStyleUsed = YES;
}
if (!isnan(_lineHeight)) {
CGFloat lineHeight = _lineHeight * self.effectiveFontSizeMultiplier;
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
isParagraphStyleUsed = YES;
}
if (isParagraphStyleUsed) {
return [paragraphStyle copy];
}
return nil;
}
- (NSDictionary<NSAttributedStringKey, id> *)effectiveTextAttributes
{
NSMutableDictionary<NSAttributedStringKey, id> *attributes =
[NSMutableDictionary dictionaryWithCapacity:10];
// Font
UIFont *font = self.effectiveFont;
if (font) {
attributes[NSFontAttributeName] = font;
}
// Colors
UIColor *effectiveForegroundColor = self.effectiveForegroundColor;
if (_foregroundColor || !isnan(_opacity)) {
attributes[NSForegroundColorAttributeName] = effectiveForegroundColor;
}
if (_backgroundColor || !isnan(_opacity)) {
attributes[NSBackgroundColorAttributeName] = self.effectiveBackgroundColor;
}
// Kerning
if (!isnan(_letterSpacing)) {
attributes[NSKernAttributeName] = @(_letterSpacing);
}
// Paragraph Style
NSParagraphStyle *paragraphStyle = [self effectiveParagraphStyle];
if (paragraphStyle) {
attributes[NSParagraphStyleAttributeName] = paragraphStyle;
}
// Decoration
BOOL isTextDecorationEnabled = NO;
if (_textDecorationLine == RCTTextDecorationLineTypeUnderline ||
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough) {
isTextDecorationEnabled = YES;
attributes[NSUnderlineStyleAttributeName] = @(_textDecorationStyle);
}
if (_textDecorationLine == RCTTextDecorationLineTypeStrikethrough ||
_textDecorationLine == RCTTextDecorationLineTypeUnderlineStrikethrough){
isTextDecorationEnabled = YES;
attributes[NSStrikethroughStyleAttributeName] = @(_textDecorationStyle);
}
if (_textDecorationColor || isTextDecorationEnabled) {
attributes[NSStrikethroughColorAttributeName] = _textDecorationColor ?: effectiveForegroundColor;
attributes[NSUnderlineColorAttributeName] = _textDecorationColor ?: effectiveForegroundColor;
}
// Shadow
if (!isnan(_textShadowRadius)) {
NSShadow *shadow = [NSShadow new];
shadow.shadowOffset = _textShadowOffset;
shadow.shadowBlurRadius = _textShadowRadius;
shadow.shadowColor = _textShadowColor;
attributes[NSShadowAttributeName] = shadow;
}
// Special
if (_isHighlighted) {
attributes[RCTTextAttributesIsHighlightedAttributeName] = @YES;
}
if (_tag) {
attributes[RCTTextAttributesTagAttributeName] = _tag;
}
return [attributes copy];
}
- (UIFont *)effectiveFont
{
// FIXME: RCTFont has thread-safety issues and must be rewritten.
return [RCTFont updateFont:nil
withFamily:_fontFamily
size:@(isnan(_fontSize) ? 0 : _fontSize)
weight:_fontWeight
style:_fontStyle
variant:_fontVariant
scaleMultiplier:self.effectiveFontSizeMultiplier];
}
- (CGFloat)effectiveFontSizeMultiplier
{
bool fontScalingEnabled = !RCTHasFontHandlerSet() && _allowFontScaling;
if (fontScalingEnabled) {
CGFloat fontSizeMultiplier = !isnan(_fontSizeMultiplier) ? _fontSizeMultiplier : 1.0;
CGFloat maxFontSizeMultiplier = !isnan(_maxFontSizeMultiplier) ? _maxFontSizeMultiplier : 0.0;
return maxFontSizeMultiplier >= 1.0 ? fminf(maxFontSizeMultiplier, fontSizeMultiplier) : fontSizeMultiplier;
} else {
return 1.0;
}
}
- (UIColor *)effectiveForegroundColor
{
UIColor *effectiveForegroundColor = _foregroundColor ?: [UIColor blackColor];
if (!isnan(_opacity)) {
effectiveForegroundColor = [effectiveForegroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveForegroundColor.CGColor) * _opacity];
}
return effectiveForegroundColor;
}
- (UIColor *)effectiveBackgroundColor
{
UIColor *effectiveBackgroundColor = _backgroundColor;// ?: [[UIColor whiteColor] colorWithAlphaComponent:0];
if (effectiveBackgroundColor && !isnan(_opacity)) {
effectiveBackgroundColor = [effectiveBackgroundColor colorWithAlphaComponent:CGColorGetAlpha(effectiveBackgroundColor.CGColor) * _opacity];
}
return effectiveBackgroundColor ?: [UIColor clearColor];
}
- (NSString *)applyTextAttributesToText:(NSString *)text
{
switch (_textTransform) {
case RCTTextTransformUndefined:
case RCTTextTransformNone:
return text;
case RCTTextTransformLowercase:
return [text lowercaseString];
case RCTTextTransformUppercase:
return [text uppercaseString];
case RCTTextTransformCapitalize:
return [text capitalizedString];
}
}
- (RCTTextAttributes *)copyWithZone:(NSZone *)zone
{
RCTTextAttributes *textAttributes = [RCTTextAttributes new];
[textAttributes applyTextAttributes:self];
return textAttributes;
}
#pragma mark - NSObject
- (BOOL)isEqual:(RCTTextAttributes *)textAttributes
{
if (!textAttributes) {
return NO;
}
if (self == textAttributes) {
return YES;
}
#define RCTTextAttributesCompareFloats(a) ((a == textAttributes->a) || (isnan(a) && isnan(textAttributes->a)))
#define RCTTextAttributesCompareSize(a) CGSizeEqualToSize(a, textAttributes->a)
#define RCTTextAttributesCompareObjects(a) ((a == textAttributes->a) || [a isEqual:textAttributes->a])
#define RCTTextAttributesCompareStrings(a) ((a == textAttributes->a) || [a isEqualToString:textAttributes->a])
#define RCTTextAttributesCompareOthers(a) (a == textAttributes->a)
return
RCTTextAttributesCompareObjects(_foregroundColor) &&
RCTTextAttributesCompareObjects(_backgroundColor) &&
RCTTextAttributesCompareFloats(_opacity) &&
// Font
RCTTextAttributesCompareObjects(_fontFamily) &&
RCTTextAttributesCompareFloats(_fontSize) &&
RCTTextAttributesCompareFloats(_fontSizeMultiplier) &&
RCTTextAttributesCompareFloats(_maxFontSizeMultiplier) &&
RCTTextAttributesCompareStrings(_fontWeight) &&
RCTTextAttributesCompareObjects(_fontStyle) &&
RCTTextAttributesCompareObjects(_fontVariant) &&
RCTTextAttributesCompareOthers(_allowFontScaling) &&
RCTTextAttributesCompareFloats(_letterSpacing) &&
// Paragraph Styles
RCTTextAttributesCompareFloats(_lineHeight) &&
RCTTextAttributesCompareFloats(_alignment) &&
RCTTextAttributesCompareOthers(_baseWritingDirection) &&
// Decoration
RCTTextAttributesCompareObjects(_textDecorationColor) &&
RCTTextAttributesCompareOthers(_textDecorationStyle) &&
RCTTextAttributesCompareOthers(_textDecorationLine) &&
// Shadow
RCTTextAttributesCompareSize(_textShadowOffset) &&
RCTTextAttributesCompareFloats(_textShadowRadius) &&
RCTTextAttributesCompareObjects(_textShadowColor) &&
// Special
RCTTextAttributesCompareOthers(_isHighlighted) &&
RCTTextAttributesCompareObjects(_tag) &&
RCTTextAttributesCompareOthers(_layoutDirection) &&
RCTTextAttributesCompareOthers(_textTransform);
}
@end

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, RCTTextTransform) {
RCTTextTransformUndefined = 0,
RCTTextTransformNone,
RCTTextTransformCapitalize,
RCTTextTransformUppercase,
RCTTextTransformLowercase,
};

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTShadowView.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTRawTextShadowView : RCTShadowView
@property (nonatomic, copy, nullable) NSString *text;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTRawTextShadowView.h>
#import <React/RCTShadowView+Layout.h>
@implementation RCTRawTextShadowView
- (void)setText:(NSString *)text
{
if (_text != text && ![_text isEqualToString:text]) {
_text = [text copy];
[self dirtyLayout];
}
}
- (void)dirtyLayout
{
[self.superview dirtyLayout];
}
- (NSString *)description
{
NSString *superDescription = super.description;
return [[superDescription substringToIndex:superDescription.length - 1] stringByAppendingFormat:@"; text: %@>", self.text];
}
@end

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTRawTextViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTRawTextViewManager.h>
#import <React/RCTRawTextShadowView.h>
@implementation RCTRawTextViewManager
RCT_EXPORT_MODULE(RCTRawText)
- (UIView *)view
{
return [UIView new];
}
- (RCTShadowView *)shadowView
{
return [RCTRawTextShadowView new];
}
RCT_EXPORT_SHADOW_PROPERTY(text, NSString)
@end

View File

@ -0,0 +1,34 @@
# Copyright (c) Facebook, Inc. and its affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip
else
source[:tag] = "v#{version}"
end
Pod::Spec.new do |s|
s.name = "React-RCTText"
s.version = version
s.summary = "A React component for displaying text."
s.homepage = "https://reactnative.dev/"
s.documentation_url = "https://reactnative.dev/docs/text"
s.license = package["license"]
s.author = "Facebook, Inc. and its affiliates"
s.platforms = { :ios => "10.0", :tvos => "10.0" }
s.source = source
s.source_files = "**/*.{h,m}"
s.preserve_paths = "package.json", "LICENSE", "LICENSE-docs"
s.header_dir = "RCTText"
s.dependency "React-Core/RCTTextHeaders", version
end

302
node_modules/react-native/Libraries/Text/Text.js generated vendored Normal file
View File

@ -0,0 +1,302 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
* @format
*/
'use strict';
const DeprecatedTextPropTypes = require('../DeprecatedPropTypes/DeprecatedTextPropTypes');
const React = require('react');
const ReactNativeViewAttributes = require('../Components/View/ReactNativeViewAttributes');
const TextAncestor = require('./TextAncestor');
const Touchable = require('../Components/Touchable/Touchable');
const UIManager = require('../ReactNative/UIManager');
const createReactNativeComponentClass = require('../Renderer/shims/createReactNativeComponentClass');
const nullthrows = require('nullthrows');
const processColor = require('../StyleSheet/processColor');
import type {PressEvent} from '../Types/CoreEventTypes';
import type {HostComponent} from '../Renderer/shims/ReactNativeTypes';
import type {PressRetentionOffset, TextProps} from './TextProps';
type ResponseHandlers = $ReadOnly<{|
onStartShouldSetResponder: () => boolean,
onResponderGrant: (event: PressEvent, dispatchID: string) => void,
onResponderMove: (event: PressEvent) => void,
onResponderRelease: (event: PressEvent) => void,
onResponderTerminate: (event: PressEvent) => void,
onResponderTerminationRequest: () => boolean,
|}>;
type Props = $ReadOnly<{|
...TextProps,
forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
|}>;
type State = {|
touchable: {|
touchState: ?string,
responderID: ?number,
|},
isHighlighted: boolean,
createResponderHandlers: () => ResponseHandlers,
responseHandlers: ?ResponseHandlers,
|};
const PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
const viewConfig = {
validAttributes: {
...ReactNativeViewAttributes.UIView,
isHighlighted: true,
numberOfLines: true,
ellipsizeMode: true,
allowFontScaling: true,
maxFontSizeMultiplier: true,
disabled: true,
selectable: true,
selectionColor: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
textBreakStrategy: true,
onTextLayout: true,
onInlineViewLayout: true,
dataDetectorType: true,
},
directEventTypes: {
topTextLayout: {
registrationName: 'onTextLayout',
},
topInlineViewLayout: {
registrationName: 'onInlineViewLayout',
},
},
uiViewClassName: 'RCTText',
};
/**
* A React component for displaying text.
*
* See https://reactnative.dev/docs/text.html
*/
class TouchableText extends React.Component<Props, State> {
static defaultProps = {
accessible: true,
allowFontScaling: true,
ellipsizeMode: 'tail',
};
touchableGetPressRectOffset: ?() => PressRetentionOffset;
touchableHandleActivePressIn: ?() => void;
touchableHandleActivePressOut: ?() => void;
touchableHandleLongPress: ?(event: PressEvent) => void;
touchableHandlePress: ?(event: PressEvent) => void;
touchableHandleResponderGrant: ?(
event: PressEvent,
dispatchID: string,
) => void;
touchableHandleResponderMove: ?(event: PressEvent) => void;
touchableHandleResponderRelease: ?(event: PressEvent) => void;
touchableHandleResponderTerminate: ?(event: PressEvent) => void;
touchableHandleResponderTerminationRequest: ?() => boolean;
state = {
...Touchable.Mixin.touchableGetInitialState(),
isHighlighted: false,
createResponderHandlers: this._createResponseHandlers.bind(this),
responseHandlers: null,
};
static getDerivedStateFromProps(
nextProps: Props,
prevState: State,
): $Shape<State> | null {
return prevState.responseHandlers == null && isTouchable(nextProps)
? {
responseHandlers: prevState.createResponderHandlers(),
}
: null;
}
static viewConfig = viewConfig;
render(): React.Node {
let props = this.props;
if (isTouchable(props)) {
props = {
...props,
...this.state.responseHandlers,
isHighlighted: this.state.isHighlighted,
};
}
if (props.selectionColor != null) {
props = {
...props,
selectionColor: processColor(props.selectionColor),
};
}
if (__DEV__) {
if (Touchable.TOUCH_TARGET_DEBUG && props.onPress != null) {
props = {
...props,
style: [props.style, {color: 'magenta'}],
};
}
}
return (
<TextAncestor.Consumer>
{hasTextAncestor =>
hasTextAncestor ? (
<RCTVirtualText {...props} ref={props.forwardedRef} />
) : (
<TextAncestor.Provider value={true}>
<RCTText {...props} ref={props.forwardedRef} />
</TextAncestor.Provider>
)
}
</TextAncestor.Consumer>
);
}
_createResponseHandlers(): ResponseHandlers {
return {
onStartShouldSetResponder: (): boolean => {
const {onStartShouldSetResponder} = this.props;
const shouldSetResponder =
(onStartShouldSetResponder == null
? false
: onStartShouldSetResponder()) || isTouchable(this.props);
if (shouldSetResponder) {
this._attachTouchHandlers();
}
return shouldSetResponder;
},
onResponderGrant: (event: PressEvent, dispatchID: string): void => {
nullthrows(this.touchableHandleResponderGrant)(event, dispatchID);
if (this.props.onResponderGrant != null) {
this.props.onResponderGrant.call(this, event, dispatchID);
}
},
onResponderMove: (event: PressEvent): void => {
nullthrows(this.touchableHandleResponderMove)(event);
if (this.props.onResponderMove != null) {
this.props.onResponderMove.call(this, event);
}
},
onResponderRelease: (event: PressEvent): void => {
nullthrows(this.touchableHandleResponderRelease)(event);
if (this.props.onResponderRelease != null) {
this.props.onResponderRelease.call(this, event);
}
},
onResponderTerminate: (event: PressEvent): void => {
nullthrows(this.touchableHandleResponderTerminate)(event);
if (this.props.onResponderTerminate != null) {
this.props.onResponderTerminate.call(this, event);
}
},
onResponderTerminationRequest: (): boolean => {
const {onResponderTerminationRequest} = this.props;
if (!nullthrows(this.touchableHandleResponderTerminationRequest)()) {
return false;
}
if (onResponderTerminationRequest == null) {
return true;
}
return onResponderTerminationRequest();
},
};
}
/**
* Lazily attaches Touchable.Mixin handlers.
*/
_attachTouchHandlers(): void {
if (this.touchableGetPressRectOffset != null) {
return;
}
for (const key in Touchable.Mixin) {
if (typeof Touchable.Mixin[key] === 'function') {
(this: any)[key] = Touchable.Mixin[key].bind(this);
}
}
this.touchableHandleActivePressIn = (): void => {
if (!this.props.suppressHighlighting && isTouchable(this.props)) {
this.setState({isHighlighted: true});
}
};
this.touchableHandleActivePressOut = (): void => {
if (!this.props.suppressHighlighting && isTouchable(this.props)) {
this.setState({isHighlighted: false});
}
};
this.touchableHandlePress = (event: PressEvent): void => {
if (this.props.onPress != null) {
this.props.onPress(event);
}
};
this.touchableHandleLongPress = (event: PressEvent): void => {
if (this.props.onLongPress != null) {
this.props.onLongPress(event);
}
};
this.touchableGetPressRectOffset = (): PressRetentionOffset =>
this.props.pressRetentionOffset == null
? PRESS_RECT_OFFSET
: this.props.pressRetentionOffset;
}
}
const isTouchable = (props: Props): boolean =>
props.onPress != null ||
props.onLongPress != null ||
props.onStartShouldSetResponder != null;
const RCTText = createReactNativeComponentClass(
viewConfig.uiViewClassName,
() => viewConfig,
);
const RCTVirtualText =
UIManager.getViewManagerConfig('RCTVirtualText') == null
? RCTText
: createReactNativeComponentClass('RCTVirtualText', () => ({
validAttributes: {
...ReactNativeViewAttributes.UIView,
isHighlighted: true,
maxFontSizeMultiplier: true,
},
uiViewClassName: 'RCTVirtualText',
}));
const Text = (
props: TextProps,
forwardedRef: ?React.Ref<'RCTText' | 'RCTVirtualText'>,
) => {
return <TouchableText {...props} forwardedRef={forwardedRef} />;
};
const TextToExport = React.forwardRef(Text);
TextToExport.displayName = 'Text';
// TODO: Deprecate this.
/* $FlowFixMe(>=0.89.0 site=react_native_fb) This comment suppresses an error
* found when Flow v0.89 was deployed. To see the error, delete this comment
* and run Flow. */
TextToExport.propTypes = DeprecatedTextPropTypes;
type TextStatics = $ReadOnly<{|
propTypes: typeof DeprecatedTextPropTypes,
|}>;
module.exports = ((TextToExport: any): React.AbstractComponent<
TextProps,
React.ElementRef<HostComponent<TextProps>>,
> &
TextStatics);

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@interface NSTextStorage (FontScaling)
- (void)scaleFontSizeToFitSize:(CGSize)size
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize;
- (void)scaleFontSizeWithRatio:(CGFloat)ratio
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize;
@end

View File

@ -0,0 +1,137 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "NSTextStorage+FontScaling.h"
typedef NS_OPTIONS(NSInteger, RCTTextSizeComparisonOptions) {
RCTTextSizeComparisonSmaller = 1 << 0,
RCTTextSizeComparisonLarger = 1 << 1,
RCTTextSizeComparisonWithinRange = 1 << 2,
};
@implementation NSTextStorage (FontScaling)
- (void)scaleFontSizeToFitSize:(CGSize)size
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize
{
CGFloat bottomRatio = 1.0/128.0;
CGFloat topRatio = 128.0;
CGFloat ratio = 1.0;
NSAttributedString *originalAttributedString = [self copy];
CGFloat lastRatioWhichFits = 0.02;
while (true) {
[self scaleFontSizeWithRatio:ratio
minimumFontSize:minimumFontSize
maximumFontSize:maximumFontSize];
RCTTextSizeComparisonOptions comparsion =
[self compareToSize:size thresholdRatio:0.01];
if (
(comparsion & RCTTextSizeComparisonWithinRange) &&
(comparsion & RCTTextSizeComparisonSmaller)
) {
return;
} else if (comparsion & RCTTextSizeComparisonSmaller) {
bottomRatio = ratio;
lastRatioWhichFits = ratio;
} else {
topRatio = ratio;
}
ratio = (topRatio + bottomRatio) / 2.0;
CGFloat kRatioThreshold = 0.005;
if (
ABS(topRatio - bottomRatio) < kRatioThreshold ||
ABS(topRatio - ratio) < kRatioThreshold ||
ABS(bottomRatio - ratio) < kRatioThreshold
) {
[self replaceCharactersInRange:(NSRange){0, self.length}
withAttributedString:originalAttributedString];
[self scaleFontSizeWithRatio:lastRatioWhichFits
minimumFontSize:minimumFontSize
maximumFontSize:maximumFontSize];
return;
}
[self replaceCharactersInRange:(NSRange){0, self.length}
withAttributedString:originalAttributedString];
}
}
- (RCTTextSizeComparisonOptions)compareToSize:(CGSize)size thresholdRatio:(CGFloat)thresholdRatio
{
NSLayoutManager *layoutManager = self.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
// Does it fit the text container?
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:glyphRange.length - 1];
if (truncatedGlyphRange.location != NSNotFound) {
return RCTTextSizeComparisonLarger;
}
CGSize measuredSize = [layoutManager usedRectForTextContainer:textContainer].size;
// Does it fit the size?
BOOL fitsSize =
size.width >= measuredSize.width &&
size.height >= measuredSize.height;
CGSize thresholdSize = (CGSize){
size.width * thresholdRatio,
size.height * thresholdRatio,
};
RCTTextSizeComparisonOptions result = 0;
result |= (fitsSize) ? RCTTextSizeComparisonSmaller : RCTTextSizeComparisonLarger;
if (ABS(measuredSize.width - size.width) < thresholdSize.width) {
result = result | RCTTextSizeComparisonWithinRange;
}
return result;
}
- (void)scaleFontSizeWithRatio:(CGFloat)ratio
minimumFontSize:(CGFloat)minimumFontSize
maximumFontSize:(CGFloat)maximumFontSize
{
[self beginEditing];
[self enumerateAttribute:NSFontAttributeName
inRange:(NSRange){0, self.length}
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:
^(UIFont *_Nullable font, NSRange range, BOOL *_Nonnull stop) {
if (!font) {
return;
}
CGFloat fontSize = MAX(MIN(font.pointSize * ratio, maximumFontSize), minimumFontSize);
[self addAttribute:NSFontAttributeName
value:[font fontWithSize:fontSize]
range:range];
}
];
[self endEditing];
}
@end

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTShadowView.h>
#import "RCTBaseTextShadowView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTTextShadowView : RCTBaseTextShadowView
- (instancetype)initWithBridge:(RCTBridge *)bridge;
@property (nonatomic, assign) NSInteger maximumNumberOfLines;
@property (nonatomic, assign) NSLineBreakMode lineBreakMode;
@property (nonatomic, assign) BOOL adjustsFontSizeToFit;
@property (nonatomic, assign) CGFloat minimumFontScale;
@property (nonatomic, copy) RCTDirectEventBlock onTextLayout;
- (void)uiManagerWillPerformMounting;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,439 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTTextShadowView.h>
#import <React/RCTBridge.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTUIManager.h>
#import <yoga/Yoga.h>
#import "NSTextStorage+FontScaling.h"
#import <React/RCTTextView.h>
@implementation RCTTextShadowView
{
__weak RCTBridge *_bridge;
BOOL _needsUpdateView;
NSMapTable<id, NSTextStorage *> *_cachedTextStorages;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
_cachedTextStorages = [NSMapTable strongToStrongObjectsMapTable];
_needsUpdateView = YES;
YGNodeSetMeasureFunc(self.yogaNode, RCTTextShadowViewMeasure);
YGNodeSetBaselineFunc(self.yogaNode, RCTTextShadowViewBaseline);
}
return self;
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
// When applying a semi-transparent background color to Text component
// we must set the root text nodes text attribute background color to nil
// because the background color is drawn on the RCTTextView itself, as well
// as on the glphy background draw step. By setting this to nil, we allow
// the RCTTextView backgroundColor to be used, without affecting nested Text
// components.
self.textAttributes.backgroundColor = nil;
self.textAttributes.opacity = NAN;
}
- (BOOL)isYogaLeafNode
{
return YES;
}
- (void)dirtyLayout
{
[super dirtyLayout];
YGNodeMarkDirty(self.yogaNode);
[self invalidateCache];
}
- (void)invalidateCache
{
[_cachedTextStorages removeAllObjects];
_needsUpdateView = YES;
}
#pragma mark - RCTUIManagerObserver
- (void)uiManagerWillPerformMounting
{
if (YGNodeIsDirty(self.yogaNode)) {
return;
}
if (!_needsUpdateView) {
return;
}
_needsUpdateView = NO;
CGRect contentFrame = self.contentFrame;
NSTextStorage *textStorage = [self textStorageAndLayoutManagerThatFitsSize:self.contentFrame.size
exclusiveOwnership:YES];
NSNumber *tag = self.reactTag;
NSMutableArray<NSNumber *> *descendantViewTags = [NSMutableArray new];
[textStorage enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName
inRange:NSMakeRange(0, textStorage.length)
options:0
usingBlock:
^(RCTShadowView *shadowView, NSRange range, __unused BOOL *stop) {
if (!shadowView) {
return;
}
[descendantViewTags addObject:shadowView.reactTag];
}
];
[_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTTextView *textView = (RCTTextView *)viewRegistry[tag];
if (!textView) {
return;
}
NSMutableArray<UIView *> *descendantViews =
[NSMutableArray arrayWithCapacity:descendantViewTags.count];
[descendantViewTags enumerateObjectsUsingBlock:^(NSNumber *_Nonnull descendantViewTag, NSUInteger index, BOOL *_Nonnull stop) {
UIView *descendantView = viewRegistry[descendantViewTag];
if (!descendantView) {
return;
}
[descendantViews addObject:descendantView];
}];
// Removing all references to Shadow Views to avoid unnecessary retaining.
[textStorage removeAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName range:NSMakeRange(0, textStorage.length)];
[textView setTextStorage:textStorage
contentFrame:contentFrame
descendantViews:descendantViews];
}];
}
- (void)postprocessAttributedText:(NSMutableAttributedString *)attributedText
{
__block CGFloat maximumLineHeight = 0;
[attributedText enumerateAttribute:NSParagraphStyleAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:
^(NSParagraphStyle *paragraphStyle, __unused NSRange range, __unused BOOL *stop) {
if (!paragraphStyle) {
return;
}
maximumLineHeight = MAX(paragraphStyle.maximumLineHeight, maximumLineHeight);
}
];
if (maximumLineHeight == 0) {
// `lineHeight` was not specified, nothing to do.
return;
}
__block CGFloat maximumFontLineHeight = 0;
[attributedText enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:
^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (!font) {
return;
}
if (maximumFontLineHeight <= font.lineHeight) {
maximumFontLineHeight = font.lineHeight;
}
}
];
if (maximumLineHeight < maximumFontLineHeight) {
return;
}
CGFloat baseLineOffset = maximumLineHeight / 2.0 - maximumFontLineHeight / 2.0;
[attributedText addAttribute:NSBaselineOffsetAttributeName
value:@(baseLineOffset)
range:NSMakeRange(0, attributedText.length)];
}
- (NSAttributedString *)attributedTextWithMeasuredAttachmentsThatFitSize:(CGSize)size
{
static UIImage *placeholderImage;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
placeholderImage = [UIImage new];
});
NSMutableAttributedString *attributedText =
[[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTextWithBaseTextAttributes:nil]];
[attributedText beginEditing];
[attributedText enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:0
usingBlock:
^(RCTShadowView *shadowView, NSRange range, __unused BOOL *stop) {
if (!shadowView) {
return;
}
CGSize fittingSize = [shadowView sizeThatFitsMinimumSize:CGSizeZero
maximumSize:size];
NSTextAttachment *attachment = [NSTextAttachment new];
attachment.bounds = (CGRect){CGPointZero, fittingSize};
attachment.image = placeholderImage;
[attributedText addAttribute:NSAttachmentAttributeName value:attachment range:range];
}
];
[attributedText endEditing];
return [attributedText copy];
}
- (NSTextStorage *)textStorageAndLayoutManagerThatFitsSize:(CGSize)size
exclusiveOwnership:(BOOL)exclusiveOwnership
{
NSValue *key = [NSValue valueWithCGSize:size];
NSTextStorage *cachedTextStorage = [_cachedTextStorages objectForKey:key];
if (cachedTextStorage) {
if (exclusiveOwnership) {
[_cachedTextStorages removeObjectForKey:key];
}
return cachedTextStorage;
}
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5.
textContainer.lineBreakMode =
_maximumNumberOfLines > 0 ? _lineBreakMode : NSLineBreakByClipping;
textContainer.maximumNumberOfLines = _maximumNumberOfLines;
NSLayoutManager *layoutManager = [NSLayoutManager new];
layoutManager.usesFontLeading = NO;
[layoutManager addTextContainer:textContainer];
NSTextStorage *textStorage =
[[NSTextStorage alloc] initWithAttributedString:[self attributedTextWithMeasuredAttachmentsThatFitSize:size]];
[self postprocessAttributedText:textStorage];
[textStorage addLayoutManager:layoutManager];
if (_adjustsFontSizeToFit) {
CGFloat minimumFontSize =
MAX(_minimumFontScale * (self.textAttributes.effectiveFont.pointSize), 4.0);
[textStorage scaleFontSizeToFitSize:size
minimumFontSize:minimumFontSize
maximumFontSize:self.textAttributes.effectiveFont.pointSize];
}
if (!exclusiveOwnership) {
[_cachedTextStorages setObject:textStorage forKey:key];
}
return textStorage;
}
- (void)layoutWithMetrics:(RCTLayoutMetrics)layoutMetrics
layoutContext:(RCTLayoutContext)layoutContext
{
// If the view got new `contentFrame`, we have to redraw it because
// and sizes of embedded views may change.
if (!CGRectEqualToRect(self.layoutMetrics.contentFrame, layoutMetrics.contentFrame)) {
_needsUpdateView = YES;
}
if (self.textAttributes.layoutDirection != layoutMetrics.layoutDirection) {
self.textAttributes.layoutDirection = layoutMetrics.layoutDirection;
[self invalidateCache];
}
[super layoutWithMetrics:layoutMetrics layoutContext:layoutContext];
}
- (void)layoutSubviewsWithContext:(RCTLayoutContext)layoutContext
{
NSTextStorage *textStorage =
[self textStorageAndLayoutManagerThatFitsSize:self.availableSize
exclusiveOwnership:NO];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange
actualGlyphRange:NULL];
[textStorage enumerateAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName
inRange:characterRange
options:0
usingBlock:
^(RCTShadowView *shadowView, NSRange range, BOOL *stop) {
if (!shadowView) {
return;
}
CGRect glyphRect = [layoutManager boundingRectForGlyphRange:range
inTextContainer:textContainer];
NSTextAttachment *attachment =
[textStorage attribute:NSAttachmentAttributeName atIndex:range.location effectiveRange:nil];
CGSize attachmentSize = attachment.bounds.size;
UIFont *font = [textStorage attribute:NSFontAttributeName atIndex:range.location effectiveRange:nil];
CGRect frame = {{
RCTRoundPixelValue(glyphRect.origin.x),
RCTRoundPixelValue(glyphRect.origin.y + glyphRect.size.height - attachmentSize.height + font.descender)
}, {
RCTRoundPixelValue(attachmentSize.width),
RCTRoundPixelValue(attachmentSize.height)
}};
NSRange truncatedGlyphRange = [layoutManager truncatedGlyphRangeInLineFragmentForGlyphAtIndex:range.location];
BOOL viewIsTruncated = NSIntersectionRange(range, truncatedGlyphRange).length != 0;
RCTLayoutContext localLayoutContext = layoutContext;
localLayoutContext.absolutePosition.x += frame.origin.x;
localLayoutContext.absolutePosition.y += frame.origin.y;
[shadowView layoutWithMinimumSize:frame.size
maximumSize:frame.size
layoutDirection:self.layoutMetrics.layoutDirection
layoutContext:localLayoutContext];
RCTLayoutMetrics localLayoutMetrics = shadowView.layoutMetrics;
localLayoutMetrics.frame.origin = frame.origin; // Reinforcing a proper frame origin for the Shadow View.
if (viewIsTruncated) {
localLayoutMetrics.displayType = RCTDisplayTypeNone;
}
[shadowView layoutWithMetrics:localLayoutMetrics layoutContext:localLayoutContext];
}
];
if (_onTextLayout) {
NSMutableArray *lineData = [NSMutableArray new];
[layoutManager
enumerateLineFragmentsForGlyphRange:glyphRange
usingBlock:^(CGRect overallRect, CGRect usedRect, NSTextContainer * _Nonnull usedTextContainer, NSRange lineGlyphRange, BOOL * _Nonnull stop) {
NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange actualGlyphRange:nil];
NSString *renderedString = [textStorage.string substringWithRange:range];
UIFont *font = [[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName atIndex:0 effectiveRange:nil];
[lineData addObject:
@{
@"text": renderedString,
@"x": @(usedRect.origin.x),
@"y": @(usedRect.origin.y),
@"width": @(usedRect.size.width),
@"height": @(usedRect.size.height),
@"descender": @(-font.descender),
@"capHeight": @(font.capHeight),
@"ascender": @(font.ascender),
@"xHeight": @(font.xHeight),
}];
}];
NSDictionary *payload =
@{
@"lines": lineData,
};
_onTextLayout(payload);
}
}
- (CGFloat)lastBaselineForSize:(CGSize)size
{
NSAttributedString *attributedText =
[self textStorageAndLayoutManagerThatFitsSize:size exclusiveOwnership:NO];
__block CGFloat maximumDescender = 0.0;
[attributedText enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:
^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (maximumDescender > font.descender) {
maximumDescender = font.descender;
}
}
];
return size.height + maximumDescender;
}
static YGSize RCTTextShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
{
CGSize maximumSize = (CGSize){
widthMode == YGMeasureModeUndefined ? CGFLOAT_MAX : RCTCoreGraphicsFloatFromYogaFloat(width),
heightMode == YGMeasureModeUndefined ? CGFLOAT_MAX : RCTCoreGraphicsFloatFromYogaFloat(height),
};
RCTTextShadowView *shadowTextView = (__bridge RCTTextShadowView *)YGNodeGetContext(node);
NSTextStorage *textStorage =
[shadowTextView textStorageAndLayoutManagerThatFitsSize:maximumSize
exclusiveOwnership:NO];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
[layoutManager ensureLayoutForTextContainer:textContainer];
CGSize size = [layoutManager usedRectForTextContainer:textContainer].size;
CGFloat letterSpacing = shadowTextView.textAttributes.letterSpacing;
if (!isnan(letterSpacing) && letterSpacing < 0) {
size.width -= letterSpacing;
}
size = (CGSize){
MIN(RCTCeilPixelValue(size.width), maximumSize.width),
MIN(RCTCeilPixelValue(size.height), maximumSize.height)
};
// Adding epsilon value illuminates problems with converting values from
// `double` to `float`, and then rounding them to pixel grid in Yoga.
CGFloat epsilon = 0.001;
return (YGSize){
RCTYogaFloatFromCoreGraphicsFloat(size.width + epsilon),
RCTYogaFloatFromCoreGraphicsFloat(size.height + epsilon)
};
}
static float RCTTextShadowViewBaseline(YGNodeRef node, const float width, const float height)
{
RCTTextShadowView *shadowTextView = (__bridge RCTTextShadowView *)YGNodeGetContext(node);
CGSize size = (CGSize){
RCTCoreGraphicsFloatFromYogaFloat(width),
RCTCoreGraphicsFloatFromYogaFloat(height)
};
CGFloat lastBaseline = [shadowTextView lastBaselineForSize:size];
return RCTYogaFloatFromCoreGraphicsFloat(lastBaseline);
}
@end

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTComponent.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTTextView : UIView
@property (nonatomic, assign) BOOL selectable;
- (void)setTextStorage:(NSTextStorage *)textStorage
contentFrame:(CGRect)contentFrame
descendantViews:(NSArray<UIView *> *)descendantViews;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,269 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTTextView.h>
#import <MobileCoreServices/UTCoreTypes.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTTextShadowView.h>
@implementation RCTTextView
{
CAShapeLayer *_highlightLayer;
UILongPressGestureRecognizer *_longPressGestureRecognizer;
NSArray<UIView *> *_Nullable _descendantViews;
NSTextStorage *_Nullable _textStorage;
CGRect _contentFrame;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
self.isAccessibilityElement = YES;
self.accessibilityTraits |= UIAccessibilityTraitStaticText;
self.opaque = NO;
self.contentMode = UIViewContentModeRedraw;
}
return self;
}
- (NSString *)description
{
NSString *superDescription = super.description;
NSRange semicolonRange = [superDescription rangeOfString:@";"];
NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@; text: %@", self.reactTag, _textStorage.string];
return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
}
- (void)setSelectable:(BOOL)selectable
{
if (_selectable == selectable) {
return;
}
_selectable = selectable;
if (_selectable) {
[self enableContextMenu];
}
else {
[self disableContextMenu];
}
}
- (void)reactSetFrame:(CGRect)frame
{
// Text looks super weird if its frame is animated.
// This disables the frame animation, without affecting opacity, etc.
[UIView performWithoutAnimation:^{
[super reactSetFrame:frame];
}];
}
- (void)didUpdateReactSubviews
{
// Do nothing, as subviews are managed by `setTextStorage:` method
}
- (void)setTextStorage:(NSTextStorage *)textStorage
contentFrame:(CGRect)contentFrame
descendantViews:(NSArray<UIView *> *)descendantViews
{
_textStorage = textStorage;
_contentFrame = contentFrame;
// FIXME: Optimize this.
for (UIView *view in _descendantViews) {
[view removeFromSuperview];
}
_descendantViews = descendantViews;
for (UIView *view in descendantViews) {
[self addSubview:view];
}
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
if (!_textStorage) {
return;
}
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:_contentFrame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:_contentFrame.origin];
__block UIBezierPath *highlightPath = nil;
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange
actualGlyphRange:NULL];
[_textStorage enumerateAttribute:RCTTextAttributesIsHighlightedAttributeName
inRange:characterRange
options:0
usingBlock:
^(NSNumber *value, NSRange range, __unused BOOL *stop) {
if (!value.boolValue) {
return;
}
[layoutManager enumerateEnclosingRectsForGlyphRange:range
withinSelectedGlyphRange:range
inTextContainer:textContainer
usingBlock:
^(CGRect enclosingRect, __unused BOOL *anotherStop) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2];
if (highlightPath) {
[highlightPath appendPath:path];
} else {
highlightPath = path;
}
}
];
}];
if (highlightPath) {
if (!_highlightLayer) {
_highlightLayer = [CAShapeLayer layer];
_highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor;
[self.layer addSublayer:_highlightLayer];
}
_highlightLayer.position = _contentFrame.origin;
_highlightLayer.path = highlightPath.CGPath;
} else {
[_highlightLayer removeFromSuperlayer];
_highlightLayer = nil;
}
}
- (NSNumber *)reactTagAtPoint:(CGPoint)point
{
NSNumber *reactTag = self.reactTag;
CGFloat fraction;
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:&fraction];
// If the point is not before (fraction == 0.0) the first character and not
// after (fraction == 1.0) the last character, then the attribute is valid.
if (_textStorage.length > 0 && (fraction > 0 || characterIndex > 0) && (fraction < 1 || characterIndex < _textStorage.length - 1)) {
reactTag = [_textStorage attribute:RCTTextAttributesTagAttributeName atIndex:characterIndex effectiveRange:NULL];
}
return reactTag;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (!self.window) {
self.layer.contents = nil;
if (_highlightLayer) {
[_highlightLayer removeFromSuperlayer];
_highlightLayer = nil;
}
} else if (_textStorage) {
[self setNeedsDisplay];
}
}
#pragma mark - Accessibility
- (NSString *)accessibilityLabel
{
NSString *superAccessibilityLabel = [super accessibilityLabel];
if (superAccessibilityLabel) {
return superAccessibilityLabel;
}
return _textStorage.string;
}
#pragma mark - Context Menu
- (void)enableContextMenu
{
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
[self addGestureRecognizer:_longPressGestureRecognizer];
}
- (void)disableContextMenu
{
[self removeGestureRecognizer:_longPressGestureRecognizer];
_longPressGestureRecognizer = nil;
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
// TODO: Adopt showMenuFromRect (necessary for UIKitForMac)
#if !TARGET_OS_TV && !TARGET_OS_UIKITFORMAC
UIMenuController *menuController = [UIMenuController sharedMenuController];
if (menuController.isMenuVisible) {
return;
}
if (!self.isFirstResponder) {
[self becomeFirstResponder];
}
[menuController setTargetRect:self.bounds inView:self];
[menuController setMenuVisible:YES animated:YES];
#endif
}
- (BOOL)canBecomeFirstResponder
{
return _selectable;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (_selectable && action == @selector(copy:)) {
return YES;
}
return [self.nextResponder canPerformAction:action withSender:sender];
}
- (void)copy:(id)sender
{
#if !TARGET_OS_TV
NSAttributedString *attributedText = _textStorage;
NSMutableDictionary *item = [NSMutableDictionary new];
NSData *rtf = [attributedText dataFromRange:NSMakeRange(0, attributedText.length)
documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType}
error:nil];
if (rtf) {
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
}
[item setObject:attributedText.string forKey:(id)kUTTypeUTF8PlainText];
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.items = @[item];
#endif
}
@end

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
#import "RCTBaseTextViewManager.h"
@interface RCTTextViewManager : RCTBaseTextViewManager
@end

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTTextViewManager.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTShadowView.h>
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <React/RCTUIManagerObserverCoordinator.h>
#import <React/RCTTextShadowView.h>
#import <React/RCTTextView.h>
@interface RCTTextViewManager () <RCTUIManagerObserver>
@end
@implementation RCTTextViewManager
{
NSHashTable<RCTTextShadowView *> *_shadowViews;
}
RCT_EXPORT_MODULE(RCTText)
RCT_REMAP_SHADOW_PROPERTY(numberOfLines, maximumNumberOfLines, NSInteger)
RCT_REMAP_SHADOW_PROPERTY(ellipsizeMode, lineBreakMode, NSLineBreakMode)
RCT_REMAP_SHADOW_PROPERTY(adjustsFontSizeToFit, adjustsFontSizeToFit, BOOL)
RCT_REMAP_SHADOW_PROPERTY(minimumFontScale, minimumFontScale, CGFloat)
RCT_EXPORT_SHADOW_PROPERTY(onTextLayout, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL)
- (void)setBridge:(RCTBridge *)bridge
{
[super setBridge:bridge];
_shadowViews = [NSHashTable weakObjectsHashTable];
[bridge.uiManager.observerCoordinator addObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleDidUpdateMultiplierNotification)
name:@"RCTAccessibilityManagerDidUpdateMultiplierNotification"
object:[bridge moduleForName:@"AccessibilityManager"
lazilyLoadIfNecessary:YES]];
}
- (UIView *)view
{
return [RCTTextView new];
}
- (RCTShadowView *)shadowView
{
RCTTextShadowView *shadowView = [[RCTTextShadowView alloc] initWithBridge:self.bridge];
shadowView.textAttributes.fontSizeMultiplier = [[[self.bridge moduleForName:@"AccessibilityManager"]
valueForKey:@"multiplier"] floatValue];
[_shadowViews addObject:shadowView];
return shadowView;
}
#pragma mark - RCTUIManagerObserver
- (void)uiManagerWillPerformMounting:(__unused RCTUIManager *)uiManager
{
for (RCTTextShadowView *shadowView in _shadowViews) {
[shadowView uiManagerWillPerformMounting];
}
}
#pragma mark - Font Size Multiplier
- (void)handleDidUpdateMultiplierNotification
{
CGFloat fontSizeMultiplier = [[[self.bridge moduleForName:@"AccessibilityManager"]
valueForKey:@"multiplier"] floatValue];
NSHashTable<RCTTextShadowView *> *shadowViews = _shadowViews;
RCTExecuteOnUIManagerQueue(^{
for (RCTTextShadowView *shadowView in shadowViews) {
shadowView.textAttributes.fontSizeMultiplier = fontSizeMultiplier;
[shadowView dirtyLayout];
}
[self.bridge.uiManager setNeedsLayout];
});
}
@end

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
'use strict';
const React = require('react');
/**
* Whether the current element is the descendant of a <Text> element.
*/
module.exports = (React.createContext(false): React$Context<$FlowFixMe>);

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextInputView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTMultilineTextInputView : RCTBaseTextInputView
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTMultilineTextInputView.h>
#import <React/RCTUtils.h>
#import <React/RCTUITextView.h>
@implementation RCTMultilineTextInputView
{
RCTUITextView *_backedTextInputView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `false` for <TextInput multiline={true}> by design.
self.blurOnSubmit = NO;
_backedTextInputView = [[RCTUITextView alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
[self addSubview:_backedTextInputView];
}
return self;
}
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
return _backedTextInputView;
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
RCTDirectEventBlock onScroll = self.onScroll;
if (onScroll) {
CGPoint contentOffset = scrollView.contentOffset;
CGSize contentSize = scrollView.contentSize;
CGSize size = scrollView.bounds.size;
UIEdgeInsets contentInset = scrollView.contentInset;
onScroll(@{
@"contentOffset": @{
@"x": @(contentOffset.x),
@"y": @(contentOffset.y)
},
@"contentInset": @{
@"top": @(contentInset.top),
@"left": @(contentInset.left),
@"bottom": @(contentInset.bottom),
@"right": @(contentInset.right)
},
@"contentSize": @{
@"width": @(contentSize.width),
@"height": @(contentSize.height)
},
@"layoutMeasurement": @{
@"width": @(size.width),
@"height": @(size.height)
},
@"zoomScale": @(scrollView.zoomScale ?: 1),
});
}
}
@end

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextInputViewManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTMultilineTextInputViewManager : RCTBaseTextInputViewManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTMultilineTextInputViewManager.h>
#import <React/RCTMultilineTextInputView.h>
@implementation RCTMultilineTextInputViewManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge];
}
#pragma mark - Multiline <TextInput> (aka TextView) specific properties
#if !TARGET_OS_TV
RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes)
#endif
@end

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTBackedTextInputDelegate.h>
NS_ASSUME_NONNULL_BEGIN
/*
* Just regular UITextView... but much better!
*/
@interface RCTUITextView : UITextView <RCTBackedTextInputViewProtocol>
- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) CGFloat preferredMaxLayoutWidth;
// The `clearButtonMode` property actually is not supported yet;
// it's declared here only to conform to the interface.
@property (nonatomic, assign) UITextFieldViewMode clearButtonMode;
// The `caretHidden` property actually is not supported yet;
// it's declared here only to conform to the interface.
@property (nonatomic, assign) BOOL caretHidden;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,309 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTUITextView.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTBackedTextInputDelegateAdapter.h>
#import <React/RCTTextAttributes.h>
@implementation RCTUITextView
{
UILabel *_placeholderView;
UITextView *_detachedTextView;
RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter;
NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
}
static UIFont *defaultPlaceholderFont()
{
return [UIFont systemFontOfSize:17];
}
static UIColor *defaultPlaceholderColor()
{
// Default placeholder color from UITextField.
return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(textDidChange)
name:UITextViewTextDidChangeNotification
object:self];
_placeholderView = [[UILabel alloc] initWithFrame:self.bounds];
_placeholderView.isAccessibilityElement = NO;
_placeholderView.numberOfLines = 0;
[self addSubview:_placeholderView];
_textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self];
self.backgroundColor = [UIColor clearColor];
self.textColor = [UIColor blackColor];
// This line actually removes 5pt (default value) left and right padding in UITextView.
self.textContainer.lineFragmentPadding = 0;
#if !TARGET_OS_TV
self.scrollsToTop = NO;
#endif
self.scrollEnabled = YES;
}
return self;
}
#pragma mark - Accessibility
- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement
{
// UITextView is accessible by default (some nested views are) and disabling that is not supported.
// On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view
// (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component.
}
- (NSString *)accessibilityLabel
{
NSMutableString *accessibilityLabel = [NSMutableString new];
NSString *superAccessibilityLabel = [super accessibilityLabel];
if (superAccessibilityLabel.length > 0) {
[accessibilityLabel appendString:superAccessibilityLabel];
}
if (self.placeholder.length > 0 && self.attributedText.string.length == 0) {
if (accessibilityLabel.length > 0) {
[accessibilityLabel appendString:@" "];
}
[accessibilityLabel appendString:self.placeholder];
}
return accessibilityLabel;
}
#pragma mark - Properties
- (void)setPlaceholder:(NSString *)placeholder
{
_placeholder = placeholder;
[self _updatePlaceholder];
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
_placeholderColor = placeholderColor;
[self _updatePlaceholder];
}
- (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) {
return;
}
_defaultTextAttributes = defaultTextAttributes;
self.typingAttributes = defaultTextAttributes;
[self _updatePlaceholder];
}
- (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
return _defaultTextAttributes;
}
- (void)textDidChange
{
_textWasPasted = NO;
[self _invalidatePlaceholderVisibility];
}
#pragma mark - Overrides
- (void)setFont:(UIFont *)font
{
[super setFont:font];
[self _updatePlaceholder];
}
- (void)setTextAlignment:(NSTextAlignment)textAlignment
{
[super setTextAlignment:textAlignment];
_placeholderView.textAlignment = textAlignment;
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
// Using `setAttributedString:` while user is typing breaks some internal mechanics
// when entering complex input languages such as Chinese, Korean or Japanese.
// see: https://github.com/facebook/react-native/issues/19339
// We try to avoid calling this method as much as we can.
// If the text has changed, there is nothing we can do.
if (![super.attributedText.string isEqualToString:attributedText.string]) {
[super setAttributedText:attributedText];
} else {
// But if the text is preserved, we just copying the attributes from the source string.
if (![super.attributedText isEqualToAttributedString:attributedText]) {
[self copyTextAttributesFrom:attributedText];
}
}
[self textDidChange];
}
#pragma mark - Overrides
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate
{
if (!notifyDelegate) {
// We have to notify an adapter that following selection change was initiated programmatically,
// so the adapter must not generate a notification for it.
[_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange];
}
[super setSelectedTextRange:selectedTextRange];
}
- (void)paste:(id)sender
{
[super paste:sender];
_textWasPasted = YES;
}
- (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated
{
// Turning off scroll animation.
// This fixes the problem also known as "flaky scrolling".
[super setContentOffset:contentOffset animated:NO];
}
- (void)selectAll:(id)sender
{
[super selectAll:sender];
// `selectAll:` does not work for UITextView when it's being called inside UITextView's delegate methods.
dispatch_async(dispatch_get_main_queue(), ^{
UITextRange *selectionRange = [self textRangeFromPosition:self.beginningOfDocument toPosition:self.endOfDocument];
[self setSelectedTextRange:selectionRange notifyDelegate:NO];
});
}
#pragma mark - Layout
- (CGFloat)preferredMaxLayoutWidth
{
// Returning size DOES contain `textContainerInset` (aka `padding`).
return _preferredMaxLayoutWidth ?: self.placeholderSize.width;
}
- (CGSize)placeholderSize
{
UIEdgeInsets textContainerInset = self.textContainerInset;
NSString *placeholder = self.placeholder ?: @"";
CGSize maxPlaceholderSize = CGSizeMake(UIEdgeInsetsInsetRect(self.bounds, textContainerInset).size.width, CGFLOAT_MAX);
CGSize placeholderSize = [placeholder boundingRectWithSize:maxPlaceholderSize options:NSStringDrawingUsesLineFragmentOrigin attributes:[self _placeholderTextAttributes] context:nil].size;
placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width), RCTCeilPixelValue(placeholderSize.height));
placeholderSize.width += textContainerInset.left + textContainerInset.right;
placeholderSize.height += textContainerInset.top + textContainerInset.bottom;
// Returning size DOES contain `textContainerInset` (aka `padding`; as `sizeThatFits:` does).
return placeholderSize;
}
- (CGSize)contentSize
{
CGSize contentSize = super.contentSize;
CGSize placeholderSize = _placeholderView.isHidden ? CGSizeZero : self.placeholderSize;
// When a text input is empty, it actually displays a placehoder.
// So, we have to consider `placeholderSize` as a minimum `contentSize`.
// Returning size DOES contain `textContainerInset` (aka `padding`).
return CGSizeMake(
MAX(contentSize.width, placeholderSize.width),
MAX(contentSize.height, placeholderSize.height));
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset);
CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height;
textFrame.size.height = MIN(placeholderHeight, textFrame.size.height);
_placeholderView.frame = textFrame;
}
- (CGSize)intrinsicContentSize
{
// Returning size DOES contain `textContainerInset` (aka `padding`).
return [self sizeThatFits:CGSizeMake(self.preferredMaxLayoutWidth, CGFLOAT_MAX)];
}
- (CGSize)sizeThatFits:(CGSize)size
{
// Returned fitting size depends on text size and placeholder size.
CGSize textSize = [super sizeThatFits:size];
CGSize placeholderSize = self.placeholderSize;
// Returning size DOES contain `textContainerInset` (aka `padding`).
return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height));
}
#pragma mark - Context Menu
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (_contextMenuHidden) {
return NO;
}
return [super canPerformAction:action withSender:sender];
}
#pragma mark - Placeholder
- (void)_invalidatePlaceholderVisibility
{
BOOL isVisible = _placeholder.length != 0 && self.attributedText.length == 0;
_placeholderView.hidden = !isVisible;
}
- (void)_updatePlaceholder
{
_placeholderView.attributedText = [[NSAttributedString alloc] initWithString:_placeholder ?: @"" attributes:[self _placeholderTextAttributes]];
}
- (NSDictionary<NSAttributedStringKey, id> *)_placeholderTextAttributes
{
NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new];
[textAttributes setValue:self.placeholderColor ?: defaultPlaceholderColor() forKey:NSForegroundColorAttributeName];
if (![textAttributes objectForKey:NSFontAttributeName]) {
[textAttributes setValue:defaultPlaceholderFont() forKey:NSFontAttributeName];
}
return textAttributes;
}
#pragma mark - Utility Methods
- (void)copyTextAttributesFrom:(NSAttributedString *)sourceString
{
[self.textStorage beginEditing];
NSTextStorage *textStorage = self.textStorage;
[sourceString enumerateAttributesInRange:NSMakeRange(0, sourceString.length)
options:NSAttributedStringEnumerationReverse
usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
[textStorage setAttributes:attrs range:range];
}];
[self.textStorage endEditing];
}
@end

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@protocol RCTBackedTextInputViewProtocol;
NS_ASSUME_NONNULL_BEGIN
@protocol RCTBackedTextInputDelegate <NSObject>
- (BOOL)textInputShouldBeginEditing; // Return `NO` to disallow editing.
- (void)textInputDidBeginEditing;
- (BOOL)textInputShouldEndEditing; // Return `YES` to allow editing to stop and to resign first responder status. `NO` to disallow the editing session to end.
- (void)textInputDidEndEditing; // May be called if forced even if `textInputShouldEndEditing` returns `NO` (e.g. view removed from window) or `[textInput endEditing:YES]` called.
- (BOOL)textInputShouldReturn; // May be called right before `textInputShouldEndEditing` if "Return" button was pressed.
- (void)textInputDidReturn;
/*
* Called before any change in the TextInput. The delegate has the opportunity to change the replacement string or reject the change completely.
* To change the replacement, return the changed version of the `text`.
* To accept the change, return `text` argument as-is.
* To reject the change, return `nil`.
*/
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range;
- (void)textInputDidChange;
- (void)textInputDidChangeSelection;
@optional
- (void)scrollViewDidScroll:(UIScrollView *)scrollView;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import "RCTBackedTextInputViewProtocol.h"
#import "RCTBackedTextInputDelegate.h"
NS_ASSUME_NONNULL_BEGIN
#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField)
@interface RCTBackedTextFieldDelegateAdapter : NSObject
- (instancetype)initWithTextField:(UITextField<RCTBackedTextInputViewProtocol> *)backedTextInputView;
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange;
- (void)selectedTextRangeWasSet;
@end
#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView)
@interface RCTBackedTextViewDelegateAdapter : NSObject
- (instancetype)initWithTextView:(UITextView<RCTBackedTextInputViewProtocol> *)backedTextInputView;
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,286 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBackedTextInputDelegateAdapter.h>
#pragma mark - RCTBackedTextFieldDelegateAdapter (for UITextField)
static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingContext;
@interface RCTBackedTextFieldDelegateAdapter () <UITextFieldDelegate>
@end
@implementation RCTBackedTextFieldDelegateAdapter {
__weak UITextField<RCTBackedTextInputViewProtocol> *_backedTextInputView;
BOOL _textDidChangeIsComing;
UITextRange *_previousSelectedTextRange;
}
- (instancetype)initWithTextField:(UITextField<RCTBackedTextInputViewProtocol> *)backedTextInputView
{
if (self = [super init]) {
_backedTextInputView = backedTextInputView;
backedTextInputView.delegate = self;
[_backedTextInputView addTarget:self action:@selector(textFieldDidChange) forControlEvents:UIControlEventEditingChanged];
[_backedTextInputView addTarget:self action:@selector(textFieldDidEndEditingOnExit) forControlEvents:UIControlEventEditingDidEndOnExit];
}
return self;
}
- (void)dealloc
{
[_backedTextInputView removeTarget:self action:nil forControlEvents:UIControlEventEditingChanged];
[_backedTextInputView removeTarget:self action:nil forControlEvents:UIControlEventEditingDidEndOnExit];
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldBeginEditing:(__unused UITextField *)textField
{
return [_backedTextInputView.textInputDelegate textInputShouldBeginEditing];
}
- (void)textFieldDidBeginEditing:(__unused UITextField *)textField
{
[_backedTextInputView.textInputDelegate textInputDidBeginEditing];
}
- (BOOL)textFieldShouldEndEditing:(__unused UITextField *)textField
{
return [_backedTextInputView.textInputDelegate textInputShouldEndEditing];
}
- (void)textFieldDidEndEditing:(__unused UITextField *)textField
{
if (_textDidChangeIsComing) {
// iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection
// which was triggered by losing focus. So, we call it manually.
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
}
[_backedTextInputView.textInputDelegate textInputDidEndEditing];
}
- (BOOL)textField:(__unused UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSString *newText =
[_backedTextInputView.textInputDelegate textInputShouldChangeText:string inRange:range];
if (newText == nil) {
return NO;
}
if ([newText isEqualToString:string]) {
_textDidChangeIsComing = YES;
return YES;
}
NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy];
[attributedString replaceCharactersInRange:range withString:newText];
[_backedTextInputView setAttributedText:[attributedString copy]];
// Setting selection to the end of the replaced text.
UITextPosition *position =
[_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:(range.location + newText.length)];
[_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
[self textFieldDidChange];
return NO;
}
- (BOOL)textFieldShouldReturn:(__unused UITextField *)textField
{
return [_backedTextInputView.textInputDelegate textInputShouldReturn];
}
#pragma mark - UIControlEventEditing* Family Events
- (void)textFieldDidChange
{
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
// `selectedTextRangeWasSet` isn't triggered during typing.
[self textFieldProbablyDidChangeSelection];
}
- (void)textFieldDidEndEditingOnExit
{
[_backedTextInputView.textInputDelegate textInputDidReturn];
}
#pragma mark - UIKeyboardInput (private UIKit protocol)
// This method allows us to detect a [Backspace] `keyPress`
// even when there is no more text in the `UITextField`.
- (BOOL)keyboardInputShouldDelete:(__unused UITextField *)textField
{
[_backedTextInputView.textInputDelegate textInputShouldChangeText:@"" inRange:NSMakeRange(0, 0)];
return YES;
}
#pragma mark - Public Interface
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange
{
_previousSelectedTextRange = textRange;
}
- (void)selectedTextRangeWasSet
{
[self textFieldProbablyDidChangeSelection];
}
#pragma mark - Generalization
- (void)textFieldProbablyDidChangeSelection
{
if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) {
return;
}
_previousSelectedTextRange = _backedTextInputView.selectedTextRange;
[_backedTextInputView.textInputDelegate textInputDidChangeSelection];
}
@end
#pragma mark - RCTBackedTextViewDelegateAdapter (for UITextView)
@interface RCTBackedTextViewDelegateAdapter () <UITextViewDelegate>
@end
@implementation RCTBackedTextViewDelegateAdapter {
__weak UITextView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
BOOL _textDidChangeIsComing;
UITextRange *_previousSelectedTextRange;
}
- (instancetype)initWithTextView:(UITextView<RCTBackedTextInputViewProtocol> *)backedTextInputView
{
if (self = [super init]) {
_backedTextInputView = backedTextInputView;
backedTextInputView.delegate = self;
}
return self;
}
#pragma mark - UITextViewDelegate
- (BOOL)textViewShouldBeginEditing:(__unused UITextView *)textView
{
return [_backedTextInputView.textInputDelegate textInputShouldBeginEditing];
}
- (void)textViewDidBeginEditing:(__unused UITextView *)textView
{
[_backedTextInputView.textInputDelegate textInputDidBeginEditing];
}
- (BOOL)textViewShouldEndEditing:(__unused UITextView *)textView
{
return [_backedTextInputView.textInputDelegate textInputShouldEndEditing];
}
- (void)textViewDidEndEditing:(__unused UITextView *)textView
{
if (_textDidChangeIsComing) {
// iOS does't call `textViewDidChange:` delegate method if the change was happened because of autocorrection
// which was triggered by losing focus. So, we call it manually.
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
}
[_backedTextInputView.textInputDelegate textInputDidEndEditing];
}
- (BOOL)textView:(__unused UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
// Custom implementation of `textInputShouldReturn` and `textInputDidReturn` pair for `UITextView`.
if (!_backedTextInputView.textWasPasted && [text isEqualToString:@"\n"]) {
if ([_backedTextInputView.textInputDelegate textInputShouldReturn]) {
[_backedTextInputView.textInputDelegate textInputDidReturn];
[_backedTextInputView endEditing:NO];
return NO;
}
}
NSString *newText =
[_backedTextInputView.textInputDelegate textInputShouldChangeText:text inRange:range];
if (newText == nil) {
return NO;
}
if ([newText isEqualToString:text]) {
_textDidChangeIsComing = YES;
return YES;
}
NSMutableAttributedString *attributedString = [_backedTextInputView.attributedText mutableCopy];
[attributedString replaceCharactersInRange:range withString:newText];
[_backedTextInputView setAttributedText:[attributedString copy]];
// Setting selection to the end of the replaced text.
UITextPosition *position =
[_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:(range.location + newText.length)];
[_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
[self textViewDidChange:_backedTextInputView];
return NO;
}
- (void)textViewDidChange:(__unused UITextView *)textView
{
_textDidChangeIsComing = NO;
[_backedTextInputView.textInputDelegate textInputDidChange];
}
- (void)textViewDidChangeSelection:(__unused UITextView *)textView
{
[self textViewProbablyDidChangeSelection];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ([_backedTextInputView.textInputDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) {
[_backedTextInputView.textInputDelegate scrollViewDidScroll:scrollView];
}
}
#pragma mark - Public Interface
- (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange
{
_previousSelectedTextRange = textRange;
}
#pragma mark - Generalization
- (void)textViewProbablyDidChangeSelection
{
if ([_backedTextInputView.selectedTextRange isEqual:_previousSelectedTextRange]) {
return;
}
_previousSelectedTextRange = _backedTextInputView.selectedTextRange;
[_backedTextInputView.textInputDelegate textInputDidChangeSelection];
}
@end

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@protocol RCTBackedTextInputDelegate;
@class RCTTextAttributes;
NS_ASSUME_NONNULL_BEGIN
@protocol RCTBackedTextInputViewProtocol <UITextInput>
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, strong, nullable) UIView *inputAccessoryView;
@property (nonatomic, weak, nullable) id<RCTBackedTextInputDelegate> textInputDelegate;
@property (nonatomic, readonly) CGSize contentSize;
@property (nonatomic, strong, nullable) NSDictionary<NSAttributedStringKey,id> *defaultTextAttributes;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL enablesReturnKeyAutomatically;
@property (nonatomic, assign) UITextFieldViewMode clearButtonMode;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
// This protocol disallows direct access to `selectedTextRange` property because
// unwise usage of it can break the `delegate` behavior. So, we always have to
// explicitly specify should `delegate` be notified about the change or not.
// If the change was initiated programmatically, we must NOT notify the delegate.
// If the change was a result of user actions (like typing or touches), we MUST notify the delegate.
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange NS_UNAVAILABLE;
- (void)setSelectedTextRange:(nullable UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate;
// This protocol disallows direct access to `text` property because
// unwise usage of it can break the `attributeText` behavior.
// Use `attributedText.string` instead.
@property (nonatomic, copy, nullable) NSString *text NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextShadowView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTBaseTextInputShadowView : RCTBaseTextShadowView
- (instancetype)initWithBridge:(RCTBridge *)bridge;
@property (nonatomic, copy, nullable) NSString *text;
@property (nonatomic, copy, nullable) NSString *placeholder;
@property (nonatomic, assign) NSInteger maximumNumberOfLines;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange;
- (void)uiManagerWillPerformMounting;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,326 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBaseTextInputShadowView.h>
#import <React/RCTBridge.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTUIManager.h>
#import <yoga/Yoga.h>
#import "NSTextStorage+FontScaling.h"
#import <React/RCTBaseTextInputView.h>
@implementation RCTBaseTextInputShadowView
{
__weak RCTBridge *_bridge;
NSAttributedString *_Nullable _previousAttributedText;
BOOL _needsUpdateView;
NSAttributedString *_Nullable _localAttributedText;
CGSize _previousContentSize;
NSString *_text;
NSTextStorage *_textStorage;
NSTextContainer *_textContainer;
NSLayoutManager *_layoutManager;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_bridge = bridge;
_needsUpdateView = YES;
YGNodeSetMeasureFunc(self.yogaNode, RCTBaseTextInputShadowViewMeasure);
YGNodeSetBaselineFunc(self.yogaNode, RCTTextInputShadowViewBaseline);
}
return self;
}
- (BOOL)isYogaLeafNode
{
return YES;
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
[super didSetProps:changedProps];
// `backgroundColor` and `opacity` are being applied directly to a UIView,
// therefore we need to exclude them from base `textAttributes`.
self.textAttributes.backgroundColor = nil;
self.textAttributes.opacity = NAN;
}
- (void)layoutSubviewsWithContext:(RCTLayoutContext)layoutContext
{
// Do nothing.
}
- (void)setLocalData:(NSObject *)localData
{
NSAttributedString *attributedText = (NSAttributedString *)localData;
if ([attributedText isEqualToAttributedString:_localAttributedText]) {
return;
}
_localAttributedText = attributedText;
[self dirtyLayout];
}
- (void)dirtyLayout
{
[super dirtyLayout];
_needsUpdateView = YES;
YGNodeMarkDirty(self.yogaNode);
[self invalidateContentSize];
}
- (void)invalidateContentSize
{
if (!_onContentSizeChange) {
return;
}
CGSize maximumSize = self.layoutMetrics.frame.size;
if (_maximumNumberOfLines == 1) {
maximumSize.width = CGFLOAT_MAX;
} else {
maximumSize.height = CGFLOAT_MAX;
}
CGSize contentSize = [self sizeThatFitsMinimumSize:(CGSize)CGSizeZero maximumSize:maximumSize];
if (CGSizeEqualToSize(_previousContentSize, contentSize)) {
return;
}
_previousContentSize = contentSize;
_onContentSizeChange(@{
@"contentSize": @{
@"height": @(contentSize.height),
@"width": @(contentSize.width),
},
@"target": self.reactTag,
});
}
- (NSString *)text
{
return _text;
}
- (void)setText:(NSString *)text
{
_text = text;
// Clear `_previousAttributedText` to notify the view about the change
// when `text` native prop is set.
_previousAttributedText = nil;
[self dirtyLayout];
}
#pragma mark - RCTUIManagerObserver
- (void)uiManagerWillPerformMounting
{
if (YGNodeIsDirty(self.yogaNode)) {
return;
}
if (!_needsUpdateView) {
return;
}
_needsUpdateView = NO;
UIEdgeInsets borderInsets = self.borderAsInsets;
UIEdgeInsets paddingInsets = self.paddingAsInsets;
RCTTextAttributes *textAttributes = [self.textAttributes copy];
NSMutableAttributedString *attributedText =
[[NSMutableAttributedString alloc] initWithAttributedString:[self attributedTextWithBaseTextAttributes:nil]];
// Removing all references to Shadow Views and tags to avoid unnecessary retaining
// and problems with comparing the strings.
[attributedText removeAttribute:RCTBaseTextShadowViewEmbeddedShadowViewAttributeName
range:NSMakeRange(0, attributedText.length)];
[attributedText removeAttribute:RCTTextAttributesTagAttributeName
range:NSMakeRange(0, attributedText.length)];
if (self.text.length) {
NSAttributedString *propertyAttributedText =
[[NSAttributedString alloc] initWithString:self.text
attributes:self.textAttributes.effectiveTextAttributes];
[attributedText insertAttributedString:propertyAttributedText atIndex:0];
}
BOOL isAttributedTextChanged = NO;
if (![_previousAttributedText isEqualToAttributedString:attributedText]) {
// We have to follow `set prop` pattern:
// If the value has not changed, we must not notify the view about the change,
// otherwise we may break local (temporary) state of the text input.
isAttributedTextChanged = YES;
_previousAttributedText = [attributedText copy];
}
NSNumber *tag = self.reactTag;
[_bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTBaseTextInputView *baseTextInputView = (RCTBaseTextInputView *)viewRegistry[tag];
if (!baseTextInputView) {
return;
}
baseTextInputView.textAttributes = textAttributes;
baseTextInputView.reactBorderInsets = borderInsets;
baseTextInputView.reactPaddingInsets = paddingInsets;
if (isAttributedTextChanged) {
// Don't set `attributedText` if length equal to zero, otherwise it would shrink when attributes contain like `lineHeight`.
if (attributedText.length != 0) {
baseTextInputView.attributedText = attributedText;
} else {
baseTextInputView.attributedText = nil;
}
}
}];
}
#pragma mark -
- (NSAttributedString *)measurableAttributedText
{
// Only for the very first render when we don't have `_localAttributedText`,
// we use value directly from the property and/or nested content.
NSAttributedString *attributedText =
_localAttributedText ?: [self attributedTextWithBaseTextAttributes:nil];
if (attributedText.length == 0) {
// It's impossible to measure empty attributed string because all attributes are
// associated with some characters, so no characters means no data.
// Placeholder also can represent the intrinsic size when it is visible.
NSString *text = self.placeholder;
if (!text.length) {
// Note: `zero-width space` is insufficient in some cases.
text = @"I";
}
attributedText = [[NSAttributedString alloc] initWithString:text attributes:self.textAttributes.effectiveTextAttributes];
}
return attributedText;
}
- (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize
{
NSAttributedString *attributedText = [self measurableAttributedText];
if (!_textStorage) {
_textContainer = [NSTextContainer new];
_textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5.
_layoutManager = [NSLayoutManager new];
[_layoutManager addTextContainer:_textContainer];
_textStorage = [NSTextStorage new];
[_textStorage addLayoutManager:_layoutManager];
}
_textContainer.size = maximumSize;
_textContainer.maximumNumberOfLines = _maximumNumberOfLines;
[_textStorage replaceCharactersInRange:(NSRange){0, _textStorage.length}
withAttributedString:attributedText];
[_layoutManager ensureLayoutForTextContainer:_textContainer];
CGSize size = [_layoutManager usedRectForTextContainer:_textContainer].size;
return (CGSize){
MAX(minimumSize.width, MIN(RCTCeilPixelValue(size.width), maximumSize.width)),
MAX(minimumSize.height, MIN(RCTCeilPixelValue(size.height), maximumSize.height))
};
}
- (CGFloat)lastBaselineForSize:(CGSize)size
{
NSAttributedString *attributedText = [self measurableAttributedText];
__block CGFloat maximumDescender = 0.0;
[attributedText enumerateAttribute:NSFontAttributeName
inRange:NSMakeRange(0, attributedText.length)
options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
usingBlock:
^(UIFont *font, NSRange range, __unused BOOL *stop) {
if (maximumDescender > font.descender) {
maximumDescender = font.descender;
}
}
];
return size.height + maximumDescender;
}
static YGSize RCTBaseTextInputShadowViewMeasure(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode)
{
RCTShadowView *shadowView = (__bridge RCTShadowView *)YGNodeGetContext(node);
CGSize minimumSize = CGSizeMake(0, 0);
CGSize maximumSize = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
CGSize size = {
RCTCoreGraphicsFloatFromYogaFloat(width),
RCTCoreGraphicsFloatFromYogaFloat(height)
};
switch (widthMode) {
case YGMeasureModeUndefined:
break;
case YGMeasureModeExactly:
minimumSize.width = size.width;
maximumSize.width = size.width;
break;
case YGMeasureModeAtMost:
maximumSize.width = size.width;
break;
}
switch (heightMode) {
case YGMeasureModeUndefined:
break;
case YGMeasureModeExactly:
minimumSize.height = size.height;
maximumSize.height = size.height;
break;
case YGMeasureModeAtMost:
maximumSize.height = size.height;
break;
}
CGSize measuredSize = [shadowView sizeThatFitsMinimumSize:minimumSize maximumSize:maximumSize];
return (YGSize){
RCTYogaFloatFromCoreGraphicsFloat(measuredSize.width),
RCTYogaFloatFromCoreGraphicsFloat(measuredSize.height)
};
}
static float RCTTextInputShadowViewBaseline(YGNodeRef node, const float width, const float height)
{
RCTBaseTextInputShadowView *shadowTextView = (__bridge RCTBaseTextInputShadowView *)YGNodeGetContext(node);
CGSize size = (CGSize){
RCTCoreGraphicsFloatFromYogaFloat(width),
RCTCoreGraphicsFloatFromYogaFloat(height)
};
CGFloat lastBaseline = [shadowTextView lastBaselineForSize:size];
return RCTYogaFloatFromCoreGraphicsFloat(lastBaseline);
}
@end

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
#import "RCTBackedTextInputDelegate.h"
#import "RCTBackedTextInputViewProtocol.h"
@class RCTBridge;
@class RCTEventDispatcher;
@class RCTTextAttributes;
@class RCTTextSelection;
NS_ASSUME_NONNULL_BEGIN
@interface RCTBaseTextInputView : RCTView <RCTBackedTextInputDelegate>
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE;
@property (nonatomic, readonly) UIView<RCTBackedTextInputViewProtocol> *backedTextInputView;
@property (nonatomic, strong, nullable) RCTTextAttributes *textAttributes;
@property (nonatomic, assign) UIEdgeInsets reactPaddingInsets;
@property (nonatomic, assign) UIEdgeInsets reactBorderInsets;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onSelectionChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll;
@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, assign, readonly) NSInteger nativeEventCount;
@property (nonatomic, assign) BOOL autoFocus;
@property (nonatomic, assign) BOOL blurOnSubmit;
@property (nonatomic, assign) BOOL selectTextOnFocus;
@property (nonatomic, assign) BOOL clearTextOnFocus;
@property (nonatomic, assign) BOOL secureTextEntry;
@property (nonatomic, copy) RCTTextSelection *selection;
@property (nonatomic, strong, nullable) NSNumber *maxLength;
@property (nonatomic, copy, nullable) NSAttributedString *attributedText;
@property (nonatomic, copy) NSString *inputAccessoryViewID;
@property (nonatomic, assign) UIKeyboardType keyboardType;
/**
Sets selection intext input if both start and end are within range of the text input.
**/
- (void)setSelectionStart:(NSInteger)start
selectionEnd:(NSInteger)end;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,680 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBaseTextInputView.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTInputAccessoryView.h>
#import <React/RCTInputAccessoryViewContent.h>
#import <React/RCTTextAttributes.h>
#import <React/RCTTextSelection.h>
@implementation RCTBaseTextInputView {
__weak RCTBridge *_bridge;
__weak RCTEventDispatcher *_eventDispatcher;
BOOL _hasInputAccesoryView;
NSString *_Nullable _predictedText;
BOOL _didMoveToWindow;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
RCTAssertParam(bridge);
if (self = [super initWithFrame:CGRectZero]) {
_bridge = bridge;
_eventDispatcher = bridge.eventDispatcher;
}
return self;
}
RCT_NOT_IMPLEMENTED(- (instancetype)init)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
- (UIView<RCTBackedTextInputViewProtocol> *)backedTextInputView
{
RCTAssert(NO, @"-[RCTBaseTextInputView backedTextInputView] must be implemented in subclass.");
return nil;
}
#pragma mark - RCTComponent
- (void)didUpdateReactSubviews
{
// Do nothing.
}
#pragma mark - Properties
- (void)setTextAttributes:(RCTTextAttributes *)textAttributes
{
_textAttributes = textAttributes;
[self enforceTextAttributesIfNeeded];
}
- (void)enforceTextAttributesIfNeeded
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
NSDictionary<NSAttributedStringKey,id> *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy];
if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) {
[textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName];
}
backedTextInputView.defaultTextAttributes = textAttributes;
}
- (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
{
_reactPaddingInsets = reactPaddingInsets;
// We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`.
self.backedTextInputView.textContainerInset = reactPaddingInsets;
[self setNeedsLayout];
}
- (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets
{
_reactBorderInsets = reactBorderInsets;
// We apply `borderInsets` as `backedTextInputView` layout offset.
self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets);
[self setNeedsLayout];
}
- (NSAttributedString *)attributedText
{
return self.backedTextInputView.attributedText;
}
- (BOOL)textOf:(NSAttributedString*)newText equals:(NSAttributedString*)oldText{
// When the dictation is running we can't update the attributed text on the backed up text view
// because setting the attributed string will kill the dictation. This means that we can't impose
// the settings on a dictation.
// Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
// text that we should disregard. See https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc
// for more info.
// If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in NSOriginalFont.
// Lastly, when entering a password, etc., there will be additional styling on the field as the native text view
// handles showing the last character for a split second.
__block BOOL fontHasBeenUpdatedBySystem = false;
[oldText enumerateAttribute:@"NSOriginalFont" inRange:NSMakeRange(0, oldText.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value){
fontHasBeenUpdatedBySystem = true;
}
}];
BOOL shouldFallbackToBareTextComparison =
[self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
self.backedTextInputView.markedTextRange ||
self.backedTextInputView.isSecureTextEntry ||
fontHasBeenUpdatedBySystem;
if (shouldFallbackToBareTextComparison) {
return ([newText.string isEqualToString:oldText.string]);
} else {
return ([newText isEqualToAttributedString:oldText]);
}
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
BOOL textNeedsUpdate = NO;
// Remove tag attribute to ensure correct attributed string comparison.
NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy];
NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy];
[backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName
range:NSMakeRange(0, backedTextInputViewTextCopy.length)];
[attributedTextCopy removeAttribute:RCTTextAttributesTagAttributeName
range:NSMakeRange(0, attributedTextCopy.length)];
textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO);
if (eventLag == 0 && textNeedsUpdate) {
UITextRange *selection = self.backedTextInputView.selectedTextRange;
NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
self.backedTextInputView.attributedText = attributedText;
if (selection.empty) {
// Maintaining a cursor position relative to the end of the old text.
NSInteger offsetStart =
[self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument
toPosition:selection.start];
NSInteger offsetFromEnd = oldTextLength - offsetStart;
NSInteger newOffset = attributedText.string.length - offsetFromEnd;
UITextPosition *position =
[self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
offset:newOffset];
[self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
}
[self updateLocalData];
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", self.backedTextInputView.attributedText.string, (long long)eventLag);
}
}
- (RCTTextSelection *)selection
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
UITextRange *selectedTextRange = backedTextInputView.selectedTextRange;
return [[RCTTextSelection new] initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start]
end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.end]];
}
- (void)setSelection:(RCTTextSelection *)selection
{
if (!selection) {
return;
}
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
UITextRange *previousSelectedTextRange = backedTextInputView.selectedTextRange;
UITextPosition *start = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.start];
UITextPosition *end = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.end];
UITextRange *selectedTextRange = [backedTextInputView textRangeFromPosition:start toPosition:end];
NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
if (eventLag == 0 && ![previousSelectedTextRange isEqual:selectedTextRange]) {
[backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO];
} else if (eventLag > RCTTextUpdateLagWarningThreshold) {
RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", backedTextInputView.attributedText.string, (long long)eventLag);
}
}
- (void)setSelectionStart:(NSInteger)start
selectionEnd:(NSInteger)end
{
UITextPosition *startPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
offset:start];
UITextPosition *endPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
offset:end];
if (startPosition && endPosition) {
UITextRange *range = [self.backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
[self.backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
}
}
- (void)setTextContentType:(NSString *)type
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED)
static dispatch_once_t onceToken;
static NSDictionary<NSString *, NSString *> *contentTypeMap;
dispatch_once(&onceToken, ^{
contentTypeMap = @{@"none": @"",
@"URL": UITextContentTypeURL,
@"addressCity": UITextContentTypeAddressCity,
@"addressCityAndState":UITextContentTypeAddressCityAndState,
@"addressState": UITextContentTypeAddressState,
@"countryName": UITextContentTypeCountryName,
@"creditCardNumber": UITextContentTypeCreditCardNumber,
@"emailAddress": UITextContentTypeEmailAddress,
@"familyName": UITextContentTypeFamilyName,
@"fullStreetAddress": UITextContentTypeFullStreetAddress,
@"givenName": UITextContentTypeGivenName,
@"jobTitle": UITextContentTypeJobTitle,
@"location": UITextContentTypeLocation,
@"middleName": UITextContentTypeMiddleName,
@"name": UITextContentTypeName,
@"namePrefix": UITextContentTypeNamePrefix,
@"nameSuffix": UITextContentTypeNameSuffix,
@"nickname": UITextContentTypeNickname,
@"organizationName": UITextContentTypeOrganizationName,
@"postalCode": UITextContentTypePostalCode,
@"streetAddressLine1": UITextContentTypeStreetAddressLine1,
@"streetAddressLine2": UITextContentTypeStreetAddressLine2,
@"sublocality": UITextContentTypeSublocality,
@"telephoneNumber": UITextContentTypeTelephoneNumber,
};
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
if (@available(iOS 11.0, tvOS 11.0, *)) {
NSDictionary<NSString *, NSString *> * iOS11extras = @{@"username": UITextContentTypeUsername,
@"password": UITextContentTypePassword};
NSMutableDictionary<NSString *, NSString *> * iOS11baseMap = [contentTypeMap mutableCopy];
[iOS11baseMap addEntriesFromDictionary:iOS11extras];
contentTypeMap = [iOS11baseMap copy];
}
#endif
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 /* __IPHONE_12_0 */
if (@available(iOS 12.0, tvOS 12.0, *)) {
NSDictionary<NSString *, NSString *> * iOS12extras = @{@"newPassword": UITextContentTypeNewPassword,
@"oneTimeCode": UITextContentTypeOneTimeCode};
NSMutableDictionary<NSString *, NSString *> * iOS12baseMap = [contentTypeMap mutableCopy];
[iOS12baseMap addEntriesFromDictionary:iOS12extras];
contentTypeMap = [iOS12baseMap copy];
}
#endif
});
// Setting textContentType to an empty string will disable any
// default behaviour, like the autofill bar for password inputs
self.backedTextInputView.textContentType = contentTypeMap[type] ?: type;
#endif
}
- (void)setPasswordRules:(NSString *)descriptor
{
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0
if (@available(iOS 12.0, *)) {
self.backedTextInputView.passwordRules = [UITextInputPasswordRules passwordRulesWithDescriptor:descriptor];
}
#endif
}
- (UIKeyboardType)keyboardType
{
return self.backedTextInputView.keyboardType;
}
- (void)setKeyboardType:(UIKeyboardType)keyboardType
{
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
if (textInputView.keyboardType != keyboardType) {
textInputView.keyboardType = keyboardType;
// Without the call to reloadInputViews, the keyboard will not change until the textview field (the first responder) loses and regains focus.
if (textInputView.isFirstResponder) {
[textInputView reloadInputViews];
}
}
}
#pragma mark - RCTBackedTextInputDelegate
- (BOOL)textInputShouldBeginEditing
{
return YES;
}
- (void)textInputDidBeginEditing
{
if (_clearTextOnFocus) {
self.backedTextInputView.attributedText = [NSAttributedString new];
}
if (_selectTextOnFocus) {
[self.backedTextInputView selectAll:nil];
}
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
key:nil
eventCount:_nativeEventCount];
}
- (BOOL)textInputShouldEndEditing
{
return YES;
}
- (void)textInputDidEndEditing
{
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
key:nil
eventCount:_nativeEventCount];
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
key:nil
eventCount:_nativeEventCount];
}
- (BOOL)textInputShouldReturn
{
// We send `submit` event here, in `textInputShouldReturn`
// (not in `textInputDidReturn)`, because of semantic of the event:
// `onSubmitEditing` is called when "Submit" button
// (the blue key on onscreen keyboard) did pressed
// (no connection to any specific "submitting" process).
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
reactTag:self.reactTag
text:self.backedTextInputView.attributedText.string
key:nil
eventCount:_nativeEventCount];
return _blurOnSubmit;
}
- (void)textInputDidReturn
{
// Does nothing.
}
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
{
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
if (!backedTextInputView.textWasPasted) {
[_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
reactTag:self.reactTag
text:nil
key:text
eventCount:_nativeEventCount];
}
if (_maxLength) {
NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 0);
if (text.length > allowedLength) {
// If we typed/pasted more than one character, limit the text inputted.
if (text.length > 1) {
// Truncate the input string so the result is exactly maxLength
NSString *limitedString = [text substringToIndex:allowedLength];
NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy];
// Apply text attributes if original input view doesn't have text.
if (backedTextInputView.attributedText.length == 0) {
newAttributedText = [[NSMutableAttributedString alloc] initWithString:[self.textAttributes applyTextAttributesToText:limitedString] attributes:self.textAttributes.effectiveTextAttributes];
} else {
[newAttributedText replaceCharactersInRange:range withString:limitedString];
}
backedTextInputView.attributedText = newAttributedText;
_predictedText = newAttributedText.string;
// Collapse selection at end of insert to match normal paste behavior.
UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument
offset:(range.location + allowedLength)];
[backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd]
notifyDelegate:YES];
[self textInputDidChange];
}
return nil; // Rejecting the change.
}
}
NSString *previousText = backedTextInputView.attributedText.string ?: @"";
if (range.location + range.length > backedTextInputView.attributedText.string.length) {
_predictedText = backedTextInputView.attributedText.string;
} else {
_predictedText = [backedTextInputView.attributedText.string stringByReplacingCharactersInRange:range withString:text];
}
if (_onTextInput) {
_onTextInput(@{
@"text": text,
@"previousText": previousText,
@"range": @{
@"start": @(range.location),
@"end": @(range.location + range.length)
},
@"eventCount": @(_nativeEventCount),
});
}
return text; // Accepting the change.
}
- (void)textInputDidChange
{
[self updateLocalData];
id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
// Detect when `backedTextInputView` updates happened that didn't invoke `shouldChangeTextInRange`
// (e.g. typing simplified Chinese in pinyin will insert and remove spaces without
// calling shouldChangeTextInRange). This will cause JS to get out of sync so we
// update the mismatched range.
NSRange currentRange;
NSRange predictionRange;
if (findMismatch(backedTextInputView.attributedText.string, _predictedText, &currentRange, &predictionRange)) {
NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange];
[self textInputShouldChangeText:replacement inRange:predictionRange];
// JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it.
[self textInputDidChangeSelection];
}
_nativeEventCount++;
if (_onChange) {
_onChange(@{
@"text": self.attributedText.string,
@"target": self.reactTag,
@"eventCount": @(_nativeEventCount),
});
}
}
- (void)textInputDidChangeSelection
{
if (!_onSelectionChange) {
return;
}
RCTTextSelection *selection = self.selection;
_onSelectionChange(@{
@"selection": @{
@"start": @(selection.start),
@"end": @(selection.end),
},
});
}
- (void)updateLocalData
{
[self enforceTextAttributesIfNeeded];
[_bridge.uiManager setLocalData:[self.backedTextInputView.attributedText copy]
forView:self];
}
#pragma mark - Layout (in UIKit terms, with all insets)
- (CGSize)intrinsicContentSize
{
CGSize size = self.backedTextInputView.intrinsicContentSize;
size.width += _reactBorderInsets.left + _reactBorderInsets.right;
size.height += _reactBorderInsets.top + _reactBorderInsets.bottom;
// Returning value DOES include border and padding insets.
return size;
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat compoundHorizontalBorderInset = _reactBorderInsets.left + _reactBorderInsets.right;
CGFloat compoundVerticalBorderInset = _reactBorderInsets.top + _reactBorderInsets.bottom;
size.width -= compoundHorizontalBorderInset;
size.height -= compoundVerticalBorderInset;
// Note: `paddingInsets` was already included in `backedTextInputView` size
// because it was applied as `textContainerInset`.
CGSize fittingSize = [self.backedTextInputView sizeThatFits:size];
fittingSize.width += compoundHorizontalBorderInset;
fittingSize.height += compoundVerticalBorderInset;
// Returning value DOES include border and padding insets.
return fittingSize;
}
#pragma mark - Accessibility
- (UIView *)reactAccessibilityElement
{
return self.backedTextInputView;
}
#pragma mark - Focus Control
- (void)reactFocus
{
[self.backedTextInputView reactFocus];
}
- (void)reactBlur
{
[self.backedTextInputView reactBlur];
}
- (void)didMoveToWindow
{
if (self.autoFocus && !_didMoveToWindow) {
[self.backedTextInputView reactFocus];
} else {
[self.backedTextInputView reactFocusIfNeeded];
}
_didMoveToWindow = YES;
}
#pragma mark - Custom Input Accessory View
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) {
[self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID];
} else if (!self.inputAccessoryViewID) {
[self setDefaultInputAccessoryView];
}
}
- (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID
{
#if !TARGET_OS_TV
__weak RCTBaseTextInputView *weakSelf = self;
[_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) {
RCTBaseTextInputView *strongSelf = weakSelf;
if (rootView) {
UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID
withRootTag:rootView.reactTag];
if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) {
strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).inputAccessoryView;
[strongSelf reloadInputViewsIfNecessary];
}
}
}];
#endif /* !TARGET_OS_TV */
}
- (void)setDefaultInputAccessoryView
{
#if !TARGET_OS_TV
UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
UIKeyboardType keyboardType = textInputView.keyboardType;
// These keyboard types (all are number pads) don't have a "Done" button by default,
// so we create an `inputAccessoryView` with this button for them.
BOOL shouldHaveInputAccesoryView;
if (@available(iOS 10.0, *)) {
shouldHaveInputAccesoryView =
(
keyboardType == UIKeyboardTypeNumberPad ||
keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad ||
keyboardType == UIKeyboardTypeASCIICapableNumberPad
) &&
textInputView.returnKeyType == UIReturnKeyDone;
} else {
shouldHaveInputAccesoryView =
(
keyboardType == UIKeyboardTypeNumberPad ||
keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad
) &&
textInputView.returnKeyType == UIReturnKeyDone;
}
if (_hasInputAccesoryView == shouldHaveInputAccesoryView) {
return;
}
_hasInputAccesoryView = shouldHaveInputAccesoryView;
if (shouldHaveInputAccesoryView) {
UIToolbar *toolbarView = [[UIToolbar alloc] init];
[toolbarView sizeToFit];
UIBarButtonItem *flexibleSpace =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil
action:nil];
UIBarButtonItem *doneButton =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(handleInputAccessoryDoneButton)];
toolbarView.items = @[flexibleSpace, doneButton];
textInputView.inputAccessoryView = toolbarView;
}
else {
textInputView.inputAccessoryView = nil;
}
[self reloadInputViewsIfNecessary];
#endif /* !TARGET_OS_TV */
}
- (void)reloadInputViewsIfNecessary
{
// We have to call `reloadInputViews` for focused text inputs to update an accessory view.
if (self.backedTextInputView.isFirstResponder) {
[self.backedTextInputView reloadInputViews];
}
}
- (void)handleInputAccessoryDoneButton
{
if ([self textInputShouldReturn]) {
[self.backedTextInputView endEditing:YES];
}
}
#pragma mark - Helpers
static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange)
{
NSInteger firstMismatch = -1;
for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) {
if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) {
firstMismatch = ii;
break;
}
}
if (firstMismatch == -1) {
return NO;
}
NSUInteger ii = second.length;
NSUInteger lastMismatch = first.length;
while (ii > firstMismatch && lastMismatch > firstMismatch) {
if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) {
break;
}
ii--;
lastMismatch--;
}
*firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch);
*secondRange = NSMakeRange(firstMismatch, ii - firstMismatch);
return YES;
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextViewManager.h"
@interface RCTBaseTextInputViewManager : RCTBaseTextViewManager
@end

View File

@ -0,0 +1,170 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTBaseTextInputViewManager.h>
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTFont.h>
#import <React/RCTShadowView+Layout.h>
#import <React/RCTShadowView.h>
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#import <React/RCTUIManagerObserverCoordinator.h>
#import <React/RCTBaseTextInputShadowView.h>
#import <React/RCTBaseTextInputView.h>
#import <React/RCTConvert+Text.h>
@interface RCTBaseTextInputViewManager () <RCTUIManagerObserver>
@end
@implementation RCTBaseTextInputViewManager
{
NSHashTable<RCTBaseTextInputShadowView *> *_shadowViews;
}
RCT_EXPORT_MODULE()
#pragma mark - Unified <TextInput> properties
RCT_REMAP_VIEW_PROPERTY(autoCapitalize, backedTextInputView.autocapitalizationType, UITextAutocapitalizationType)
RCT_REMAP_VIEW_PROPERTY(autoCorrect, backedTextInputView.autocorrectionType, UITextAutocorrectionType)
RCT_REMAP_VIEW_PROPERTY(contextMenuHidden, backedTextInputView.contextMenuHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(editable, backedTextInputView.editable, BOOL)
RCT_REMAP_VIEW_PROPERTY(enablesReturnKeyAutomatically, backedTextInputView.enablesReturnKeyAutomatically, BOOL)
RCT_REMAP_VIEW_PROPERTY(keyboardAppearance, backedTextInputView.keyboardAppearance, UIKeyboardAppearance)
RCT_REMAP_VIEW_PROPERTY(placeholder, backedTextInputView.placeholder, NSString)
RCT_REMAP_VIEW_PROPERTY(placeholderTextColor, backedTextInputView.placeholderColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(returnKeyType, backedTextInputView.returnKeyType, UIReturnKeyType)
RCT_REMAP_VIEW_PROPERTY(selectionColor, backedTextInputView.tintColor, UIColor)
RCT_REMAP_VIEW_PROPERTY(spellCheck, backedTextInputView.spellCheckingType, UITextSpellCheckingType)
RCT_REMAP_VIEW_PROPERTY(caretHidden, backedTextInputView.caretHidden, BOOL)
RCT_REMAP_VIEW_PROPERTY(clearButtonMode, backedTextInputView.clearButtonMode, UITextFieldViewMode)
RCT_REMAP_VIEW_PROPERTY(scrollEnabled, backedTextInputView.scrollEnabled, BOOL)
RCT_REMAP_VIEW_PROPERTY(secureTextEntry, backedTextInputView.secureTextEntry, BOOL)
RCT_EXPORT_VIEW_PROPERTY(autoFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(blurOnSubmit, BOOL)
RCT_EXPORT_VIEW_PROPERTY(clearTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(keyboardType, UIKeyboardType)
RCT_EXPORT_VIEW_PROPERTY(maxLength, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(selectTextOnFocus, BOOL)
RCT_EXPORT_VIEW_PROPERTY(selection, RCTTextSelection)
RCT_EXPORT_VIEW_PROPERTY(inputAccessoryViewID, NSString)
RCT_EXPORT_VIEW_PROPERTY(textContentType, NSString)
RCT_EXPORT_VIEW_PROPERTY(passwordRules, NSString)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(mostRecentEventCount, NSInteger)
RCT_EXPORT_SHADOW_PROPERTY(text, NSString)
RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString)
RCT_EXPORT_SHADOW_PROPERTY(onContentSizeChange, RCTBubblingEventBlock)
RCT_CUSTOM_VIEW_PROPERTY(multiline, BOOL, UIView)
{
// No op.
// This View Manager doesn't use this prop but it must be exposed here via ViewConfig to enable Fabric component use it.
}
- (RCTShadowView *)shadowView
{
RCTBaseTextInputShadowView *shadowView = [[RCTBaseTextInputShadowView alloc] initWithBridge:self.bridge];
shadowView.textAttributes.fontSizeMultiplier = [[[self.bridge
moduleForName:@"AccessibilityManager"
lazilyLoadIfNecessary:YES] valueForKey:@"multiplier"] floatValue];
[_shadowViews addObject:shadowView];
return shadowView;
}
- (void)setBridge:(RCTBridge *)bridge
{
[super setBridge:bridge];
_shadowViews = [NSHashTable weakObjectsHashTable];
[bridge.uiManager.observerCoordinator addObserver:self];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleDidUpdateMultiplierNotification)
name:@"RCTAccessibilityManagerDidUpdateMultiplierNotification"
object:[bridge moduleForName:@"AccessibilityManager"
lazilyLoadIfNecessary:YES]];
}
RCT_EXPORT_METHOD(focus : (nonnull NSNumber *)viewTag)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[viewTag];
[view reactFocus];
}];
}
RCT_EXPORT_METHOD(blur : (nonnull NSNumber *)viewTag)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[viewTag];
[view reactBlur];
}];
}
RCT_EXPORT_METHOD(setTextAndSelection : (nonnull NSNumber *)viewTag
mostRecentEventCount : (NSInteger)mostRecentEventCount
value : (NSString *)value
start : (NSInteger)start
end : (NSInteger)end)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTBaseTextInputView *view = (RCTBaseTextInputView *)viewRegistry[viewTag];
NSInteger eventLag = view.nativeEventCount - mostRecentEventCount;
if (eventLag != 0) {
return;
}
RCTExecuteOnUIManagerQueue(^{
RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[self.bridge.uiManager shadowViewForReactTag:viewTag];
[shadowView setText:value];
[self.bridge.uiManager setNeedsLayout];
RCTExecuteOnMainQueue(^{
[view setSelectionStart:start selectionEnd:end];
});
});
}];
}
#pragma mark - RCTUIManagerObserver
- (void)uiManagerWillPerformMounting:(__unused RCTUIManager *)uiManager
{
for (RCTBaseTextInputShadowView *shadowView in _shadowViews) {
[shadowView uiManagerWillPerformMounting];
}
}
#pragma mark - Font Size Multiplier
- (void)handleDidUpdateMultiplierNotification
{
CGFloat fontSizeMultiplier = [[[self.bridge moduleForName:@"AccessibilityManager"]
valueForKey:@"multiplier"] floatValue];
NSHashTable<RCTBaseTextInputShadowView *> *shadowViews = _shadowViews;
RCTExecuteOnUIManagerQueue(^{
for (RCTBaseTextInputShadowView *shadowView in shadowViews) {
shadowView.textAttributes.fontSizeMultiplier = fontSizeMultiplier;
[shadowView dirtyLayout];
}
[self.bridge.uiManager setNeedsLayout];
});
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTShadowView.h>
@interface RCTInputAccessoryShadowView : RCTShadowView
@end

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTInputAccessoryShadowView.h>
#import <React/RCTUtils.h>
@implementation RCTInputAccessoryShadowView
- (void)insertReactSubview:(RCTShadowView *)subview atIndex:(NSInteger)atIndex
{
[super insertReactSubview:subview atIndex:atIndex];
subview.width = (YGValue) { RCTScreenSize().width, YGUnitPoint };
}
@end

View File

@ -0,0 +1,17 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@class RCTBridge;
@class RCTInputAccessoryViewContent;
@interface RCTInputAccessoryView : UIView
- (instancetype)initWithBridge:(RCTBridge *)bridge;
@end

View File

@ -0,0 +1,79 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTInputAccessoryView.h>
#import <React/RCTBridge.h>
#import <React/RCTTouchHandler.h>
#import <React/UIView+React.h>
#import <React/RCTInputAccessoryViewContent.h>
@interface RCTInputAccessoryView()
// Overriding `inputAccessoryView` to `readwrite`.
@property (nonatomic, readwrite, retain) UIView *inputAccessoryView;
@end
@implementation RCTInputAccessoryView
{
BOOL _shouldBecomeFirstResponder;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super init]) {
_inputAccessoryView = [RCTInputAccessoryViewContent new];
RCTTouchHandler *const touchHandler = [[RCTTouchHandler alloc] initWithBridge:bridge];
[touchHandler attachToView:_inputAccessoryView];
}
return self;
}
- (BOOL)canBecomeFirstResponder
{
return true;
}
- (void)reactSetFrame:(CGRect)frame
{
[_inputAccessoryView reactSetFrame:frame];
if (_shouldBecomeFirstResponder) {
_shouldBecomeFirstResponder = NO;
[self becomeFirstResponder];
}
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[_inputAccessoryView insertReactSubview:subview atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
[_inputAccessoryView removeReactSubview:subview];
}
- (void)didUpdateReactSubviews
{
// Do nothing, as subviews are managed by `insertReactSubview:atIndex:`.
}
- (void)didSetProps:(NSArray<NSString *> *)changedProps
{
// If the accessory view is not linked to a text input via nativeID, assume it is
// a standalone component that should get focus whenever it is rendered.
if (![changedProps containsObject:@"nativeID"] && !self.nativeID) {
_shouldBecomeFirstResponder = YES;
}
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
@interface RCTInputAccessoryViewContent : UIView
@end

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTInputAccessoryViewContent.h>
#import <React/UIView+React.h>
@implementation RCTInputAccessoryViewContent
{
UIView *_safeAreaContainer;
NSLayoutConstraint *_heightConstraint;
}
- (instancetype)init
{
if (self = [super init]) {
_safeAreaContainer = [UIView new];
[self addSubview:_safeAreaContainer];
// Use autolayout to position the view properly and take into account
// safe area insets on iPhone X.
// TODO: Support rotation, anchor to left and right without breaking frame x coordinate (T27974328).
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
_safeAreaContainer.translatesAutoresizingMaskIntoConstraints = NO;
_heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0];
_heightConstraint.active = YES;
if (@available(iOS 11.0, tvOS 11.0, *)) {
[_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor].active = YES;
[_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor].active = YES;
[_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leadingAnchor].active = YES;
[_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor].active = YES;
} else {
[_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
[_safeAreaContainer.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
[_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.leadingAnchor].active = YES;
[_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.trailingAnchor].active = YES;
}
}
return self;
}
- (CGSize)intrinsicContentSize
{
// This is needed so the view size is based on autolayout constraints.
return CGSizeZero;
}
- (void)reactSetFrame:(CGRect)frame
{
// We still need to set the frame here, otherwise it won't be
// measured until moved to the window during the keyboard opening
// animation. If this happens, the height will be animated from 0 to
// its actual size and we don't want that.
[self setFrame:frame];
[_safeAreaContainer setFrame:frame];
_heightConstraint.constant = frame.size.height;
[self layoutIfNeeded];
}
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)index
{
[super insertReactSubview:subview atIndex:index];
[_safeAreaContainer insertSubview:subview atIndex:index];
}
- (void)removeReactSubview:(UIView *)subview
{
[super removeReactSubview:subview];
[subview removeFromSuperview];
if ([[_safeAreaContainer subviews] count] == 0 && [self isFirstResponder]) {
[self resignFirstResponder];
}
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewManager.h>
@interface RCTInputAccessoryViewManager : RCTViewManager
@end

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTInputAccessoryViewManager.h>
#import <React/RCTInputAccessoryShadowView.h>
#import <React/RCTInputAccessoryView.h>
@implementation RCTInputAccessoryViewManager
RCT_EXPORT_MODULE()
+ (BOOL)requiresMainQueueSetup
{
return NO;
}
- (UIView *)view
{
return [[RCTInputAccessoryView alloc] initWithBridge:self.bridge];
}
- (RCTShadowView *)shadowView
{
return [RCTInputAccessoryShadowView new];
}
RCT_REMAP_VIEW_PROPERTY(backgroundColor, inputAccessoryView.backgroundColor, UIColor)
@end

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTConvert.h>
/**
* Object containing information about a TextInput's selection.
*/
@interface RCTTextSelection : NSObject
@property (nonatomic, assign, readonly) NSInteger start;
@property (nonatomic, assign, readonly) NSInteger end;
- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end;
@end
@interface RCTConvert (RCTTextSelection)
+ (RCTTextSelection *)RCTTextSelection:(id)json;
@end

View File

@ -0,0 +1,36 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTTextSelection.h>
@implementation RCTTextSelection
- (instancetype)initWithStart:(NSInteger)start end:(NSInteger)end
{
if (self = [super init]) {
_start = start;
_end = end;
}
return self;
}
@end
@implementation RCTConvert (RCTTextSelection)
+ (RCTTextSelection *)RCTTextSelection:(id)json
{
if ([json isKindOfClass:[NSDictionary class]]) {
NSInteger start = [self NSInteger:json[@"start"]];
NSInteger end = [self NSInteger:json[@"end"]];
return [[RCTTextSelection alloc] initWithStart:start end:end];
}
return nil;
}
@end

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextInputView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTSinglelineTextInputView : RCTBaseTextInputView
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTSinglelineTextInputView.h>
#import <React/RCTBridge.h>
#import <React/RCTUITextField.h>
@implementation RCTSinglelineTextInputView
{
RCTUITextField *_backedTextInputView;
}
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `true` for <TextInput multiline={false}> by design.
self.blurOnSubmit = YES;
_backedTextInputView = [[RCTUITextField alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.textInputDelegate = self;
[self addSubview:_backedTextInputView];
}
return self;
}
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
return _backedTextInputView;
}
@end

View File

@ -0,0 +1,16 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextInputViewManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface RCTSinglelineTextInputViewManager : RCTBaseTextInputViewManager
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTSinglelineTextInputViewManager.h>
#import <React/RCTBaseTextInputShadowView.h>
#import <React/RCTSinglelineTextInputView.h>
@implementation RCTSinglelineTextInputViewManager
RCT_EXPORT_MODULE()
- (RCTShadowView *)shadowView
{
RCTBaseTextInputShadowView *shadowView =
(RCTBaseTextInputShadowView *)[super shadowView];
shadowView.maximumNumberOfLines = 1;
return shadowView;
}
- (UIView *)view
{
return [[RCTSinglelineTextInputView alloc] initWithBridge:self.bridge];
}
@end

View File

@ -0,0 +1,34 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <UIKit/UIKit.h>
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTBackedTextInputDelegate.h>
NS_ASSUME_NONNULL_BEGIN
/*
* Just regular UITextField... but much better!
*/
@interface RCTUITextField : UITextField <RCTBackedTextInputViewProtocol>
- (instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@property (nonatomic, weak) id<RCTBackedTextInputDelegate> textInputDelegate;
@property (nonatomic, assign) BOOL caretHidden;
@property (nonatomic, assign) BOOL contextMenuHidden;
@property (nonatomic, assign, readonly) BOOL textWasPasted;
@property (nonatomic, strong, nullable) UIColor *placeholderColor;
@property (nonatomic, assign) UIEdgeInsets textContainerInset;
@property (nonatomic, assign, getter=isEditable) BOOL editable;
@property (nonatomic, getter=isScrollEnabled) BOOL scrollEnabled;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,222 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTUITextField.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <React/RCTBackedTextInputDelegateAdapter.h>
#import <React/RCTTextAttributes.h>
@implementation RCTUITextField {
RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter;
NSDictionary<NSAttributedStringKey, id> *_defaultTextAttributes;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(_textDidChange)
name:UITextFieldTextDidChangeNotification
object:self];
_textInputDelegateAdapter = [[RCTBackedTextFieldDelegateAdapter alloc] initWithTextField:self];
_scrollEnabled = YES;
}
return self;
}
- (void)_textDidChange
{
_textWasPasted = NO;
}
#pragma mark - Accessibility
- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement
{
// UITextField is accessible by default (some nested views are) and disabling that is not supported.
// On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view
// (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component.
}
#pragma mark - Properties
- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset
{
_textContainerInset = textContainerInset;
[self setNeedsLayout];
}
- (void)setPlaceholder:(NSString *)placeholder
{
[super setPlaceholder:placeholder];
[self _updatePlaceholder];
}
- (void)setPlaceholderColor:(UIColor *)placeholderColor
{
_placeholderColor = placeholderColor;
[self _updatePlaceholder];
}
- (void)setDefaultTextAttributes:(NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) {
return;
}
_defaultTextAttributes = defaultTextAttributes;
[super setDefaultTextAttributes:defaultTextAttributes];
[self _updatePlaceholder];
}
- (NSDictionary<NSAttributedStringKey, id> *)defaultTextAttributes
{
return _defaultTextAttributes;
}
- (void)_updatePlaceholder
{
self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder ?: @""
attributes:[self _placeholderTextAttributes]];
}
- (BOOL)isEditable
{
return self.isEnabled;
}
- (void)setEditable:(BOOL)editable
{
self.enabled = editable;
}
- (void)setSecureTextEntry:(BOOL)secureTextEntry
{
if (self.secureTextEntry == secureTextEntry) {
return;
}
[super setSecureTextEntry:secureTextEntry];
// Fix for trailing whitespate issue
// Read more:
// https://stackoverflow.com/questions/14220187/uitextfield-has-trailing-whitespace-after-securetextentry-toggle/22537788#22537788
NSAttributedString *originalText = [self.attributedText copy];
self.attributedText = [NSAttributedString new];
self.attributedText = originalText;
}
#pragma mark - Placeholder
- (NSDictionary<NSAttributedStringKey, id> *)_placeholderTextAttributes
{
NSMutableDictionary<NSAttributedStringKey, id> *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new];
if (self.placeholderColor) {
[textAttributes setValue:self.placeholderColor forKey:NSForegroundColorAttributeName];
} else {
[textAttributes removeObjectForKey:NSForegroundColorAttributeName];
}
return textAttributes;
}
#pragma mark - Context Menu
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (_contextMenuHidden) {
return NO;
}
return [super canPerformAction:action withSender:sender];
}
#pragma mark - Caret Manipulation
- (CGRect)caretRectForPosition:(UITextPosition *)position
{
if (_caretHidden) {
return CGRectZero;
}
return [super caretRectForPosition:position];
}
#pragma mark - Positioning Overrides
- (CGRect)textRectForBounds:(CGRect)bounds
{
return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], _textContainerInset);
}
- (CGRect)editingRectForBounds:(CGRect)bounds
{
return [self textRectForBounds:bounds];
}
#pragma mark - Overrides
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
// Overrides selectedTextRange setter to get notify when selectedTextRange changed.
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange
{
[super setSelectedTextRange:selectedTextRange];
[_textInputDelegateAdapter selectedTextRangeWasSet];
}
#pragma clang diagnostic pop
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate
{
if (!notifyDelegate) {
// We have to notify an adapter that following selection change was initiated programmatically,
// so the adapter must not generate a notification for it.
[_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange];
}
[super setSelectedTextRange:selectedTextRange];
}
- (void)paste:(id)sender
{
[super paste:sender];
_textWasPasted = YES;
}
#pragma mark - Layout
- (CGSize)contentSize
{
// Returning size DOES contain `textContainerInset` (aka `padding`).
return self.intrinsicContentSize;
}
- (CGSize)intrinsicContentSize
{
// Note: `placeholder` defines intrinsic size for `<TextInput>`.
NSString *text = self.placeholder ?: @"";
CGSize size = [text sizeWithAttributes:[self _placeholderTextAttributes]];
size = CGSizeMake(RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height));
size.width += _textContainerInset.left + _textContainerInset.right;
size.height += _textContainerInset.top + _textContainerInset.bottom;
// Returning size DOES contain `textContainerInset` (aka `padding`).
return size;
}
- (CGSize)sizeThatFits:(CGSize)size
{
// All size values here contain `textContainerInset` (aka `padding`).
CGSize intrinsicSize = self.intrinsicContentSize;
return CGSizeMake(MIN(size.width, intrinsicSize.width), MIN(size.height, intrinsicSize.height));
}
@end

190
node_modules/react-native/Libraries/Text/TextProps.js generated vendored Normal file
View File

@ -0,0 +1,190 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
'use strict';
import type {
LayoutEvent,
PressEvent,
TextLayoutEvent,
} from '../Types/CoreEventTypes';
import type {Node} from 'react';
import type {TextStyleProp} from '../StyleSheet/StyleSheet';
import type {
AccessibilityRole,
AccessibilityState,
} from '../Components/View/ViewAccessibility';
export type PressRetentionOffset = $ReadOnly<{|
top: number,
left: number,
bottom: number,
right: number,
|}>;
/**
* @see https://reactnative.dev/docs/text.html#reference
*/
export type TextProps = $ReadOnly<{|
/**
* Indicates whether the view is an accessibility element.
*
* See https://reactnative.dev/docs/text.html#accessible
*/
accessible?: ?boolean,
accessibilityHint?: ?Stringish,
accessibilityLabel?: ?Stringish,
accessibilityRole?: ?AccessibilityRole,
accessibilityState?: ?AccessibilityState,
/**
* Whether font should be scaled down automatically.
*
* See https://reactnative.dev/docs/text.html#adjustsfontsizetofit
*/
adjustsFontSizeToFit?: ?boolean,
/**
* Whether fonts should scale to respect Text Size accessibility settings.
*
* See https://reactnative.dev/docs/text.html#allowfontscaling
*/
allowFontScaling?: ?boolean,
children?: ?Node,
/**
* When `numberOfLines` is set, this prop defines how text will be
* truncated.
*
* See https://reactnative.dev/docs/text.html#ellipsizemode
*/
ellipsizeMode?: ?('clip' | 'head' | 'middle' | 'tail'),
/**
* Specifies largest possible scale a font can reach when `allowFontScaling` is enabled.
* Possible values:
* `null/undefined` (default): inherit from the parent node or the global default (0)
* `0`: no max, ignore parent/global default
* `>= 1`: sets the maxFontSizeMultiplier of this node to this value
*/
maxFontSizeMultiplier?: ?number,
/**
* Used to locate this view from native code.
*
* See https://reactnative.dev/docs/text.html#nativeid
*/
nativeID?: ?string,
/**
* Used to truncate the text with an ellipsis.
*
* See https://reactnative.dev/docs/text.html#numberoflines
*/
numberOfLines?: ?number,
/**
* Invoked on mount and layout changes.
*
* See https://reactnative.dev/docs/text.html#onlayout
*/
onLayout?: ?(event: LayoutEvent) => mixed,
/**
* This function is called on long press.
*
* See https://reactnative.dev/docs/text.html#onlongpress
*/
onLongPress?: ?(event: PressEvent) => mixed,
/**
* This function is called on press.
*
* See https://reactnative.dev/docs/text.html#onpress
*/
onPress?: ?(event: PressEvent) => mixed,
onResponderGrant?: ?(event: PressEvent, dispatchID: string) => void,
onResponderMove?: ?(event: PressEvent) => void,
onResponderRelease?: ?(event: PressEvent) => void,
onResponderTerminate?: ?(event: PressEvent) => void,
onResponderTerminationRequest?: ?() => boolean,
onStartShouldSetResponder?: ?() => boolean,
onMoveShouldSetResponder?: ?() => boolean,
onTextLayout?: ?(event: TextLayoutEvent) => mixed,
/**
* Defines how far your touch may move off of the button, before
* deactivating the button.
*
* See https://reactnative.dev/docs/text.html#pressretentionoffset
*/
pressRetentionOffset?: ?PressRetentionOffset,
/**
* Lets the user select text.
*
* See https://reactnative.dev/docs/text.html#selectable
*/
selectable?: ?boolean,
style?: ?TextStyleProp,
/**
* Used to locate this view in end-to-end tests.
*
* See https://reactnative.dev/docs/text.html#testid
*/
testID?: ?string,
/**
* Android Only
*/
/**
* Specifies the disabled state of the text view for testing purposes.
*
* See https://reactnative.dev/docs/text.html#disabled
*/
disabled?: ?boolean,
/**
* The highlight color of the text.
*
* See https://reactnative.dev/docs/text.html#selectioncolor
*/
selectionColor?: ?string,
dataDetectorType?: ?('phoneNumber' | 'link' | 'email' | 'none' | 'all'),
/**
* Set text break strategy on Android.
*
* See https://reactnative.dev/docs/text.html#textbreakstrategy
*/
textBreakStrategy?: ?('balanced' | 'highQuality' | 'simple'),
/**
* iOS Only
*/
adjustsFontSizeToFit?: ?boolean,
/**
* Smallest possible scale a font can reach.
*
* See https://reactnative.dev/docs/text.html#minimumfontscale
*/
minimumFontScale?: ?number,
/**
* When `true`, no visual change is made when text is pressed down.
*
* See https://reactnative.dev/docs/text.html#supperhighlighting
*/
suppressHighlighting?: ?boolean,
|}>;

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextShadowView.h"
@interface RCTVirtualTextShadowView : RCTBaseTextShadowView
@end

View File

@ -0,0 +1,38 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTVirtualTextShadowView.h>
#import <React/RCTShadowView+Layout.h>
#import <yoga/Yoga.h>
#import <React/RCTRawTextShadowView.h>
@implementation RCTVirtualTextShadowView {
BOOL _isLayoutDirty;
}
#pragma mark - Layout
- (void)dirtyLayout
{
[super dirtyLayout];
if (_isLayoutDirty) {
return;
}
_isLayoutDirty = YES;
[self.superview dirtyLayout];
}
- (void)clearLayout
{
_isLayoutDirty = NO;
}
@end

View File

@ -0,0 +1,12 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTBaseTextViewManager.h"
@interface RCTVirtualTextViewManager : RCTBaseTextViewManager
@end

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTVirtualTextViewManager.h>
#import <React/RCTVirtualTextShadowView.h>
@implementation RCTVirtualTextViewManager
RCT_EXPORT_MODULE(RCTVirtualText)
- (UIView *)view
{
return [UIView new];
}
- (RCTShadowView *)shadowView
{
return [RCTVirtualTextShadowView new];
}
@end